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:
+78
-26
@@ -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) ----
|
||||
|
||||
Reference in New Issue
Block a user