Skip to content

Commit 9660e94

Browse files
committed
Accommodate column-based naming conventions for pk constraint
Repaired / implemented support for primary key constraint naming conventions that use column names/keys/etc as part of the convention. In particular, this includes that the :class:`.PrimaryKeyConstraint` object that's automatically associated with a :class:`.schema.Table` will update its name as new primary key :class:`_schema.Column` objects are added to the table and then to the constraint. Internal failure modes related to this constraint construction process including no columns present, no name present or blank name present are now accommodated. Fixes: sqlalchemy#5919 Change-Id: Ic2800b50f4a4cd5978bec48cefea0a2e198e0123
1 parent e91d918 commit 9660e94

File tree

6 files changed

+133
-12
lines changed

6 files changed

+133
-12
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
.. change::
2+
:tags: bug, schema
3+
:tickets: 5919
4+
5+
Repaired / implemented support for primary key constraint naming
6+
conventions that use column names/keys/etc as part of the convention. In
7+
particular, this includes that the :class:`.PrimaryKeyConstraint` object
8+
that's automatically associated with a :class:`.schema.Table` will update
9+
its name as new primary key :class:`_schema.Column` objects are added to
10+
the table and then to the constraint. Internal failure modes related to
11+
this constraint construction process including no columns present, no name
12+
present or blank name present are now accommodated.

lib/sqlalchemy/event/base.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,13 @@
2929

3030

3131
def _is_event_name(name):
32-
return not name.startswith("_") and name != "dispatch"
32+
# _sa_event prefix is special to support internal-only event names.
33+
# most event names are just plain method names that aren't
34+
# underscored.
35+
36+
return (
37+
not name.startswith("_") and name != "dispatch"
38+
) or name.startswith("_sa_event")
3339

3440

3541
class _UnpickleDispatch(object):

lib/sqlalchemy/sql/events.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,12 @@ def after_parent_attach(self, target, parent):
209209
210210
"""
211211

212+
def _sa_event_column_added_to_pk_constraint(self, const, col):
213+
"""internal event hook used for primary key naming convention
214+
updates.
215+
216+
"""
217+
212218
def column_reflect(self, inspector, table, column_info):
213219
"""Called for each unit of 'column info' retrieved when
214220
a :class:`_schema.Table` is being reflected.

lib/sqlalchemy/sql/naming.py

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,22 @@ def __init__(self, const, table, convention):
3939
def _key_table_name(self):
4040
return self.table.name
4141

42-
def _column_X(self, idx):
42+
def _column_X(self, idx, attrname):
4343
if self._is_fk:
44-
fk = self.const.elements[idx]
45-
return fk.parent
44+
try:
45+
fk = self.const.elements[idx]
46+
except IndexError:
47+
return ""
48+
else:
49+
return getattr(fk.parent, attrname)
4650
else:
47-
return list(self.const.columns)[idx]
51+
cols = list(self.const.columns)
52+
try:
53+
col = cols[idx]
54+
except IndexError:
55+
return ""
56+
else:
57+
return getattr(col, attrname)
4858

4959
def _key_constraint_name(self):
5060
if isinstance(self._const_name, (type(None), _defer_none_name)):
@@ -61,13 +71,13 @@ def _key_column_X_key(self, idx):
6171
# note this method was missing before
6272
# [ticket:3989], meaning tokens like ``%(column_0_key)s`` weren't
6373
# working even though documented.
64-
return self._column_X(idx).key
74+
return self._column_X(idx, "key")
6575

6676
def _key_column_X_name(self, idx):
67-
return self._column_X(idx).name
77+
return self._column_X(idx, "name")
6878

6979
def _key_column_X_label(self, idx):
70-
return self._column_X(idx)._ddl_label
80+
return self._column_X(idx, "_ddl_label")
7181

7282
def _key_referred_table_name(self):
7383
fk = self.const.elements[0]
@@ -161,10 +171,28 @@ def _constraint_name_for_table(const, table):
161171
return None
162172

163173

174+
@event.listens_for(
175+
PrimaryKeyConstraint, "_sa_event_column_added_to_pk_constraint"
176+
)
177+
def _column_added_to_pk_constraint(pk_constraint, col):
178+
if pk_constraint._implicit_generated:
179+
# only operate upon the "implicit" pk constraint for now,
180+
# as we have to force the name to None to reset it. the
181+
# "implicit" constraint will only have a naming convention name
182+
# if at all.
183+
table = pk_constraint.table
184+
pk_constraint.name = None
185+
newname = _constraint_name_for_table(pk_constraint, table)
186+
if newname:
187+
pk_constraint.name = newname
188+
189+
164190
@event.listens_for(Constraint, "after_parent_attach")
165191
@event.listens_for(Index, "after_parent_attach")
166192
def _constraint_name(const, table):
167193
if isinstance(table, Column):
194+
# this path occurs for a CheckConstraint linked to a Column
195+
168196
# for column-attached constraint, set another event
169197
# to link the column attached to the table as this constraint
170198
# associated with the table.
@@ -173,10 +201,11 @@ def _constraint_name(const, table):
173201
"after_parent_attach",
174202
lambda col, table: _constraint_name(const, table),
175203
)
204+
176205
elif isinstance(table, Table):
177206
if isinstance(const.name, (conv, _defer_name)):
178207
return
179208

180209
newname = _constraint_name_for_table(const, table)
181-
if newname is not None:
210+
if newname:
182211
const.name = newname

lib/sqlalchemy/sql/schema.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1777,6 +1777,8 @@ def _set_parent(self, table, allow_replacements=True):
17771777

17781778
table._columns.replace(self)
17791779

1780+
self.table = table
1781+
17801782
if self.primary_key:
17811783
table.primary_key._replace(self)
17821784
elif self.key in table.primary_key:
@@ -1786,8 +1788,6 @@ def _set_parent(self, table, allow_replacements=True):
17861788
% (self.key, table.fullname)
17871789
)
17881790

1789-
self.table = table
1790-
17911791
if self.index:
17921792
if isinstance(self.index, util.string_types):
17931793
raise exc.ArgumentError(
@@ -3838,7 +3838,6 @@ def _reload(self, columns):
38383838
are already present.
38393839
38403840
"""
3841-
38423841
# set the primary key flag on new columns.
38433842
# note any existing PK cols on the table also have their
38443843
# flag still set.
@@ -3854,6 +3853,8 @@ def _replace(self, col):
38543853
PrimaryKeyConstraint._autoincrement_column._reset(self)
38553854
self.columns.replace(col)
38563855

3856+
self.dispatch._sa_event_column_added_to_pk_constraint(self, col)
3857+
38573858
@property
38583859
def columns_autoinc_first(self):
38593860
autoinc = self._autoincrement_column

test/sql/test_metadata.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4841,6 +4841,73 @@ def test_colliding_col_label_from_unique_flag_no_conv(self):
48414841
AddConstraint(const[0]), "ALTER TABLE foo ADD UNIQUE (id)"
48424842
)
48434843

4844+
@testing.combinations(
4845+
("nopk",),
4846+
("column",),
4847+
("constraint",),
4848+
("explicit_name",),
4849+
argnames="pktype",
4850+
)
4851+
@testing.combinations(
4852+
("pk_%(table_name)s", "pk_t1"),
4853+
("pk_%(column_0_name)s", "pk_x"),
4854+
("pk_%(column_0_N_name)s", "pk_x_y"),
4855+
("pk_%(column_0_N_label)s", "pk_t1_x_t1_y"),
4856+
("%(column_0_name)s", "x"),
4857+
("%(column_0N_name)s", "xy"),
4858+
argnames="conv, expected_name",
4859+
)
4860+
def test_pk_conventions(self, conv, expected_name, pktype):
4861+
m1 = MetaData(naming_convention={"pk": conv})
4862+
4863+
if pktype == "column":
4864+
t1 = Table(
4865+
"t1",
4866+
m1,
4867+
Column("x", Integer, primary_key=True),
4868+
Column("y", Integer, primary_key=True),
4869+
)
4870+
elif pktype == "constraint":
4871+
t1 = Table(
4872+
"t1",
4873+
m1,
4874+
Column("x", Integer),
4875+
Column("y", Integer),
4876+
PrimaryKeyConstraint("x", "y"),
4877+
)
4878+
elif pktype == "nopk":
4879+
t1 = Table(
4880+
"t1",
4881+
m1,
4882+
Column("x", Integer, nullable=False),
4883+
Column("y", Integer, nullable=False),
4884+
)
4885+
expected_name = None
4886+
elif pktype == "explicit_name":
4887+
t1 = Table(
4888+
"t1",
4889+
m1,
4890+
Column("x", Integer, primary_key=True),
4891+
Column("y", Integer, primary_key=True),
4892+
PrimaryKeyConstraint("x", "y", name="myname"),
4893+
)
4894+
expected_name = "myname"
4895+
4896+
if expected_name:
4897+
eq_(t1.primary_key.name, expected_name)
4898+
4899+
if pktype == "nopk":
4900+
self.assert_compile(
4901+
schema.CreateTable(t1),
4902+
"CREATE TABLE t1 (x INTEGER NOT NULL, y INTEGER NOT NULL)",
4903+
)
4904+
else:
4905+
self.assert_compile(
4906+
schema.CreateTable(t1),
4907+
"CREATE TABLE t1 (x INTEGER NOT NULL, y INTEGER NOT NULL, "
4908+
"CONSTRAINT %s PRIMARY KEY (x, y))" % expected_name,
4909+
)
4910+
48444911
def test_uq_name(self):
48454912
u1 = self._fixture(
48464913
naming_convention={"uq": "uq_%(table_name)s_%(column_0_name)s"}

0 commit comments

Comments
 (0)