test(datastore): check that migrating produces the schema SpiceDB expects - #3336
Closed
vroldanbet wants to merge 4 commits into
Closed
vroldanbet wants to merge 4 commits into
vroldanbet wants to merge 4 commits into
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
vroldanbet
force-pushed
the
test/schema-drift
branch
3 times, most recently
from
September 21, 2026 18:24
a2866db to
6efc7d0
Compare
vroldanbet
force-pushed
the
test/schema-drift
branch
from
September 21, 2026 20:07
6efc7d0 to
89f0a11
Compare
vroldanbet
force-pushed
the
perf/parallel-consistency-fixtures
branch
from
September 21, 2026 20:11
cc5df29 to
c8820fe
Compare
vroldanbet
force-pushed
the
test/schema-drift
branch
from
September 21, 2026 20:30
89f0a11 to
051a84d
Compare
vroldanbet
force-pushed
the
perf/parallel-consistency-fixtures
branch
3 times, most recently
from
September 22, 2026 05:49
c6edd0d to
97d73a3
Compare
vroldanbet
force-pushed
the
test/schema-drift
branch
3 times, most recently
from
September 22, 2026 13:07
2fcac38 to
1cbf41e
Compare
MigrationTest checks that every migration applies cleanly and that data written before one survives it. Nothing checks the other half: that the schema a migration leaves behind is the one the running code expects. A gap there does not fail anything. It gets papered over at startup - the datastore issues the DDL it wants every time it is constructed, and every other test in the suite then runs against a correct schema and passes. The CockroachDB TTL changefeed parameter is exactly that: missing from the migration, added by ALTER TABLE on every datastore construction. SchemaDriftTest states the invariant instead of the expectation: migrate an empty database to head, snapshot the schema, construct the datastore the way production does, snapshot again, and require the two to match. No golden file, nothing to maintain as migrations are added, and it holds for every engine version, because both snapshots come from the same server. Each engine registers how to capture its own schema. CockroachDB and MySQL report CREATE statements, Spanner reports its DDL, and PostgreSQL, which has no such statement, is read out of the system catalogs. MySQL's output carries the auto-increment counter, which moves when a row is written rather than when the schema changes, so it is stripped. Claude-Session: https://claude.ai/code/session_017DF2mdm5e2RGbjPWtmYetd
The expiration migration puts a row-level TTL policy on both relationship tables but leaves out ttl_disable_changefeed_replication, which keeps the TTL job's deletes out of the changefeed behind the Watch API. Datastore startup notices it is missing and issues two ALTER TABLEs to add it - on every single connection to every database, forever, because the migration never learns. Setting it as part of the policy means a freshly migrated database already has it and the startup check finds nothing to do. The check stays, permanently. A database migrated before this change has the migration recorded as applied and will never run it again, so the check is the only thing that ever sets the parameter there. It also leaves alone an operator who has deliberately configured the parameter. The parameter arrived in CockroachDB v24.1, so the gate matches the one the startup check already uses. Older clusters get the policy without it and are fixed up at startup exactly as before. Claude-Session: https://claude.ai/code/session_017DF2mdm5e2RGbjPWtmYetd
…registry The engine's suite has already built a datastore in this process, and the datastores register their collectors globally, so the second one failed with "duplicate metrics collector registration attempted". This datastore only exists to let the engine apply whatever schema changes it makes on construction, so it has no use for metrics.
vroldanbet
force-pushed
the
test/schema-drift
branch
from
September 22, 2026 14:12
1cbf41e to
1750043
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds
TestSchemaDriftto the shared datastore test suite, so every engine gets it: migrate an empty database to head, record the schema, build the datastore exactly as the server does, record the schema again, and require the two to be identical.It found one difference, on CockroachDB, which this PR also fixes: the expiration migration now sets
ttl_disable_changefeed_replicationon the two relationship tables, instead of leaving it to be set by anALTER TABLEon every datastore construction.Why
We check that migrations run. We never check what they produce.
TestMigrationapplies each migration in order, confirms the recorded version advances, and confirms data written before a migration survives it. It never compares the resulting schema against anything. So a migration that is simply incomplete passes.When that happens, the usual repair is to fix the schema up at startup: the datastore issues the missing statement itself, every time it is constructed. Nothing then fails, because by the time any other test looks, the schema is correct. That is exactly how the CockroachDB TTL parameter ended up where it is - a human noticed it much later by reasoning about Watch, not because a test said so.
TestSchemaDriftturns that from something you have to notice into something that fails. It asserts a rule rather than a snapshot - migrations own the schema, startup does not change it - so there is no golden file, nothing to update as migrations are added, and nothing that breaks when a CI image moves to a new engine version.How
SchemaDriftTestlives inpkg/datastore/testunder the existingMigrationcategory, alongsideMigrationTest. memdb is the only engine excluded, and only because it has no migrations at all.Each engine registers how to read its own schema, via
test.RegisterSchemaSnapshotter. Both snapshots come from the same database on the same server seconds apart, so the text needs no normalising across engine versions:SHOW CREATE ALL TABLES, which carries the table storage parameters where the drift actually was.SHOW CREATE TABLEper table. The auto-increment counter is stripped, because it moves when a row is written rather than when the schema changes.SHOW CREATE, so columns, indexes, constraints and storage parameters are read out of the system catalogs.Run against real containers, the only difference found anywhere was on CockroachDB, on both tested versions:
The second commit closes it: the expiration migration sets the parameter as part of the TTL policy, on clusters at v24.1 or later, which is the same gate the startup check already uses. Older clusters get the policy without it, as before.
The startup check stays, permanently. A database migrated before this change has that migration recorded as applied and will never run it again, so the check is the only thing that ever sets the parameter there. It also leaves alone an operator who has deliberately configured it. The comment in
ttl_changefeed.gonow says so.References
Verified against containers for every engine: CockroachDB 25.2.0 and 26.2.5, PostgreSQL 14 and 18, MySQL 8, the Spanner emulator, and memdb (skipped, no migrations). Shown failing on CockroachDB before the migration change and passing after;
TestTTLChangefeedSuppressionParamandTestTTLChangefeedSuppressionWatchstill pass.Considered and skipped: a committed per-engine schema snapshot. CI covers CockroachDB 25.2 and 26.2, PostgreSQL 14 through 18, MySQL and Spanner, and the catalogs differ between those versions in ways that have nothing to do with SpiceDB - so a golden file would need hand-editing on every image bump, and would go stale rather than catch anything. The invariant test needs no maintenance and finds the same class of bug.