• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

joaoh82 / rust_sqlite / 25404925331

05 May 2026 10:06PM UTC coverage: 63.607% (+0.4%) from 63.201%
25404925331

push

github

web-flow
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.... (continued)

185 of 268 new or added lines in 4 files covered. (69.03%)

1 existing line in 1 file now uncovered.

7795 of 12255 relevant lines covered (63.61%)

1.21 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

66.67
/src/sql/parser/insert.rs
1
use sqlparser::ast::{Expr, Insert, SetExpr, Statement, Value as AstValue, Values};
2

3
use crate::error::{Result, SQLRiteError};
4
use crate::sql::db::table::{Value, parse_vector_literal};
5

6
/// Parsed INSERT statement: target table, declared column list, and one
7
/// or more rows of typed values.
8
///
9
/// `rows` is `Vec<Vec<Option<Value>>>` rather than `Vec<Vec<String>>` so
10
/// SQL `NULL` can be represented faithfully as `None` instead of leaking
11
/// out as the string sentinel `"Null"` (which used to break INTEGER /
12
/// REAL / BOOL inserts and silently round-trip as the literal text
13
/// `"Null"` in TEXT columns).
14
#[derive(Debug)]
15
pub struct InsertQuery {
16
    pub table_name: String,
17
    pub columns: Vec<String>,
18
    pub rows: Vec<Vec<Option<Value>>>,
19
}
20

21
impl InsertQuery {
22
    pub fn new(statement: &Statement) -> Result<InsertQuery> {
2✔
23
        let tname: Option<String>;
2✔
24
        let mut columns: Vec<String> = vec![];
2✔
25
        let mut all_values: Vec<Vec<Option<Value>>> = vec![];
2✔
26

27
        match statement {
2✔
28
            Statement::Insert(Insert {
×
29
                table,
2✔
30
                columns: cols,
2✔
31
                source,
2✔
32
                ..
×
33
            }) => {
×
34
                tname = Some(table.to_string());
4✔
35
                for col in cols {
2✔
36
                    columns.push(col.to_string());
4✔
37
                }
38

39
                let source = source.as_ref().ok_or_else(|| {
2✔
40
                    SQLRiteError::Internal(
×
41
                        "INSERT statement is missing a source expression".to_string(),
×
42
                    )
43
                })?;
44

45
                if let SetExpr::Values(Values { rows, .. }) = source.body.as_ref() {
2✔
46
                    for row in rows {
6✔
47
                        let mut value_set: Vec<Option<Value>> = vec![];
2✔
48
                        for e in row {
4✔
49
                            match e {
2✔
50
                                Expr::Value(v) => match &v.value {
2✔
51
                                    AstValue::Number(n, _) => {
1✔
52
                                        // Try integer first; if the literal has
53
                                        // a decimal point or exponent, i64 fails
54
                                        // and we fall through to f64.
55
                                        if let Ok(i) = n.parse::<i64>() {
3✔
56
                                            value_set.push(Some(Value::Integer(i)));
1✔
57
                                        } else if let Ok(f) = n.parse::<f64>() {
3✔
58
                                            value_set.push(Some(Value::Real(f)));
1✔
59
                                        } else {
NEW
60
                                            return Err(SQLRiteError::General(format!(
×
NEW
61
                                                "Could not parse numeric literal '{n}'"
×
62
                                            )));
63
                                        }
64
                                    }
65
                                    AstValue::Boolean(b) => {
1✔
66
                                        value_set.push(Some(Value::Bool(*b)));
2✔
67
                                    }
68
                                    AstValue::SingleQuotedString(sqs) => {
2✔
69
                                        value_set.push(Some(Value::Text(sqs.to_string())));
4✔
70
                                    }
NEW
71
                                    AstValue::Null => {
×
72
                                        value_set.push(None);
2✔
73
                                    }
74
                                    _ => {}
×
75
                                },
76
                                Expr::Identifier(i) => {
1✔
77
                                    // Phase 7a — sqlparser parses bracket-array
78
                                    // literals like `[0.1, 0.2, 0.3]` as
79
                                    // bracket-quoted identifiers (it inherits
80
                                    // MSSQL-style `[name]` quoting). Detect
81
                                    // that by `quote_style == Some('[')` and
82
                                    // parse it eagerly into a typed
83
                                    // `Value::Vector` so the rest of the
84
                                    // pipeline sees a real vector. Dimension
85
                                    // checking against the column declaration
86
                                    // happens at insert_row time.
87
                                    if i.quote_style == Some('[') {
2✔
88
                                        let raw = format!("[{}]", i.value);
2✔
89
                                        let parsed = parse_vector_literal(&raw).map_err(|e| {
2✔
NEW
90
                                            SQLRiteError::General(format!(
×
NEW
91
                                                "Could not parse vector literal '{raw}': {e}"
×
92
                                            ))
93
                                        })?;
94
                                        value_set.push(Some(Value::Vector(parsed)));
1✔
95
                                    } else {
NEW
96
                                        value_set.push(Some(Value::Text(i.to_string())));
×
97
                                    }
98
                                }
99
                                _ => {}
×
100
                            }
101
                        }
102
                        all_values.push(value_set);
2✔
103
                    }
104
                }
105
            }
106
            _ => {
×
107
                return Err(SQLRiteError::Internal(
×
108
                    "Error parsing insert query".to_string(),
×
109
                ));
110
            }
111
        }
112

113
        match tname {
2✔
114
            Some(t) => Ok(InsertQuery {
4✔
115
                table_name: t,
×
116
                columns,
2✔
117
                rows: all_values,
2✔
118
            }),
119
            None => Err(SQLRiteError::Internal(
×
120
                "Error parsing insert query".to_string(),
×
121
            )),
122
        }
123
    }
124
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc