fix(sql): bind positional INSERT/UPSERT values to declared column names#204
Merged
farhan-syah merged 2 commits intoJul 22, 2026
Merged
Conversation
A `VALUES` clause with no explicit column list left `columns` empty, so
`convert_value_rows` fell back to synthetic `col0`, `col1`, ... names for
every value. The row stored fine and `SELECT *` returned it correctly,
but named projections and WHERE predicates could never address it again:
CREATE COLLECTION probe (id INT PRIMARY KEY, note TEXT);
INSERT INTO probe VALUES (42, 'stored?');
SELECT * FROM probe; -- 42|stored?
SELECT id FROM probe; -- empty
SELECT count(*) FROM probe WHERE id = 42; -- 0
Resolve the effective column list from the collection's declared column
order when the statement omits one. Named inserts and schemaless
collections (no declared order to bind to) are unchanged. A row carrying
more values than the collection declares is now rejected rather than
bound to an invented `colN`, which would reproduce the same
unaddressable-column failure.
The KV path is deliberately untouched: it matches columns by name
against pk_col/"key"/"ttl", which is orthogonal to declared order.
Fixes NodeDB-Lab#202
KV splits key/value by matching column names against pk_col/"key"/"ttl", so a positional VALUES row with no column list has no principled key to bind to and would silently produce an empty-keyed, empty-valued row (all such rows colliding). Reject with a dedicated error instead.
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 join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Fixes #202.
The defect
convert_value_rowsnames each value by position against the caller's column list, falling back to a synthetic name when the list runs out:For a positional
INSERT INTO t VALUES (…)that list is empty, so every value is stored undercol0,col1, … instead of the declared column names. The row is written correctly andSELECT *returns it (projection-star doesn't care about names), but nothing that addresses a column by name can ever reach it again.The fix
resolve_insert_columns()(new, indml_helpers.rs) resolves the effective column list fromCollectionInfo.columnswhen the statement omits one. It is applied at the threeVALUES-clause planning sites indml.rs—plan_insert,plan_upsert, andplan_upsert_with_on_conflict— immediately after the collection metadata is already in scope.Judgment calls
These are the places where I had to choose without knowing your intent. Each is one line to change if you'd rather go the other way:
SqlError::InsertColumnArityMismatchrather than binding the surplus to an inventedcolN. Inventing one would recreate exactly the unaddressable-column failure this PR closes. This is the only behaviour change for input that previously "succeeded".INSERT INTO t (id) VALUES (1)on a wider table binds onlyid), so positional and named forms stay consistent.info.columnsis empty there is no declared order to bind against, so the existingcol{i}fallback remains for that case. Not treated as an error.build_kv_insert_planmatches columns by name againstpk_col/"key"/"ttl", which is orthogonal to declared column order — binding positionally there looked wrong rather than merely unnecessary, so I left it and noted it in a comment. Happy to extend if you disagree.col{i}fallback itself is unchanged. After this fix it should only be reachable for the schemaless case. Removing it entirely is a wider blast radius than this issue warrants.INSERT … SELECTunaffected — it returns beforecolumnsis consumed. Verified, not changed.Tests
New:
nodedb-sql/tests/positional_insert_column_binding.rs, following the local-Catalog-fixture pattern inpoint_get_operand_order.rs/schema_qualified_rejection.rs.positional_insert_binds_declared_column_namespositional_upsert_binds_declared_column_namespositional_insert_arity_overflow_is_rejectedcol2named_insert_still_binds_by_namepositional_insert_on_schemaless_collection_keeps_col_i_fallbackpositional_insert_on_kv_collection_is_unaffectedVerified failing before the fix, not just passing after — with the binding logic neutered (error variant left in place so it still compiles), the three fix-asserting tests fail behaviourally and the three non-regression tests pass unchanged:
With the fix:
6 passed. Fullcargo test -p nodedb-sqlgreen;cargo clippy -p nodedb-sql --all-targets -- -D warningsclean.Notes
I'm new to this codebase, so please push back on anything that doesn't match how you'd want it done — particularly the arity-overflow decision, which is the one change in observable behaviour for previously-accepted input.