Insert grammar: Form C type-awareness via lookahead (ADR-0024 §Phase D)
Form C (`insert into T (vals)`) shared the `(` opener with Form A, so its paren was an untyped Repeated(Choice(literal, ident)) — values weren't type- or count-checked at parse time (handoff-12 §2.2). New Node::Lookahead variant: a factory that peeks the source. The insert first-paren factory inspects the first token — a value literal routes the contents through the typed column_value_list (Form B dispatch contract: per-non-auto-column typed slots); an identifier or empty paren routes to a Form A column-name list. So Form C now gets the same per-column typed slots, hints, and parse-time type/count checking Form B has. The explicit-Choice-branch split is impossible here (committed-choice semantics commit after `(` matches); lookahead is the only route, and DynamicSubgrammar factories couldn't see the source. Node::Lookahead is not memoized — its output depends on source — but it returns only a small node (a Repeated, or a thin DynamicSubgrammar wrapper that delegates to the memoized column_value_list). `insert into T (` now cleanly shows Form A column candidates instead of mixed Form-A/C suggestions. Form C matrix tests updated for the type-aware behaviour.
This commit is contained in:
+79
-55
@@ -18,9 +18,10 @@
|
|||||||
|
|
||||||
use crate::dsl::command::{Command, RowFilter};
|
use crate::dsl::command::{Command, RowFilter};
|
||||||
use crate::dsl::grammar::{
|
use crate::dsl::grammar::{
|
||||||
CommandNode, HintMode, IdentSource, Node, ValidationError, Word,
|
CommandNode, IdentSource, Node, ValidationError, Word,
|
||||||
shared::{column_value_list, current_column_value},
|
shared::{column_value_list, current_column_value},
|
||||||
};
|
};
|
||||||
|
use crate::dsl::walker::context::WalkContext;
|
||||||
use crate::dsl::value::Value;
|
use crate::dsl::value::Value;
|
||||||
use crate::dsl::walker::outcome::{MatchedItem, MatchedKind, MatchedPath};
|
use crate::dsl::walker::outcome::{MatchedItem, MatchedKind, MatchedPath};
|
||||||
|
|
||||||
@@ -52,25 +53,6 @@ const TABLE_NAME_INSERT: Node = Node::Ident {
|
|||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
// `value_literal` — null / true / false / number / string. The
|
|
||||||
// chumsky-side equivalent (`value_literal()` in dsl/parser.rs).
|
|
||||||
const VALUE_LITERAL_CHOICES: &[Node] = &[
|
|
||||||
Node::Word(Word::keyword("null")),
|
|
||||||
Node::Word(Word::keyword("true")),
|
|
||||||
Node::Word(Word::keyword("false")),
|
|
||||||
Node::NumberLit { validator: None },
|
|
||||||
Node::StringLit,
|
|
||||||
];
|
|
||||||
const VALUE_LITERAL_INNER: Node = Node::Choice(VALUE_LITERAL_CHOICES);
|
|
||||||
/// Value-literal slot with the `ProseOnly` HintMode
|
|
||||||
/// (ADR-0024 §HintMode-per-node) — the hint resolver surfaces
|
|
||||||
/// the generic "Type a value: …" prose rather than the
|
|
||||||
/// misleading `null`/`true`/`false` candidate trio.
|
|
||||||
const VALUE_LITERAL: Node = Node::Hinted {
|
|
||||||
mode: HintMode::ProseOnly("hint.value_literal_slot"),
|
|
||||||
inner: &VALUE_LITERAL_INNER,
|
|
||||||
};
|
|
||||||
|
|
||||||
// =================================================================
|
// =================================================================
|
||||||
// show — `show (data|table) <T>`
|
// show — `show (data|table) <T>`
|
||||||
// =================================================================
|
// =================================================================
|
||||||
@@ -97,43 +79,85 @@ const SHOW_SHAPE: Node = Node::Choice(SHOW_CHOICES);
|
|||||||
// =================================================================
|
// =================================================================
|
||||||
//
|
//
|
||||||
// Forms A (with column list) and C (bare value list) both start
|
// Forms A (with column list) and C (bare value list) both start
|
||||||
// with `(`. To avoid the walker's "first commit wins" semantics
|
// with `(`. The walker's "first commit wins" Choice semantics
|
||||||
// rejecting Form C when the inner content is values rather than
|
// can't pick between them after the `(` matches, so the first
|
||||||
// column names, the inside of the first paren is parsed as a
|
// paren's contents are resolved by a `Node::Lookahead` factory
|
||||||
// repeated `Choice(Ident, ValueLiteral)`. The AST builder then
|
// (`insert_first_paren`): it peeks the first token to decide.
|
||||||
// disambiguates: if a `values` keyword follows the first paren,
|
//
|
||||||
// the inner content was column names; otherwise it was values.
|
// - First token is a value literal (number / string /
|
||||||
|
// null / true / false) → Form C → the typed `column_value_list`
|
||||||
|
// (same dispatch contract as Form B — ADR-0024 §Phase D Form-C
|
||||||
|
// type-awareness). Form C values are now type-checked at parse
|
||||||
|
// time, not only at bind time.
|
||||||
|
// - Otherwise (column-name identifier, or an empty paren) →
|
||||||
|
// Form A → a repeated column-name list. The idents write
|
||||||
|
// `WalkContext::user_listed_columns` so the trailing
|
||||||
|
// `values (…)` slots mirror the user's selection.
|
||||||
|
|
||||||
const INSERT_PAREN_ITEM_CHOICES: &[Node] = &[
|
/// Form A's column-name slot. `static` (not `const`) so the
|
||||||
// VALUE_LITERAL first so that `true`/`false`/`null` match
|
/// `insert_first_paren` factory can take a `&'static` reference
|
||||||
// their Word branch rather than the broader Ident{Columns}
|
/// to it when building the repeated list at walk time.
|
||||||
// catch-all (consume_ident doesn't filter against the
|
static FORM_A_COLUMN: Node = Node::Ident {
|
||||||
// keyword set; without this ordering, `(true)` would lex
|
source: IdentSource::Columns,
|
||||||
// as a column-name list).
|
role: "insert_first_item",
|
||||||
VALUE_LITERAL,
|
validator: None,
|
||||||
Node::Ident {
|
highlight_override: None,
|
||||||
source: IdentSource::Columns,
|
writes_table: false,
|
||||||
role: "insert_first_item",
|
writes_column: false,
|
||||||
validator: None,
|
writes_user_listed_column: true,
|
||||||
highlight_override: None,
|
|
||||||
writes_table: false,
|
|
||||||
writes_column: false,
|
|
||||||
// Form A signal: when the user lists explicit columns
|
|
||||||
// in `insert into <T> (col1, col2, …)`, the walker
|
|
||||||
// appends each matched name to
|
|
||||||
// `WalkContext::user_listed_columns`. The inner
|
|
||||||
// `values (…)` slot list then mirrors that user
|
|
||||||
// selection instead of the auto-filtered default
|
|
||||||
// (ADR-0024 §Phase D §column_value_list).
|
|
||||||
writes_user_listed_column: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const INSERT_PAREN_ITEM: Node = Node::Choice(INSERT_PAREN_ITEM_CHOICES);
|
|
||||||
const INSERT_PAREN_LIST: Node = Node::Repeated {
|
|
||||||
inner: &INSERT_PAREN_ITEM,
|
|
||||||
separator: Some(&Node::Punct(',')),
|
|
||||||
min: 1,
|
|
||||||
};
|
};
|
||||||
|
static INSERT_COMMA: Node = Node::Punct(',');
|
||||||
|
|
||||||
|
/// First-paren resolver (ADR-0024 §Phase D Form-C type-awareness).
|
||||||
|
/// Peeks the first token after `(` to route to Form A's
|
||||||
|
/// column-name list or Form C's typed value list.
|
||||||
|
fn insert_first_paren(_ctx: &WalkContext, source: &str, pos: usize) -> Node {
|
||||||
|
if first_paren_item_is_value_literal(source, pos) {
|
||||||
|
// Form C — bare value list. `column_value_list` with no
|
||||||
|
// user-listed columns dispatches per non-auto-generated
|
||||||
|
// column, exactly as Form B does.
|
||||||
|
Node::DynamicSubgrammar(column_value_list)
|
||||||
|
} else {
|
||||||
|
// Form A (or Form A in progress / empty paren).
|
||||||
|
Node::Repeated {
|
||||||
|
inner: &FORM_A_COLUMN,
|
||||||
|
separator: Some(&INSERT_COMMA),
|
||||||
|
min: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// True when the first token after the insert `(` is a
|
||||||
|
/// value literal — the signal that the paren is a Form C value
|
||||||
|
/// list rather than a Form A column-name list. An empty paren
|
||||||
|
/// or an identifier-shaped token (a column name) returns false.
|
||||||
|
fn first_paren_item_is_value_literal(source: &str, pos: usize) -> bool {
|
||||||
|
use crate::dsl::walker::lex_helpers::{
|
||||||
|
consume_ident, consume_number_literal, consume_string_literal,
|
||||||
|
skip_whitespace,
|
||||||
|
};
|
||||||
|
let p = skip_whitespace(source, pos);
|
||||||
|
if p >= source.len() {
|
||||||
|
return false; // empty paren — treat as Form A
|
||||||
|
}
|
||||||
|
if consume_string_literal(source, p).is_some() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if consume_number_literal(source, p).is_some() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if let Some((s, e)) = consume_ident(source, p) {
|
||||||
|
let word = &source[s..e];
|
||||||
|
// `null` / `true` / `false` are value literals; any
|
||||||
|
// other identifier is a column name (Form A).
|
||||||
|
return word.eq_ignore_ascii_case("null")
|
||||||
|
|| word.eq_ignore_ascii_case("true")
|
||||||
|
|| word.eq_ignore_ascii_case("false");
|
||||||
|
}
|
||||||
|
false // punctuation (e.g. `)`) — treat as Form A
|
||||||
|
}
|
||||||
|
|
||||||
|
const INSERT_PAREN_LIST: Node = Node::Lookahead(insert_first_paren);
|
||||||
|
|
||||||
/// Schema-aware value list: when the walker has a populated
|
/// Schema-aware value list: when the walker has a populated
|
||||||
/// `current_table_columns`, unfolds to a `Seq` of typed slots
|
/// `current_table_columns`, unfolds to a `Seq` of typed slots
|
||||||
|
|||||||
+12
-1
@@ -286,9 +286,20 @@ pub enum Node {
|
|||||||
min: usize,
|
min: usize,
|
||||||
},
|
},
|
||||||
/// Resolves at walk time using the active `WalkContext`.
|
/// Resolves at walk time using the active `WalkContext`.
|
||||||
/// Phase D+ uses this for `column_value_list`.
|
/// Phase D+ uses this for `column_value_list`. The factory
|
||||||
|
/// is pure in `ctx`, so the walker memoizes the resolution
|
||||||
|
/// (one leak per distinct schema shape).
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
DynamicSubgrammar(fn(&WalkContext) -> Self),
|
DynamicSubgrammar(fn(&WalkContext) -> Self),
|
||||||
|
/// Like `DynamicSubgrammar` but the factory also sees the
|
||||||
|
/// source and the current byte position, so it can look
|
||||||
|
/// ahead. Used by the insert first-paren to discriminate
|
||||||
|
/// Form A (`(cols) values (...)`) from Form C (`(vals)`)
|
||||||
|
/// before walking the contents — Form C then routes through
|
||||||
|
/// the typed `column_value_list` (ADR-0024 §Phase D, Form C
|
||||||
|
/// type-awareness). Not memoized: the output depends on the
|
||||||
|
/// source, not just `ctx`.
|
||||||
|
Lookahead(fn(&WalkContext, &str, usize) -> Self),
|
||||||
/// Typed value-literal slot (ADR-0024 §Phase D §typed-value-slots).
|
/// Typed value-literal slot (ADR-0024 §Phase D §typed-value-slots).
|
||||||
///
|
///
|
||||||
/// Walks `inner` to consume the literal but records the
|
/// Walks `inner` to consume the literal but records the
|
||||||
|
|||||||
@@ -212,6 +212,20 @@ fn walk_node_inner(
|
|||||||
let resolved = resolve_dynamic(*factory, ctx);
|
let resolved = resolve_dynamic(*factory, ctx);
|
||||||
walk_node(source, pos, resolved, ctx, path, per_byte)
|
walk_node(source, pos, resolved, ctx, path, per_byte)
|
||||||
}
|
}
|
||||||
|
Node::Lookahead(factory) => {
|
||||||
|
// ADR-0024 §Phase D Form-C type-awareness: the
|
||||||
|
// factory peeks the source at `pos` (e.g. to tell a
|
||||||
|
// Form A column list from a Form C value list) and
|
||||||
|
// returns the shape to walk. Not memoized — the
|
||||||
|
// result depends on the source — but the factory
|
||||||
|
// returns a small node (a Repeated, or a thin
|
||||||
|
// DynamicSubgrammar wrapper that delegates to the
|
||||||
|
// memoized `column_value_list`), so the per-walk
|
||||||
|
// leak is a few bytes, not a whole typed tree.
|
||||||
|
let resolved: &'static Node =
|
||||||
|
Box::leak(Box::new(factory(ctx, source, pos)));
|
||||||
|
walk_node(source, pos, resolved, ctx, path, per_byte)
|
||||||
|
}
|
||||||
Node::TypedValueSlot {
|
Node::TypedValueSlot {
|
||||||
ty,
|
ty,
|
||||||
column_name,
|
column_name,
|
||||||
|
|||||||
@@ -1,24 +1,30 @@
|
|||||||
//! Matrix coverage for `insert into T (vals)` (Form C — bare
|
//! Matrix coverage for `insert into T (vals)` (Form C — bare
|
||||||
//! value list, no `values` keyword).
|
//! value list, no `values` keyword).
|
||||||
//!
|
//!
|
||||||
//! Form C shares the `( ... )` opener with Form A but resolves
|
//! Form C and Form B produce the identical AST and dispatch
|
||||||
//! the paren contents as values rather than column names. Per
|
//! identically (`Insert { columns: None, … }`). As of the
|
||||||
//! handoff-12 §2.2 the Form C path is *type-unaware* — its
|
//! Form-C type-awareness work (handoff-14), Form C's paren is
|
||||||
//! grammar uses the schemaless `INSERT_PAREN_LIST` shape, not
|
//! resolved by the `insert_first_paren` lookahead: a value
|
||||||
//! the typed `column_value_list`. Type validation happens at
|
//! literal as the first token routes the contents through the
|
||||||
//! bind time, not parse time.
|
//! typed `column_value_list` — the same per-column typed slots
|
||||||
|
//! Form B uses. So Form C values are now type-checked and
|
||||||
|
//! count-checked at parse time, not only at bind time.
|
||||||
//!
|
//!
|
||||||
//! The previous commit's Form C/A disambiguation means
|
//! An identifier (column name) as the first token, or an empty
|
||||||
//! column-shaped items (idents) inside the parens now flag as
|
//! paren, routes to Form A instead — `insert into T (Name)`
|
||||||
//! "did you mean Form A?". This file pins both the happy-path
|
//! still surfaces the "did you mean Form A?" recovery.
|
||||||
//! (literals only) and the Form-A-recovery (column-shaped
|
|
||||||
//! items).
|
|
||||||
|
|
||||||
use crate::typing_surface::*;
|
use crate::typing_surface::*;
|
||||||
use rdbms_playground::input_render::InputState;
|
use rdbms_playground::input_render::InputState;
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// Form C happy path: type-correct values parse to Insert.
|
||||||
|
// =========================================================
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn form_c_with_text_literals_parses() {
|
fn form_c_text_pk_correct_values_parses() {
|
||||||
|
// Items(Code:text, Title:text) — Form C expects two text
|
||||||
|
// values (no auto-gen columns to skip).
|
||||||
let schema = schema_text_pk();
|
let schema = schema_text_pk();
|
||||||
let a = assess_at_end(
|
let a = assess_at_end(
|
||||||
"insert into Items ('SKU-1', 'Widget')",
|
"insert into Items ('SKU-1', 'Widget')",
|
||||||
@@ -26,32 +32,128 @@ fn form_c_with_text_literals_parses() {
|
|||||||
);
|
);
|
||||||
assert!(matches!(a.state, InputState::Valid));
|
assert!(matches!(a.state, InputState::Valid));
|
||||||
assert_eq!(a.parse_result.as_deref(), Ok("Insert"));
|
assert_eq!(a.parse_result.as_deref(), Ok("Insert"));
|
||||||
crate::snap!("form_c_text_literals", a);
|
crate::snap!("form_c_text_pk_valid", a);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn form_c_with_mixed_literals_parses() {
|
fn form_c_serial_pk_correct_values_parses() {
|
||||||
|
// Customers(id:serial, Name:text, Email:text) — Form C
|
||||||
|
// skips the serial `id`, expects two text values.
|
||||||
let schema = schema_serial_pk();
|
let schema = schema_serial_pk();
|
||||||
let a = assess_at_end(
|
let a = assess_at_end(
|
||||||
"insert into Customers (1, 'Alice', 'a@b.c')",
|
"insert into Customers ('Alice', 'a@b.c')",
|
||||||
&schema,
|
&schema,
|
||||||
);
|
);
|
||||||
assert!(matches!(a.state, InputState::Valid));
|
assert!(matches!(a.state, InputState::Valid));
|
||||||
assert_eq!(a.parse_result.as_deref(), Ok("Insert"));
|
assert_eq!(a.parse_result.as_deref(), Ok("Insert"));
|
||||||
crate::snap!("form_c_mixed_literals", a);
|
crate::snap!("form_c_serial_pk_valid", a);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn form_c_with_null_first_parses() {
|
fn form_c_with_null_value_parses() {
|
||||||
|
// null is type-compatible with any slot.
|
||||||
let schema = schema_serial_pk();
|
let schema = schema_serial_pk();
|
||||||
let a = assess_at_end(
|
let a = assess_at_end(
|
||||||
"insert into Customers (null, 'Alice', 'a@b.c')",
|
"insert into Customers (null, 'a@b.c')",
|
||||||
&schema,
|
&schema,
|
||||||
);
|
);
|
||||||
assert!(matches!(a.state, InputState::Valid));
|
assert!(matches!(a.state, InputState::Valid));
|
||||||
crate::snap!("form_c_null_first", a);
|
crate::snap!("form_c_null_value", a);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// Form C is now type-aware (the §2.2 limitation is fixed).
|
||||||
|
// =========================================================
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn form_c_rejects_number_for_text_column() {
|
||||||
|
// `3.14` lands in the Name(text) slot — the typed slot
|
||||||
|
// rejects it at parse time. Before Form-C type-awareness
|
||||||
|
// this parsed Valid and only failed at bind time.
|
||||||
|
let schema = schema_serial_pk();
|
||||||
|
let a = assess_at_end(
|
||||||
|
"insert into Customers (3.14, 'a@b.c')",
|
||||||
|
&schema,
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!matches!(a.state, InputState::Valid),
|
||||||
|
"Form C should now type-check `3.14` against Name(text), got {:?}",
|
||||||
|
a.state,
|
||||||
|
);
|
||||||
|
crate::snap!("form_c_type_mismatch", a);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn form_c_wrong_value_count_is_invalid() {
|
||||||
|
// Customers Form C expects exactly two values (id:serial
|
||||||
|
// skipped). Three values is a count mismatch — caught at
|
||||||
|
// parse time now.
|
||||||
|
let schema = schema_serial_pk();
|
||||||
|
let a = assess_at_end(
|
||||||
|
"insert into Customers ('Alice', 'a@b.c', 'extra')",
|
||||||
|
&schema,
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!matches!(a.state, InputState::Valid),
|
||||||
|
"Form C with too many values must be invalid, got {:?}",
|
||||||
|
a.state,
|
||||||
|
);
|
||||||
|
crate::snap!("form_c_wrong_count", a);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// Form C typed-slot prose — the per-column hint Form B has
|
||||||
|
// is now available in Form C too.
|
||||||
|
// =========================================================
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn form_c_second_slot_shows_typed_prose_for_column() {
|
||||||
|
// First token `'Alice'` is a string literal → Form C. At
|
||||||
|
// the second slot the hint names the Email column.
|
||||||
|
let schema = schema_serial_pk();
|
||||||
|
let a = assess_at_end(
|
||||||
|
"insert into Customers ('Alice', ",
|
||||||
|
&schema,
|
||||||
|
);
|
||||||
|
let prose = hint_prose(&a).unwrap_or_else(|| {
|
||||||
|
panic!("expected Prose at Form C second slot, got {:?}", a.hint)
|
||||||
|
});
|
||||||
|
assert!(
|
||||||
|
prose.contains("Email"),
|
||||||
|
"Form C second slot should name `Email`, got prose: {prose:?}",
|
||||||
|
);
|
||||||
|
crate::snap!("form_c_typed_prose", a);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// In-progress Form C classifies as IncompleteAtEof.
|
||||||
|
// =========================================================
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn form_c_in_progress_after_comma_is_incomplete() {
|
||||||
|
let schema = schema_serial_pk();
|
||||||
|
let a = assess_at_end("insert into Customers ('Alice', ", &schema);
|
||||||
|
assert!(matches!(a.state, InputState::IncompleteAtEof));
|
||||||
|
crate::snap!("form_c_in_progress_after_comma", a);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn form_c_in_progress_without_close_paren_is_incomplete() {
|
||||||
|
let schema = schema_serial_pk();
|
||||||
|
let a = assess_at_end(
|
||||||
|
"insert into Customers ('Alice', 'a@b.c'",
|
||||||
|
&schema,
|
||||||
|
);
|
||||||
|
assert!(matches!(a.state, InputState::IncompleteAtEof));
|
||||||
|
crate::snap!("form_c_in_progress_no_close", a);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// Form A recovery: a column-name identifier as the first
|
||||||
|
// paren token routes to Form A — `insert into T (Name)`
|
||||||
|
// without `values` flags as Form-A-in-progress.
|
||||||
|
// =========================================================
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn form_c_with_column_shaped_item_flags_as_form_a_in_progress() {
|
fn form_c_with_column_shaped_item_flags_as_form_a_in_progress() {
|
||||||
let schema = schema_serial_pk();
|
let schema = schema_serial_pk();
|
||||||
@@ -73,37 +175,3 @@ fn form_c_with_two_columns_flags_as_form_a_in_progress() {
|
|||||||
assert_candidate_present(&a, &["values"]);
|
assert_candidate_present(&a, &["values"]);
|
||||||
crate::snap!("form_c_two_columns_recovery", a);
|
crate::snap!("form_c_two_columns_recovery", a);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn form_c_type_unaware_grammar_accepts_decimal_for_int_column() {
|
|
||||||
// Form C's grammar uses INSERT_PAREN_LIST (the pre-Phase-D
|
|
||||||
// schemaless choice), so type mismatches aren't caught at
|
|
||||||
// parse time. Bind time catches them. Handoff §2.2
|
|
||||||
// documents this as known.
|
|
||||||
let schema = schema_serial_pk();
|
|
||||||
let a = assess_at_end(
|
|
||||||
"insert into Customers (3.14, 'Alice', 'a@b.c')",
|
|
||||||
&schema,
|
|
||||||
);
|
|
||||||
assert!(matches!(a.state, InputState::Valid));
|
|
||||||
crate::snap!("form_c_type_unaware", a);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn form_c_in_progress_after_comma_is_incomplete() {
|
|
||||||
let schema = schema_serial_pk();
|
|
||||||
let a = assess_at_end("insert into Customers (1, ", &schema);
|
|
||||||
assert!(matches!(a.state, InputState::IncompleteAtEof));
|
|
||||||
crate::snap!("form_c_in_progress_after_comma", a);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn form_c_in_progress_without_close_paren_is_incomplete() {
|
|
||||||
let schema = schema_serial_pk();
|
|
||||||
let a = assess_at_end(
|
|
||||||
"insert into Customers (1, 'Alice'",
|
|
||||||
&schema,
|
|
||||||
);
|
|
||||||
assert!(matches!(a.state, InputState::IncompleteAtEof));
|
|
||||||
crate::snap!("form_c_in_progress_no_close", a);
|
|
||||||
}
|
|
||||||
|
|||||||
+13
-15
@@ -8,9 +8,19 @@ Assessment {
|
|||||||
cursor: 23,
|
cursor: 23,
|
||||||
state: IncompleteAtEof,
|
state: IncompleteAtEof,
|
||||||
hint: Some(
|
hint: Some(
|
||||||
Prose(
|
Candidates {
|
||||||
"Type a value: number, 'text', true/false, null (dates as 'YYYY-MM-DD', datetimes as 'YYYY-MM-DDTHH:MM:SS')",
|
items: [
|
||||||
),
|
Candidate {
|
||||||
|
text: "Name",
|
||||||
|
kind: Identifier,
|
||||||
|
},
|
||||||
|
Candidate {
|
||||||
|
text: "id",
|
||||||
|
kind: Identifier,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
selected: None,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
completion: Some(
|
completion: Some(
|
||||||
Completion {
|
Completion {
|
||||||
@@ -20,18 +30,6 @@ Assessment {
|
|||||||
),
|
),
|
||||||
partial_prefix: "",
|
partial_prefix: "",
|
||||||
candidates: [
|
candidates: [
|
||||||
Candidate {
|
|
||||||
text: "null",
|
|
||||||
kind: Keyword,
|
|
||||||
},
|
|
||||||
Candidate {
|
|
||||||
text: "true",
|
|
||||||
kind: Keyword,
|
|
||||||
},
|
|
||||||
Candidate {
|
|
||||||
text: "false",
|
|
||||||
kind: Keyword,
|
|
||||||
},
|
|
||||||
Candidate {
|
Candidate {
|
||||||
text: "Name",
|
text: "Name",
|
||||||
kind: Identifier,
|
kind: Identifier,
|
||||||
|
|||||||
+17
-15
@@ -8,9 +8,23 @@ Assessment {
|
|||||||
cursor: 23,
|
cursor: 23,
|
||||||
state: IncompleteAtEof,
|
state: IncompleteAtEof,
|
||||||
hint: Some(
|
hint: Some(
|
||||||
Prose(
|
Candidates {
|
||||||
"Type a value: number, 'text', true/false, null (dates as 'YYYY-MM-DD', datetimes as 'YYYY-MM-DDTHH:MM:SS')",
|
items: [
|
||||||
),
|
Candidate {
|
||||||
|
text: "Email",
|
||||||
|
kind: Identifier,
|
||||||
|
},
|
||||||
|
Candidate {
|
||||||
|
text: "Name",
|
||||||
|
kind: Identifier,
|
||||||
|
},
|
||||||
|
Candidate {
|
||||||
|
text: "id",
|
||||||
|
kind: Identifier,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
selected: None,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
completion: Some(
|
completion: Some(
|
||||||
Completion {
|
Completion {
|
||||||
@@ -20,18 +34,6 @@ Assessment {
|
|||||||
),
|
),
|
||||||
partial_prefix: "",
|
partial_prefix: "",
|
||||||
candidates: [
|
candidates: [
|
||||||
Candidate {
|
|
||||||
text: "null",
|
|
||||||
kind: Keyword,
|
|
||||||
},
|
|
||||||
Candidate {
|
|
||||||
text: "true",
|
|
||||||
kind: Keyword,
|
|
||||||
},
|
|
||||||
Candidate {
|
|
||||||
text: "false",
|
|
||||||
kind: Keyword,
|
|
||||||
},
|
|
||||||
Candidate {
|
Candidate {
|
||||||
text: "Email",
|
text: "Email",
|
||||||
kind: Identifier,
|
kind: Identifier,
|
||||||
|
|||||||
+13
-15
@@ -8,9 +8,19 @@ Assessment {
|
|||||||
cursor: 19,
|
cursor: 19,
|
||||||
state: IncompleteAtEof,
|
state: IncompleteAtEof,
|
||||||
hint: Some(
|
hint: Some(
|
||||||
Prose(
|
Candidates {
|
||||||
"Type a value: number, 'text', true/false, null (dates as 'YYYY-MM-DD', datetimes as 'YYYY-MM-DDTHH:MM:SS')",
|
items: [
|
||||||
),
|
Candidate {
|
||||||
|
text: "Code",
|
||||||
|
kind: Identifier,
|
||||||
|
},
|
||||||
|
Candidate {
|
||||||
|
text: "Title",
|
||||||
|
kind: Identifier,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
selected: None,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
completion: Some(
|
completion: Some(
|
||||||
Completion {
|
Completion {
|
||||||
@@ -20,18 +30,6 @@ Assessment {
|
|||||||
),
|
),
|
||||||
partial_prefix: "",
|
partial_prefix: "",
|
||||||
candidates: [
|
candidates: [
|
||||||
Candidate {
|
|
||||||
text: "null",
|
|
||||||
kind: Keyword,
|
|
||||||
},
|
|
||||||
Candidate {
|
|
||||||
text: "true",
|
|
||||||
kind: Keyword,
|
|
||||||
},
|
|
||||||
Candidate {
|
|
||||||
text: "false",
|
|
||||||
kind: Keyword,
|
|
||||||
},
|
|
||||||
Candidate {
|
Candidate {
|
||||||
text: "Code",
|
text: "Code",
|
||||||
kind: Identifier,
|
kind: Identifier,
|
||||||
|
|||||||
+17
-15
@@ -8,9 +8,23 @@ Assessment {
|
|||||||
cursor: 20,
|
cursor: 20,
|
||||||
state: IncompleteAtEof,
|
state: IncompleteAtEof,
|
||||||
hint: Some(
|
hint: Some(
|
||||||
Prose(
|
Candidates {
|
||||||
"Type a value: number, 'text', true/false, null (dates as 'YYYY-MM-DD', datetimes as 'YYYY-MM-DDTHH:MM:SS')",
|
items: [
|
||||||
),
|
Candidate {
|
||||||
|
text: "CustId",
|
||||||
|
kind: Identifier,
|
||||||
|
},
|
||||||
|
Candidate {
|
||||||
|
text: "OrderId",
|
||||||
|
kind: Identifier,
|
||||||
|
},
|
||||||
|
Candidate {
|
||||||
|
text: "Total",
|
||||||
|
kind: Identifier,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
selected: None,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
completion: Some(
|
completion: Some(
|
||||||
Completion {
|
Completion {
|
||||||
@@ -20,18 +34,6 @@ Assessment {
|
|||||||
),
|
),
|
||||||
partial_prefix: "",
|
partial_prefix: "",
|
||||||
candidates: [
|
candidates: [
|
||||||
Candidate {
|
|
||||||
text: "null",
|
|
||||||
kind: Keyword,
|
|
||||||
},
|
|
||||||
Candidate {
|
|
||||||
text: "true",
|
|
||||||
kind: Keyword,
|
|
||||||
},
|
|
||||||
Candidate {
|
|
||||||
text: "false",
|
|
||||||
kind: Keyword,
|
|
||||||
},
|
|
||||||
Candidate {
|
Candidate {
|
||||||
text: "CustId",
|
text: "CustId",
|
||||||
kind: Identifier,
|
kind: Identifier,
|
||||||
|
|||||||
+17
-15
@@ -8,9 +8,23 @@ Assessment {
|
|||||||
cursor: 27,
|
cursor: 27,
|
||||||
state: IncompleteAtEof,
|
state: IncompleteAtEof,
|
||||||
hint: Some(
|
hint: Some(
|
||||||
Prose(
|
Candidates {
|
||||||
"Type a value: number, 'text', true/false, null (dates as 'YYYY-MM-DD', datetimes as 'YYYY-MM-DDTHH:MM:SS')",
|
items: [
|
||||||
),
|
Candidate {
|
||||||
|
text: "Email",
|
||||||
|
kind: Identifier,
|
||||||
|
},
|
||||||
|
Candidate {
|
||||||
|
text: "Name",
|
||||||
|
kind: Identifier,
|
||||||
|
},
|
||||||
|
Candidate {
|
||||||
|
text: "id",
|
||||||
|
kind: Identifier,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
selected: None,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
completion: Some(
|
completion: Some(
|
||||||
Completion {
|
Completion {
|
||||||
@@ -20,18 +34,6 @@ Assessment {
|
|||||||
),
|
),
|
||||||
partial_prefix: "",
|
partial_prefix: "",
|
||||||
candidates: [
|
candidates: [
|
||||||
Candidate {
|
|
||||||
text: "null",
|
|
||||||
kind: Keyword,
|
|
||||||
},
|
|
||||||
Candidate {
|
|
||||||
text: "true",
|
|
||||||
kind: Keyword,
|
|
||||||
},
|
|
||||||
Candidate {
|
|
||||||
text: "false",
|
|
||||||
kind: Keyword,
|
|
||||||
},
|
|
||||||
Candidate {
|
Candidate {
|
||||||
text: "Email",
|
text: "Email",
|
||||||
kind: Identifier,
|
kind: Identifier,
|
||||||
|
|||||||
+6
-26
@@ -1,22 +1,22 @@
|
|||||||
---
|
---
|
||||||
source: tests/typing_surface/insert_form_c.rs
|
source: tests/typing_surface/insert_form_c.rs
|
||||||
description: "input=\"insert into Customers (1, \" cursor=26"
|
description: "input=\"insert into Customers ('Alice', \" cursor=32"
|
||||||
expression: "& a"
|
expression: "& a"
|
||||||
---
|
---
|
||||||
Assessment {
|
Assessment {
|
||||||
input: "insert into Customers (1, ",
|
input: "insert into Customers ('Alice', ",
|
||||||
cursor: 26,
|
cursor: 32,
|
||||||
state: IncompleteAtEof,
|
state: IncompleteAtEof,
|
||||||
hint: Some(
|
hint: Some(
|
||||||
Prose(
|
Prose(
|
||||||
"Type a value: number, 'text', true/false, null (dates as 'YYYY-MM-DD', datetimes as 'YYYY-MM-DDTHH:MM:SS')",
|
"for `Email`: Type a quoted string (e.g. 'Alice') or null",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
completion: Some(
|
completion: Some(
|
||||||
Completion {
|
Completion {
|
||||||
replaced_range: (
|
replaced_range: (
|
||||||
26,
|
32,
|
||||||
26,
|
32,
|
||||||
),
|
),
|
||||||
partial_prefix: "",
|
partial_prefix: "",
|
||||||
candidates: [
|
candidates: [
|
||||||
@@ -24,26 +24,6 @@ Assessment {
|
|||||||
text: "null",
|
text: "null",
|
||||||
kind: Keyword,
|
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,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
+3
-3
@@ -1,11 +1,11 @@
|
|||||||
---
|
---
|
||||||
source: tests/typing_surface/insert_form_c.rs
|
source: tests/typing_surface/insert_form_c.rs
|
||||||
description: "input=\"insert into Customers (1, 'Alice'\" cursor=33"
|
description: "input=\"insert into Customers ('Alice', 'a@b.c'\" cursor=39"
|
||||||
expression: "& a"
|
expression: "& a"
|
||||||
---
|
---
|
||||||
Assessment {
|
Assessment {
|
||||||
input: "insert into Customers (1, 'Alice'",
|
input: "insert into Customers ('Alice', 'a@b.c'",
|
||||||
cursor: 33,
|
cursor: 39,
|
||||||
state: IncompleteAtEof,
|
state: IncompleteAtEof,
|
||||||
hint: Some(
|
hint: Some(
|
||||||
Prose(
|
Prose(
|
||||||
|
|||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
source: tests/typing_surface/insert_form_c.rs
|
||||||
|
description: "input=\"insert into Customers (3.14, 'a@b.c')\" cursor=37"
|
||||||
|
expression: "& a"
|
||||||
|
---
|
||||||
|
Assessment {
|
||||||
|
input: "insert into Customers (3.14, 'a@b.c')",
|
||||||
|
cursor: 37,
|
||||||
|
state: DefiniteErrorAt(
|
||||||
|
23,
|
||||||
|
),
|
||||||
|
hint: Some(
|
||||||
|
Prose(
|
||||||
|
"for `Name`: Type a quoted string (e.g. 'Alice') or null (`id` auto-generated — skipped here; list columns explicitly, e.g. `insert into T (...) values (...)`, to set it.)",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
completion: Some(
|
||||||
|
Completion {
|
||||||
|
replaced_range: (
|
||||||
|
37,
|
||||||
|
37,
|
||||||
|
),
|
||||||
|
partial_prefix: "",
|
||||||
|
candidates: [
|
||||||
|
Candidate {
|
||||||
|
text: "null",
|
||||||
|
kind: Keyword,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
parse_result: Err(
|
||||||
|
"Invalid(definite)",
|
||||||
|
),
|
||||||
|
}
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
---
|
||||||
|
source: tests/typing_surface/insert_form_c.rs
|
||||||
|
description: "input=\"insert into Customers ('Alice', \" cursor=32"
|
||||||
|
expression: "& a"
|
||||||
|
---
|
||||||
|
Assessment {
|
||||||
|
input: "insert into Customers ('Alice', ",
|
||||||
|
cursor: 32,
|
||||||
|
state: IncompleteAtEof,
|
||||||
|
hint: Some(
|
||||||
|
Prose(
|
||||||
|
"for `Email`: Type a quoted string (e.g. 'Alice') or null",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
completion: Some(
|
||||||
|
Completion {
|
||||||
|
replaced_range: (
|
||||||
|
32,
|
||||||
|
32,
|
||||||
|
),
|
||||||
|
partial_prefix: "",
|
||||||
|
candidates: [
|
||||||
|
Candidate {
|
||||||
|
text: "null",
|
||||||
|
kind: Keyword,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
parse_result: Err(
|
||||||
|
"Invalid(at_eof)",
|
||||||
|
),
|
||||||
|
}
|
||||||
+5
-5
@@ -1,11 +1,11 @@
|
|||||||
---
|
---
|
||||||
source: tests/typing_surface/insert_form_c.rs
|
source: tests/typing_surface/insert_form_c.rs
|
||||||
description: "input=\"insert into Customers (1, 'Alice', 'a@b.c')\" cursor=43"
|
description: "input=\"insert into Customers ('Alice', 'a@b.c')\" cursor=40"
|
||||||
expression: "& a"
|
expression: "& a"
|
||||||
---
|
---
|
||||||
Assessment {
|
Assessment {
|
||||||
input: "insert into Customers (1, 'Alice', 'a@b.c')",
|
input: "insert into Customers ('Alice', 'a@b.c')",
|
||||||
cursor: 43,
|
cursor: 40,
|
||||||
state: Valid,
|
state: Valid,
|
||||||
hint: Some(
|
hint: Some(
|
||||||
Candidates {
|
Candidates {
|
||||||
@@ -21,8 +21,8 @@ Assessment {
|
|||||||
completion: Some(
|
completion: Some(
|
||||||
Completion {
|
Completion {
|
||||||
replaced_range: (
|
replaced_range: (
|
||||||
43,
|
40,
|
||||||
43,
|
40,
|
||||||
),
|
),
|
||||||
partial_prefix: "",
|
partial_prefix: "",
|
||||||
candidates: [
|
candidates: [
|
||||||
-39
@@ -1,39 +0,0 @@
|
|||||||
---
|
|
||||||
source: tests/typing_surface/insert_form_c.rs
|
|
||||||
description: "input=\"insert into Customers (null, 'Alice', 'a@b.c')\" cursor=46"
|
|
||||||
expression: "& a"
|
|
||||||
---
|
|
||||||
Assessment {
|
|
||||||
input: "insert into Customers (null, 'Alice', 'a@b.c')",
|
|
||||||
cursor: 46,
|
|
||||||
state: Valid,
|
|
||||||
hint: Some(
|
|
||||||
Candidates {
|
|
||||||
items: [
|
|
||||||
Candidate {
|
|
||||||
text: "values",
|
|
||||||
kind: Keyword,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
selected: None,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
completion: Some(
|
|
||||||
Completion {
|
|
||||||
replaced_range: (
|
|
||||||
46,
|
|
||||||
46,
|
|
||||||
),
|
|
||||||
partial_prefix: "",
|
|
||||||
candidates: [
|
|
||||||
Candidate {
|
|
||||||
text: "values",
|
|
||||||
kind: Keyword,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
parse_result: Ok(
|
|
||||||
"Insert",
|
|
||||||
),
|
|
||||||
}
|
|
||||||
+5
-5
@@ -1,11 +1,11 @@
|
|||||||
---
|
---
|
||||||
source: tests/typing_surface/insert_form_c.rs
|
source: tests/typing_surface/insert_form_c.rs
|
||||||
description: "input=\"insert into Customers (3.14, 'Alice', 'a@b.c')\" cursor=46"
|
description: "input=\"insert into Customers (null, 'a@b.c')\" cursor=37"
|
||||||
expression: "& a"
|
expression: "& a"
|
||||||
---
|
---
|
||||||
Assessment {
|
Assessment {
|
||||||
input: "insert into Customers (3.14, 'Alice', 'a@b.c')",
|
input: "insert into Customers (null, 'a@b.c')",
|
||||||
cursor: 46,
|
cursor: 37,
|
||||||
state: Valid,
|
state: Valid,
|
||||||
hint: Some(
|
hint: Some(
|
||||||
Candidates {
|
Candidates {
|
||||||
@@ -21,8 +21,8 @@ Assessment {
|
|||||||
completion: Some(
|
completion: Some(
|
||||||
Completion {
|
Completion {
|
||||||
replaced_range: (
|
replaced_range: (
|
||||||
46,
|
37,
|
||||||
46,
|
37,
|
||||||
),
|
),
|
||||||
partial_prefix: "",
|
partial_prefix: "",
|
||||||
candidates: [
|
candidates: [
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
---
|
||||||
|
source: tests/typing_surface/insert_form_c.rs
|
||||||
|
description: "input=\"insert into Customers ('Alice', 'a@b.c', 'extra')\" cursor=49"
|
||||||
|
expression: "& a"
|
||||||
|
---
|
||||||
|
Assessment {
|
||||||
|
input: "insert into Customers ('Alice', 'a@b.c', 'extra')",
|
||||||
|
cursor: 49,
|
||||||
|
state: DefiniteErrorAt(
|
||||||
|
39,
|
||||||
|
),
|
||||||
|
hint: Some(
|
||||||
|
Prose(
|
||||||
|
"Submit with Enter",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
completion: None,
|
||||||
|
parse_result: Err(
|
||||||
|
"Invalid(definite)",
|
||||||
|
),
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user