feat(sql): IS NULL / IS NOT NULL + typed Option<Value> INSERT pipeline (SQLR-7)#95
Merged
Conversation
…e (SQLR-7)
Two related fixes from SQLR-2 fallout:
1) `WHERE col IS NULL` / `IS NOT NULL` now work — two new arms in
`eval_expr`. NULLs aren't stored in secondary / HNSW / FTS indexes,
so these predicates correctly fall through to a full scan via the
existing `select_rowids` logic.
2) INSERT no longer carries values as `Vec<Vec<String>>` with the
string sentinel `"Null"` standing in for SQL NULL. `InsertQuery.rows`
is now `Vec<Vec<Option<Value>>>`; the parser emits `None` for NULL
and typed `Value::*` for everything else. `Table::insert_row` and
`Table::validate_unique_constraint` dispatch on `Option<Value>`,
leaving the per-column BTreeMap entry absent for NULL (reads come
back as `Value::Null` via the existing missing-rowid path).
Fixes:
- `INSERT INTO t (n) VALUES (NULL)` for INTEGER / REAL / BOOLEAN /
VECTOR columns no longer errors via `"Null".parse::<T>()`
- `INSERT INTO t (s) VALUES (NULL)` for TEXT no longer stores the
literal string `"Null"`
- `Table::restore_row` accepts NULL for non-text column types so
persisted NULLs reopen cleanly (without this, a saved DB
containing any NULL outside TEXT failed to reload)
- SQL UNIQUE allows multiple NULLs (standard SQL three-valued
logic)
- `DEFAULT NULL` collapses to "no default" — explicit NULL in
INSERT is preserved over a column DEFAULT, matching SQLite
Tests: 8 new INSERT-NULL tests across all column types; 5 IS NULL /
IS NOT NULL executor tests (non-indexed, indexed, omitted-column,
combined-with-AND); restored the SQLR-2 `default_does_not_override_
explicit_null` test that had to be dropped because of this bug; new
file-backed `null_values_round_trip_through_disk`. The previous
`process_command_insert_missing_integer_returns_error_test` codified
the bug as a graceful error and is replaced with a positive
"omitted INTEGER stores NULL" assertion.
Docs: removed "not yet supported" notes for IS NULL / IS NOT NULL
from README, supported-sql.md, usage.md, roadmap.md.
Out of scope (deferred):
- Removing the legacy `Row::Text "Null"` sentinel-strip on read
(kept as back-compat for pre-fix saved DBs; revisit at next
file-format bump)
- Negative numeric literals in INSERT VALUES (pre-existing gap)
- IS NULL index optimization (full scan is fine for now)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.
Summary
WHERE col IS NULL/IS NOT NULLto the executor (two neweval_exprarms; full-scan fallback since NULLs aren't indexed).Vec<Vec<String>>(with"Null"as a sentinel) to typedVec<Vec<Option<Value>>>from the parser throughTable::insert_rowandvalidate_unique_constraint.Table::restore_rowto accept NULL for INTEGER / REAL / BOOLEAN / VECTOR columns so persisted NULLs reopen cleanly — without this, a saved DB containing any NULL outside TEXT failed to reload.Bugs fixed
INSERT INTO t (n) VALUES (NULL)for INTEGER / REAL / BOOLEAN / VECTOR columns errored via"Null".parse::<T>()failing.INSERT INTO t (s) VALUES (NULL)for TEXT silently stored the literal string"Null".DEFAULT NULLcollapses to "no default" — explicit NULL in INSERT is preserved over the column DEFAULT (matches SQLite).default_does_not_override_explicit_nulltest that had to be dropped because of this bug.Test plan
cargo test --workspace --exclude sqlrite-desktop --exclude sqlrite-python --exclude sqlrite-nodejs— 397 engine tests pass (was 396)cargo fmt --all -- --checkcleancargo clippy --workspace --exclude sqlrite-desktop --exclude sqlrite-python --exclude sqlrite-nodejs --all-targetsclean (no new warnings beyond the pre-existing set)IS NULL/IS NOT NULL, close, reopen — values round-tripnull_values_round_trip_through_diskexercises every column type's NULL through save/reloadOut of scope (deferred)
Row::Text "Null"sentinel-strip on read (kept as back-compat for pre-fix saved DBs; revisit at next file-format bump).Expr::UnaryOp(Minus, …)gap).🤖 Generated with Claude Code