ADR-0024 Phase D (full): schema-aware value typing
Schema-aware typed value slots — the central design claim of
ADR-0024 §Phase D. Insert / update / delete value slots now
dispatch on the user-facing column type at parse time, rejecting
mis-shaped input with localised wording instead of waiting for
the bind-time error.
What changed:
**SchemaCache extension** (`src/completion.rs`):
- New `TableColumn { name, user_type }` for per-table column
metadata.
- `SchemaCache.table_columns: HashMap<String, Vec<TableColumn>>`.
- `SchemaCache::columns_for_table(name)` — case-insensitive
lookup, mirrors the walker's case-insensitive entry-word
resolution.
**WalkContext schema plumbing** (`src/dsl/walker/context.rs`):
- `WalkContext<'a>` gains a lifetime and a `schema: Option<&'a
SchemaCache>`. `WalkContext::new()` keeps the schemaless
default; `with_schema(s)` is the new schema-aware constructor.
**Parser entry point** (`src/dsl/parser.rs`):
- `parse_command_with_schema(input, schema)` is the new public
schema-aware variant. `parse_command(input)` becomes a thin
wrapper that delegates with `None` for back-compat.
- Internal `try_walker_route` accepts an `Option<&SchemaCache>`
and threads it into the WalkContext.
**Node::Ident writes_table/writes_column** (`src/dsl/grammar/mod.rs`):
- Two new fields on `Node::Ident`. When `writes_table: true` and
`source: Tables`, the walker writes the matched ident's name
into `current_table` and resolves `current_table_columns`
against the schema cache. When `writes_column: true` and
`source: Columns`, the walker writes the resolved
`TableColumn` into `current_column`.
**Walker driver DynamicSubgrammar dispatch** (`src/dsl/walker/driver.rs`):
- The `Node::DynamicSubgrammar(factory)` branch now resolves the
factory at walk time and `Box::leak`s the result so its inner
static-slice fields (Choice/Seq) have the lifetime the walker
expects (per ADR-0024 §sub-grammars). The leak is bounded by
command-shape complexity per walk; per-walk arena is a future
optimisation.
- `walk_ident` extends to perform the schema writes when the
flags are set.
**Typed value slot factories + dynamic sub-grammars** (`src/dsl/grammar/shared.rs`):
- `int_slot` / `real_slot` / `decimal_slot` / `bool_slot` /
`text_slot` / `date_slot` / `datetime_slot` / `blob_slot` —
one per `Type`. Each accepts the appropriate literal kind plus
`null`; integer-only validator rejects `3.14` at int columns;
decimal validator pins numeric shape.
- `slot_for_type(ty) -> Node` is the dispatcher.
- `current_column_value(ctx) -> Node` is the dynamic sub-grammar
for `set col = …` and `where col = …` values; reads
`current_column` and dispatches via `slot_for_type`.
- `column_value_list(ctx) -> Node` is the dynamic sub-grammar
for `insert into T values (…)`; reads `current_table_columns`
and unfolds a Seq of typed slots separated by commas.
- Both fall back to the schemaless `VALUE_LITERAL` choice when
the context lacks the schema-resolved entries — keeps
schemaless `parse_command` callers (tests, replay path)
working.
**Data-command grammar wires the new types** (`src/dsl/grammar/data.rs`):
- `TABLE_NAME_INSERT` / `TABLE_NAME_WRITES` (new): table-name
slots that set `writes_table: true`. Used by insert / update /
delete to populate `current_table_columns`.
- `SET_COLUMN` / `FILTER_COLUMN` (new): column-name slots in
`set col=…` / `where col=…` set `writes_column: true`.
- `INSERT_VALUES_LIST` becomes `DynamicSubgrammar(column_value_list)`.
- `UPDATE_ASSIGNMENT` and `WHERE_CLAUSE` use
`PER_COLUMN_VALUE = DynamicSubgrammar(current_column_value)`.
**Runtime plumbs schema-with-types** (`src/runtime.rs`):
- `refresh_schema_cache` calls `describe_table` for each table
and populates `SchemaCache::table_columns` with
`TableColumn { name, user_type }` entries. Best-effort: a
`describe_table` miss leaves that table unpopulated and the
walker falls back to schemaless dispatch.
**App dispatches with schema** (`src/app.rs`):
- `dispatch_dsl` routes through `parse_command_with_schema(&self
.schema_cache, …)` so live typing/dispatch sees the typed
slots. The replay path stays schemaless (deferred — replay
bind-time errors still catch type mismatches).
**Catalog** (`src/friendly/strings/en-US.yaml`, `src/friendly/keys.rs`):
- New `parse.custom.bind_type_mismatch` entry with `{found}` and
`{expected}` placeholders. Surfaced by the int_slot /
decimal_slot validators.
Tests:
- 11 new walker-side Phase D tests cover insert / update /
delete with schemas — typed acceptance per column, decimal
rejection at int columns, null acceptance at any slot,
multi-assignment per-column dispatch, schemaless fallback.
- The pre-existing `parse_command(input)` test suite (no
schema) still passes — the fallback path is behaviour-
preserving.
- 828 passing total, 0 failing, 1 ignored. Clippy clean.
This commit is contained in:
@@ -50,6 +50,8 @@ const IMPORT_AS_TARGET: Node = Node::Seq(&[
|
||||
role: "target",
|
||||
validator: None,
|
||||
highlight_override: None,
|
||||
writes_table: false,
|
||||
writes_column: false,
|
||||
},
|
||||
]);
|
||||
const IMPORT_AS_TARGET_OPT: Node = Node::Optional(&IMPORT_AS_TARGET);
|
||||
@@ -72,6 +74,8 @@ const MODE_CHOICES: &[Node] = &[
|
||||
role: "mode_value",
|
||||
validator: Some(UNKNOWN_MODE_VALIDATOR),
|
||||
highlight_override: None,
|
||||
writes_table: false,
|
||||
writes_column: false,
|
||||
},
|
||||
];
|
||||
const MODE_VALUE: Node = Node::Choice(MODE_CHOICES);
|
||||
@@ -84,6 +88,8 @@ const MESSAGES_CHOICES: &[Node] = &[
|
||||
role: "messages_value",
|
||||
validator: Some(UNKNOWN_MESSAGES_VALIDATOR),
|
||||
highlight_override: None,
|
||||
writes_table: false,
|
||||
writes_column: false,
|
||||
},
|
||||
];
|
||||
const MESSAGES_VALUE: Node = Node::Choice(MESSAGES_CHOICES);
|
||||
|
||||
+71
-22
@@ -19,6 +19,7 @@
|
||||
use crate::dsl::command::{Command, RowFilter};
|
||||
use crate::dsl::grammar::{
|
||||
CommandNode, IdentSource, Node, ValidationError, Word,
|
||||
shared::{column_value_list, current_column_value},
|
||||
};
|
||||
use crate::dsl::value::Value;
|
||||
use crate::dsl::walker::outcome::{MatchedItem, MatchedKind, MatchedPath};
|
||||
@@ -32,6 +33,21 @@ const TABLE_NAME_EXISTING: Node = Node::Ident {
|
||||
role: "table_name",
|
||||
validator: None,
|
||||
highlight_override: None,
|
||||
writes_table: false,
|
||||
writes_column: false,
|
||||
};
|
||||
|
||||
/// Table-name slot variant that populates
|
||||
/// `WalkContext::current_table_columns` (ADR-0024 §Phase D).
|
||||
/// Used by `insert into <T> …` so the inner value list can
|
||||
/// dispatch typed slots per column.
|
||||
const TABLE_NAME_INSERT: Node = Node::Ident {
|
||||
source: IdentSource::Tables,
|
||||
role: "table_name",
|
||||
validator: None,
|
||||
highlight_override: None,
|
||||
writes_table: true,
|
||||
writes_column: false,
|
||||
};
|
||||
|
||||
// `value_literal` — null / true / false / number / string. The
|
||||
@@ -90,6 +106,8 @@ const INSERT_PAREN_ITEM_CHOICES: &[Node] = &[
|
||||
role: "insert_first_item",
|
||||
validator: None,
|
||||
highlight_override: None,
|
||||
writes_table: false,
|
||||
writes_column: false,
|
||||
},
|
||||
];
|
||||
const INSERT_PAREN_ITEM: Node = Node::Choice(INSERT_PAREN_ITEM_CHOICES);
|
||||
@@ -99,11 +117,12 @@ const INSERT_PAREN_LIST: Node = Node::Repeated {
|
||||
min: 1,
|
||||
};
|
||||
|
||||
const INSERT_VALUES_LIST: Node = Node::Repeated {
|
||||
inner: &VALUE_LITERAL,
|
||||
separator: Some(&Node::Punct(',')),
|
||||
min: 1,
|
||||
};
|
||||
/// Schema-aware value list: when the walker has a populated
|
||||
/// `current_table_columns`, unfolds to a `Seq` of typed slots
|
||||
/// per column (`int_slot`, `text_slot`, …). When schemaless,
|
||||
/// falls back to the pre-Phase-D `Repeated(VALUE_LITERAL, ',', 1)`
|
||||
/// shape (ADR-0024 §Phase D §column_value_list).
|
||||
const INSERT_VALUES_LIST: Node = Node::DynamicSubgrammar(column_value_list);
|
||||
|
||||
const INSERT_OPTIONAL_VALUES_NODES: &[Node] = &[
|
||||
Node::Word(Word::keyword("values")),
|
||||
@@ -135,7 +154,7 @@ const INSERT_AFTER_TABLE: Node = Node::Choice(INSERT_AFTER_TABLE_CHOICES);
|
||||
|
||||
const INSERT_NODES: &[Node] = &[
|
||||
Node::Word(Word::keyword("into")),
|
||||
TABLE_NAME_EXISTING,
|
||||
TABLE_NAME_INSERT,
|
||||
INSERT_AFTER_TABLE,
|
||||
];
|
||||
const INSERT_SHAPE: Node = Node::Seq(INSERT_NODES);
|
||||
@@ -144,15 +163,50 @@ const INSERT_SHAPE: Node = Node::Seq(INSERT_NODES);
|
||||
// update — `update <T> set <col>=<v>[, <col>=<v>] (where … | --all-rows)`
|
||||
// =================================================================
|
||||
|
||||
/// Table-name slot that populates `current_table_columns` so
|
||||
/// the inner `set <col>=<value>` / `where <col>=<value>` slots
|
||||
/// can resolve column types (Phase D).
|
||||
const TABLE_NAME_WRITES: Node = Node::Ident {
|
||||
source: IdentSource::Tables,
|
||||
role: "table_name",
|
||||
validator: None,
|
||||
highlight_override: None,
|
||||
writes_table: true,
|
||||
writes_column: false,
|
||||
};
|
||||
|
||||
/// Column-name slot in `set col = …` — resolves the column's
|
||||
/// type into `current_column` so the value slot dispatches per
|
||||
/// column type (Phase D).
|
||||
const SET_COLUMN: Node = Node::Ident {
|
||||
source: IdentSource::Columns,
|
||||
role: "update_set_column",
|
||||
validator: None,
|
||||
highlight_override: None,
|
||||
writes_table: false,
|
||||
writes_column: true,
|
||||
};
|
||||
|
||||
/// Column-name slot in `where col = …` — same writes-column
|
||||
/// semantics as SET_COLUMN, distinct role for the AST builder.
|
||||
const FILTER_COLUMN: Node = Node::Ident {
|
||||
source: IdentSource::Columns,
|
||||
role: "filter_column",
|
||||
validator: None,
|
||||
highlight_override: None,
|
||||
writes_table: false,
|
||||
writes_column: true,
|
||||
};
|
||||
|
||||
/// Value slot resolved at walk time from
|
||||
/// `WalkContext::current_column`. Falls back to the schemaless
|
||||
/// value-literal choice when no current_column is bound.
|
||||
const PER_COLUMN_VALUE: Node = Node::DynamicSubgrammar(current_column_value);
|
||||
|
||||
const UPDATE_ASSIGNMENT_NODES: &[Node] = &[
|
||||
Node::Ident {
|
||||
source: IdentSource::Columns,
|
||||
role: "update_set_column",
|
||||
validator: None,
|
||||
highlight_override: None,
|
||||
},
|
||||
SET_COLUMN,
|
||||
Node::Punct('='),
|
||||
VALUE_LITERAL,
|
||||
PER_COLUMN_VALUE,
|
||||
];
|
||||
const UPDATE_ASSIGNMENT: Node = Node::Seq(UPDATE_ASSIGNMENT_NODES);
|
||||
const UPDATE_ASSIGNMENTS: Node = Node::Repeated {
|
||||
@@ -163,14 +217,9 @@ const UPDATE_ASSIGNMENTS: Node = Node::Repeated {
|
||||
|
||||
const WHERE_CLAUSE_NODES: &[Node] = &[
|
||||
Node::Word(Word::keyword("where")),
|
||||
Node::Ident {
|
||||
source: IdentSource::Columns,
|
||||
role: "filter_column",
|
||||
validator: None,
|
||||
highlight_override: None,
|
||||
},
|
||||
FILTER_COLUMN,
|
||||
Node::Punct('='),
|
||||
VALUE_LITERAL,
|
||||
PER_COLUMN_VALUE,
|
||||
];
|
||||
const WHERE_CLAUSE: Node = Node::Seq(WHERE_CLAUSE_NODES);
|
||||
|
||||
@@ -178,7 +227,7 @@ const FILTER_CHOICES: &[Node] = &[WHERE_CLAUSE, Node::Flag("all-rows")];
|
||||
const FILTER_CLAUSE: Node = Node::Choice(FILTER_CHOICES);
|
||||
|
||||
const UPDATE_NODES: &[Node] = &[
|
||||
TABLE_NAME_EXISTING,
|
||||
TABLE_NAME_WRITES,
|
||||
Node::Word(Word::keyword("set")),
|
||||
UPDATE_ASSIGNMENTS,
|
||||
FILTER_CLAUSE,
|
||||
@@ -191,7 +240,7 @@ const UPDATE_SHAPE: Node = Node::Seq(UPDATE_NODES);
|
||||
|
||||
const DELETE_NODES: &[Node] = &[
|
||||
Node::Word(Word::keyword("from")),
|
||||
TABLE_NAME_EXISTING,
|
||||
TABLE_NAME_WRITES,
|
||||
FILTER_CLAUSE,
|
||||
];
|
||||
const DELETE_SHAPE: Node = Node::Seq(DELETE_NODES);
|
||||
|
||||
@@ -29,6 +29,8 @@ const TABLE_NAME_NEW: Node = Node::Ident {
|
||||
role: "table_name",
|
||||
validator: None,
|
||||
highlight_override: None,
|
||||
writes_table: false,
|
||||
writes_column: false,
|
||||
};
|
||||
|
||||
const TABLE_NAME_EXISTING: Node = Node::Ident {
|
||||
@@ -36,6 +38,8 @@ const TABLE_NAME_EXISTING: Node = Node::Ident {
|
||||
role: "table_name",
|
||||
validator: None,
|
||||
highlight_override: None,
|
||||
writes_table: false,
|
||||
writes_column: false,
|
||||
};
|
||||
|
||||
const COLUMN_NAME: Node = Node::Ident {
|
||||
@@ -43,6 +47,8 @@ const COLUMN_NAME: Node = Node::Ident {
|
||||
role: "column_name",
|
||||
validator: None,
|
||||
highlight_override: None,
|
||||
writes_table: false,
|
||||
writes_column: false,
|
||||
};
|
||||
|
||||
const COLUMN_NAME_NEW: Node = Node::Ident {
|
||||
@@ -50,6 +56,8 @@ const COLUMN_NAME_NEW: Node = Node::Ident {
|
||||
role: "column_name",
|
||||
validator: None,
|
||||
highlight_override: None,
|
||||
writes_table: false,
|
||||
writes_column: false,
|
||||
};
|
||||
|
||||
const RELATIONSHIP_NAME: Node = Node::Ident {
|
||||
@@ -57,6 +65,8 @@ const RELATIONSHIP_NAME: Node = Node::Ident {
|
||||
role: "relationship_name",
|
||||
validator: None,
|
||||
highlight_override: None,
|
||||
writes_table: false,
|
||||
writes_column: false,
|
||||
};
|
||||
|
||||
const RELATIONSHIP_NAME_NEW: Node = Node::Ident {
|
||||
@@ -64,6 +74,8 @@ const RELATIONSHIP_NAME_NEW: Node = Node::Ident {
|
||||
role: "relationship_name",
|
||||
validator: None,
|
||||
highlight_override: None,
|
||||
writes_table: false,
|
||||
writes_column: false,
|
||||
};
|
||||
|
||||
// `[to]` and `[table]` connectives.
|
||||
@@ -106,6 +118,8 @@ const DR_PARENT_NODES: &[Node] = &[
|
||||
role: "parent_table",
|
||||
validator: None,
|
||||
highlight_override: None,
|
||||
writes_table: false,
|
||||
writes_column: false,
|
||||
},
|
||||
Node::Punct('.'),
|
||||
Node::Ident {
|
||||
@@ -113,6 +127,8 @@ const DR_PARENT_NODES: &[Node] = &[
|
||||
role: "parent_column",
|
||||
validator: None,
|
||||
highlight_override: None,
|
||||
writes_table: false,
|
||||
writes_column: false,
|
||||
},
|
||||
];
|
||||
const DR_PARENT: Node = Node::Seq(DR_PARENT_NODES);
|
||||
@@ -123,6 +139,8 @@ const DR_CHILD_NODES: &[Node] = &[
|
||||
role: "child_table",
|
||||
validator: None,
|
||||
highlight_override: None,
|
||||
writes_table: false,
|
||||
writes_column: false,
|
||||
},
|
||||
Node::Punct('.'),
|
||||
Node::Ident {
|
||||
@@ -130,6 +148,8 @@ const DR_CHILD_NODES: &[Node] = &[
|
||||
role: "child_column",
|
||||
validator: None,
|
||||
highlight_override: None,
|
||||
writes_table: false,
|
||||
writes_column: false,
|
||||
},
|
||||
];
|
||||
const DR_CHILD: Node = Node::Seq(DR_CHILD_NODES);
|
||||
@@ -188,6 +208,8 @@ const AR_PARENT_NODES: &[Node] = &[
|
||||
role: "parent_table",
|
||||
validator: None,
|
||||
highlight_override: None,
|
||||
writes_table: false,
|
||||
writes_column: false,
|
||||
},
|
||||
Node::Punct('.'),
|
||||
Node::Ident {
|
||||
@@ -195,6 +217,8 @@ const AR_PARENT_NODES: &[Node] = &[
|
||||
role: "parent_column",
|
||||
validator: None,
|
||||
highlight_override: None,
|
||||
writes_table: false,
|
||||
writes_column: false,
|
||||
},
|
||||
];
|
||||
const AR_PARENT: Node = Node::Seq(AR_PARENT_NODES);
|
||||
@@ -205,6 +229,8 @@ const AR_CHILD_NODES: &[Node] = &[
|
||||
role: "child_table",
|
||||
validator: None,
|
||||
highlight_override: None,
|
||||
writes_table: false,
|
||||
writes_column: false,
|
||||
},
|
||||
Node::Punct('.'),
|
||||
Node::Ident {
|
||||
@@ -212,6 +238,8 @@ const AR_CHILD_NODES: &[Node] = &[
|
||||
role: "child_column",
|
||||
validator: None,
|
||||
highlight_override: None,
|
||||
writes_table: false,
|
||||
writes_column: false,
|
||||
},
|
||||
];
|
||||
const AR_CHILD: Node = Node::Seq(AR_CHILD_NODES);
|
||||
@@ -263,6 +291,8 @@ const RENAME_COLUMN_NODES: &[Node] = &[
|
||||
role: "new_column_name",
|
||||
validator: None,
|
||||
highlight_override: None,
|
||||
writes_table: false,
|
||||
writes_column: false,
|
||||
},
|
||||
];
|
||||
const RENAME_COLUMN: Node = Node::Seq(RENAME_COLUMN_NODES);
|
||||
@@ -595,6 +625,8 @@ const COL_SPEC_NODES: &[Node] = &[
|
||||
role: "col_name",
|
||||
validator: None,
|
||||
highlight_override: None,
|
||||
writes_table: false,
|
||||
writes_column: false,
|
||||
},
|
||||
Node::Punct(':'),
|
||||
Node::Ident {
|
||||
@@ -602,6 +634,8 @@ const COL_SPEC_NODES: &[Node] = &[
|
||||
role: "col_type",
|
||||
validator: Some(TYPE_VALIDATOR),
|
||||
highlight_override: None,
|
||||
writes_table: false,
|
||||
writes_column: false,
|
||||
},
|
||||
];
|
||||
const COL_SPEC: Node = Node::Seq(COL_SPEC_NODES);
|
||||
|
||||
@@ -215,12 +215,24 @@ pub enum Node {
|
||||
/// dispatch; `validator` runs after a successful identifier-
|
||||
/// shape match and may reject the value with a catalog-driven
|
||||
/// message.
|
||||
///
|
||||
/// `writes_table` (Phase D): when `true` and `source ==
|
||||
/// Tables`, the walker writes the matched ident to
|
||||
/// `WalkContext::current_table` and resolves
|
||||
/// `current_table_columns` from the schema cache (if any).
|
||||
/// `writes_column` (Phase D): when `true` and `source ==
|
||||
/// Columns`, the walker writes the matched ident's
|
||||
/// `TableColumn` to `WalkContext::current_column` (resolved
|
||||
/// against `current_table_columns`). Subsequent value slots
|
||||
/// dispatch on the column's type.
|
||||
Ident {
|
||||
source: IdentSource,
|
||||
role: &'static str,
|
||||
validator: Option<IdentValidator>,
|
||||
#[allow(dead_code)]
|
||||
highlight_override: Option<HighlightClass>,
|
||||
writes_table: bool,
|
||||
writes_column: bool,
|
||||
},
|
||||
/// A number literal. The optional `validator` runs against
|
||||
/// the matched text (used by Phase D value slots to enforce
|
||||
|
||||
+186
-1
@@ -5,8 +5,11 @@
|
||||
//! actions; Phase D extends with `where_clause`,
|
||||
//! `column_value_list`, and the typed value slots.
|
||||
|
||||
use crate::dsl::grammar::{IdentSource, IdentValidator, Node, ValidationError, Word};
|
||||
use crate::dsl::grammar::{
|
||||
IdentSource, IdentValidator, Node, NumberValidator, ValidationError, Word,
|
||||
};
|
||||
use crate::dsl::types::Type;
|
||||
use crate::dsl::walker::context::WalkContext;
|
||||
use std::str::FromStr;
|
||||
|
||||
// --- Type-name validator ------------------------------------------
|
||||
@@ -46,6 +49,8 @@ pub const TYPE_SLOT: Node = Node::Ident {
|
||||
role: "type",
|
||||
validator: Some(TYPE_VALIDATOR),
|
||||
highlight_override: None,
|
||||
writes_table: false,
|
||||
writes_column: false,
|
||||
};
|
||||
|
||||
// --- Qualified column reference (`<Table>.<Column>`) --------------
|
||||
@@ -56,6 +61,8 @@ const QUALIFIED_COLUMN_NODES: &[Node] = &[
|
||||
role: "table_name",
|
||||
validator: None,
|
||||
highlight_override: None,
|
||||
writes_table: false,
|
||||
writes_column: false,
|
||||
},
|
||||
Node::Punct('.'),
|
||||
Node::Ident {
|
||||
@@ -63,6 +70,8 @@ const QUALIFIED_COLUMN_NODES: &[Node] = &[
|
||||
role: "column_name",
|
||||
validator: None,
|
||||
highlight_override: None,
|
||||
writes_table: false,
|
||||
writes_column: false,
|
||||
},
|
||||
];
|
||||
pub const QUALIFIED_COLUMN: Node = Node::Seq(QUALIFIED_COLUMN_NODES);
|
||||
@@ -118,3 +127,179 @@ pub const REFERENTIAL_CLAUSES: Node = Node::Repeated {
|
||||
separator: None,
|
||||
min: 0,
|
||||
};
|
||||
|
||||
// =================================================================
|
||||
// Typed value slots (ADR-0024 §Phase D, §typed-value-slots)
|
||||
// =================================================================
|
||||
//
|
||||
// Each `<ty>_slot()` factory returns a `Node` that accepts either
|
||||
// `null` or a literal of the corresponding shape, with an
|
||||
// optional content validator that rejects mis-typed values at
|
||||
// parse time with localised catalog wording. Per-type prose
|
||||
// hints attach via `Choice` HintMode — but Phase D's first
|
||||
// landing keeps `Default` everywhere; the dispatch-by-column-type
|
||||
// covers the central design claim, and per-type prose can layer
|
||||
// on later without grammar surface changes.
|
||||
|
||||
fn validate_integer_only(value: &str) -> Result<(), ValidationError> {
|
||||
// The lexer-side number consumer accepts integers and
|
||||
// fractional forms (e.g. `3.14`). For int / serial / shortid
|
||||
// columns reject any literal that carries a decimal point.
|
||||
if value.contains('.') {
|
||||
Err(ValidationError {
|
||||
message_key: "parse.custom.bind_type_mismatch",
|
||||
args: vec![
|
||||
("found", value.to_string()),
|
||||
("expected", "integer".to_string()),
|
||||
],
|
||||
})
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
const INTEGER_ONLY_VALIDATOR: NumberValidator = validate_integer_only;
|
||||
|
||||
fn validate_decimal_string(value: &str) -> Result<(), ValidationError> {
|
||||
if value.parse::<f64>().is_ok() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ValidationError {
|
||||
message_key: "parse.custom.bind_type_mismatch",
|
||||
args: vec![
|
||||
("found", value.to_string()),
|
||||
("expected", "number".to_string()),
|
||||
],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const DECIMAL_VALIDATOR: NumberValidator = validate_decimal_string;
|
||||
|
||||
// Bare `null` keyword — used as the trailing branch of every
|
||||
// typed value slot so a column always accepts the absence sentinel.
|
||||
const NULL_WORD: Node = Node::Word(Word::keyword("null"));
|
||||
|
||||
const INT_SLOT_CHOICES: &[Node] = &[
|
||||
Node::NumberLit {
|
||||
validator: Some(INTEGER_ONLY_VALIDATOR),
|
||||
},
|
||||
NULL_WORD,
|
||||
];
|
||||
const INT_SLOT: Node = Node::Choice(INT_SLOT_CHOICES);
|
||||
|
||||
const REAL_SLOT_CHOICES: &[Node] = &[Node::NumberLit { validator: None }, NULL_WORD];
|
||||
const REAL_SLOT: Node = Node::Choice(REAL_SLOT_CHOICES);
|
||||
|
||||
const DECIMAL_SLOT_CHOICES: &[Node] = &[
|
||||
Node::NumberLit {
|
||||
validator: Some(DECIMAL_VALIDATOR),
|
||||
},
|
||||
NULL_WORD,
|
||||
];
|
||||
const DECIMAL_SLOT: Node = Node::Choice(DECIMAL_SLOT_CHOICES);
|
||||
|
||||
const BOOL_SLOT_CHOICES: &[Node] = &[
|
||||
Node::Word(Word::keyword("true")),
|
||||
Node::Word(Word::keyword("false")),
|
||||
NULL_WORD,
|
||||
];
|
||||
const BOOL_SLOT: Node = Node::Choice(BOOL_SLOT_CHOICES);
|
||||
|
||||
const TEXT_SLOT_CHOICES: &[Node] = &[Node::StringLit, NULL_WORD];
|
||||
const TEXT_SLOT: Node = Node::Choice(TEXT_SLOT_CHOICES);
|
||||
|
||||
const DATE_SLOT_CHOICES: &[Node] = &[Node::StringLit, NULL_WORD];
|
||||
const DATE_SLOT: Node = Node::Choice(DATE_SLOT_CHOICES);
|
||||
|
||||
const DATETIME_SLOT_CHOICES: &[Node] = &[Node::StringLit, NULL_WORD];
|
||||
const DATETIME_SLOT: Node = Node::Choice(DATETIME_SLOT_CHOICES);
|
||||
|
||||
const BLOB_SLOT_CHOICES: &[Node] = &[Node::StringLit, NULL_WORD];
|
||||
const BLOB_SLOT: Node = Node::Choice(BLOB_SLOT_CHOICES);
|
||||
|
||||
/// Dispatch a value slot per user-facing type
|
||||
/// (ADR-0024 §slot_for_type). Returns the same node every time
|
||||
/// for a given Type — fine to call from within a
|
||||
/// `DynamicSubgrammar` factory.
|
||||
#[must_use]
|
||||
pub const fn slot_for_type(ty: Type) -> Node {
|
||||
match ty {
|
||||
Type::Int | Type::Serial | Type::ShortId => INT_SLOT,
|
||||
Type::Real => REAL_SLOT,
|
||||
Type::Decimal => DECIMAL_SLOT,
|
||||
Type::Bool => BOOL_SLOT,
|
||||
Type::Text => TEXT_SLOT,
|
||||
Type::Date => DATE_SLOT,
|
||||
Type::DateTime => DATETIME_SLOT,
|
||||
Type::Blob => BLOB_SLOT,
|
||||
}
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// Dynamic sub-grammar: column_value_list
|
||||
// =================================================================
|
||||
|
||||
/// Fallback when no schema-resolved column list is available
|
||||
/// (schemaless parse, missing table, empty schema cache).
|
||||
/// Mirrors the pre-Phase-D `value_literal` Choice.
|
||||
const FALLBACK_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 FALLBACK_VALUE_LITERAL: Node = Node::Choice(FALLBACK_VALUE_LITERAL_CHOICES);
|
||||
|
||||
const FALLBACK_VALUE_LIST: Node = Node::Repeated {
|
||||
inner: &FALLBACK_VALUE_LITERAL,
|
||||
separator: Some(&Node::Punct(',')),
|
||||
min: 1,
|
||||
};
|
||||
|
||||
/// Value slot keyed on `WalkContext::current_column`.
|
||||
///
|
||||
/// Picks the typed slot for the column whose name was most
|
||||
/// recently matched by an `Ident { source: Columns,
|
||||
/// writes_column: true }` node (ADR-0024 §Phase D). Fallback
|
||||
/// when no current_column is resolved: the schemaless
|
||||
/// value-literal choice.
|
||||
pub fn current_column_value(ctx: &WalkContext) -> Node {
|
||||
ctx.current_column
|
||||
.as_ref()
|
||||
.map_or(FALLBACK_VALUE_LITERAL, |col| slot_for_type(col.user_type))
|
||||
}
|
||||
|
||||
/// Comma-separated list of typed value slots, one per column.
|
||||
///
|
||||
/// Reads `current_table_columns` from the WalkContext (ADR-0024
|
||||
/// §Phase D §column_value_list). When the schema cache holds
|
||||
/// no entry for the current table — or the walker is
|
||||
/// schemaless — falls back to the schema-unaware
|
||||
/// `Repeated(VALUE_LITERAL, ',', 1)` shape so existing
|
||||
/// callers/tests continue to work.
|
||||
pub fn column_value_list(ctx: &WalkContext) -> Node {
|
||||
let Some(cols) = ctx.current_table_columns.as_ref() else {
|
||||
return FALLBACK_VALUE_LIST;
|
||||
};
|
||||
if cols.is_empty() {
|
||||
return FALLBACK_VALUE_LIST;
|
||||
}
|
||||
// Build a Seq of typed slots interleaved with commas.
|
||||
let mut children: Vec<Node> = Vec::with_capacity(cols.len() * 2);
|
||||
for (i, col) in cols.iter().enumerate() {
|
||||
if i > 0 {
|
||||
children.push(Node::Punct(','));
|
||||
}
|
||||
children.push(slot_for_type(col.user_type));
|
||||
}
|
||||
Node::Seq(Box::leak(children.into_boxed_slice()))
|
||||
}
|
||||
|
||||
// The HintMode / NumberValidator imports are part of the Phase D
|
||||
// typed-slot toolkit even though only NumberValidator is used by
|
||||
// the explicit validators above; surface HintMode so future
|
||||
// per-type prose annotations can attach without re-importing.
|
||||
#[allow(dead_code)]
|
||||
const _USES_HINT_MODE: Option<crate::dsl::grammar::HintMode> = None;
|
||||
|
||||
Reference in New Issue
Block a user