WHERE expressions: wire into update/delete/show data + SQL gen (ADR-0026 steps 3-4)

Wires the stratified WHERE-expression fragment into the three
filter commands and compiles the resulting Expr to SQL.

Grammar (data.rs): the `update` / `delete` `where` clause is
now the expression fragment (`Subgrammar(&expr::OR_EXPR)`) in
place of the single `col = val` slot; `show data` gains an
optional `where <expr>` and an optional `limit <n>` (a
non-negative integer, validated at parse time). The
expression's right-hand operands are a schema-aware
`DynamicSubgrammar` so the hint panel still narrows to the
left column's type (ADR-0026 §8) — but the inner grammar is
permissive: a type-mismatched literal still parses (§7).

AST: `RowFilter::Where{column,value}` -> `RowFilter::Where(Expr)`;
`ShowData` gains `filter: Option<Expr>` and `limit: Option<u64>`.
A `RowFilter::eq` convenience constructor keeps simple-equality
call sites and tests readable.

SQL (db.rs): `compile_expr` lowers an `Expr` to a
parameterised WHERE — every literal a `?` placeholder,
identifiers `quote_ident`-quoted, `<>` for inequality. A
literal compared against a column binds through that column's
type where compatible and falls back to its syntactic shape on
a mismatch (§7 — permissive). `show data ... limit n` emits
`LIMIT ?` with an implicit primary-key `ORDER BY`, so it is a
stable "first n by primary key".

completion.rs: `invalid_ident_at_cursor` no longer mis-flags a
digit-led literal (`1`) as an unknown column now that the
WHERE operand slot also accepts a column reference; a
`ProseOnly` slot suppresses keyword candidates even when the
expected set also carries a column ident.

11 db integration tests cover AND / OR / NOT, BETWEEN, IN,
LIKE, filtered `show data`, and limit ordering; walker and
expr unit tests cover the parse surface. Type-mismatch /
`= NULL` diagnostic flagging (§7 highlight + hint) is the
remaining ADR-0026 piece.
This commit is contained in:
claude@clouddev1
2026-05-18 23:12:33 +00:00
parent 59e6a541bf
commit f75f71bbe4
22 changed files with 1013 additions and 193 deletions
+78 -26
View File
@@ -229,6 +229,15 @@ pub struct CompletionProbe {
/// earlier in the walk). `None` when the walker is
/// schemaless or the table didn't resolve.
pub current_table_columns: Option<Vec<crate::completion::TableColumn>>,
/// The grammar-declared `HintMode` at the cursor's slot
/// (`Node::Hinted`), if any. A `ProseOnly` slot tells the
/// completion engine to suppress its keyword candidates —
/// the node-attached signal that supersedes the
/// expected-set signature heuristic where the grammar
/// explicitly marks a slot prose-only (e.g. the
/// WHERE-expression operand, which also accepts a column
/// reference — ADR-0026 §8).
pub pending_hint_mode: Option<crate::dsl::grammar::HintMode>,
}
/// Run a schema-aware walk and report the completion-engine's
@@ -247,6 +256,7 @@ pub fn completion_probe(
.map(|c| outcome::Expectation::Word(c.entry.primary))
.collect(),
current_table_columns: None,
pending_hint_mode: None,
};
}
let mut ctx = context::WalkContext::with_schema(schema);
@@ -258,6 +268,7 @@ pub fn completion_probe(
.map(|c| outcome::Expectation::Word(c.entry.primary))
.collect(),
current_table_columns: None,
pending_hint_mode: None,
};
};
let expected = match result.outcome {
@@ -276,6 +287,7 @@ pub fn completion_probe(
CompletionProbe {
expected,
current_table_columns: ctx.current_table_columns,
pending_hint_mode: ctx.pending_hint_mode,
}
}
@@ -1155,7 +1167,9 @@ mod tests {
assert_eq!(
parse("show data Customers").unwrap(),
Command::ShowData {
name: "Customers".to_string()
name: "Customers".to_string(),
filter: None,
limit: None,
}
);
}
@@ -1170,6 +1184,58 @@ mod tests {
);
}
#[test]
fn walker_parses_show_data_with_where_and_limit() {
// ADR-0026 §5: `show data` gains an optional `where`
// and an optional `limit <n>`.
match parse("show data Customers where id=1 limit 10").unwrap() {
Command::ShowData {
name,
filter: Some(_),
limit: Some(10),
} => assert_eq!(name, "Customers"),
other => panic!("expected ShowData with filter + limit, got {other:?}"),
}
}
#[test]
fn walker_parses_show_data_with_limit_only() {
assert!(matches!(
parse("show data Customers limit 5").unwrap(),
Command::ShowData {
filter: None,
limit: Some(5),
..
}
));
}
#[test]
fn walker_parses_update_with_complex_where() {
// The WHERE is a full boolean expression, not a single
// equality (ADR-0026).
match parse("update T set Active=true where Age>30 and Name like 'A%'")
.unwrap()
{
Command::Update {
filter: RowFilter::Where(crate::dsl::Expr::And(terms)),
..
} => assert_eq!(terms.len(), 2, "two AND-ed predicates"),
other => panic!("expected Update with And-expression filter, got {other:?}"),
}
}
#[test]
fn walker_parses_delete_with_or_where() {
assert!(matches!(
parse("delete from T where id=1 or id=2").unwrap(),
Command::Delete {
filter: RowFilter::Where(crate::dsl::Expr::Or(_)),
..
}
));
}
#[test]
fn walker_parses_insert_with_explicit_column_list() {
assert_eq!(
@@ -1233,10 +1299,7 @@ mod tests {
Command::Update {
table: "Customers".to_string(),
assignments: vec![("Email".to_string(), Value::Text("new@b.c".to_string()))],
filter: RowFilter::Where {
column: "id".to_string(),
value: Value::Number("1".to_string()),
},
filter: RowFilter::eq("id", Value::Number("1".to_string())),
}
);
}
@@ -1251,10 +1314,7 @@ mod tests {
("Email".to_string(), Value::Text("a@b.c".to_string())),
("Name".to_string(), Value::Text("Alice".to_string())),
],
filter: RowFilter::Where {
column: "id".to_string(),
value: Value::Number("1".to_string()),
},
filter: RowFilter::eq("id", Value::Number("1".to_string())),
}
);
}
@@ -1277,10 +1337,7 @@ mod tests {
parse("delete from Customers where id=42").unwrap(),
Command::Delete {
table: "Customers".to_string(),
filter: RowFilter::Where {
column: "id".to_string(),
value: Value::Number("42".to_string()),
},
filter: RowFilter::eq("id", Value::Number("42".to_string())),
}
);
}
@@ -1708,20 +1765,15 @@ mod tests {
}
#[test]
fn phase_d_delete_where_rejects_decimal_at_int_column() {
// `where id=3.14` — id is Int; the typed slot rejects.
fn phase_d_delete_where_permits_decimal_at_int_column() {
// ADR-0026 §7: a type-mismatched WHERE comparison is
// flagged in the editor but never blocks. `id` is Int
// and `3.14` is not — yet the command still parses and
// would run (this relaxes the pre-ADR-0026 rejection).
let schema = schema_with("T", &[("id", Type::Int)]);
let err = parse_command_with_schema("delete from T where id=3.14", &schema)
.expect_err("should reject");
match err {
crate::dsl::ParseError::Invalid { message, .. } => {
assert!(
message.contains("integer") || message.contains("3.14"),
"got: {message}"
);
}
other => panic!("expected Invalid, got {other:?}"),
}
let cmd = parse_command_with_schema("delete from T where id=3.14", &schema)
.expect("type-mismatched WHERE comparisons are permissive");
assert!(matches!(cmd, crate::dsl::Command::Delete { .. }), "got {cmd:?}");
}
// ---- Typed-slot HintMode (Phase D + HintMode dispatch) ----