diff --git a/web/pgadmin/__init__.py b/web/pgadmin/__init__.py index 171f02ed53f..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,7 +338,8 @@ 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 + 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/check_external_config_db.py b/web/pgadmin/utils/check_external_config_db.py index 7ac91aa6e08..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,6 +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://", "postgres://")): + database_uri = re.sub( + r"^postgres(ql)?://", "postgresql+psycopg://", + database_uri, count=1 + ) engine = create_engine(database_uri) try: connection = engine.connect() 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)