Added ability to use SQL in the 'DB Restriction' field. #2767

pull/8637/head
Akshay Joshi 2025-04-04 15:53:12 +05:30 committed by GitHub
parent 0d8b3c4389
commit 627aa5d695
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 239 additions and 59 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 121 KiB

View File

@ -12,6 +12,7 @@ notes for it.
:maxdepth: 1
release_notes_9_3
release_notes_9_2
release_notes_9_1
release_notes_9_0

View File

@ -0,0 +1,34 @@
***********
Version 9.3
***********
Release date: 2025-04-30
This release contains a number of bug fixes and new features since the release of pgAdmin 4 v9.2.
Supported Database Servers
**************************
**PostgreSQL**: 13, 14, 15, 16 and 17
**EDB Advanced Server**: 13, 14, 15, 16 and 17
Bundled PostgreSQL Utilities
****************************
**psql**, **pg_dump**, **pg_dumpall**, **pg_restore**: 17.2
New features
************
| `Issue #2767 <https://github.com/pgadmin-org/pgadmin4/issues/2767>`_ - Added ability to use SQL in the "DB Restriction" field.
| `Issue #8629 <https://github.com/pgadmin-org/pgadmin4/issues/8629>`_ - Added support for font ligatures.
Housekeeping
************
Bug fixes
*********
| `Issue #8443 <https://github.com/pgadmin-org/pgadmin4/issues/8443>`_ - Fixed an issue where the debugger hangs when stepping into nested function/procedure.
| `Issue #8556 <https://github.com/pgadmin-org/pgadmin4/issues/8556>`_ - Ensure that graph data is updated even when the Dashboard tab is inactive.

View File

@ -195,11 +195,19 @@ Click the *Advanced* tab to continue.
Use the fields in the *Advanced* tab to configure a connection:
* Use the *DB restriction* field to provide a SQL restriction that will be used
against the pg_database table to limit the databases that you see. For
example, you might enter: *live_db test_db* so that only live_db and test_db
are shown in the pgAdmin browser. Separate entries with a comma or tab as you
type.
* Specify the type of the database restriction that will be used to filter
out the databases for restriction in the *DB restriction type* field:
* Select the *Databases* option to specify the name of the databases
that will be used against the pg_database table to limit the databases
that you see. This is the default.
* Select the *SQL* option to provide a SQL restriction that will be used
against the pg_database table to limit the databases that you see.
* Use the *DB restriction* field to provide a SQL restriction OR Database names
that will be used against the pg_database table to limit the databases that you see.
For example, you might enter: *live_db test_db* so that only live_db and test_db
are shown in the pgAdmin object explorer.
* Use the *Password exec command* field to specify a shell command to be executed
to retrieve a password to be used for SQL authentication. The ``stdout`` of the
command will be used as the SQL password. This may be useful when the password

View File

@ -0,0 +1,36 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2025, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
"""
Revision ID: 1f0eddc8fc79
Revises: e982c040d9b5
Create Date: 2025-03-26 15:58:24.131719
"""
from alembic import op
import sqlalchemy as sa
from pgadmin.utils.constants import RESTRICTION_TYPE_DATABASES
# revision identifiers, used by Alembic.
revision = '1f0eddc8fc79'
down_revision = 'e982c040d9b5'
branch_labels = None
depends_on = None
def upgrade():
op.add_column('server',
sa.Column('db_res_type', sa.String(length=32),
server_default=RESTRICTION_TYPE_DATABASES))
def downgrade():
# pgAdmin only upgrades, downgrade not implemented.
pass

View File

@ -34,9 +34,9 @@ from pgadmin.utils.exception import CryptKeyMissing
from pgadmin.tools.schema_diff.node_registry import SchemaDiffRegistry
from pgadmin.browser.server_groups.servers.utils import \
(is_valid_ipaddress, get_replication_type, convert_connection_parameter,
check_ssl_fields)
check_ssl_fields, get_db_restriction)
from pgadmin.utils.constants import UNAUTH_REQ, MIMETYPE_APP_JS, \
SERVER_CONNECTION_CLOSED
SERVER_CONNECTION_CLOSED, RESTRICTION_TYPE_SQL
from sqlalchemy import or_
from sqlalchemy.orm.attributes import flag_modified
from pgadmin.utils.preferences import Preferences
@ -746,6 +746,7 @@ class ServerNode(PGChildNodeView):
'comment': 'comment',
'role': 'role',
'db_res': 'db_res',
'db_res_type': 'db_res_type',
'passexec_cmd': 'passexec_cmd',
'passexec_expiration': 'passexec_expiration',
'bgcolor': 'bgcolor',
@ -776,12 +777,11 @@ class ServerNode(PGChildNodeView):
'role': gettext('Role')
}
idx = 0
data = request.form if request.form else json.loads(
request.data
)
if 'db_res' in data:
if 'db_res' in data and isinstance(data['db_res'], list):
data['db_res'] = ','.join(data['db_res'])
# Update connection parameter if any.
@ -948,7 +948,7 @@ class ServerNode(PGChildNodeView):
'connected': connected,
'version': manager.ver,
'server_type': manager.server_type if connected else 'pg',
'db_res': server.db_res.split(',') if server.db_res else None
'db_res': get_db_restriction(server.db_res_type, server.db_res)
})
return ajax_response(
@ -1031,7 +1031,8 @@ class ServerNode(PGChildNodeView):
'server_type': manager.server_type if connected else 'pg',
'bgcolor': server.bgcolor,
'fgcolor': server.fgcolor,
'db_res': server.db_res.split(',') if server.db_res else None,
'db_res': get_db_restriction(server.db_res_type, server.db_res),
'db_res_type': server.db_res_type,
'passexec_cmd':
server.passexec_cmd if server.passexec_cmd else None,
'passexec_expiration':
@ -1137,6 +1138,12 @@ class ServerNode(PGChildNodeView):
data['connection_params'] = connection_params
server = None
db_restriction = None
if 'db_res' in data and isinstance(data['db_res'], list):
db_restriction = ','.join(data['db_res'])
elif 'db_res' in data and 'db_res_type' in data and \
data['db_res_type'] == RESTRICTION_TYPE_SQL:
db_restriction = data['db_res']
try:
server = Server(
@ -1151,8 +1158,8 @@ class ServerNode(PGChildNodeView):
config.ALLOW_SAVE_PASSWORD else 0,
comment=data.get('comment', None),
role=data.get('role', None),
db_res=','.join(data['db_res']) if 'db_res' in data and
isinstance(data['db_res'], list) else None,
db_res=db_restriction,
db_res_type=data.get('db_res_type', None),
bgcolor=data.get('bgcolor', None),
fgcolor=data.get('fgcolor', None),
service=data.get('service', None),

View File

@ -13,7 +13,7 @@ import re
from functools import wraps
import json
from flask import render_template, current_app, request, jsonify
from flask import render_template, current_app, request, jsonify, Response
from flask_babel import gettext as _
from flask_security import current_user
@ -24,7 +24,7 @@ from pgadmin.browser.server_groups.servers.databases.utils import \
parse_sec_labels_from_db, parse_variables_from_db, \
get_attributes_from_db_info
from pgadmin.browser.server_groups.servers.utils import parse_priv_from_db, \
parse_priv_to_db
parse_priv_to_db, get_db_disp_restriction
from pgadmin.browser.utils import PGChildNodeView
from pgadmin.utils.ajax import gone
from pgadmin.utils.ajax import make_json_response, \
@ -266,14 +266,7 @@ class DatabaseView(PGChildNodeView):
def list(self, gid, sid):
last_system_oid = self.retrieve_last_system_oid()
db_disp_res = None
params = None
if self.manager and self.manager.db_res:
db_disp_res = ", ".join(
['%s'] * len(self.manager.db_res.split(','))
)
params = tuple(self.manager.db_res.split(','))
db_disp_res, params = get_db_disp_restriction(self.manager)
SQL = render_template(
"/".join([self.template_path, self._PROPERTIES_SQL]),
conn=self.conn,
@ -351,15 +344,7 @@ class DatabaseView(PGChildNodeView):
self.manager.did in self.manager.db_info:
last_system_oid = self._DATABASE_LAST_SYSTEM_OID
server_node_res = self.manager
db_disp_res = None
params = None
if server_node_res and server_node_res.db_res:
db_disp_res = ", ".join(
['%s'] * len(server_node_res.db_res.split(','))
)
params = tuple(server_node_res.db_res.split(','))
db_disp_res, params = get_db_disp_restriction(self.manager)
SQL = render_template(
"/".join([self.template_path, self._NODES_SQL]),
last_system_oid=last_system_oid,
@ -411,6 +396,8 @@ class DatabaseView(PGChildNodeView):
@check_precondition(action="nodes")
def nodes(self, gid, sid, is_schema_diff=False):
res = self.get_nodes(gid, sid, is_schema_diff)
if isinstance(res, Response):
return res
return make_json_response(
data=res,
@ -1251,13 +1238,7 @@ class DatabaseView(PGChildNodeView):
"""
last_system_oid = self.retrieve_last_system_oid()
db_disp_res = None
params = None
if self.manager and self.manager.db_res:
db_disp_res = ", ".join(
['%s'] * len(self.manager.db_res.split(','))
)
params = tuple(self.manager.db_res.split(','))
db_disp_res, params = get_db_disp_restriction(self.manager)
conn = self.manager.connection()
status, res = conn.execute_dict(render_template(

View File

@ -155,7 +155,8 @@ export default class ServerSchema extends BaseUISchema {
connect_now: true,
password: undefined,
save_password: false,
db_res: [],
db_res: undefined,
db_res_type: 'databases',
passexec: undefined,
passexec_expiration: undefined,
service: undefined,
@ -468,11 +469,41 @@ export default class ServerSchema extends BaseUISchema {
readonly: obj.isConnected,
},
{
id: 'db_res', label: gettext('DB restriction'), type: 'select', group: gettext('Advanced'),
options: [],
id: 'db_res_type', label: gettext('DB restriction type'), type: 'toggle',
mode: ['properties', 'edit', 'create'], group: gettext('Advanced'),
options: [
{'label': gettext('Databases'), value: 'databases'},
{'label': gettext('SQL'), value: 'sql'},
],
readonly: obj.isConnectedOrShared,
depChange: ()=>{
return {
db_res: null,
};
}
},
{
id: 'db_res', label: gettext('DB restriction'), group: gettext('Advanced'),
mode: ['properties', 'edit', 'create'], readonly: obj.isConnectedOrShared,
controlProps: {
multiple: true, allowClear: false, creatable: true, noDropdown: true, placeholder: 'Specify the databases to be restrict...'
deps: ['db_res_type'],
type: (state) => {
if (state.db_res_type == 'databases') {
return {
type: 'select',
options: [],
controlProps: {
multiple: true,
allowClear: false,
creatable: true,
noDropdown: true,
placeholder: 'Specify the databases to be restrict...'
}
};
} else {
return {
type: 'sql',
};
}
},
},
{

View File

@ -240,6 +240,45 @@
"expected_data": {
"status_code": 200
}
},
{
"name": "Add server with post connection sql",
"url": "/browser/server/obj/",
"is_positive_test": true,
"test_data": {
"post_connection_sql": "set timezone to 'Asia/Kolkata'"
},
"mocking_required": false,
"mock_data": {},
"expected_data": {
"status_code": 200
}
},
{
"name": "Add server with DB Restriction (Database names)",
"url": "/browser/server/obj/",
"is_positive_test": true,
"test_data": {
"db_res": ["dev", "qa"]
},
"mocking_required": false,
"mock_data": {},
"expected_data": {
"status_code": 200
}
},
{
"name": "Add server with DB Restriction (SQL)",
"url": "/browser/server/obj/",
"is_positive_test": true,
"test_data": {
"db_res": "SELECT datname FROM pg_database\nWHERE datistemplate = false AND datname ILIKE '%myprefix_%'\nORDER BY datname"
},
"mocking_required": false,
"mock_data": {},
"expected_data": {
"status_code": 200
}
}
],
"is_password_saved": [

View File

@ -91,6 +91,14 @@ class AddServerTest(BaseTestGenerator):
if 'tags' in self.test_data:
self.server['tags'] = self.test_data['tags']
if 'post_connection_sql' in self.test_data:
self.server['post_connection_sql'] = (
self.test_data)['post_connection_sql']
if 'db_res' in self.test_data:
self.server['db_res'] = (
self.test_data)['db_res']
if self.is_positive_test:
if hasattr(self, 'with_save'):
self.server['save_password'] = self.with_save

View File

@ -14,9 +14,10 @@ import keyring
from flask_login import current_user
from werkzeug.exceptions import InternalServerError
from flask import render_template
from pgadmin.utils.constants import KEY_RING_USERNAME_FORMAT, \
KEY_RING_SERVICE_NAME, KEY_RING_TUNNEL_FORMAT, \
KEY_RING_DESKTOP_USER, SSL_MODES
from pgadmin.utils.constants import (
KEY_RING_USERNAME_FORMAT, KEY_RING_SERVICE_NAME, KEY_RING_TUNNEL_FORMAT,
KEY_RING_DESKTOP_USER, SSL_MODES, RESTRICTION_TYPE_DATABASES,
RESTRICTION_TYPE_SQL)
from pgadmin.utils.crypto import encrypt, decrypt
from pgadmin.model import db, Server
from flask import current_app
@ -680,3 +681,35 @@ def delete_adhoc_servers(sid=None):
except Exception:
db.session.rollback()
raise
def get_db_restriction(res_type, restriction):
"""
This function is used to return the database restriction based on
restriction type.
"""
if restriction and res_type == RESTRICTION_TYPE_DATABASES:
return restriction.split(',')
elif restriction and res_type == RESTRICTION_TYPE_SQL:
return restriction
return None
def get_db_disp_restriction(manager_obj):
"""
This function is used to return db disp restriction aand params
to run the query.
"""
db_disp_res = None
params = None
if (manager_obj and manager_obj.db_res and
manager_obj.db_res_type == RESTRICTION_TYPE_DATABASES):
db_disp_res = ", ".join(
['%s'] * len(manager_obj.db_res.split(','))
)
params = tuple(manager_obj.db_res.split(','))
elif (manager_obj and manager_obj.db_res and
manager_obj.db_res_type == RESTRICTION_TYPE_SQL):
db_disp_res = manager_obj.db_res
return db_disp_res, params

View File

@ -23,6 +23,7 @@ import Loader from 'sources/components/Loader';
import { evalFunc } from '../../static/js/utils';
import { usePgAdmin } from '../../static/js/PgAdminProvider';
import { getSwitchCell } from '../../static/js/components/PgReactTableStyled';
import { parseApiError } from '../../static/js/api_instance';
const StyledBox = styled(Box)(({theme}) => ({
height: '100%',
@ -272,9 +273,11 @@ export default function CollectionNodeProperties({
setLoaderText('');
})
.catch((err) => {
setData([]);
setLoaderText('');
pgAdmin.Browser.notifier.alert(
gettext('Failed to retrieve data from the server.'),
gettext(err.message)
parseApiError(err)
);
});
setIsStale(false);

View File

@ -21,6 +21,7 @@ import { toPrettySize } from '../../../../static/js/utils';
import withStandardTabInfo from '../../../../static/js/helpers/withStandardTabInfo';
import { BROWSER_PANELS } from '../../../../browser/static/js/constants';
import { usePgAdmin } from '../../../../static/js/PgAdminProvider';
import { parseApiError } from '../../../../static/js/api_instance';
const Root = styled('div')(({theme}) => ({
height : '100%',
@ -193,7 +194,7 @@ function Statistics({ nodeData, nodeItem, node, treeNodeInfo, isActive, isStale,
} else {
pgAdmin.Browser.notifier.alert(
gettext('Failed to retrieve data from the server.'),
gettext(err.message)
parseApiError(err)
);
setMsg(gettext('Failed to retrieve data from the server.'));
}

View File

@ -33,7 +33,7 @@ import config
#
##########################################################################
SCHEMA_VERSION = 43
SCHEMA_VERSION = 44
##########################################################################
#
@ -178,6 +178,7 @@ class Server(db.Model):
lazy='joined'
)
db_res = db.Column(db.Text(), nullable=True)
db_res_type = db.Column(db.String(32), default='databases')
passexec_cmd = db.Column(db.Text(), nullable=True)
passexec_expiration = db.Column(db.Integer(), nullable=True)
bgcolor = db.Column(db.String(10), nullable=True)

View File

@ -68,7 +68,7 @@ from pgadmin.utils.preferences import Preferences
from pgadmin.tools.sqleditor.utils.apply_explain_plan_wrapper import \
get_explain_query_length
from pgadmin.browser.server_groups.servers.utils import \
convert_connection_parameter
convert_connection_parameter, get_db_disp_restriction
from pgadmin.misc.workspaces import check_and_delete_adhoc_server
MODULE_NAME = 'sqleditor'
@ -2426,15 +2426,8 @@ def get_new_connection_database(sgid, sid=None):
if sid:
template_path = 'databases/sql/#{0}#'.format(manager.version)
last_system_oid = 0
server_node_res = manager
db_disp_res = None
params = None
if server_node_res and server_node_res.db_res:
db_disp_res = ", ".join(
['%s'] * len(server_node_res.db_res.split(','))
)
params = tuple(server_node_res.db_res.split(','))
db_disp_res, params = get_db_disp_restriction(manager)
sql = render_template(
"/".join([template_path, _NODES_SQL]),
last_system_oid=last_system_oid,

View File

@ -160,3 +160,6 @@ DATA_TYPE_WITH_LENGTH = [1560, 'bit', 1561, 'bit[]',
1015, 'varchar[]', 'character varying[]',
'vector', 'vector[]', 'halfvec', 'halfvec[]',
'sparsevec', 'sparsevec[]']
RESTRICTION_TYPE_DATABASES = 'databases'
RESTRICTION_TYPE_SQL = 'sql'

View File

@ -81,6 +81,7 @@ class ServerManager(object):
self.db_info = dict()
self.server_types = None
self.db_res = server.db_res
self.db_res_type = server.db_res_type
self.name = server.name
self.passexec = \
PasswordExec(server.passexec_cmd, server.host, server.port,