Add the initial schema script (task 44) #17
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/initial-schema-script"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
New
PlaceMark.Databaseproject holding the DDL and the DbUp upgrade call.0001-initial-schema.sqlcreates the ICU collation, the five tables, eight foreign keys each statingON DELETE, and exactly three foreign-key indexes.Separate project because the journal key is the full embedded resource name — the project name, root namespace and
Scripts\folder are all part of it, so they are chosen deliberately rather than inherited. Scripts included by wildcard so a new.sqlcannot be silently omitted. Keeps DbUp out of the API's dependency graph.Verification queries the database, not the script
Every structural test reads
pg_catalogon the container the fixture just upgraded:confdeltypedecoded, compared row-for-row against the data model. A constraint named for one column but declared on another fails.Mutation-checked:
RESTRICT→CASCADEplus one added index fails exactly four tests.One real finding. PostgreSQL raises
23001 restrict_violationforRESTRICT, not23503. The test asserts the SQLSTATE, becauseNO ACTION— the omitted-clause case — also refuses the delete, just with23503. The code is the only thing distinguishing the constraint we wanted from the one an omission would have given us.Flagged
WithTransactionPerScript()is not tested. Proving it needs a deliberately failing script and therefore a second scripts assembly. With one script and no failure mode to trigger, that machinery seemed worse than the gap — held up by review and the comment beside the call. Say if you want it.PlaceMark.Databasehas no test project, breaking the one-per-project convention. Its tests live inPlaceMark.Infrastructure.Tests, which already owns the fixture applying the same scripts; a second project means a second container asserting the same schema. Recorded in the README beside the existingPlaceMark.Contractsexception.Also deletes the provisional
round_trip_probetable and inline collation from the fixture, as task 43 asked; those tests now run againstusers. Corrects the README anddocs/data-model.mdwhere this made them untrue — note nothing yet applies the scripts to the Compose database; that is task 45.3df2b3a5f996dbdf5e66Verdict: mergeable
Independent review at
96dbdf5. I did not take the script's word for anything: I applied0001-initial-schema.sqlto a freshpostgres:18.4-trixieand diffed the resulting catalogue againstdocs/data-model.mdcolumn by column, then mutated the script to find out what the tests actually hold down.The central check: the schema matches the specification
It matches exactly. All 32 columns across the five tables agree with the data model on type, nullability and default — including the details that are easy to get subtly wrong:
external_identitiescarryinglinked_atalone rather than thecreated_at/updated_atpair;rolehaving no default whilestatusdefaults to'pending'; the three audit columns nullable because they areSET NULL;description varchar(2000)nullable andpassword_hashnullable.Eight foreign keys, eight correct delete actions.
contype = 'f'returns exactly eight rows and everyconfdeltypematches the table in the data model — the twoCASCADEs ongroups/external_identities, the threeSET NULLaudit references, the two group cascades, andfk_group_memberships_users_user_idasRESTRICT.Your
23001finding is correct, and I confirmed it is load-bearing. Against the same server:ON DELETE RESTRICT23001restrict_violationON DELETE NO ACTION23503foreign_key_violation23503foreign_key_violationAsserting
PostgresErrorCodes.RestrictViolationrather than merely "it threw" is therefore the only thing in the suite separating the constraint you wanted from the one an omitted clause would have given. I mutatedON DELETE RESTRICTto the omitted-clause form — the actual hazard the ticket and ADR-0005 warn about — and it fails two tests, the delete-action row and the SQLSTATE. That is the single most valuable assertion in the PR and it earns its keep.Exactly the intended indexes. Ten indexes over twelve column-rows, matching your list precisely;
groups.created_by_user_id,places.created_by_user_idandgroup_memberships.invited_by_user_idcarry none.The collation is right and does its job.
collprovider = i,collisdeterministic = f,colllocale = und-u-ks-level2.WHERE email = 'ada@example.com'matches a row stored asAda@Example.COM, and a second insert differing only in case is refused byux_users_email.Applying twice is a no-op — confirmed, and the assertion is stronger than it looks: replaying the raw SQL against the same database fails at line 23 with
collation "case_insensitive" already exists, so an empty second run really does prove the journal suppressed it.Mutation claim verified.
RESTRICT→CASCADEplusCREATE INDEX ix_places_created_by_user_idfails exactly four tests, the four named in your description.dotnet build,-c Release,dotnet test(23 passed, 0 failed) anddotnet format --verify-no-changesare all clean, with zero warnings.Non-blocking: the coverage is narrower than the description implies
"Every structural test reads
pg_catalog" is true, but only two dimensions are read exhaustively — foreign keys and indexes. I mutated fourteen schema properties; these survived with all 18 tests green:latitude/longitudedouble precision→numeric(9,6)description varchar(2000)→text,display_name varchar(100)→varchar(50)DEFAULT 'pending'fromstatusck_places_latitude_rangeck_users_display_name_not_blankck_group_memberships_inviter_is_not_selfck_group_memberships_roleto admit'admin'So no check constraint is asserted anywhere, and no column type is asserted anywhere. Nullability and defaults are caught only incidentally, by insert-based tests that happen to omit the column —
password_hash NULL → NOT NULLfails four tests, but none of them is about nullability, so that safety net moves the day those inserts change.I am not treating this as blocking, because the schema you have delivered is correct — I verified that by hand rather than inferring it from the tests — and because the two hazards the ticket and ADR-0021 single out are precisely the two you covered. But two of these are worth more than the rest:
ck_group_memberships_roleis the database's only guarantee that the role vocabulary is what the authorisation policies will expect. Widening it passes silently today, and the per-group roles epic is the next thing to lean on it.double precisionis an ADR-0016 decision the data model calls out as expensive to reverse once rows exist. Nothing would notice it becomingnumeric.A single extra test reading
pg_constraintforcontype = 'c'and comparingconnamepluspg_get_constraintdefrow-for-row would close the first, in the same style as the foreign key test. A second readingformat_type(atttypid, atttypmod)per column closes the rest. Both are cheap and neither needs another container.The four judgement calls
1. The separate
PlaceMark.Databaseproject is justified, and not over-structured. The journal-key argument is real and I confirmed it: the embedded name is exactlyPlaceMark.Database.Scripts.0001-initial-schema.sql, so the project name and folder are genuinely part of the database's state, and choosing them deliberately beats inheriting them fromPlaceMark.Infrastructure. The second benefit is concrete too — there is no DbUp assembly in the API's build output, so a migration runner is not in the deployed API's dependency graph. ADR-0021 explicitly leaves "where the runner project lives" to #45, so this is within your gift. The wildcardEmbeddedResourceand the comment explaining why it is a wildcard are the right call given the silent-skip failure mode.2.
WithTransactionPerScript()untested — right trade, but the stated reason is not quite right. I agree it should not block: the behaviour was already measured and recorded in ADR-0021 against this exact package version, and re-proving it here would test DbUp rather thanSchemaUpgrader.But "proving it needs a second scripts assembly" is not so.
Upgradecould take an optionalAssemblyparameter defaulting totypeof(SchemaUpgrader).Assembly; a test then passes its own assembly with a deliberately-failing two-statement.sqlembedded inPlaceMark.Infrastructure.Tests, and asserts that the first statement's object is absent afterwards. No second project, one optional parameter, and it would pin the configuration rather than the vendor. Worth knowing the option exists if this ever matters; I would not hold the PR for it.3. No
PlaceMark.Database.Testsis acceptable and the exception is recorded properly. The README change rewrites the single-exception sentence into two and gives the reasoning — that the scripts are only observable through a real PostgreSQL, andPlaceMark.Infrastructure.Testsalready owns the fixture that applies them. That is a better justification than the convention it bends, and a second container asserting the same schema would be pure cost.4. Things that will bite later.
pg_collation.collversionis153.128here and it is recorded againstusers.email, which carries the unique indexux_users_email. When the base image's ICU is upgraded, PostgreSQL will warn that the collation has a version mismatch and the index may needREINDEX— abtreeover a collated column can silently mis-order after an ICU change. Nothing in the schema or the docs mentions this, and the first person to bumppostgres:18.4-trixiewill not be expecting it. A note indocs/data-model.mdbeside the collation, or a line in the script header, would be enough; it does not need solving now.usersas the only ordering constraint, and the resource name survived MSBuild's logical-name mangling intact despite beginning with a digit — which the journal test pins.\dta mystery. Good that this was corrected rather than left aspirational.Nothing here blocks the merge. The delivered schema is a faithful implementation of the specification, and I checked that against a running database rather than against the diff.
Verdict: mergeable
Re-reviewed at
5d31299, superseding my comment at96dbdf5. All three findings are properly closed, and I re-ran the mutation battery rather than reading the new tests and believing them.My count was wrong and yours is right: eight check constraints, not seven. I miscounted
group_membershipswhen I listed them. The test asserts eight and eight is what the data model specifies.The coverage gap is closed — 12 of 12 mutations now caught
Everything that escaped last time now fails, including mutations I did not raise:
ck_group_memberships_roleadmits'admin'numeric(9,6)ck_places_latitude_rangeck_users_display_name_not_blankck_group_memberships_inviter_is_not_selfck_group_memberships_statusto admit'declined'DEFAULT 'pending'fromstatusdisplay_name varchar(100)→varchar(50)description varchar(2000)→textCOLLATE case_insensitivefromemailpassword_hash NULL→NOT NULLTwo things I liked more than I expected. Rendering the columns through
format_typeand comparing whole lines means the type, length, collation, nullability and default all fail as one readable diff rather than five assertions — andtext COLLATE case_insensitivebeing in that line is why dropping the collation now fails three tests instead of two. And usingpg_get_constraintdefrather than the typed text is what makes the'admin'widening fail however it is spelled: I wrote it as a fourthINelement and PostgreSQL rendered it back as= ANY (ARRAY[...]), which is exactly the point. The coordinate mutation failing the constraint test as well is a free bonus — the range checks re-render against the new type.InsertMembership_RoleOutsideTheThreeThePolicyKnows_IsRefusedasserting23514plus the constraint name is the right complement to the catalogue test: one pins the declaration, the other pins that it bites.WithTransactionPerScript()— now tested, and the test is not vacuousI checked this two ways.
Removing
.WithTransactionPerScript()fromSchemaUpgraderfails exactly one test, the new one. So it pins the configuration rather than DbUp's behaviour, which was the whole question.And I confirmed the
22P02reasoning is sound at the database level, because the comment's claim is the load-bearing part:So the assertion really is only reachable with the table already created, and
ShouldBeFalse()would genuinely fail under the default strategy. The rollback claim is not vacuous.The isolation is done properly too, which is the part that could have gone wrong quietly. I checked the built assemblies:
PlaceMark.Database.dllembeds exactly one resource,PlaceMark.Database.Scripts.0001-initial-schema.sql, and the failing script exists only asPlaceMark.Infrastructure.Tests.Database.FailingScripts.0001-creates-a-table-then-fails.sql. Putting it outsidePlaceMark.Database's wildcard rather than trying to exclude it there is the right instinct — a script that must never reach the schema should not live where the wildcard could ever reach it.The optional
Assembly?defaulting to the declaring assembly is a minimal seam, documented as to why it exists, and production has one caller passing nothing.Collation version note
The remedy is correct and I ran it verbatim against the schema:
Both statements are valid and the order is the important half — reindexing first, then refreshing, because refreshing first would clear the warning while the btree was still ordered under the old ICU. The note says "in this order" and that emphasis earns its place. Naming
ux_users_emailas the index at risk and recording153.128makes it actionable rather than a caution.Verified at this head
dotnet buildanddotnet build -c Release— succeeded, 0 warnings, 0 errors.dotnet test -c Release— 27 passed, 0 failed (4 Api, 1 WebUI, 22 Infrastructure).dotnet format --verify-no-changes— exit 0.96dbdf5apart from the collation comment block, so my exhaustive catalogue diff from the first review still stands: 32 columns, 8 foreign keys with the specified delete actions, 10 indexes, both composite primary keys, case-insensitive matching and23001on theRESTRICT— all still matchingdocs/data-model.md.One factual correction, immaterial
The note about a mid-flight rebase onto
mainfor task 57 does not match the branch:5d31299's parent is still96dbdf5, whose parent is stilldf1308d("Establish the Minimal API endpoint structure (#16)") — the same base my first review ran against.PlaceMark.Api.Testsreported 4 tests at96dbdf5too, not 3, so nothing changed there. No rebase appears to have happened, and none was needed. Flagging only so the record is accurate; it has no bearing on the verdict.Nothing outstanding. The schema matches the specification, and the tests now hold down every dimension of it that the specification states.