diff --git a/CLAUDE.md b/CLAUDE.md index 1b067f0..fadc957 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -178,17 +178,24 @@ not yet implemented: 1–4 of ADR-0015). Pending pieces: `export` / `import` (Iter 5), `--resume` + persistent input history hydration + migration framework scaffold (Iter 6). -- **Complex WHERE expressions** (C5a): AND/OR/comparison/LIKE - in UPDATE/DELETE/show-data filters. The bridge from DSL - fluency to real SQL. +- **Complex WHERE expressions** (C5a): implemented through + ADR-0026 steps 1–4 — the stratified expression grammar + (AND/OR/NOT, the six comparisons, LIKE/IN/BETWEEN/IS NULL, + parentheses) reached through a new `Subgrammar` node, the + recursive `Expr` AST + parameterised SQL, and `where` / + `limit` on `show data`. Type-mismatched comparisons run + permissively (§7). Still pending: the §7 advisory + *flagging* of type mismatches / `= NULL`, the seam with + ADR-0027's diagnostics-severity model. - **SQL handling in advanced mode** (Q1): `sqlparser-rs` parser + a defined SQL subset (Q4 — its own ADR). - **Column drops/renames/type changes** (B2 / C2 partial): the rebuild-table primitive (ADR-0013) is in place; the grammar and dispatch are pending. - **Indexes**: `add index` / `drop index` done (ADR-0025). - `EXPLAIN QUERY PLAN` rendering for QA1 still pending (needs - its own QA2 rendering ADR). + `EXPLAIN QUERY PLAN` (QA1 / QA2) is designed in ADR-0028 — + the `explain` prefix command + span-styled plan tree — + but not yet implemented. - **Modify relationship** (C3a): drop+add covers the use case today. - **m:n convenience** (C4): auto-generates a junction table diff --git a/docs/adr/0026-complex-where-expressions.md b/docs/adr/0026-complex-where-expressions.md index f1e5cb6..364a048 100644 --- a/docs/adr/0026-complex-where-expressions.md +++ b/docs/adr/0026-complex-where-expressions.md @@ -416,6 +416,66 @@ suite and the typing-surface matrix: walker. 6. Typing-surface matrix cells for the new surface. +### As-built notes (2026-05-18) + +Steps 1–4 are implemented and committed; step 5 (the §7 +diagnostic flagging) is deferred — see below. Realization +choices, and where they deviate from the design sketch above: + +- **§3 builder — option 1 ("reconstruct in builder"),** + chosen with the project owner before implementation. The + stratified grammar is walked normally; its terminals flow + into the flat `MatchedPath` unchanged (driving highlight / + completion / the expected-set). `grammar::expr::build_expr` + then folds that flat terminal slice into the `Expr` — a + deterministic recursive descent mirroring the grammar + tiers, run only at submit-time dispatch, never per + keystroke. Two honest deviations from the §3 wording: + - **No `MatchedKind::Expr` variant.** `MatchedPath` stays + purely terminals — arguably more faithful to "MatchedPath + stays flat" than carrying a built `Expr` in it. The + `Expr` is assembled in the command `ast_builder`s + (`build_update` / `build_delete` / `build_show`), which + already reconstruct structured `Command`s from the flat + path; `build_expr` is the same pattern, one tier deeper. + - **There is a second structural pass** over the + expression tokens, scoped to submit-time dispatch. "No + second parse" is read as "no separate parser framework": + the walk validates and drives assistance, `build_expr` + is the single `ast_builder` for the fragment — the same + category as `build_insert`. +- **Grammar shape.** `predicate` is factored as `operand + predicate_tail` (shared operand prefix), and the infix + `NOT` is factored in front of the `LIKE` / `BETWEEN` / + `IN` choice — so the walker's first-commit-wins `Choice` + semantics discriminate branches on a cleanly-failing first + token. +- **`Subgrammar` depth.** `MAX_SUBGRAMMAR_DEPTH = 64` counts + active `Subgrammar` recursion frames. The stratified + grammar descends ~4–5 frames per parenthesis level, so the + effective parenthesis-nesting limit is roughly a dozen — + far past any hand-written filter; the cap is purely a + stack-overflow guard. +- **§8 hints.** The expression's right-hand operands resolve + through a schema-aware `DynamicSubgrammar` (`where_rhs_ + operand`) so the hint panel narrows to the compared + column's type, exactly as the pre-ADR `where col = val` + slot did. The operand grammar carries no validators — + permissive per §7. +- **Step 5 deferred to ADR-0027.** The §7 *behaviour* + relaxation is done: `bind_where_literal` binds a + type-mismatched WHERE literal by its syntactic shape, and + the pre-ADR bind-time rejection is gone. The §7 + *diagnostic flagging* — surfacing a type-mismatched + comparison or `= NULL` as an error-class highlight + hint + — is the seam with ADR-0027, which designs the walker + diagnostics-severity model that flagging belongs in (and + whose WARNING severity is defined to have "no triggers + until ADR-0026 is implemented"). Building the flagging as + a standalone mechanism first would be reworked by + ADR-0027; the recommendation is to implement it as the + first triggers of ADR-0027's model. + ## See also - ADR-0009 — DSL command syntax conventions (`--` flags, diff --git a/docs/requirements.md b/docs/requirements.md index da90ac7..5886228 100644 --- a/docs/requirements.md +++ b/docs/requirements.md @@ -26,12 +26,13 @@ repo is pushed). ## Test baseline -After ADR-0025 (indexes): **1037 passing, 0 failing, 1 -ignored** (`cargo test` — the one ignored test is a -long-standing `` ```ignore `` doc-test in -`src/friendly/mod.rs`). Clippy clean with the nursery lint -group enabled. (Earlier reference points: 1006 after ADR-0024 -+ the handoff-14 cleanup; 449 after B2/C2.) +After ADR-0026 steps 1–4 (complex WHERE expressions): +**1070 passing, 0 failing, 1 ignored** (`cargo test` — the +one ignored test is a long-standing `` ```ignore `` doc-test +in `src/friendly/mod.rs`). Clippy clean with the nursery lint +group enabled. (Earlier reference points: 1039 after ADR-0025 +(indexes); 1006 after ADR-0024 + the handoff-14 cleanup; 449 +after B2/C2.) --- @@ -157,12 +158,20 @@ group enabled. (Earlier reference points: 1006 after ADR-0024 writes. Bulk insert, complex WHERE expressions, and SELECT in advanced mode are explicitly tracked separately — see C5a below.)* -- [ ] **C5a** Complex WHERE expressions (AND/OR, comparison +- [x] **C5a** Complex WHERE expressions (AND/OR, comparison operators, LIKE, IS NULL, IN, BETWEEN) for UPDATE/DELETE/ - show-data filtering; also adds `where` and `limit` to - `show data`. Tracks the natural progression from DSL into - real SQL fluency that motivates the playground. Designed in - ADR-0026; implementation pending. + show-data filtering; `show data` also gains `where` and + `limit`. + *(ADR-0026 steps 1–4: the stratified expression grammar + reached through a new `Subgrammar` node, the recursive + `Expr` AST + `build_expr`, wiring into update / delete / + show data, and `Expr` → parameterised SQL with an implicit + primary-key `ORDER BY` for `limit`. Type-mismatched WHERE + comparisons are permissive — they run rather than being + rejected (§7). The §7 advisory **flagging** of type + mismatches / `= NULL` is the seam with ADR-0027's + diagnostics-severity model and is tracked there — see + ADR-0026 "As-built notes".)* ## SQL handling diff --git a/src/dsl/grammar/expr.rs b/src/dsl/grammar/expr.rs index b458abe..38c2ade 100644 --- a/src/dsl/grammar/expr.rs +++ b/src/dsl/grammar/expr.rs @@ -201,37 +201,43 @@ static IN_FORM_NODES: &[Node] = &[ Node::Punct(')'), ]; -/// The negatable predicates — each starts with a distinct -/// keyword, so this `Choice` discriminates cleanly. +/// The negatable predicate bodies — each starts with a +/// distinct keyword, so this `Choice` discriminates cleanly. static NEGATABLE_CHOICES: &[Node] = &[ Node::Seq(LIKE_FORM_NODES), Node::Seq(BETWEEN_FORM_NODES), Node::Seq(IN_FORM_NODES), ]; -/// `[NOT] (LIKE … | BETWEEN … | IN …)`. -static NEGATABLE_NODES: &[Node] = &[ - Node::Optional(&Node::Word(Word::keyword("not"))), +/// `NOT (LIKE … | BETWEEN … | IN …)` — the infix `NOT` is +/// factored in front of the negatable choice (rather than +/// repeated inside each, which would strand `not between` on +/// the `LIKE` branch). +static NOT_NEGATABLE_NODES: &[Node] = &[ + Node::Word(Word::keyword("not")), Node::Choice(NEGATABLE_CHOICES), ]; -/// `predicate_tail := cmp_op operand | IS [NOT] NULL | [NOT] -/// negatable`. +/// `predicate_tail := cmp_op operand | IS [NOT] NULL +/// | NOT negatable | negatable`. /// -/// Branch discrimination: a `Choice` branch falls through to -/// the next branch only when its *first* child reports a clean -/// `NoMatch`. Branch 1's first child is `Choice(CMP_OP_CHOICES)` -/// (all punctuation — clean `NoMatch` on a non-operator); -/// branch 2's is `Word("is")`. Branch 3 starts with -/// `Optional(not)`, which always "matches" — so it must be -/// **last**, and the infix `NOT` is factored out in front of -/// the `LIKE` / `BETWEEN` / `IN` choice rather than repeated -/// inside each (which would strand `not between` on the `LIKE` -/// branch). +/// Branch discrimination relies on each branch's *first* child +/// reporting a clean `NoMatch` on a non-match: branch 1 is a +/// `Choice` of punctuation operators, the rest start with a +/// `Word`. Crucially **no branch starts with an `Optional`** — +/// an `Optional`-first `Seq` always "commits", which turns its +/// failure into an `Incomplete` that the walker's `Choice` +/// returns early, discarding every sibling branch's expected +/// set (and so its completion candidates). The infix `NOT` is +/// therefore its own explicit `NOT negatable` branch, with a +/// bare `negatable` branch alongside. static PREDICATE_TAIL_CHOICES: &[Node] = &[ Node::Seq(COMPARE_FORM_NODES), Node::Seq(IS_NULL_NODES), - Node::Seq(NEGATABLE_NODES), + Node::Seq(NOT_NEGATABLE_NODES), + Node::Seq(LIKE_FORM_NODES), + Node::Seq(BETWEEN_FORM_NODES), + Node::Seq(IN_FORM_NODES), ]; // ================================================================= diff --git a/tests/typing_surface/mod.rs b/tests/typing_surface/mod.rs index 8703771..96840e6 100644 --- a/tests/typing_surface/mod.rs +++ b/tests/typing_surface/mod.rs @@ -27,6 +27,7 @@ pub mod insert_form_c; pub mod update_with_where; pub mod update_all_rows; pub mod delete_with_where; +pub mod where_expression; pub mod delete_all_rows; pub mod create_table; pub mod drop_column; diff --git a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__where_expression__after_complete_predicate_offers_and_or@after_predicate.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__where_expression__after_complete_predicate_offers_and_or@after_predicate.snap new file mode 100644 index 0000000..41bf676 --- /dev/null +++ b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__where_expression__after_complete_predicate_offers_and_or@after_predicate.snap @@ -0,0 +1,47 @@ +--- +source: tests/typing_surface/where_expression.rs +description: "input=\"delete from Customers where id=1 \" cursor=33" +expression: "& a" +--- +Assessment { + input: "delete from Customers where id=1 ", + cursor: 33, + state: Valid, + hint: Some( + Candidates { + items: [ + Candidate { + text: "and", + kind: Keyword, + }, + Candidate { + text: "or", + kind: Keyword, + }, + ], + selected: None, + }, + ), + completion: Some( + Completion { + replaced_range: ( + 33, + 33, + ), + partial_prefix: "", + candidates: [ + Candidate { + text: "and", + kind: Keyword, + }, + Candidate { + text: "or", + kind: Keyword, + }, + ], + }, + ), + parse_result: Ok( + "Delete", + ), +} diff --git a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__where_expression__after_not_keyword_expects_a_predicate@after_not.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__where_expression__after_not_keyword_expects_a_predicate@after_not.snap new file mode 100644 index 0000000..c3c6747 --- /dev/null +++ b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__where_expression__after_not_keyword_expects_a_predicate@after_not.snap @@ -0,0 +1,95 @@ +--- +source: tests/typing_surface/where_expression.rs +description: "input=\"delete from Customers where not \" cursor=32" +expression: "& a" +--- +Assessment { + input: "delete from Customers where not ", + cursor: 32, + state: IncompleteAtEof, + 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: "Email", + kind: Identifier, + }, + Candidate { + text: "Name", + kind: Identifier, + }, + Candidate { + text: "id", + kind: Identifier, + }, + ], + selected: None, + }, + ), + completion: Some( + Completion { + replaced_range: ( + 32, + 32, + ), + 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: "Email", + kind: Identifier, + }, + Candidate { + text: "Name", + kind: Identifier, + }, + Candidate { + text: "id", + kind: Identifier, + }, + ], + }, + ), + parse_result: Err( + "Invalid(at_eof)", + ), +} diff --git a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__where_expression__after_where_column_offers_predicate_operators@after_column.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__where_expression__after_where_column_offers_predicate_operators@after_column.snap new file mode 100644 index 0000000..a097584 --- /dev/null +++ b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__where_expression__after_where_column_offers_predicate_operators@after_column.snap @@ -0,0 +1,71 @@ +--- +source: tests/typing_surface/where_expression.rs +description: "input=\"delete from Customers where Email \" cursor=34" +expression: "& a" +--- +Assessment { + input: "delete from Customers where Email ", + cursor: 34, + state: IncompleteAtEof, + hint: Some( + Candidates { + items: [ + Candidate { + text: "is", + kind: Keyword, + }, + Candidate { + text: "not", + kind: Keyword, + }, + Candidate { + text: "like", + kind: Keyword, + }, + Candidate { + text: "between", + kind: Keyword, + }, + Candidate { + text: "in", + kind: Keyword, + }, + ], + selected: None, + }, + ), + completion: Some( + Completion { + replaced_range: ( + 34, + 34, + ), + partial_prefix: "", + candidates: [ + Candidate { + text: "is", + kind: Keyword, + }, + Candidate { + text: "not", + kind: Keyword, + }, + Candidate { + text: "like", + kind: Keyword, + }, + Candidate { + text: "between", + kind: Keyword, + }, + Candidate { + text: "in", + kind: Keyword, + }, + ], + }, + ), + parse_result: Err( + "Invalid(at_eof)", + ), +} diff --git a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__where_expression__between_expects_a_low_bound@between_low.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__where_expression__between_expects_a_low_bound@between_low.snap new file mode 100644 index 0000000..189cb4f --- /dev/null +++ b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__where_expression__between_expects_a_low_bound@between_low.snap @@ -0,0 +1,81 @@ +--- +source: tests/typing_surface/where_expression.rs +description: "input=\"delete from Things where k between \" cursor=35" +expression: "& a" +--- +Assessment { + input: "delete from Things where k between ", + cursor: 35, + state: IncompleteAtEof, + hint: Some( + Prose( + "for `k`: Type an integer (e.g. 42, -7) or null", + ), + ), + completion: Some( + Completion { + replaced_range: ( + 35, + 35, + ), + partial_prefix: "", + candidates: [ + Candidate { + 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, + }, + ], + }, + ), + parse_result: Err( + "Invalid(at_eof)", + ), +} diff --git a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__where_expression__complete_show_data_with_where_and_limit_parses@complete_show_where_limit.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__where_expression__complete_show_data_with_where_and_limit_parses@complete_show_where_limit.snap new file mode 100644 index 0000000..946b14c --- /dev/null +++ b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__where_expression__complete_show_data_with_where_and_limit_parses@complete_show_where_limit.snap @@ -0,0 +1,19 @@ +--- +source: tests/typing_surface/where_expression.rs +description: "input=\"show data Customers where id=1 limit 10\" cursor=39" +expression: "& a" +--- +Assessment { + input: "show data Customers where id=1 limit 10", + cursor: 39, + state: Valid, + hint: Some( + Prose( + "Submit with Enter", + ), + ), + completion: None, + parse_result: Ok( + "ShowData", + ), +} diff --git a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__where_expression__complex_and_or_expression_parses@complex_and_or.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__where_expression__complex_and_or_expression_parses@complex_and_or.snap new file mode 100644 index 0000000..3b019d0 --- /dev/null +++ b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__where_expression__complex_and_or_expression_parses@complex_and_or.snap @@ -0,0 +1,19 @@ +--- +source: tests/typing_surface/where_expression.rs +description: "input=\"delete from Things where k > 1 and t like 'a%' or k = 9\" cursor=55" +expression: "& a" +--- +Assessment { + input: "delete from Things where k > 1 and t like 'a%' or k = 9", + cursor: 55, + state: Valid, + hint: Some( + Prose( + "Submit with Enter", + ), + ), + completion: None, + parse_result: Ok( + "Delete", + ), +} diff --git a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__where_expression__in_list_open_paren_expects_an_item@in_open_paren.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__where_expression__in_list_open_paren_expects_an_item@in_open_paren.snap new file mode 100644 index 0000000..9801954 --- /dev/null +++ b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__where_expression__in_list_open_paren_expects_an_item@in_open_paren.snap @@ -0,0 +1,81 @@ +--- +source: tests/typing_surface/where_expression.rs +description: "input=\"delete from Things where k in (\" cursor=31" +expression: "& a" +--- +Assessment { + input: "delete from Things where k in (", + cursor: 31, + state: IncompleteAtEof, + hint: Some( + Prose( + "for `k`: Type an integer (e.g. 42, -7) or null", + ), + ), + completion: Some( + Completion { + replaced_range: ( + 31, + 31, + ), + partial_prefix: "", + candidates: [ + Candidate { + 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, + }, + ], + }, + ), + parse_result: Err( + "Invalid(at_eof)", + ), +} diff --git a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__where_expression__show_data_after_table_offers_where_and_limit@show_after_table.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__where_expression__show_data_after_table_offers_where_and_limit@show_after_table.snap new file mode 100644 index 0000000..8c55992 --- /dev/null +++ b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__where_expression__show_data_after_table_offers_where_and_limit@show_after_table.snap @@ -0,0 +1,47 @@ +--- +source: tests/typing_surface/where_expression.rs +description: "input=\"show data Customers \" cursor=20" +expression: "& a" +--- +Assessment { + input: "show data Customers ", + cursor: 20, + state: Valid, + hint: Some( + Candidates { + items: [ + Candidate { + text: "where", + kind: Keyword, + }, + Candidate { + text: "limit", + kind: Keyword, + }, + ], + selected: None, + }, + ), + completion: Some( + Completion { + replaced_range: ( + 20, + 20, + ), + partial_prefix: "", + candidates: [ + Candidate { + text: "where", + kind: Keyword, + }, + Candidate { + text: "limit", + kind: Keyword, + }, + ], + }, + ), + parse_result: Ok( + "ShowData", + ), +} diff --git a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__where_expression__show_data_after_where_predicate_offers_limit@show_after_where.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__where_expression__show_data_after_where_predicate_offers_limit@show_after_where.snap new file mode 100644 index 0000000..9519d09 --- /dev/null +++ b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__where_expression__show_data_after_where_predicate_offers_limit@show_after_where.snap @@ -0,0 +1,55 @@ +--- +source: tests/typing_surface/where_expression.rs +description: "input=\"show data Customers where id=1 \" cursor=31" +expression: "& a" +--- +Assessment { + input: "show data Customers where id=1 ", + cursor: 31, + state: Valid, + hint: Some( + Candidates { + items: [ + Candidate { + text: "and", + kind: Keyword, + }, + Candidate { + text: "or", + kind: Keyword, + }, + Candidate { + text: "limit", + kind: Keyword, + }, + ], + selected: None, + }, + ), + completion: Some( + Completion { + replaced_range: ( + 31, + 31, + ), + partial_prefix: "", + candidates: [ + Candidate { + text: "and", + kind: Keyword, + }, + Candidate { + text: "or", + kind: Keyword, + }, + Candidate { + text: "limit", + kind: Keyword, + }, + ], + }, + ), + parse_result: Ok( + "ShowData", + ), +} diff --git a/tests/typing_surface/where_expression.rs b/tests/typing_surface/where_expression.rs new file mode 100644 index 0000000..7bd9130 --- /dev/null +++ b/tests/typing_surface/where_expression.rs @@ -0,0 +1,101 @@ +//! Matrix coverage for the complex WHERE-expression surface +//! and `show data` `where` / `limit` (ADR-0026). +//! +//! The simple `where col = val` cells live in +//! `delete_with_where` / `update_with_where`; this file covers +//! the expression grammar's wider surface — operators, the +//! AND / OR connectives, the negatable predicates, and the +//! `show data` filter / limit clauses. + +use crate::typing_surface::*; +use rdbms_playground::input_render::InputState; + +#[test] +fn after_where_column_offers_predicate_operators() { + let schema = schema_serial_pk(); + let a = assess_at_end("delete from Customers where Email ", &schema); + assert!(matches!(a.state, InputState::IncompleteAtEof)); + // After an operand the grammar expects a predicate tail — + // the negatable keywords surface as candidates. + assert_candidate_present(&a, &["like", "between", "in", "is"]); + crate::snap!("after_column", a); +} + +#[test] +fn after_complete_predicate_offers_and_or() { + let schema = schema_serial_pk(); + let a = assess_at_end("delete from Customers where id=1 ", &schema); + assert!(matches!(a.state, InputState::Valid)); + // A complete predicate can be extended with a connective. + assert_candidate_present(&a, &["and", "or"]); + crate::snap!("after_predicate", a); +} + +#[test] +fn after_not_keyword_expects_a_predicate() { + let schema = schema_serial_pk(); + let a = assess_at_end("delete from Customers where not ", &schema); + assert!(matches!(a.state, InputState::IncompleteAtEof)); + // `not` is followed by another predicate — the column + // operands of the active table are offered. + assert_candidate_present(&a, &["Email", "Name"]); + crate::snap!("after_not", a); +} + +#[test] +fn between_expects_a_low_bound() { + let schema = schema_every_type(); + let a = assess_at_end("delete from Things where k between ", &schema); + assert!(matches!(a.state, InputState::IncompleteAtEof)); + crate::snap!("between_low", a); +} + +#[test] +fn in_list_open_paren_expects_an_item() { + let schema = schema_every_type(); + let a = assess_at_end("delete from Things where k in (", &schema); + assert!(matches!(a.state, InputState::IncompleteAtEof)); + crate::snap!("in_open_paren", a); +} + +#[test] +fn complex_and_or_expression_parses() { + let schema = schema_every_type(); + let a = assess_at_end( + "delete from Things where k > 1 and t like 'a%' or k = 9", + &schema, + ); + assert!(matches!(a.state, InputState::Valid)); + assert_eq!(a.parse_result.as_deref(), Ok("Delete")); + crate::snap!("complex_and_or", a); +} + +#[test] +fn show_data_after_table_offers_where_and_limit() { + let schema = schema_serial_pk(); + let a = assess_at_end("show data Customers ", &schema); + assert!(matches!(a.state, InputState::Valid)); + assert_candidate_present(&a, &["where", "limit"]); + crate::snap!("show_after_table", a); +} + +#[test] +fn show_data_after_where_predicate_offers_limit() { + let schema = schema_serial_pk(); + let a = assess_at_end("show data Customers where id=1 ", &schema); + assert!(matches!(a.state, InputState::Valid)); + assert_candidate_present(&a, &["limit", "and", "or"]); + crate::snap!("show_after_where", a); +} + +#[test] +fn complete_show_data_with_where_and_limit_parses() { + let schema = schema_serial_pk(); + let a = assess_at_end( + "show data Customers where id=1 limit 10", + &schema, + ); + assert!(matches!(a.state, InputState::Valid)); + assert_eq!(a.parse_result.as_deref(), Ok("ShowData")); + crate::snap!("complete_show_where_limit", a); +}