Check field order of DB models against DB column order#10800
Check field order of DB models against DB column order#10800david-crespo wants to merge 2 commits into
Conversation
c42b718 to
a71f6ca
Compare
a71f6ca to
8db321e
Compare
hawkw
left a comment
There was a problem hiding this comment.
I wonder if it is worth trying to factor out just the fix for the incorrect models in alert_rx and dns, so that we can merge the fix separately while the test stuff is still under review?
| }; | ||
| } | ||
|
|
||
| full_row_models! { |
There was a problem hiding this comment.
i love that we are automatically generating tests for this. i wonder if there's also a way to detect models that don't have these tests, so that we can also get a warning when one has forgotten to write such tests. perhaps there is a way to use the list of tables that nexus-db-schema generates for test builds for this?
There was a problem hiding this comment.
Heh yes, I just added a second paragraph to the intro above — the trait is designed to at least ensure we've checked any struct that gets used with check_if_exists, but we would have to get elaborate to really check all relevant structs.
🤖's idea on how to do this
The designed-but-unimplemented fix is a meta-test in nexus-db-model (deliberately deferred; it's elaborate and the marginal value is guarding against future forgetfulness, not finding present bugs):
-
Add
synas a dev-dependency. -
#[test]walksnexus/db-model/src/**/*.rsviaCARGO_MANIFEST_DIR, parses each file with syn, and collects every struct with#[diesel(table_name = ...)]that derivesQueryable/Selectable— exactly the logic already validated in the scratchpad script (it found 241 structs: 239 Selectable + 2 Insertable-only). -
Assert every collected struct appears in exactly one of two macros:
full_row_models!— table-keyed (1:1 with tables once projections are out), exact column-order test +FullRowModelimpl.projections!— the "exempt list with teeth": no trait impl, but a weaker generated test asserting the model's columns are an in-order subsequence of the table's columns. Projections feed parents via#[diesel(embed)], so their relative order still matters. This makes exemption a verified claim rather than a comment.
Simplest comparison wiring: have the macros also emit a static array of
(struct_name, table_name)string pairs for the meta-test to diff against, avoiding a second parse ofcolumn_order.rs. -
Misclassification is bounded: a full-row model wrongly listed as a projection passes the subsequence test only until the table grows a column it lacks, and
check_if_existsrejects it regardless (no trait impl).
Resulting chain when implemented: meta-test forces every model into a list → lists generate order tests → order tests pin model ↔ schema.rs → crdb_alignment_test pins schema.rs ↔ dbinit-produced database. Residual trust: none of the classification is external — a wrong "projection" label is caught late (see 4) but not never.
There was a problem hiding this comment.
the trait is designed to at least ensure we've checked any struct that gets used with
check_if_exists,
Oh, I see, making that a requirement of check_if_exists seems good enough honestly.
| } | ||
|
|
||
| #[tokio::test] | ||
| async fn test_queryable_silently_swaps_same_typed_columns() { |
There was a problem hiding this comment.
do we really need regression tests for this since (IIUC) the new column_order tests should also catch regressions here? i presume this test was written to demonstrate the original bug that motivated this...
There was a problem hiding this comment.
Yeah, I don't think these are to keep, this was to prove the problem is real.
|
For sure, I think some or most of the order mismatches can just be fixed directly in the meantime. |
🤖 audit of whether we are actually hitting the bug (no)No current production path triggers the mismatch. Two structs could silently corrupt values under implicit table-order decoding:
Their current reads use Custom collection-insert paths for
🤖 full auditGoalAudit the 22 full-row Diesel models with field order differing from MethodFor each model:
FindingsNo current production query decodes any of the 22 models from an implicit Two models are capable of a successful but incorrect full-row positional
The other 20 mismatches eventually put incompatible SQL representations in a
Decode-site audit
ConclusionThe mismatches are latent hazards, not evidence of current data corruption. |
While reviewing #10785 I noticed the order of the fields on the
AlertReceiverDB model struct do not match the column order inschema.rs. This is only a problem if you try to use it withcheck_if_existsor some other DB thing which wants those to match, which runs into a deserialization error.The big test in this PR that turns up the structs with mismatches is fine and not a big deal to keep around — the problem is that there's no way to guarantee you test all the relevant structs. The trait constraint on
check_if_existsis designed to at least guarantee (or, let's say, make it hard to mess up) that you have a test for the column order on any struct that gets passed tocheck_if_exists.failing models
🤖 Summary
Diesel’s
Queryabledecoding is positional. A mismatch matters whenever a query returns columns in table order but decodes them into a model in field order. That includescheck_if_exists, ordinary.load/.get_resultcalls withoutModel::as_select(), and insert/update.get_resultcalls withoutModel::as_returning(). If the mismatched columns have different types, this usually produces a deserialization error. If they have compatible types, it can silently put values into the wrong fields (demonstrated intest_queryable_silently_swaps_same_typed_columnsandtest_default_selection_silently_swaps_same_typed_model_fields).We already test that the column order in
schema.rsmatches the dbinit-produced CockroachDB schema. This draft checks the other side of that invariant by comparing the SQL generated forModel::as_select()with the table’sall_columns().Of the 239 existing selectable models, 204 match exactly. The 35 failures break down into:
The projection models need to be classified separately rather than “fixed.” For the 22 actual order mismatches, reordering the struct fields could itself break custom queries whose select lists were written to match the current field order, so each model’s positional decode sites need to be audited before changing it.
The
FullRowModeltrait in this draft is an experiment in preventing the test list from becoming stale forcheck_if_exists: the helper requires a registered model associated with the same table, and registration also generates its order test. It does not provide exhaustive future coverage for the other implicit-selection and default-RETURNINGpaths. Doing that would require a lint or meta-test that discovers model declarations and verifies that each one is classified as either a full-row model or a projection.Example of how this can go wrong
Correct production model:
Model with two same-typed fields accidentally reversed:
The query does not specify a column order:
Diesel implicitly selects in table order:
Given:
the decoded model contains:
Both columns are
TEXT/String, so the query type-checks and deserializes successfully.