Use resolve_fks=False in migration reflect() so stale FKs don't crash startup (#9976)

An old configuration database (e.g. a pre-9.0 pgadmin4.db lingering in
%appdata%) can carry a stale foreign key such as server.user_id ->
user_old, a table removed long ago. SQLAlchemy's MetaData.reflect()
defaults to resolve_fks=True, which auto-follows the reflected table's
foreign keys and reflects their targets too; the orphan target trips
NoSuchTableError and aborts the startup migration, surfacing in the GUI
as the misleading "Server could not be contacted".

None of the migrations use the FK-target tables -- each operates only on
the explicitly requested table(s). Pass resolve_fks=False at all 14
meta.reflect() call sites across the 12 migration files so reflection no
longer cascades into broken FK targets. Behaviour is unchanged for
healthy databases.

Adds a 9.16 release note.
This commit is contained in:
Ashesh Vashi authored and Dave Page committed 2026-06-09 14:32:19 +01:00
1 parent 8afe14ec36
commit 303f6a2dd8
13 files changed
+20 -15

No files matched your search

+1
View File
@@ -37,3 +37,4 @@ Bug fixes
| `Issue #9935 <https://github.com/pgadmin-org/pgadmin4/issues/9935>`_ - Fix "Illegal instruction" crash on startup of the Linux DEB and RPM packages on older x86_64 CPUs by pinning the psycopg C extension build to the x86-64 baseline.
| `Issue #9936 <https://github.com/pgadmin-org/pgadmin4/issues/9936>`_ - Fix the AI panel silently falling back to the default provider when a custom LLM API URL or key file was set, and allow self-hosted LLM endpoints on any loopback port.
| `Issue #9939 <https://github.com/pgadmin-org/pgadmin4/issues/9939>`_ - Fix saving a newly-added row in the Query Tool failing when the result set includes expression or alias columns that are not real columns of the underlying table.
| `Issue #9976 <https://github.com/pgadmin-org/pgadmin4/issues/9976>`_ - Fix a startup migration crash (NoSuchTableError) when an old configuration database contains a stale foreign-key reference.
+2 -2
View File
@@ -125,7 +125,7 @@ def upgrade():
# get metadata from current connection
meta = sa.MetaData()
# define table representation
meta.reflect(op.get_bind(), only=('role',))
meta.reflect(op.get_bind(), only=('role',), resolve_fks=False)
role_table = sa.Table('role', meta)
op.execute(
@@ -168,7 +168,7 @@ def upgrade():
# get metadata from current connection
meta = sa.MetaData()
# define table representation
meta.reflect(op.get_bind(), only=('version',))
meta.reflect(op.get_bind(), only=('version',), resolve_fks=False)
version_table = sa.Table('version', meta)
op.execute(
+1 -1
View File
@@ -36,7 +36,7 @@ def upgrade():
# get metadata from current connection
meta = sa.MetaData()
# define table representation
meta.reflect(op.get_bind(), only=('role',))
meta.reflect(op.get_bind(), only=('role',), resolve_fks=False)
role_table = sa.Table('role', meta)
from pgadmin.tools.user_management.PgAdminPermissions import (
+3 -1
View File
@@ -39,7 +39,9 @@ def upgrade():
# get metadata from current connection
meta = sa.MetaData()
# define table representation
meta.reflect(op.get_bind(), only=('server', 'sharedserver'))
meta.reflect(op.get_bind(),
only=('server', 'sharedserver'),
resolve_fks=False)
table = sa.Table('server', meta)
op.execute(
table.update().values(prepare_threshold=5)
+1 -1
View File
@@ -37,7 +37,7 @@ def upgrade():
# For internal email is a user name, so update the existing records.
meta = sa.MetaData()
# define table representation
meta.reflect(op.get_bind(), only=('user',))
meta.reflect(op.get_bind(), only=('user',), resolve_fks=False)
user_table = sa.Table('user', meta)
op.execute(
+2 -2
View File
@@ -33,7 +33,7 @@ def upgrade():
session.commit()
meta = sa.MetaData()
meta.reflect(op.get_bind(), only=('user_macros',))
meta.reflect(op.get_bind(), only=('user_macros',), resolve_fks=False)
user_macros_table = sa.Table('user_macros', meta)
# Create a select statement
@@ -61,7 +61,7 @@ def upgrade():
sa.PrimaryKeyConstraint('id',))
# Reflect the new table structure
meta.reflect(op.get_bind(), only=('user_macros',))
meta.reflect(op.get_bind(), only=('user_macros',), resolve_fks=False)
new_user_macros_table = sa.Table('user_macros', meta)
# Bulk insert the fetched data into the new user_macros table
@@ -27,7 +27,7 @@ depends_on = None
def upgrade():
# Get metadata from current connection
meta = sa.MetaData()
meta.reflect(op.get_bind(), only=('role',))
meta.reflect(op.get_bind(), only=('role',), resolve_fks=False)
role_table = sa.Table('role', meta)
# Get all roles with permissions
+1 -1
View File
@@ -29,7 +29,7 @@ def upgrade():
# get metadata from current connection
meta = sa.MetaData()
# define table representation
meta.reflect(op.get_bind(), only=('server',))
meta.reflect(op.get_bind(), only=('server',), resolve_fks=False)
server_table = sa.Table('server', meta)
op.execute(
server_table.update().where(server_table.c.connect_timeout == 0 or
+1 -1
View File
@@ -31,7 +31,7 @@ def upgrade():
meta = sa.MetaData()
# define table representation
meta.reflect(op.get_bind(), only=('user',))
meta.reflect(op.get_bind(), only=('user',), resolve_fks=False)
user_table = sa.Table('user', meta)
op.execute(
+1 -1
View File
@@ -27,7 +27,7 @@ depends_on = None
def upgrade():
# Add 'change_password' permission to all roles except 'Administrator'.
meta = sa.MetaData()
meta.reflect(op.get_bind(), only=('role', 'setting'))
meta.reflect(op.get_bind(), only=('role', 'setting'), resolve_fks=False)
role_table = sa.Table('role', meta)
perm = role_table.c.permissions
+1 -1
View File
@@ -32,7 +32,7 @@ def upgrade():
# get metadata from current connection
meta = sa.MetaData()
# define table representation
meta.reflect(op.get_bind(), only=('server',))
meta.reflect(op.get_bind(), only=('server',), resolve_fks=False)
server_table = sa.Table('server', meta)
op.execute(
+4 -2
View File
@@ -66,8 +66,10 @@ def upgrade():
# get metadata from current connection
meta = sa.MetaData()
# define table representation
meta.reflect(op.get_bind(), only=('user_preferences', 'module_preference',
'preference_category', 'preferences'))
meta.reflect(op.get_bind(),
only=('user_preferences', 'module_preference',
'preference_category', 'preferences'),
resolve_fks=False)
module_pref_table = sa.Table('module_preference', meta)
module_id = session.query(ModulePreference).filter_by(
+1 -1
View File
@@ -53,7 +53,7 @@ def migrate_connection_params(table_name):
# define table representation
meta = sa.MetaData()
meta.reflect(op.get_bind(), only=(table_name,))
meta.reflect(op.get_bind(), only=(table_name,), resolve_fks=False)
server_table = sa.Table(table_name, meta)
# Create a select statement