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:
# The constraint goes first: it names `slots`, so dropping the column out
# from under it fails on Postgres.
op.drop_constraint("ck_worker_lane_slots_within_cap", "worker_lane", type_="check")
# The constraints go first: they name `slots`, so dropping the column out
# from under them fails on Postgres.
#
# `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(
"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", "enabled")
+17 -27
View File
@@ -246,22 +246,16 @@ def test_an_unrecognised_queue_set_still_gets_a_true_label():
# --- the model's invariant ---------------------------------------------------
@pytest.mark.parametrize(
"slots,cap,ok",
[
(0, 0, True),
(1, 1, True),
(1, 4, True),
(5, 4, False), # slots above its own cap
(-1, 1, False), # negative slots
(1, -1, False), # negative cap
],
)
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."""
def test_the_row_carries_exactly_one_constraint_now():
"""`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
wrong with it is being negative.
Asserted as the WHOLE set rather than as a membership check: a constraint
left behind naming a dropped column is not a harmless leftover, it is a
table the migration cannot have produced, and the model would then
describe a schema no database has.
"""
from backend.app.models import WorkerLane
constraints = {
@@ -269,17 +263,13 @@ def test_worker_lane_check_constraints(slots, cap, ok):
if hasattr(c, "sqltext")
}
# The names carry the convention's `ck_worker_lane_` prefix ALREADY — the
# model declares them bare and Base.metadata's naming_convention applies it.
# Asserting the prefixed form is what pins the thing that actually went
# wrong once: alembic 0088 had to rename four constraints that shipped as
# `ck_x_ck_x_name`, because the migration pre-prefixed a name the
# convention then prefixed again (#3275). A bare-name assertion here would
# pass just as happily against a doubled one.
assert constraints == {
"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",
}
# model declares them bare and Base.metadata's naming_convention applies
# it. Asserting the prefixed form is what pins the thing that actually
# went wrong once: alembic 0088 had to rename four constraints that
# shipped as `ck_x_ck_x_name`, because the migration pre-prefixed a name
# the convention then prefixed again (#3275). A bare-name assertion here
# would pass just as happily against a doubled one.
assert constraints == {"ck_worker_lane_cap_non_negative": "slots_cap >= 0"}
for name in constraints:
assert not name.startswith("ck_worker_lane_ck_"), f"doubled prefix: {name}"