feat: ADR-0036 Phase 3b — live typed-slot hints + highlighting for INSERT VALUES
Give each positional INSERT VALUES position its column identity so a lone
literal gets the column-typed slot (live per-column hint + mismatch
highlight) and any expression falls through to sql_expr — completing the
typed-DML-values feature for the INSERT surface (single/multi-row, Form A
and Form B).
New zero-width Node::SetColumn(&TableColumn) primitive establishes the
active column for the value position that follows (sets current_column +
pending_value_column, like an Ident{writes_column} but without consuming
input); a DynamicSubgrammar emits SetColumn(col) + the shared SET_VALUE
per position. Column mapping mirrors do_sql_insert: Form A → listed
columns; Form B → all columns in declaration order (advanced-mode Form B
auto-fills nothing; an omitted shortid in Form A is auto-filled and has no
VALUES position).
Reconcile with the per-tuple arity diagnostic (ADR-0033 §8.1): a
fixed-length typed Seq would reject wrong-arity tuples and suppress that
post-walk diagnostic, so the tuple value list is an arity-gating lookahead
— a correct-arity tuple uses the typed Seq; a wrong-arity tuple keeps the
type-blind sql_expr repeat so §8.1 fires unchanged. Correct-arity tuples
get full live feedback, including a wrong-kind literal like 'text' into an
int column.
Records ADR-0036 Amendment 1 (Phase 3b detail + the arity reconciliation);
ADR-0036 is now fully implemented.
Tests: 1947 passing (+8), 0 failed, 0 skipped, 1 ignored; clippy clean.
This commit is contained in:
@@ -375,6 +375,25 @@ pub enum Node {
|
||||
/// type-awareness). Not memoized: the output depends on the
|
||||
/// source, not just `ctx`.
|
||||
Lookahead(fn(&WalkContext, &str, usize) -> Self),
|
||||
/// Zero-width node that *establishes the active column* for the
|
||||
/// value slot that follows it (ADR-0036 Phase 3b). Matches the
|
||||
/// empty string and, as a side effect, sets
|
||||
/// `WalkContext::current_column` to the referenced column and
|
||||
/// `pending_value_column` to its name — exactly as an
|
||||
/// `Ident { writes_column: true }` does, but without consuming a
|
||||
/// column identifier from the input.
|
||||
///
|
||||
/// This is the primitive that gives `INSERT … VALUES (…)`
|
||||
/// positions a per-position column identity: the positions are
|
||||
/// positional (no per-position column ident to write
|
||||
/// `current_column`), so a `DynamicSubgrammar` factory
|
||||
/// (`sql_insert::sql_value_list`) emits `SetColumn(colᵢ)` before
|
||||
/// each value position, then the shared boundary-aware `SET_VALUE`
|
||||
/// slot routes a lone literal to that column's typed slot and any
|
||||
/// expression to `sql_expr`. The referenced `TableColumn` is
|
||||
/// leaked by the factory (bounded by the column count, like the
|
||||
/// `DynamicSubgrammar` `Box::leak`).
|
||||
SetColumn(&'static crate::completion::TableColumn),
|
||||
/// Typed value-literal slot (ADR-0024 §Phase D §typed-value-slots).
|
||||
///
|
||||
/// Walks `inner` to consume the literal but records the
|
||||
|
||||
+163
-10
@@ -13,12 +13,14 @@
|
||||
//! (3g), and `ON CONFLICT … ` UPSERT (3h) land in later
|
||||
//! sub-phases.
|
||||
|
||||
use crate::completion::TableColumn;
|
||||
use crate::dsl::grammar::shared::SET_VALUE;
|
||||
use crate::dsl::grammar::sql_expr;
|
||||
use crate::dsl::grammar::sql_select::{
|
||||
RETURNING_CLAUSE, SQL_SELECT_COMPOUND, WHERE_CLAUSE, reject_internal_table,
|
||||
};
|
||||
use crate::dsl::grammar::{IdentSource, Node, Word};
|
||||
use crate::dsl::walker::context::WalkContext;
|
||||
|
||||
static COMMA: Node = Node::Punct(',');
|
||||
|
||||
@@ -41,10 +43,13 @@ const TARGET_TABLE: Node = Node::Ident {
|
||||
|
||||
/// One column name inside the optional `(col1, col2, …)` list.
|
||||
///
|
||||
/// `writes_user_listed_column` stays `false` in 3b — the worker
|
||||
/// requires explicit values for every column, so the listed-column
|
||||
/// set isn't needed yet. Sub-phase 3d (`shortid` auto-fill) turns
|
||||
/// it on and threads `listed_columns` into `Command::SqlInsert`.
|
||||
/// `writes_user_listed_column: true` records the listed columns into
|
||||
/// `WalkContext::user_listed_columns` so the `VALUES` factory
|
||||
/// (`sql_value_list`, ADR-0036 Phase 3b) maps each value position to
|
||||
/// the listed column in the user's order (Form A). `build_sql_insert`
|
||||
/// still collects `listed_columns` independently from the matched
|
||||
/// `insert_column` idents, so this flag only adds the live typed-slot
|
||||
/// mapping — nothing else reads `user_listed_columns` on the SQL path.
|
||||
static COLUMN_NAME: Node = Node::Ident {
|
||||
source: IdentSource::Columns,
|
||||
role: "insert_column",
|
||||
@@ -52,7 +57,7 @@ static COLUMN_NAME: Node = Node::Ident {
|
||||
highlight_override: None,
|
||||
writes_table: false,
|
||||
writes_column: false,
|
||||
writes_user_listed_column: false,
|
||||
writes_user_listed_column: true,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
@@ -72,19 +77,167 @@ const OPTIONAL_COLUMN_LIST: Node = Node::Optional(&Node::Seq(COLUMN_LIST_NODES))
|
||||
/// One value expression inside a `VALUES` tuple. Consumes the
|
||||
/// shared `sql_expr` grammar (ADR-0031), so literals, operators,
|
||||
/// `CASE`, function calls, etc. are all admitted; the engine
|
||||
/// evaluates them at execution time.
|
||||
/// evaluates them at execution time. Used as the schemaless / fallback
|
||||
/// value (see `sql_value_list`).
|
||||
static VALUE_EXPR: Node = Node::Subgrammar(&sql_expr::SQL_OR_EXPR);
|
||||
|
||||
static VALUE_TUPLE_NODES: &[Node] = &[
|
||||
Node::Punct('('),
|
||||
/// The fallback value list — the pre-Phase-3b type-blind
|
||||
/// `Repeated(sql_expr)`. Used for schemaless walks and (crucially) for
|
||||
/// any tuple whose value-count does NOT match the target column count,
|
||||
/// so the post-walk per-tuple arity diagnostic (ADR-0033 §8.1) still
|
||||
/// sees all the values in the matched path and fires its friendly
|
||||
/// message — a fixed-length typed `Seq` would instead reject the tuple
|
||||
/// and suppress that diagnostic.
|
||||
fn fallback_value_list() -> Node {
|
||||
Node::Repeated {
|
||||
inner: &VALUE_EXPR,
|
||||
separator: Some(&COMMA),
|
||||
min: 1,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// The target columns a `VALUES` tuple's positions map onto (ADR-0036
|
||||
/// Phase 3b). Mirrors `db::do_sql_insert`'s positional rule — NOT the
|
||||
/// DSL's `column_value_list`:
|
||||
/// - **Form A** (`user_listed_columns` set, from the `(col, …)`
|
||||
/// list): the listed columns, in the user's order. An *omitted*
|
||||
/// `shortid` is auto-filled at execution (the X4 note) and has no
|
||||
/// `VALUES` position, so it is correctly absent here.
|
||||
/// - **Form B** (no column list): ALL columns in declaration order,
|
||||
/// including `serial` / `shortid` — advanced-mode Form B auto-fills
|
||||
/// *nothing* (`plan_shortid_autofill` returns early on an empty
|
||||
/// column list), so the user supplies a value for every column.
|
||||
///
|
||||
/// Empty when schemaless, the table is unknown, or a Form A list
|
||||
/// resolves to nothing (callers fall back to the type-blind list).
|
||||
fn target_value_columns(ctx: &WalkContext) -> Vec<TableColumn> {
|
||||
let Some(table_cols) = ctx.current_table_columns.as_ref() else {
|
||||
return Vec::new();
|
||||
};
|
||||
ctx.user_listed_columns.as_ref().map_or_else(
|
||||
|| table_cols.clone(),
|
||||
|listed| {
|
||||
listed
|
||||
.iter()
|
||||
.filter_map(|name| {
|
||||
table_cols.iter().find(|c| c.name.eq_ignore_ascii_case(name)).cloned()
|
||||
})
|
||||
.collect()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Count the value positions in the `VALUES` tuple whose contents begin
|
||||
/// at `pos` (just past the opening `(`), and whether the tuple is
|
||||
/// *closed* (a depth-0 `)` was reached) vs still being typed (scan hit
|
||||
/// end-of-input first). Depth-aware: commas nested in a function call /
|
||||
/// subquery (paren depth ≥ 1) or inside a string literal are not
|
||||
/// separators. Returns `(0, _)` for an empty tuple `()`.
|
||||
fn count_tuple_values(source: &str, pos: usize) -> (usize, bool) {
|
||||
let bytes = source.as_bytes();
|
||||
let mut i = pos;
|
||||
let mut depth: i32 = 0;
|
||||
let mut commas = 0usize;
|
||||
let mut seen_value = false;
|
||||
let mut closed = false;
|
||||
while i < bytes.len() {
|
||||
match bytes[i] {
|
||||
b'\'' => {
|
||||
// Skip a single-quoted string literal (`''` escape).
|
||||
i += 1;
|
||||
seen_value = true;
|
||||
while i < bytes.len() {
|
||||
if bytes[i] == b'\'' {
|
||||
if bytes.get(i + 1) == Some(&b'\'') {
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
i += 1;
|
||||
break;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
b'(' => {
|
||||
depth += 1;
|
||||
seen_value = true;
|
||||
}
|
||||
b')' => {
|
||||
if depth == 0 {
|
||||
closed = true;
|
||||
break; // tuple close
|
||||
}
|
||||
depth -= 1;
|
||||
}
|
||||
b',' if depth == 0 => commas += 1,
|
||||
b if !b.is_ascii_whitespace() => seen_value = true,
|
||||
_ => {}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
(if seen_value { commas + 1 } else { 0 }, closed)
|
||||
}
|
||||
|
||||
/// Tuple value-list lookahead (ADR-0036 Phase 3b). Gates the typed
|
||||
/// per-column path on arity so the typed `Seq` is used only where it
|
||||
/// can succeed, leaving wrong-arity tuples to the type-blind path (and
|
||||
/// thus to the per-tuple arity diagnostic, ADR-0033 §8.1, which a
|
||||
/// fixed-length `Seq` would otherwise suppress by rejecting the tuple):
|
||||
/// - a **closed** tuple routes to typed slots only on an *exact*
|
||||
/// match (`count == columns`);
|
||||
/// - an **open** (still-typing) tuple routes to typed slots while
|
||||
/// there is still room (`count <= columns`), so the per-column hint
|
||||
/// shows from the moment `(` is opened through each position.
|
||||
///
|
||||
/// Returns a small node — the heavy typed `Seq` is built + memoized by
|
||||
/// the `DynamicSubgrammar` — matching `insert_first_paren`'s leak
|
||||
/// discipline. Schemaless / unknown table → type-blind fallback.
|
||||
fn tuple_value_list(ctx: &WalkContext, source: &str, pos: usize) -> Node {
|
||||
let cols = target_value_columns(ctx);
|
||||
let (count, closed) = count_tuple_values(source, pos);
|
||||
let arity_ok = if closed { count == cols.len() } else { count <= cols.len() };
|
||||
if !cols.is_empty() && arity_ok {
|
||||
Node::DynamicSubgrammar(sql_value_list)
|
||||
} else {
|
||||
fallback_value_list()
|
||||
}
|
||||
}
|
||||
|
||||
/// Schema-aware typed value list for one correct-arity `VALUES` tuple
|
||||
/// (ADR-0036 Phase 3b). Emits, per target column, a zero-width
|
||||
/// `SetColumn(col)` marker (establishes the active column) followed by
|
||||
/// the shared boundary-aware [`SET_VALUE`] slot — so a lone literal
|
||||
/// routes to the column's typed slot (live hint + numeric-shape
|
||||
/// highlight) and any expression falls through to `sql_expr`. Reached
|
||||
/// only via [`tuple_value_list`] when arity matches and the schema is
|
||||
/// known; the empty-cols guard is defensive.
|
||||
fn sql_value_list(ctx: &WalkContext) -> Node {
|
||||
let cols = target_value_columns(ctx);
|
||||
if cols.is_empty() {
|
||||
return fallback_value_list();
|
||||
}
|
||||
let mut children: Vec<Node> = Vec::with_capacity(cols.len() * 3);
|
||||
for (i, col) in cols.into_iter().enumerate() {
|
||||
if i > 0 {
|
||||
children.push(Node::Punct(','));
|
||||
}
|
||||
let leaked: &'static TableColumn = Box::leak(Box::new(col));
|
||||
children.push(Node::SetColumn(leaked));
|
||||
children.push(SET_VALUE);
|
||||
}
|
||||
Node::Seq(Box::leak(children.into_boxed_slice()))
|
||||
}
|
||||
|
||||
static VALUE_TUPLE_NODES: &[Node] = &[
|
||||
Node::Punct('('),
|
||||
Node::Lookahead(tuple_value_list),
|
||||
Node::Punct(')'),
|
||||
];
|
||||
/// `'(' sql_expr (',' sql_expr)* ')'` — one row of values.
|
||||
/// `'(' <value-list> ')'` — one row of values. The value list is the
|
||||
/// arity-gated `tuple_value_list` (ADR-0036 Phase 3b): a correct-arity
|
||||
/// tuple gets per-column typed slots; a wrong-arity tuple keeps the
|
||||
/// type-blind `sql_expr` repeat so the §8.1 arity diagnostic fires.
|
||||
static VALUE_TUPLE: Node = Node::Seq(VALUE_TUPLE_NODES);
|
||||
|
||||
static VALUES_CLAUSE_NODES: &[Node] = &[
|
||||
|
||||
Reference in New Issue
Block a user