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:
+59
-9
@@ -53,8 +53,9 @@
|
||||
//! `build_expr` owns the tree shape.
|
||||
|
||||
use crate::dsl::command::{CompareOp, Expr, Operand, Predicate};
|
||||
use crate::dsl::grammar::{IdentSource, Node, ValidationError, Word};
|
||||
use crate::dsl::grammar::{HintMode, IdentSource, Node, ValidationError, Word};
|
||||
use crate::dsl::value::Value;
|
||||
use crate::dsl::walker::context::WalkContext;
|
||||
use crate::dsl::walker::outcome::{MatchedItem, MatchedKind};
|
||||
|
||||
// =================================================================
|
||||
@@ -64,19 +65,27 @@ use crate::dsl::walker::outcome::{MatchedItem, MatchedKind};
|
||||
/// A column reference inside an expression. The `expr_column`
|
||||
/// role lets [`build_expr`] (and the command AST builders) tell
|
||||
/// an expression column apart from other identifier slots.
|
||||
///
|
||||
/// `writes_column` so that, once a predicate's left operand
|
||||
/// resolves to a known column, the walker records it in
|
||||
/// `WalkContext::current_column` — the right operand's
|
||||
/// schema-aware slot (`where_rhs_operand`) reads it to narrow
|
||||
/// the hint panel to that column's type (ADR-0026 §8).
|
||||
const EXPR_COLUMN: Node = Node::Ident {
|
||||
source: IdentSource::Columns,
|
||||
role: "expr_column",
|
||||
validator: None,
|
||||
highlight_override: None,
|
||||
writes_table: false,
|
||||
writes_column: false,
|
||||
writes_column: true,
|
||||
writes_user_listed_column: false,
|
||||
};
|
||||
|
||||
/// Operand alternatives. The literal keywords (`null` / `true`
|
||||
/// / `false`) come before the column slot so they parse as
|
||||
/// literals; any other identifier is a column reference.
|
||||
/// literals; any other identifier is a column reference. No
|
||||
/// validators — a type-mismatched literal in a comparison is
|
||||
/// flagged in the editor but still parses (ADR-0026 §7).
|
||||
static OPERAND_CHOICES: &[Node] = &[
|
||||
Node::Word(Word::keyword("null")),
|
||||
Node::Word(Word::keyword("true")),
|
||||
@@ -86,6 +95,45 @@ static OPERAND_CHOICES: &[Node] = &[
|
||||
EXPR_COLUMN,
|
||||
];
|
||||
|
||||
/// The operand alternatives as a single node — the permissive
|
||||
/// inner of the schema-aware right-hand operand slot.
|
||||
static OPERAND_NODE: Node = Node::Choice(OPERAND_CHOICES);
|
||||
|
||||
/// The right-hand operand of a predicate — the comparison RHS,
|
||||
/// a `LIKE` pattern, the `BETWEEN` bounds, the `IN` items —
|
||||
/// resolved at walk time (ADR-0026 §8).
|
||||
///
|
||||
/// When the predicate's left operand resolved to a known
|
||||
/// column, this wraps the operand in a `TypedValueSlot` keyed
|
||||
/// on that column's user-facing type, so the hint panel
|
||||
/// narrows to the column's type and names the column.
|
||||
/// Otherwise a generic value-literal `Hinted` slot. Either way
|
||||
/// the inner grammar is the *permissive* operand choice
|
||||
/// (`OPERAND_NODE`) — a type-mismatched literal still matches
|
||||
/// (ADR-0026 §7); the mismatch is an editor flag, never a
|
||||
/// parse failure.
|
||||
fn where_rhs_operand(ctx: &WalkContext) -> Node {
|
||||
ctx.current_column.as_ref().map_or_else(
|
||||
|| Node::Hinted {
|
||||
mode: HintMode::ProseOnly("hint.value_literal_slot"),
|
||||
inner: &OPERAND_NODE,
|
||||
},
|
||||
|col| {
|
||||
// `Box::leak` mirrors `shared::slot_for_column` —
|
||||
// the leak is per distinct column (the walker
|
||||
// memoizes `DynamicSubgrammar` resolution on
|
||||
// `current_column`), not per keystroke.
|
||||
let leaked: &'static str =
|
||||
Box::leak(col.name.clone().into_boxed_str());
|
||||
Node::TypedValueSlot {
|
||||
ty: col.user_type,
|
||||
column_name: Some(leaked),
|
||||
inner: &OPERAND_NODE,
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// cmp_op := <= | <> | >= | != | < | > | =
|
||||
// =================================================================
|
||||
@@ -110,10 +158,12 @@ static CMP_OP_CHOICES: &[Node] = &[
|
||||
// predicate_tail branches
|
||||
// =================================================================
|
||||
|
||||
/// `cmp_op operand`.
|
||||
/// `cmp_op operand`. The right operand is the schema-aware
|
||||
/// `where_rhs_operand` so the hint panel can narrow to the
|
||||
/// left column's type.
|
||||
static COMPARE_FORM_NODES: &[Node] = &[
|
||||
Node::Choice(CMP_OP_CHOICES),
|
||||
Node::Choice(OPERAND_CHOICES),
|
||||
Node::DynamicSubgrammar(where_rhs_operand),
|
||||
];
|
||||
|
||||
/// `IS [NOT] NULL`.
|
||||
@@ -126,7 +176,7 @@ static IS_NULL_NODES: &[Node] = &[
|
||||
/// `LIKE operand`.
|
||||
static LIKE_FORM_NODES: &[Node] = &[
|
||||
Node::Word(Word::keyword("like")),
|
||||
Node::Choice(OPERAND_CHOICES),
|
||||
Node::DynamicSubgrammar(where_rhs_operand),
|
||||
];
|
||||
|
||||
/// `BETWEEN operand AND operand`. The inner `and` is consumed
|
||||
@@ -134,9 +184,9 @@ static LIKE_FORM_NODES: &[Node] = &[
|
||||
/// connective.
|
||||
static BETWEEN_FORM_NODES: &[Node] = &[
|
||||
Node::Word(Word::keyword("between")),
|
||||
Node::Choice(OPERAND_CHOICES),
|
||||
Node::DynamicSubgrammar(where_rhs_operand),
|
||||
Node::Word(Word::keyword("and")),
|
||||
Node::Choice(OPERAND_CHOICES),
|
||||
Node::DynamicSubgrammar(where_rhs_operand),
|
||||
];
|
||||
|
||||
/// `IN ( operand [, operand]* )`.
|
||||
@@ -144,7 +194,7 @@ static IN_FORM_NODES: &[Node] = &[
|
||||
Node::Word(Word::keyword("in")),
|
||||
Node::Punct('('),
|
||||
Node::Repeated {
|
||||
inner: &Node::Choice(OPERAND_CHOICES),
|
||||
inner: &Node::DynamicSubgrammar(where_rhs_operand),
|
||||
separator: Some(&Node::Punct(',')),
|
||||
min: 1,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user