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:
@@ -181,19 +181,13 @@ fn enrich_unique_update_resolves_value_from_assignments() {
|
||||
let cmd = Command::Update {
|
||||
table: "Customers".to_string(),
|
||||
assignments: vec![("id".to_string(), Value::Number("1".to_string()))],
|
||||
filter: RowFilter::Where {
|
||||
column: "name".to_string(),
|
||||
value: Value::Text("Bob".to_string()),
|
||||
},
|
||||
filter: RowFilter::eq("name", Value::Text("Bob".to_string())),
|
||||
};
|
||||
let err = db
|
||||
.update(
|
||||
"Customers".to_string(),
|
||||
vec![("id".to_string(), Value::Number("1".to_string()))],
|
||||
RowFilter::Where {
|
||||
column: "name".to_string(),
|
||||
value: Value::Text("Bob".to_string()),
|
||||
},
|
||||
RowFilter::eq("name", Value::Text("Bob".to_string())),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
@@ -464,18 +458,12 @@ fn enrich_fk_delete_resolves_child_table() {
|
||||
// Delete the parent that has children — engine refuses.
|
||||
let cmd = Command::Delete {
|
||||
table: "Customers".to_string(),
|
||||
filter: RowFilter::Where {
|
||||
column: "id".to_string(),
|
||||
value: Value::Number("1".to_string()),
|
||||
},
|
||||
filter: RowFilter::eq("id", Value::Number("1".to_string())),
|
||||
};
|
||||
let err = db
|
||||
.delete(
|
||||
"Customers".to_string(),
|
||||
RowFilter::Where {
|
||||
column: "id".to_string(),
|
||||
value: Value::Number("1".to_string()),
|
||||
},
|
||||
RowFilter::eq("id", Value::Number("1".to_string())),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -229,10 +229,7 @@ fn delete_with_cascade_rewrites_both_csvs() {
|
||||
let result = db
|
||||
.delete(
|
||||
"Customers".to_string(),
|
||||
RowFilter::Where {
|
||||
column: "id".to_string(),
|
||||
value: Value::Number("1".to_string()),
|
||||
},
|
||||
RowFilter::eq("id", Value::Number("1".to_string())),
|
||||
Some("delete from Customers where id=1".to_string()),
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -143,7 +143,7 @@ fn rebuild_restores_rows_from_csv() {
|
||||
});
|
||||
|
||||
let rows = rt()
|
||||
.block_on(async { db.query_data("Customers".to_string(), None).await })
|
||||
.block_on(async { db.query_data("Customers".to_string(), None, None, None).await })
|
||||
.expect("query_data");
|
||||
assert_eq!(rows.rows.len(), 2);
|
||||
let names: Vec<Option<String>> = rows.rows.iter().map(|r| r[1].clone()).collect();
|
||||
|
||||
@@ -173,7 +173,7 @@ fn rebuild_against_populated_db_wipes_and_reloads() {
|
||||
.expect("rebuild");
|
||||
});
|
||||
let rows = rt()
|
||||
.block_on(async { db.query_data("Customers".to_string(), None).await })
|
||||
.block_on(async { db.query_data("Customers".to_string(), None, None, None).await })
|
||||
.unwrap();
|
||||
assert_eq!(rows.rows.len(), 1);
|
||||
assert_eq!(rows.rows[0][1].as_deref(), Some("Edna"));
|
||||
|
||||
@@ -357,7 +357,7 @@ fn end_to_end_export_then_import_real_project() {
|
||||
|
||||
// Round-trip: the inserted row is back.
|
||||
let data_view = rt()
|
||||
.block_on(async { imported_db.query_data("Customers".to_string(), None).await })
|
||||
.block_on(async { imported_db.query_data("Customers".to_string(), None, None, None).await })
|
||||
.expect("query data");
|
||||
assert_eq!(data_view.rows.len(), 1);
|
||||
// Serial id auto-filled to 1; Name was the inserted value.
|
||||
|
||||
@@ -107,7 +107,7 @@ fn replay_three_lines_dispatches_three_commands() {
|
||||
|
||||
// The dispatched commands actually mutated state.
|
||||
let data_result = rt()
|
||||
.block_on(async { db.query_data("T".to_string(), None).await })
|
||||
.block_on(async { db.query_data("T".to_string(), None, None, None).await })
|
||||
.expect("query_data");
|
||||
assert_eq!(data_result.rows.len(), 1, "row inserted");
|
||||
assert_eq!(data_result.rows[0][1].as_deref(), Some("Alice"));
|
||||
@@ -219,7 +219,7 @@ fn replay_aborts_on_first_parse_failure_and_reports_line() {
|
||||
"earlier add column should have stayed applied"
|
||||
);
|
||||
let data_result = rt()
|
||||
.block_on(async { db.query_data("T".to_string(), None).await })
|
||||
.block_on(async { db.query_data("T".to_string(), None, None, None).await })
|
||||
.expect("query_data");
|
||||
assert!(
|
||||
data_result.rows.is_empty(),
|
||||
@@ -268,7 +268,7 @@ fn replay_rejects_typed_slot_violation_at_parse_time() {
|
||||
// The earlier two lines stayed applied; the failing insert
|
||||
// did not run.
|
||||
let data_result = rt()
|
||||
.block_on(async { db.query_data("T".to_string(), None).await })
|
||||
.block_on(async { db.query_data("T".to_string(), None, None, None).await })
|
||||
.expect("query_data");
|
||||
assert!(
|
||||
data_result.rows.is_empty(),
|
||||
|
||||
+20
@@ -24,6 +24,26 @@ Assessment {
|
||||
text: "null",
|
||||
kind: Keyword,
|
||||
},
|
||||
Candidate {
|
||||
text: "true",
|
||||
kind: Keyword,
|
||||
},
|
||||
Candidate {
|
||||
text: "false",
|
||||
kind: Keyword,
|
||||
},
|
||||
Candidate {
|
||||
text: "Email",
|
||||
kind: Identifier,
|
||||
},
|
||||
Candidate {
|
||||
text: "Name",
|
||||
kind: Identifier,
|
||||
},
|
||||
Candidate {
|
||||
text: "id",
|
||||
kind: Identifier,
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
|
||||
+40
@@ -10,6 +10,26 @@ Assessment {
|
||||
hint: Some(
|
||||
Candidates {
|
||||
items: [
|
||||
Candidate {
|
||||
text: "not",
|
||||
kind: Keyword,
|
||||
},
|
||||
Candidate {
|
||||
text: "null",
|
||||
kind: Keyword,
|
||||
},
|
||||
Candidate {
|
||||
text: "true",
|
||||
kind: Keyword,
|
||||
},
|
||||
Candidate {
|
||||
text: "false",
|
||||
kind: Keyword,
|
||||
},
|
||||
Candidate {
|
||||
text: "(",
|
||||
kind: Punct,
|
||||
},
|
||||
Candidate {
|
||||
text: "Name",
|
||||
kind: Identifier,
|
||||
@@ -30,6 +50,26 @@ Assessment {
|
||||
),
|
||||
partial_prefix: "",
|
||||
candidates: [
|
||||
Candidate {
|
||||
text: "not",
|
||||
kind: Keyword,
|
||||
},
|
||||
Candidate {
|
||||
text: "null",
|
||||
kind: Keyword,
|
||||
},
|
||||
Candidate {
|
||||
text: "true",
|
||||
kind: Keyword,
|
||||
},
|
||||
Candidate {
|
||||
text: "false",
|
||||
kind: Keyword,
|
||||
},
|
||||
Candidate {
|
||||
text: "(",
|
||||
kind: Punct,
|
||||
},
|
||||
Candidate {
|
||||
text: "Name",
|
||||
kind: Identifier,
|
||||
|
||||
+48
@@ -24,6 +24,54 @@ Assessment {
|
||||
text: "null",
|
||||
kind: Keyword,
|
||||
},
|
||||
Candidate {
|
||||
text: "true",
|
||||
kind: Keyword,
|
||||
},
|
||||
Candidate {
|
||||
text: "false",
|
||||
kind: Keyword,
|
||||
},
|
||||
Candidate {
|
||||
text: "auto",
|
||||
kind: Identifier,
|
||||
},
|
||||
Candidate {
|
||||
text: "b",
|
||||
kind: Identifier,
|
||||
},
|
||||
Candidate {
|
||||
text: "d",
|
||||
kind: Identifier,
|
||||
},
|
||||
Candidate {
|
||||
text: "data",
|
||||
kind: Identifier,
|
||||
},
|
||||
Candidate {
|
||||
text: "dt",
|
||||
kind: Identifier,
|
||||
},
|
||||
Candidate {
|
||||
text: "k",
|
||||
kind: Identifier,
|
||||
},
|
||||
Candidate {
|
||||
text: "note",
|
||||
kind: Identifier,
|
||||
},
|
||||
Candidate {
|
||||
text: "r",
|
||||
kind: Identifier,
|
||||
},
|
||||
Candidate {
|
||||
text: "sid",
|
||||
kind: Identifier,
|
||||
},
|
||||
Candidate {
|
||||
text: "ts",
|
||||
kind: Identifier,
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
|
||||
+20
@@ -24,6 +24,26 @@ Assessment {
|
||||
text: "null",
|
||||
kind: Keyword,
|
||||
},
|
||||
Candidate {
|
||||
text: "true",
|
||||
kind: Keyword,
|
||||
},
|
||||
Candidate {
|
||||
text: "false",
|
||||
kind: Keyword,
|
||||
},
|
||||
Candidate {
|
||||
text: "Email",
|
||||
kind: Identifier,
|
||||
},
|
||||
Candidate {
|
||||
text: "Name",
|
||||
kind: Identifier,
|
||||
},
|
||||
Candidate {
|
||||
text: "id",
|
||||
kind: Identifier,
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
|
||||
+40
@@ -10,6 +10,26 @@ Assessment {
|
||||
hint: Some(
|
||||
Candidates {
|
||||
items: [
|
||||
Candidate {
|
||||
text: "not",
|
||||
kind: Keyword,
|
||||
},
|
||||
Candidate {
|
||||
text: "null",
|
||||
kind: Keyword,
|
||||
},
|
||||
Candidate {
|
||||
text: "true",
|
||||
kind: Keyword,
|
||||
},
|
||||
Candidate {
|
||||
text: "false",
|
||||
kind: Keyword,
|
||||
},
|
||||
Candidate {
|
||||
text: "(",
|
||||
kind: Punct,
|
||||
},
|
||||
Candidate {
|
||||
text: "Name",
|
||||
kind: Identifier,
|
||||
@@ -30,6 +50,26 @@ Assessment {
|
||||
),
|
||||
partial_prefix: "",
|
||||
candidates: [
|
||||
Candidate {
|
||||
text: "not",
|
||||
kind: Keyword,
|
||||
},
|
||||
Candidate {
|
||||
text: "null",
|
||||
kind: Keyword,
|
||||
},
|
||||
Candidate {
|
||||
text: "true",
|
||||
kind: Keyword,
|
||||
},
|
||||
Candidate {
|
||||
text: "false",
|
||||
kind: Keyword,
|
||||
},
|
||||
Candidate {
|
||||
text: "(",
|
||||
kind: Punct,
|
||||
},
|
||||
Candidate {
|
||||
text: "Name",
|
||||
kind: Identifier,
|
||||
|
||||
@@ -561,6 +561,8 @@ fn show_data_for_empty_table_renders_placeholder() {
|
||||
app.update(AppEvent::DslDataSucceeded {
|
||||
command: Command::ShowData {
|
||||
name: "Customers".to_string(),
|
||||
filter: None,
|
||||
limit: None,
|
||||
},
|
||||
data,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user