Skip to content

Commit a589f56

Browse files
committed
Disallow deferrable unique constraints
Prior to this change, if Psycopack was used on a table with a deferrable unique constraint, then after the "swap" stage, any updates to the original table would cause the trigger that updates the psycopack table to fail with this error: ON CONFLICT does not support deferrable unique constraints/exclusion constraints as arbiters That error results from the use of "ON CONFLICT DO NOTHING" in the copy function used by the trigger. This change removes test coverage of deferrable unique constraints and adds a check to the pre_validate stage to reject a table that has a deferrable unique constraint.
1 parent ce8aa66 commit a589f56

File tree

4 files changed

+54
-14
lines changed

4 files changed

+54
-14
lines changed

src/psycopack/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from ._repack import (
99
BasePsycopackError,
1010
CompositePrimaryKey,
11+
DeferrableUniqueConstraint,
1112
InheritedTable,
1213
InvalidIndexes,
1314
InvalidPrimaryKeyTypeForConversion,
@@ -32,6 +33,7 @@
3233
"BackfillBatch",
3334
"BasePsycopackError",
3435
"CompositePrimaryKey",
36+
"DeferrableUniqueConstraint",
3537
"FailureDueToLockTimeout",
3638
"InheritedTable",
3739
"InvalidIndexes",

src/psycopack/_commands.py

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -295,23 +295,12 @@ def create_unique_constraint_using_idx(
295295
table: str,
296296
constraint: str,
297297
index: str,
298-
is_deferrable: bool,
299-
is_deferred: bool,
300298
) -> None:
301299
add_constraint_sql = dedent("""
302300
ALTER TABLE {schema}.{table}
303301
ADD CONSTRAINT {constraint}
304-
UNIQUE USING INDEX {index}
302+
UNIQUE USING INDEX {index} NOT DEFERRABLE
305303
""")
306-
if is_deferrable:
307-
add_constraint_sql += " DEFERRABLE"
308-
else:
309-
add_constraint_sql += " NOT DEFERRABLE"
310-
311-
if is_deferred:
312-
add_constraint_sql += " INITIALLY DEFERRED"
313-
else:
314-
add_constraint_sql += " INITIALLY IMMEDIATE"
315304

316305
self.cur.execute(
317306
psycopg.sql.SQL(add_constraint_sql)

src/psycopack/_repack.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ class ReferringForeignKeyInDifferentSchema(BasePsycopackError):
5959
pass
6060

6161

62+
class DeferrableUniqueConstraint(BasePsycopackError):
63+
pass
64+
65+
6266
class NoCreateAndUsagePrivilegeOnSchema(BasePsycopackError):
6367
pass
6468

@@ -333,6 +337,21 @@ def pre_validate(self) -> None:
333337
f"schema: {fks_in_different_schema}."
334338
)
335339

340+
deferrable_unique_constraints = [
341+
c.name
342+
for c in self.introspector.get_constraints(
343+
table=self.table, types=["u"]
344+
)
345+
if c.is_deferrable
346+
]
347+
if deferrable_unique_constraints:
348+
raise DeferrableUniqueConstraint(
349+
f"Psycopack does not currently support tables with "
350+
f"deferrable unique constraints. The table {self.table} "
351+
f"has the following deferrable unique constraints: "
352+
f"{deferrable_unique_constraints}."
353+
)
354+
336355
def setup_repacking(self) -> None:
337356
with (
338357
self.tracker.track(_tracker.Stage.SETUP),
@@ -758,8 +777,6 @@ def _create_unique_constraints(self) -> None:
758777
# From previous steps, the index name is the same as the
759778
# constraint, not a typo!
760779
index=constraint_name,
761-
is_deferrable=cons.is_deferrable,
762-
is_deferred=cons.is_deferred,
763780
)
764781

765782
def _create_check_and_fk_constraints(self) -> None:

tests/test_repack.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from psycopack import (
88
BackfillBatch,
99
CompositePrimaryKey,
10+
DeferrableUniqueConstraint,
1011
FailureDueToLockTimeout,
1112
InheritedTable,
1213
InvalidIndexes,
@@ -1752,6 +1753,37 @@ def test_with_fks_from_another_schema(connection: _psycopg.Connection) -> None:
17521753
repack.full()
17531754

17541755

1756+
def test_with_deferrable_unique_constraint(connection: _psycopg.Connection) -> None:
1757+
with _cur.get_cursor(connection, logged=True) as cur:
1758+
factories.create_table_for_repacking(
1759+
connection=connection,
1760+
cur=cur,
1761+
table_name="to_repack",
1762+
rows=100,
1763+
)
1764+
repack = Psycopack(
1765+
table="to_repack",
1766+
batch_size=1,
1767+
conn=connection,
1768+
cur=cur,
1769+
)
1770+
cur.execute(
1771+
"ALTER TABLE to_repack DROP CONSTRAINT to_repack_var_with_unique_const_key;"
1772+
)
1773+
cur.execute(
1774+
dedent("""
1775+
ALTER TABLE to_repack ADD CONSTRAINT to_repack_var_with_unique_const_key
1776+
UNIQUE (var_with_unique_const)
1777+
DEFERRABLE;
1778+
""")
1779+
)
1780+
1781+
with pytest.raises(
1782+
DeferrableUniqueConstraint, match="to_repack_var_with_unique_const_key"
1783+
):
1784+
repack.full()
1785+
1786+
17551787
def test_without_schema_privileges(connection: _psycopg.Connection) -> None:
17561788
schema = "sweet_schema"
17571789
with _cur.get_cursor(connection, logged=True) as cur:

0 commit comments

Comments
 (0)