Skip to content

Commit 668cd0f

Browse files
committed
Added ability to use SQL in the 'DB Restriction' field. #2767
Use parseApiError in Statistics.jsx Fixed the documentation. Updated release note.
1 parent 8031c35 commit 668cd0f

File tree

17 files changed

+239
-60
lines changed

17 files changed

+239
-60
lines changed

docs/en_US/images/server_advanced.png

22.6 KB
Loading

docs/en_US/release_notes.rst

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ notes for it.
1212
:maxdepth: 1
1313

1414

15+
release_notes_9_3
1516
release_notes_9_2
1617
release_notes_9_1
1718
release_notes_9_0

docs/en_US/release_notes_9_3.rst

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
***********
2+
Version 9.3
3+
***********
4+
5+
Release date: 2025-04-30
6+
7+
This release contains a number of bug fixes and new features since the release of pgAdmin 4 v9.2.
8+
9+
Supported Database Servers
10+
**************************
11+
**PostgreSQL**: 13, 14, 15, 16 and 17
12+
13+
**EDB Advanced Server**: 13, 14, 15, 16 and 17
14+
15+
Bundled PostgreSQL Utilities
16+
****************************
17+
**psql**, **pg_dump**, **pg_dumpall**, **pg_restore**: 17.2
18+
19+
20+
New features
21+
************
22+
23+
| `Issue #2767 <https://github.com/pgadmin-org/pgadmin4/issues/2767>`_ - Added ability to use SQL in the "DB Restriction" field.
24+
| `Issue #8629 <https://github.com/pgadmin-org/pgadmin4/issues/8629>`_ - Added support for font ligatures.
25+
26+
Housekeeping
27+
************
28+
29+
30+
Bug fixes
31+
*********
32+
33+
| `Issue #8443 <https://github.com/pgadmin-org/pgadmin4/issues/8443>`_ - Fixed an issue where the debugger hangs when stepping into nested function/procedure.

docs/en_US/server_dialog.rst

+13-5
Original file line numberDiff line numberDiff line change
@@ -195,11 +195,19 @@ Click the *Advanced* tab to continue.
195195

196196
Use the fields in the *Advanced* tab to configure a connection:
197197

198-
* Use the *DB restriction* field to provide a SQL restriction that will be used
199-
against the pg_database table to limit the databases that you see. For
200-
example, you might enter: *live_db test_db* so that only live_db and test_db
201-
are shown in the pgAdmin browser. Separate entries with a comma or tab as you
202-
type.
198+
* Specify the type of the database restriction that will be used to filter
199+
out the databases for restriction in the *DB restriction type* field:
200+
201+
* Select the *Databases* option to specify the name of the databases
202+
that will be used against the pg_database table to limit the databases
203+
that you see. This is the default.
204+
* Select the *SQL* option to provide a SQL restriction that will be used
205+
against the pg_database table to limit the databases that you see.
206+
207+
* Use the *DB restriction* field to provide a SQL restriction OR Database names
208+
that will be used against the pg_database table to limit the databases that you see.
209+
For example, you might enter: *live_db test_db* so that only live_db and test_db
210+
are shown in the pgAdmin object explorer.
203211
* Use the *Password exec command* field to specify a shell command to be executed
204212
to retrieve a password to be used for SQL authentication. The ``stdout`` of the
205213
command will be used as the SQL password. This may be useful when the password
+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
##########################################################################
2+
#
3+
# pgAdmin 4 - PostgreSQL Tools
4+
#
5+
# Copyright (C) 2013 - 2025, The pgAdmin Development Team
6+
# This software is released under the PostgreSQL Licence
7+
#
8+
##########################################################################
9+
10+
"""
11+
12+
Revision ID: 1f0eddc8fc79
13+
Revises: e982c040d9b5
14+
Create Date: 2025-03-26 15:58:24.131719
15+
16+
"""
17+
from alembic import op
18+
import sqlalchemy as sa
19+
from pgadmin.utils.constants import RESTRICTION_TYPE_DATABASES
20+
21+
# revision identifiers, used by Alembic.
22+
revision = '1f0eddc8fc79'
23+
down_revision = 'e982c040d9b5'
24+
branch_labels = None
25+
depends_on = None
26+
27+
28+
def upgrade():
29+
op.add_column('server',
30+
sa.Column('db_res_type', sa.String(length=32),
31+
server_default=RESTRICTION_TYPE_DATABASES))
32+
33+
34+
def downgrade():
35+
# pgAdmin only upgrades, downgrade not implemented.
36+
pass

web/pgadmin/browser/server_groups/servers/__init__.py

+15-8
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,9 @@
3434
from pgadmin.tools.schema_diff.node_registry import SchemaDiffRegistry
3535
from pgadmin.browser.server_groups.servers.utils import \
3636
(is_valid_ipaddress, get_replication_type, convert_connection_parameter,
37-
check_ssl_fields)
37+
check_ssl_fields, get_db_restriction)
3838
from pgadmin.utils.constants import UNAUTH_REQ, MIMETYPE_APP_JS, \
39-
SERVER_CONNECTION_CLOSED
39+
SERVER_CONNECTION_CLOSED, RESTRICTION_TYPE_SQL
4040
from sqlalchemy import or_
4141
from sqlalchemy.orm.attributes import flag_modified
4242
from pgadmin.utils.preferences import Preferences
@@ -746,6 +746,7 @@ def update(self, gid, sid):
746746
'comment': 'comment',
747747
'role': 'role',
748748
'db_res': 'db_res',
749+
'db_res_type': 'db_res_type',
749750
'passexec_cmd': 'passexec_cmd',
750751
'passexec_expiration': 'passexec_expiration',
751752
'bgcolor': 'bgcolor',
@@ -776,12 +777,11 @@ def update(self, gid, sid):
776777
'role': gettext('Role')
777778
}
778779

779-
idx = 0
780780
data = request.form if request.form else json.loads(
781781
request.data
782782
)
783783

784-
if 'db_res' in data:
784+
if 'db_res' in data and isinstance(data['db_res'], list):
785785
data['db_res'] = ','.join(data['db_res'])
786786

787787
# Update connection parameter if any.
@@ -948,7 +948,7 @@ def list(self, gid):
948948
'connected': connected,
949949
'version': manager.ver,
950950
'server_type': manager.server_type if connected else 'pg',
951-
'db_res': server.db_res.split(',') if server.db_res else None
951+
'db_res': get_db_restriction(server.db_res_type, server.db_res)
952952
})
953953

954954
return ajax_response(
@@ -1031,7 +1031,8 @@ def properties(self, gid, sid):
10311031
'server_type': manager.server_type if connected else 'pg',
10321032
'bgcolor': server.bgcolor,
10331033
'fgcolor': server.fgcolor,
1034-
'db_res': server.db_res.split(',') if server.db_res else None,
1034+
'db_res': get_db_restriction(server.db_res_type, server.db_res),
1035+
'db_res_type': server.db_res_type,
10351036
'passexec_cmd':
10361037
server.passexec_cmd if server.passexec_cmd else None,
10371038
'passexec_expiration':
@@ -1137,6 +1138,12 @@ def create(self, gid):
11371138
data['connection_params'] = connection_params
11381139

11391140
server = None
1141+
db_restriction = None
1142+
if 'db_res' in data and isinstance(data['db_res'], list):
1143+
db_restriction = ','.join(data['db_res'])
1144+
elif 'db_res' in data and 'db_res_type' in data and \
1145+
data['db_res_type'] == RESTRICTION_TYPE_SQL:
1146+
db_restriction = data['db_res']
11401147

11411148
try:
11421149
server = Server(
@@ -1151,8 +1158,8 @@ def create(self, gid):
11511158
config.ALLOW_SAVE_PASSWORD else 0,
11521159
comment=data.get('comment', None),
11531160
role=data.get('role', None),
1154-
db_res=','.join(data['db_res']) if 'db_res' in data and
1155-
isinstance(data['db_res'], list) else None,
1161+
db_res=db_restriction,
1162+
db_res_type=data.get('db_res_type', None),
11561163
bgcolor=data.get('bgcolor', None),
11571164
fgcolor=data.get('fgcolor', None),
11581165
service=data.get('service', None),

web/pgadmin/browser/server_groups/servers/databases/__init__.py

+7-26
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from functools import wraps
1414

1515
import json
16-
from flask import render_template, current_app, request, jsonify
16+
from flask import render_template, current_app, request, jsonify, Response
1717
from flask_babel import gettext as _
1818
from flask_security import current_user
1919

@@ -24,7 +24,7 @@
2424
parse_sec_labels_from_db, parse_variables_from_db, \
2525
get_attributes_from_db_info
2626
from pgadmin.browser.server_groups.servers.utils import parse_priv_from_db, \
27-
parse_priv_to_db
27+
parse_priv_to_db, get_db_disp_restriction
2828
from pgadmin.browser.utils import PGChildNodeView
2929
from pgadmin.utils.ajax import gone
3030
from pgadmin.utils.ajax import make_json_response, \
@@ -266,14 +266,7 @@ def wrapped(self, *args, **kwargs):
266266
def list(self, gid, sid):
267267
last_system_oid = self.retrieve_last_system_oid()
268268

269-
db_disp_res = None
270-
params = None
271-
if self.manager and self.manager.db_res:
272-
db_disp_res = ", ".join(
273-
['%s'] * len(self.manager.db_res.split(','))
274-
)
275-
params = tuple(self.manager.db_res.split(','))
276-
269+
db_disp_res, params = get_db_disp_restriction(self.manager)
277270
SQL = render_template(
278271
"/".join([self.template_path, self._PROPERTIES_SQL]),
279272
conn=self.conn,
@@ -351,15 +344,7 @@ def get_nodes(self, gid, sid, is_schema_diff=False):
351344
self.manager.did in self.manager.db_info:
352345
last_system_oid = self._DATABASE_LAST_SYSTEM_OID
353346

354-
server_node_res = self.manager
355-
356-
db_disp_res = None
357-
params = None
358-
if server_node_res and server_node_res.db_res:
359-
db_disp_res = ", ".join(
360-
['%s'] * len(server_node_res.db_res.split(','))
361-
)
362-
params = tuple(server_node_res.db_res.split(','))
347+
db_disp_res, params = get_db_disp_restriction(self.manager)
363348
SQL = render_template(
364349
"/".join([self.template_path, self._NODES_SQL]),
365350
last_system_oid=last_system_oid,
@@ -411,6 +396,8 @@ def get_nodes(self, gid, sid, is_schema_diff=False):
411396
@check_precondition(action="nodes")
412397
def nodes(self, gid, sid, is_schema_diff=False):
413398
res = self.get_nodes(gid, sid, is_schema_diff)
399+
if isinstance(res, Response):
400+
return res
414401

415402
return make_json_response(
416403
data=res,
@@ -1251,13 +1238,7 @@ def statistics(self, gid, sid, did=None):
12511238
"""
12521239
last_system_oid = self.retrieve_last_system_oid()
12531240

1254-
db_disp_res = None
1255-
params = None
1256-
if self.manager and self.manager.db_res:
1257-
db_disp_res = ", ".join(
1258-
['%s'] * len(self.manager.db_res.split(','))
1259-
)
1260-
params = tuple(self.manager.db_res.split(','))
1241+
db_disp_res, params = get_db_disp_restriction(self.manager)
12611242

12621243
conn = self.manager.connection()
12631244
status, res = conn.execute_dict(render_template(

web/pgadmin/browser/server_groups/servers/static/js/server.ui.js

+36-5
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,8 @@ export default class ServerSchema extends BaseUISchema {
155155
connect_now: true,
156156
password: undefined,
157157
save_password: false,
158-
db_res: [],
158+
db_res: undefined,
159+
db_res_type: 'databases',
159160
passexec: undefined,
160161
passexec_expiration: undefined,
161162
service: undefined,
@@ -468,11 +469,41 @@ export default class ServerSchema extends BaseUISchema {
468469
readonly: obj.isConnected,
469470
},
470471
{
471-
id: 'db_res', label: gettext('DB restriction'), type: 'select', group: gettext('Advanced'),
472-
options: [],
472+
id: 'db_res_type', label: gettext('DB restriction type'), type: 'toggle',
473+
mode: ['properties', 'edit', 'create'], group: gettext('Advanced'),
474+
options: [
475+
{'label': gettext('Databases'), value: 'databases'},
476+
{'label': gettext('SQL'), value: 'sql'},
477+
],
478+
readonly: obj.isConnectedOrShared,
479+
depChange: ()=>{
480+
return {
481+
db_res: null,
482+
};
483+
}
484+
},
485+
{
486+
id: 'db_res', label: gettext('DB restriction'), group: gettext('Advanced'),
473487
mode: ['properties', 'edit', 'create'], readonly: obj.isConnectedOrShared,
474-
controlProps: {
475-
multiple: true, allowClear: false, creatable: true, noDropdown: true, placeholder: 'Specify the databases to be restrict...'
488+
deps: ['db_res_type'],
489+
type: (state) => {
490+
if (state.db_res_type == 'databases') {
491+
return {
492+
type: 'select',
493+
options: [],
494+
controlProps: {
495+
multiple: true,
496+
allowClear: false,
497+
creatable: true,
498+
noDropdown: true,
499+
placeholder: 'Specify the databases to be restrict...'
500+
}
501+
};
502+
} else {
503+
return {
504+
type: 'sql',
505+
};
506+
}
476507
},
477508
},
478509
{

web/pgadmin/browser/server_groups/servers/tests/servers_test_data.json

+39
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,45 @@
240240
"expected_data": {
241241
"status_code": 200
242242
}
243+
},
244+
{
245+
"name": "Add server with post connection sql",
246+
"url": "/browser/server/obj/",
247+
"is_positive_test": true,
248+
"test_data": {
249+
"post_connection_sql": "set timezone to 'Asia/Kolkata'"
250+
},
251+
"mocking_required": false,
252+
"mock_data": {},
253+
"expected_data": {
254+
"status_code": 200
255+
}
256+
},
257+
{
258+
"name": "Add server with DB Restriction (Database names)",
259+
"url": "/browser/server/obj/",
260+
"is_positive_test": true,
261+
"test_data": {
262+
"db_res": ["dev", "qa"]
263+
},
264+
"mocking_required": false,
265+
"mock_data": {},
266+
"expected_data": {
267+
"status_code": 200
268+
}
269+
},
270+
{
271+
"name": "Add server with DB Restriction (SQL)",
272+
"url": "/browser/server/obj/",
273+
"is_positive_test": true,
274+
"test_data": {
275+
"db_res": "SELECT datname FROM pg_database\nWHERE datistemplate = false AND datname ILIKE '%myprefix_%'\nORDER BY datname"
276+
},
277+
"mocking_required": false,
278+
"mock_data": {},
279+
"expected_data": {
280+
"status_code": 200
281+
}
243282
}
244283
],
245284
"is_password_saved": [

web/pgadmin/browser/server_groups/servers/tests/test_add_server.py

+8
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,14 @@ def runTest(self):
9191
if 'tags' in self.test_data:
9292
self.server['tags'] = self.test_data['tags']
9393

94+
if 'post_connection_sql' in self.test_data:
95+
self.server['post_connection_sql'] = (
96+
self.test_data)['post_connection_sql']
97+
98+
if 'db_res' in self.test_data:
99+
self.server['db_res'] = (
100+
self.test_data)['db_res']
101+
94102
if self.is_positive_test:
95103
if hasattr(self, 'with_save'):
96104
self.server['save_password'] = self.with_save

0 commit comments

Comments
 (0)