You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(sql): IS NULL / IS NOT NULL + typed Option<Value> INSERT pipeline (SQLR-7) (#95)
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>
Copy file name to clipboardExpand all lines: docs/supported-sql.md
+3-4Lines changed: 3 additions & 4 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -158,7 +158,7 @@ FROM <table>
158
158
### What works
159
159
160
160
-**Projection**: `*` (all columns in declaration order) or a bare column list. Columns not declared on the table are rejected.
161
-
-**`WHERE`**: any [expression](#expressions). Evaluated per row; NULL-as-false in WHERE context (three-valued logic collapsed to two-valued for filtering).
161
+
-**`WHERE`**: any [expression](#expressions). Evaluated per row; NULL-as-false in WHERE context (three-valued logic collapsed to two-valued for filtering). Includes **`IS NULL`** / **`IS NOT NULL`** for explicit null tests.
162
162
-**`ORDER BY`**: single sort key, `ASC` (default) or `DESC`. The sort key can be a bare column reference OR any expression — including function calls — so KNN queries like `ORDER BY vec_distance_l2(embedding, [...]) LIMIT k` work end-to-end *(Phase 7b)*. Sort key types must match; mixing `INTEGER` and `TEXT` across rows under a single `ORDER BY` is a runtime error.
163
163
-**`LIMIT`**: non-negative integer literal. `LIMIT 0` is valid (returns zero rows).
164
164
@@ -172,7 +172,7 @@ The executor includes a tiny optimizer: if the `WHERE` is exactly `<indexed_col>
-**`LIKE`**, **`IN`**, **`IS NULL`** / **`IS NOT NULL`**, `BETWEEN`
175
+
-**`LIKE`**, **`IN`**, `BETWEEN`
176
176
-**Expressions in the projection list** (`SELECT age + 1 FROM users`) — projection is bare column references only
177
177
-**Multi-column `ORDER BY`**, `NULLS FIRST/LAST` (single sort key only; the sort key itself can be an expression as of Phase 7b)
178
178
-**`OFFSET`**
@@ -382,7 +382,7 @@ See [`docs/fts.md`](fts.md) for the canonical FTS reference (tokenizer rules, BM
382
382
383
383
SQLRite follows standard SQL three-valued logic:
384
384
385
-
-**Comparisons involving NULL** (`NULL = 1`, `1 < NULL`) evaluate to unknown, which behaves as `false` inside `WHERE`. Neither the NULL = NULL equality nor the NULL <> NULL inequality is true — use `IS NULL` / `IS NOT NULL` for explicit null tests (both **not yet supported**).
385
+
-**Comparisons involving NULL** (`NULL = 1`, `1 < NULL`) evaluate to unknown, which behaves as `false` inside `WHERE`. Neither the NULL = NULL equality nor the NULL <> NULL inequality is true — use **`IS NULL`** / **`IS NOT NULL`** for explicit null tests (`SELECT … WHERE col IS NULL`). NULLs are not stored in secondary, HNSW, or FTS indexes, so `IS NULL` always falls through to a full scan; that's correct, just not as fast as an indexed equality probe.
386
386
-**Logical operators with NULL**: `NULL AND false` → `false`, `NULL AND true` → `NULL`, `NULL OR true` → `true`, `NOT NULL` → `NULL`. The short-circuit rules prevent NULL from propagating when one operand already decides the result.
387
387
-**Arithmetic with NULL**: any operand NULL → result NULL. `NULL + 1` → `NULL`.
388
388
-**String concat with NULL**: `'foo' || NULL` → `NULL` (same propagation as arithmetic).
@@ -503,7 +503,6 @@ For context when you hit `NotImplemented`. See [Roadmap](roadmap.md) for when th
503
503
### Predicate & expression
504
504
-`LIKE`, `GLOB`, `REGEXP`
505
505
-`IN (...)`, `NOT IN`, `BETWEEN`
506
-
-`IS NULL`, `IS NOT NULL` (pending — use `col = NULL` is NOT a workaround since it's always false; the only current way to select NULL rows is to rely on the NULL-as-false-in-WHERE behavior being absent when the column isn't referenced)
Copy file name to clipboardExpand all lines: docs/usage.md
+2-2Lines changed: 2 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -42,9 +42,9 @@ Quick hits worth knowing when you're working at the REPL:
42
42
-**One statement per call.** The REPL / `Connection::execute` expects a single statement; multi-statement strings return `Expected a single query statement, but there are N`. For batch execution use the SDKs' `executescript` / `execute_batch`.
43
43
-**Transactions are real.**`BEGIN` / `COMMIT` / `ROLLBACK` land as expected; auto-save is suppressed inside a transaction and everything flushes in one WAL commit frame on `COMMIT`. No nested transactions yet.
44
44
-**Arithmetic stays honest.** Integer-only operations stay integer; any `REAL` operand promotes to `f64`; divide-by-zero is a typed runtime error, never a panic.
45
-
-**NULL follows three-valued logic.**`NULL = NULL` is unknown (not true) — treated as false in `WHERE`. Use `IS NULL` / `IS NOT NULL`— oh wait, [those aren't supported yet](supported-sql.md#not-yet-supported). Track via Roadmap.
45
+
-**NULL follows three-valued logic.**`NULL = NULL` is unknown (not true) — treated as false in `WHERE`. Use `IS NULL` / `IS NOT NULL`for explicit null tests, e.g. `SELECT id FROM t WHERE qty IS NULL;`.
46
46
-**Identifiers are case-sensitive** (table / column names; no normalization), but keywords aren't. String literals preserve case.
47
-
-**Not yet supported**: joins, subqueries, `GROUP BY` / aggregates, `DISTINCT`, `LIKE` / `IN` / `IS NULL`, projection expressions, column aliases, `OFFSET`, multi-column `ORDER BY`, savepoints, `ALTER TABLE`, `DROP TABLE`, `DROP INDEX`. See the [full list in the reference](supported-sql.md#not-yet-supported).
47
+
-**Not yet supported**: joins, subqueries, `GROUP BY` / aggregates, `DISTINCT`, `LIKE` / `IN`, projection expressions, column aliases, `OFFSET`, multi-column `ORDER BY`, savepoints, `ALTER TABLE`, `DROP TABLE`, `DROP INDEX`. See the [full list in the reference](supported-sql.md#not-yet-supported).
0 commit comments