fix: the migration's DROP CONSTRAINT names doubled their own prefix (4295)
CI and images / lint (push) Failing after 3s
CI and images / extension-version (push) Successful in 3s
CI and images / frontend-build (push) Successful in 19s
CI and images / backend-lint-and-test (push) Failing after 30s
CI and images / integration (push) Failing after 2m15s
CI and images / sign-extension (push) Skipped
CI and images / build-web (push) Skipped
CI and images / smoke-web (push) Skipped
CI and images / promote (push) Skipped
CI and images / build-agent (push) Skipped

Run 7365, integration lane:

    (psycopg.errors.UndefinedObject) constraint
    "ck_worker_lane_ck_worker_lane_slots_within_cap" of relation
    "worker_lane" does not exist

That is #3275 exactly, from the other direction. alembic 0088 had to RENAME
four constraints CREATED with a doubled prefix; this one tried to DROP two
with the same doubling. `op.drop_constraint` runs its name through
Base.metadata's naming convention, which prepends `ck_worker_lane_` to a
string that already carries it — `op.f()` is what marks a name as final, and
0103 used it on the way in.

The model test also went red, correctly: `test_worker_lane_check_constraints`
was parametrised over (slots, cap) pairs and asserted all three constraints,
and two of them went with the `slots` column. It is one unparametrised test
now, asserting the whole remaining set rather than a membership — a constraint
left behind naming a dropped column is not a harmless leftover, it is a table
the migration cannot have produced.

Worth recording: **the gate worked.** Run 7365 skipped `sign-extension`,
`build-web`, `smoke-web`, `promote` and `build-agent`, and `:dev` still names
the previous digest. That is the red-direction verification #4339 owed, and it
arrived by accident rather than by a forced failure — which is the better
evidence.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVjrnpQjRgHdvq95rASoiR
This commit is contained in:
2026-09-23 12:51:27 -04:00
co-authored by Claude Opus 5
parent 445164c852
commit 830394ed5e
2 changed files with 31 additions and 31 deletions
+14 -4
View File
@@ -70,11 +70,21 @@ _CAPS = (
def upgrade() -> None: def upgrade() -> None:
# The constraint goes first: it names `slots`, so dropping the column out # The constraints go first: they name `slots`, so dropping the column out
# from under it fails on Postgres. # from under them fails on Postgres.
op.drop_constraint("ck_worker_lane_slots_within_cap", "worker_lane", type_="check") #
# `op.f()` around each name, and it is load-bearing. Without it alembic
# runs the name through Base.metadata's naming convention, which prepends
# `ck_worker_lane_` to a string that already carries it — and the DROP
# goes looking for `ck_worker_lane_ck_worker_lane_slots_within_cap`, which
# no database has. That is #3275 exactly, from the other direction:
# alembic 0088 had to RENAME four constraints created with the same
# doubling. Caught here by the integration lane, run 7365.
op.drop_constraint( op.drop_constraint(
"ck_worker_lane_slots_non_negative", "worker_lane", type_="check", op.f("ck_worker_lane_slots_within_cap"), "worker_lane", type_="check",
)
op.drop_constraint(
op.f("ck_worker_lane_slots_non_negative"), "worker_lane", type_="check",
) )
op.drop_column("worker_lane", "slots") op.drop_column("worker_lane", "slots")
op.drop_column("worker_lane", "enabled") op.drop_column("worker_lane", "enabled")
+17 -27
View File
@@ -246,22 +246,16 @@ def test_an_unrecognised_queue_set_still_gets_a_true_label():
# --- the model's invariant --------------------------------------------------- # --- the model's invariant ---------------------------------------------------
@pytest.mark.parametrize( def test_the_row_carries_exactly_one_constraint_now():
"slots,cap,ok", """`slots >= 0` and `slots <= slots_cap` went with the `slots` column on
[ 2026-09-23 — there is one number left, and the only thing that can be
(0, 0, True), wrong with it is being negative.
(1, 1, True),
(1, 4, True), Asserted as the WHOLE set rather than as a membership check: a constraint
(5, 4, False), # slots above its own cap left behind naming a dropped column is not a harmless leftover, it is a
(-1, 1, False), # negative slots table the migration cannot have produced, and the model would then
(1, -1, False), # negative cap describe a schema no database has.
], """
)
def test_worker_lane_check_constraints(slots, cap, ok):
"""The constraints live in the database, not only in the service, because
a row violating `slots <= slots_cap` is not a rejected request — it is a
lane the reconcile (step 3) will drive UP to a number the operator
capped."""
from backend.app.models import WorkerLane from backend.app.models import WorkerLane
constraints = { constraints = {
@@ -269,17 +263,13 @@ def test_worker_lane_check_constraints(slots, cap, ok):
if hasattr(c, "sqltext") if hasattr(c, "sqltext")
} }
# The names carry the convention's `ck_worker_lane_` prefix ALREADY — the # The names carry the convention's `ck_worker_lane_` prefix ALREADY — the
# model declares them bare and Base.metadata's naming_convention applies it. # model declares them bare and Base.metadata's naming_convention applies
# Asserting the prefixed form is what pins the thing that actually went # it. Asserting the prefixed form is what pins the thing that actually
# wrong once: alembic 0088 had to rename four constraints that shipped as # went wrong once: alembic 0088 had to rename four constraints that
# `ck_x_ck_x_name`, because the migration pre-prefixed a name the # shipped as `ck_x_ck_x_name`, because the migration pre-prefixed a name
# convention then prefixed again (#3275). A bare-name assertion here would # the convention then prefixed again (#3275). A bare-name assertion here
# pass just as happily against a doubled one. # would pass just as happily against a doubled one.
assert constraints == { assert constraints == {"ck_worker_lane_cap_non_negative": "slots_cap >= 0"}
"ck_worker_lane_slots_non_negative": "slots >= 0",
"ck_worker_lane_cap_non_negative": "slots_cap >= 0",
"ck_worker_lane_slots_within_cap": "slots <= slots_cap",
}
for name in constraints: for name in constraints:
assert not name.startswith("ck_worker_lane_ck_"), f"doubled prefix: {name}" assert not name.startswith("ck_worker_lane_ck_"), f"doubled prefix: {name}"