From 42a287bc26269a30fb847471fa0edff4b20906f4 Mon Sep 17 00:00:00 2001 From: Yogesh Mahajan Date: Tue, 5 May 2026 10:44:28 +0530 Subject: [PATCH 1/3] Ensure to use psycopg3 is used while connecting to postgres DB when PGADMIN_CONFIG_CONFIG_DATABASE_URI is specified. --- web/pgadmin/utils/check_external_config_db.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/web/pgadmin/utils/check_external_config_db.py b/web/pgadmin/utils/check_external_config_db.py index 7ac91aa6e08..acd44a4a2d9 100644 --- a/web/pgadmin/utils/check_external_config_db.py +++ b/web/pgadmin/utils/check_external_config_db.py @@ -15,6 +15,9 @@ def check_external_config_db(database_uri): Check if external config database exists if it is being used. """ + if database_uri.startswith("postgresql://") or database_uri.startswith( + "postgres://"): + database_uri = database_uri.replace("://", "+psycopg://", 1) engine = create_engine(database_uri) try: connection = engine.connect() From 1b7ad097ed1fc14792072591e477c062316303a6 Mon Sep 17 00:00:00 2001 From: Yogesh Mahajan Date: Tue, 5 May 2026 20:16:58 +0530 Subject: [PATCH 2/3] Fix the review comments. --- web/pgadmin/__init__.py | 6 +++++- web/pgadmin/utils/check_external_config_db.py | 10 +++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/web/pgadmin/__init__.py b/web/pgadmin/__init__.py index 171f02ed53f..8a3b1357788 100644 --- a/web/pgadmin/__init__.py +++ b/web/pgadmin/__init__.py @@ -337,7 +337,11 @@ def get_locale(): ########################################################################## if config.CONFIG_DATABASE_URI is not None and \ len(config.CONFIG_DATABASE_URI) > 0: - app.config['SQLALCHEMY_DATABASE_URI'] = config.CONFIG_DATABASE_URI + _db_uri = re.sub( + r"^postgres(ql)?://", "postgresql+psycopg://", + config.CONFIG_DATABASE_URI, count=1 + ) + app.config['SQLALCHEMY_DATABASE_URI'] = _db_uri else: app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///{0}?timeout={1}' \ .format(config.SQLITE_PATH.replace('\\', '/'), diff --git a/web/pgadmin/utils/check_external_config_db.py b/web/pgadmin/utils/check_external_config_db.py index acd44a4a2d9..a844daca81c 100644 --- a/web/pgadmin/utils/check_external_config_db.py +++ b/web/pgadmin/utils/check_external_config_db.py @@ -7,6 +7,8 @@ # ########################################################################## +import re + from sqlalchemy import create_engine, inspect @@ -15,9 +17,11 @@ def check_external_config_db(database_uri): Check if external config database exists if it is being used. """ - if database_uri.startswith("postgresql://") or database_uri.startswith( - "postgres://"): - database_uri = database_uri.replace("://", "+psycopg://", 1) + if database_uri.startswith(("postgresql://", "postgres://")): + database_uri = re.sub( + r"^postgres(ql)?://", "postgresql+psycopg://", + database_uri, count=1 + ) engine = create_engine(database_uri) try: connection = engine.connect() From 2b1861ff245c3b98498c85ea474010c64463b4b5 Mon Sep 17 00:00:00 2001 From: Yogesh Mahajan Date: Thu, 21 May 2026 11:55:18 +0530 Subject: [PATCH 3/3] fix the review comments. --- web/pgadmin/__init__.py | 8 ++-- web/pgadmin/evaluate_config.py | 9 ++-- web/pgadmin/utils/db_utils.py | 20 ++++++++ web/pgadmin/utils/tests/test_db_utils.py | 58 ++++++++++++++++++++++++ 4 files changed, 86 insertions(+), 9 deletions(-) create mode 100644 web/pgadmin/utils/db_utils.py create mode 100644 web/pgadmin/utils/tests/test_db_utils.py diff --git a/web/pgadmin/__init__.py b/web/pgadmin/__init__.py index 8a3b1357788..ae8a986016b 100644 --- a/web/pgadmin/__init__.py +++ b/web/pgadmin/__init__.py @@ -41,6 +41,7 @@ from pgadmin.model import db, Role, Server, SharedServer, ServerGroup, \ User, Keys, Version, SCHEMA_VERSION as CURRENT_SCHEMA_VERSION from pgadmin.utils import PgAdminModule, driver, KeyManager, heartbeat +from pgadmin.utils.db_utils import normalize_database_uri from pgadmin.utils.preferences import Preferences from pgadmin.utils.session import create_session_interface, pga_unauthorised from pgadmin.utils.versioned_template_loader import VersionedTemplateLoader @@ -337,11 +338,8 @@ def get_locale(): ########################################################################## if config.CONFIG_DATABASE_URI is not None and \ len(config.CONFIG_DATABASE_URI) > 0: - _db_uri = re.sub( - r"^postgres(ql)?://", "postgresql+psycopg://", - config.CONFIG_DATABASE_URI, count=1 - ) - app.config['SQLALCHEMY_DATABASE_URI'] = _db_uri + app.config['SQLALCHEMY_DATABASE_URI'] = \ + normalize_database_uri(config.CONFIG_DATABASE_URI) else: app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///{0}?timeout={1}' \ .format(config.SQLITE_PATH.replace('\\', '/'), diff --git a/web/pgadmin/evaluate_config.py b/web/pgadmin/evaluate_config.py index 1952e72a707..644e02bca06 100644 --- a/web/pgadmin/evaluate_config.py +++ b/web/pgadmin/evaluate_config.py @@ -12,6 +12,8 @@ import keyring import importlib.util +from pgadmin.utils.db_utils import normalize_database_uri + # User configs loaded from config_local, config_distro etc. custom_config_settings = {} @@ -114,10 +116,9 @@ def evaluate_and_patch_config(config: dict) -> dict: # To use psycopg3 driver, need to specify +psycopg in conn URI if 'CONFIG_DATABASE_URI' in custom_config_settings: - db_uri = custom_config_settings['CONFIG_DATABASE_URI'] - if db_uri.startswith('postgresql:'): - custom_config_settings['CONFIG_DATABASE_URI'] = \ - 'postgresql+psycopg:{0}'.format(db_uri[db_uri.find(':') + 1:]) + custom_config_settings['CONFIG_DATABASE_URI'] = \ + normalize_database_uri( + custom_config_settings['CONFIG_DATABASE_URI']) # Finally update config user configs config.update(custom_config_settings) diff --git a/web/pgadmin/utils/db_utils.py b/web/pgadmin/utils/db_utils.py new file mode 100644 index 00000000000..27aae7cf37c --- /dev/null +++ b/web/pgadmin/utils/db_utils.py @@ -0,0 +1,20 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2026, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +import re + + +def normalize_database_uri(uri): + """ + Rewrite a bare postgres(ql):// URI to use the psycopg3 driver scheme + postgresql+psycopg://. URIs that already carry a driver specifier + (e.g. postgresql+psycopg://) are returned unchanged. + """ + return re.sub(r"^postgres(?:ql)?://", + "postgresql+psycopg://", uri, count=1) diff --git a/web/pgadmin/utils/tests/test_db_utils.py b/web/pgadmin/utils/tests/test_db_utils.py new file mode 100644 index 00000000000..86225b2bcf0 --- /dev/null +++ b/web/pgadmin/utils/tests/test_db_utils.py @@ -0,0 +1,58 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2026, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +"""Unit tests for pgadmin.utils.db_utils.normalize_database_uri.""" + +import unittest + +from pgadmin.utils.db_utils import normalize_database_uri +from pgadmin.utils.route import BaseTestGenerator + + +class TestNormalizeDatabaseUri(BaseTestGenerator): + """normalize_database_uri must rewrite bare postgres(ql):// to + postgresql+psycopg:// and leave everything else unchanged.""" + + # Bypass the server-connection setUp in BaseTestGenerator. + def setUp(self): + unittest.TestCase.setUp(self) + + scenarios = [ + ( + 'postgres:// is rewritten to postgresql+psycopg://', + { + 'uri': 'postgres://u:p@h/db', + 'expected': 'postgresql+psycopg://u:p@h/db', + } + ), + ( + 'postgresql:// is rewritten to postgresql+psycopg://', + { + 'uri': 'postgresql://u:p@h/db', + 'expected': 'postgresql+psycopg://u:p@h/db', + } + ), + ( + 'postgresql+psycopg:// is returned unchanged (idempotent)', + { + 'uri': 'postgresql+psycopg://u:p@h/db', + 'expected': 'postgresql+psycopg://u:p@h/db', + } + ), + ( + 'sqlite:/// is returned unchanged', + { + 'uri': 'sqlite:///path/to/db.sqlite3', + 'expected': 'sqlite:///path/to/db.sqlite3', + } + ), + ] + + def runTest(self): + self.assertEqual(normalize_database_uri(self.uri), self.expected)