style: format the whole tree with cargo fmt (stock defaults, #35)
One-time, mechanical reformat — no functional changes. The tree was not rustfmt-clean (~1800 hunks across ~100 files); this brings it to stock `cargo fmt` defaults so a `cargo fmt --check` CI gate can follow. Behaviour-preserving: 2509 pass / 0 fail / 1 ignored (unchanged baseline), clippy clean. A .git-blame-ignore-revs entry follows so `git blame` skips this commit.
This commit is contained in:
+10
-11
@@ -549,9 +549,7 @@ pub enum AppCommand {
|
||||
/// word like `insert` / `create` / `show`, or `types`), the
|
||||
/// focused detail for that command (or command group sharing
|
||||
/// the entry word).
|
||||
Help {
|
||||
topic: Option<String>,
|
||||
},
|
||||
Help { topic: Option<String> },
|
||||
/// Show a contextual tier-3 hint (H2 / ADR-0053). No argument:
|
||||
/// when submitted, it expands on the most recent runtime error
|
||||
/// (the buffer is empty post-submit). The live-input surface is
|
||||
@@ -580,7 +578,10 @@ pub enum AppCommand {
|
||||
/// Unpack a zip into a new project and switch to it.
|
||||
/// `target` overrides the project name (default: taken from
|
||||
/// the zip).
|
||||
Import { path: String, target: Option<String> },
|
||||
Import {
|
||||
path: String,
|
||||
target: Option<String>,
|
||||
},
|
||||
/// Switch the persistent input mode.
|
||||
Mode { value: ModeValue },
|
||||
/// Show or set the messages verbosity.
|
||||
@@ -791,9 +792,7 @@ impl PartialEq for Operand {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(Self::Column { name: a, .. }, Self::Column { name: b, .. }) => a == b,
|
||||
(Self::Literal { value: a, .. }, Self::Literal { value: b, .. }) => {
|
||||
a == b
|
||||
}
|
||||
(Self::Literal { value: a, .. }, Self::Literal { value: b, .. }) => a == b,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
@@ -817,7 +816,9 @@ pub enum CompareOp {
|
||||
/// a single row in the metadata table.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum RelationshipSelector {
|
||||
Named { name: String },
|
||||
Named {
|
||||
name: String,
|
||||
},
|
||||
Endpoints {
|
||||
parent_table: String,
|
||||
parent_column: String,
|
||||
@@ -1156,9 +1157,7 @@ impl Command {
|
||||
parent_column,
|
||||
child_table,
|
||||
child_column,
|
||||
} => format!(
|
||||
"from {parent_table}.{parent_column} to {child_table}.{child_column}"
|
||||
),
|
||||
} => format!("from {parent_table}.{parent_column} to {child_table}.{child_column}"),
|
||||
},
|
||||
// A constraint command's subject is the dotted
|
||||
// `<table>.<column>` it acts on (ADR-0029 §2.2).
|
||||
|
||||
+41
-30
@@ -9,8 +9,7 @@
|
||||
|
||||
use crate::dsl::command::{AppCommand, Command, CopyScope, MessagesValue, ModeValue};
|
||||
use crate::dsl::grammar::{
|
||||
CommandNode, HintMode, IdentSource, IdentValidator, Node, ValidationError,
|
||||
Word,
|
||||
CommandNode, HintMode, IdentSource, IdentValidator, Node, ValidationError, Word,
|
||||
};
|
||||
use crate::dsl::walker::outcome::{MatchedKind, MatchedPath};
|
||||
|
||||
@@ -60,19 +59,16 @@ const IMPORT_TARGET_IDENT: Node = Node::Ident {
|
||||
writes_table: false,
|
||||
writes_column: false,
|
||||
writes_user_listed_column: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
};
|
||||
const IMPORT_TARGET: Node = Node::Hinted {
|
||||
mode: HintMode::ForceProse("hint.ambient_typing_name"),
|
||||
inner: &IMPORT_TARGET_IDENT,
|
||||
};
|
||||
|
||||
const IMPORT_AS_TARGET: Node = Node::Seq(&[
|
||||
Node::Word(Word::keyword("as")),
|
||||
IMPORT_TARGET,
|
||||
]);
|
||||
const IMPORT_AS_TARGET: Node = Node::Seq(&[Node::Word(Word::keyword("as")), IMPORT_TARGET]);
|
||||
const IMPORT_AS_TARGET_OPT: Node = Node::Optional(&IMPORT_AS_TARGET);
|
||||
|
||||
const IMPORT_PATH_AND_TARGET: Node = Node::Seq(&[Node::BarePath, IMPORT_AS_TARGET_OPT]);
|
||||
@@ -101,9 +97,9 @@ const MODE_CHOICES: &[Node] = &[
|
||||
writes_table: false,
|
||||
writes_column: false,
|
||||
writes_user_listed_column: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
},
|
||||
];
|
||||
const MODE_VALUE: Node = Node::Choice(MODE_CHOICES);
|
||||
@@ -119,9 +115,9 @@ const MESSAGES_CHOICES: &[Node] = &[
|
||||
writes_table: false,
|
||||
writes_column: false,
|
||||
writes_user_listed_column: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
},
|
||||
];
|
||||
const MESSAGES_VALUE: Node = Node::Choice(MESSAGES_CHOICES);
|
||||
@@ -271,7 +267,8 @@ pub static QUIT: CommandNode = CommandNode {
|
||||
ast_builder: build_quit,
|
||||
help_id: Some("app.quit"),
|
||||
hint_ids: &["quit"],
|
||||
usage_ids: &["parse.usage.quit"],};
|
||||
usage_ids: &["parse.usage.quit"],
|
||||
};
|
||||
|
||||
pub static HELP: CommandNode = CommandNode {
|
||||
entry: Word::keyword("help"),
|
||||
@@ -279,7 +276,8 @@ pub static HELP: CommandNode = CommandNode {
|
||||
ast_builder: build_help,
|
||||
help_id: Some("app.help"),
|
||||
hint_ids: &["help"],
|
||||
usage_ids: &["parse.usage.help"],};
|
||||
usage_ids: &["parse.usage.help"],
|
||||
};
|
||||
|
||||
pub static HINT: CommandNode = CommandNode {
|
||||
entry: Word::keyword("hint"),
|
||||
@@ -288,7 +286,8 @@ pub static HINT: CommandNode = CommandNode {
|
||||
help_id: Some("app.hint"),
|
||||
// hint_id assigned in Phase C with the tier-3 corpus (ADR-0053).
|
||||
hint_ids: &["hint"],
|
||||
usage_ids: &["parse.usage.hint"],};
|
||||
usage_ids: &["parse.usage.hint"],
|
||||
};
|
||||
|
||||
pub static REBUILD: CommandNode = CommandNode {
|
||||
entry: Word::keyword("rebuild"),
|
||||
@@ -296,7 +295,8 @@ pub static REBUILD: CommandNode = CommandNode {
|
||||
ast_builder: build_rebuild,
|
||||
help_id: Some("app.rebuild"),
|
||||
hint_ids: &["rebuild"],
|
||||
usage_ids: &["parse.usage.rebuild"],};
|
||||
usage_ids: &["parse.usage.rebuild"],
|
||||
};
|
||||
|
||||
pub static VERSION: CommandNode = CommandNode {
|
||||
entry: Word::keyword("version"),
|
||||
@@ -304,7 +304,8 @@ pub static VERSION: CommandNode = CommandNode {
|
||||
ast_builder: build_version,
|
||||
help_id: Some("app.version"),
|
||||
hint_ids: &["version"],
|
||||
usage_ids: &["parse.usage.version"],};
|
||||
usage_ids: &["parse.usage.version"],
|
||||
};
|
||||
|
||||
pub static SAVE: CommandNode = CommandNode {
|
||||
entry: Word::keyword("save"),
|
||||
@@ -312,7 +313,8 @@ pub static SAVE: CommandNode = CommandNode {
|
||||
ast_builder: build_save,
|
||||
help_id: Some("app.save"),
|
||||
hint_ids: &["save"],
|
||||
usage_ids: &["parse.usage.save"],};
|
||||
usage_ids: &["parse.usage.save"],
|
||||
};
|
||||
|
||||
pub static NEW: CommandNode = CommandNode {
|
||||
entry: Word::keyword("new"),
|
||||
@@ -320,7 +322,8 @@ pub static NEW: CommandNode = CommandNode {
|
||||
ast_builder: build_new,
|
||||
help_id: Some("app.new"),
|
||||
hint_ids: &["new"],
|
||||
usage_ids: &["parse.usage.new"],};
|
||||
usage_ids: &["parse.usage.new"],
|
||||
};
|
||||
|
||||
pub static LOAD: CommandNode = CommandNode {
|
||||
entry: Word::keyword("load"),
|
||||
@@ -328,7 +331,8 @@ pub static LOAD: CommandNode = CommandNode {
|
||||
ast_builder: build_load,
|
||||
help_id: Some("app.load"),
|
||||
hint_ids: &["load"],
|
||||
usage_ids: &["parse.usage.load"],};
|
||||
usage_ids: &["parse.usage.load"],
|
||||
};
|
||||
|
||||
pub static EXPORT: CommandNode = CommandNode {
|
||||
entry: Word::keyword("export"),
|
||||
@@ -336,7 +340,8 @@ pub static EXPORT: CommandNode = CommandNode {
|
||||
ast_builder: build_export,
|
||||
help_id: Some("app.export"),
|
||||
hint_ids: &["export"],
|
||||
usage_ids: &["parse.usage.export"],};
|
||||
usage_ids: &["parse.usage.export"],
|
||||
};
|
||||
|
||||
pub static IMPORT: CommandNode = CommandNode {
|
||||
entry: Word::keyword("import"),
|
||||
@@ -344,7 +349,8 @@ pub static IMPORT: CommandNode = CommandNode {
|
||||
ast_builder: build_import,
|
||||
help_id: Some("app.import"),
|
||||
hint_ids: &["import"],
|
||||
usage_ids: &["parse.usage.import"],};
|
||||
usage_ids: &["parse.usage.import"],
|
||||
};
|
||||
|
||||
pub static MODE: CommandNode = CommandNode {
|
||||
entry: Word::keyword("mode"),
|
||||
@@ -352,7 +358,8 @@ pub static MODE: CommandNode = CommandNode {
|
||||
ast_builder: build_mode,
|
||||
help_id: Some("app.mode"),
|
||||
hint_ids: &["mode"],
|
||||
usage_ids: &["parse.usage.mode"],};
|
||||
usage_ids: &["parse.usage.mode"],
|
||||
};
|
||||
|
||||
pub static MESSAGES: CommandNode = CommandNode {
|
||||
entry: Word::keyword("messages"),
|
||||
@@ -360,7 +367,8 @@ pub static MESSAGES: CommandNode = CommandNode {
|
||||
ast_builder: build_messages,
|
||||
help_id: Some("app.messages"),
|
||||
hint_ids: &["messages"],
|
||||
usage_ids: &["parse.usage.messages"],};
|
||||
usage_ids: &["parse.usage.messages"],
|
||||
};
|
||||
|
||||
pub static UNDO: CommandNode = CommandNode {
|
||||
entry: Word::keyword("undo"),
|
||||
@@ -368,7 +376,8 @@ pub static UNDO: CommandNode = CommandNode {
|
||||
ast_builder: build_undo,
|
||||
help_id: Some("app.undo"),
|
||||
hint_ids: &["undo"],
|
||||
usage_ids: &["parse.usage.undo"],};
|
||||
usage_ids: &["parse.usage.undo"],
|
||||
};
|
||||
|
||||
pub static REDO: CommandNode = CommandNode {
|
||||
entry: Word::keyword("redo"),
|
||||
@@ -376,7 +385,8 @@ pub static REDO: CommandNode = CommandNode {
|
||||
ast_builder: build_redo,
|
||||
help_id: Some("app.redo"),
|
||||
hint_ids: &["redo"],
|
||||
usage_ids: &["parse.usage.redo"],};
|
||||
usage_ids: &["parse.usage.redo"],
|
||||
};
|
||||
|
||||
pub static COPY: CommandNode = CommandNode {
|
||||
entry: Word::keyword("copy"),
|
||||
@@ -384,4 +394,5 @@ pub static COPY: CommandNode = CommandNode {
|
||||
ast_builder: build_copy,
|
||||
help_id: Some("app.copy"),
|
||||
hint_ids: &["copy"],
|
||||
usage_ids: &["parse.usage.copy"],};
|
||||
usage_ids: &["parse.usage.copy"],
|
||||
};
|
||||
|
||||
+101
-78
@@ -24,19 +24,17 @@
|
||||
//! later swap that capture for the same typed slots used here, adding
|
||||
//! live hints/highlighting.
|
||||
|
||||
use crate::dsl::command::{
|
||||
Command, Expr, RowFilter, SeedOverride, SeedOverrideKind, ShowListKind,
|
||||
};
|
||||
use crate::dsl::command::{Command, Expr, RowFilter, SeedOverride, SeedOverrideKind, ShowListKind};
|
||||
use crate::dsl::grammar::{
|
||||
CommandNode, IdentSource, Node, NumberValidator, ValidationError, Word, expr,
|
||||
shared::{
|
||||
FALLBACK_VALUE_LIST, column_value_list, count_tuple_values,
|
||||
current_column_value, insert_target_columns,
|
||||
FALLBACK_VALUE_LIST, column_value_list, count_tuple_values, current_column_value,
|
||||
insert_target_columns,
|
||||
},
|
||||
sql_delete, sql_insert, sql_select, sql_update,
|
||||
};
|
||||
use crate::dsl::walker::context::WalkContext;
|
||||
use crate::dsl::value::Value;
|
||||
use crate::dsl::walker::context::WalkContext;
|
||||
use crate::dsl::walker::outcome::{MatchedItem, MatchedKind, MatchedPath};
|
||||
|
||||
// =================================================================
|
||||
@@ -56,10 +54,10 @@ const TABLE_NAME_EXISTING: Node = Node::Ident {
|
||||
highlight_override: None,
|
||||
writes_table: false,
|
||||
writes_column: false,
|
||||
writes_user_listed_column: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
writes_user_listed_column: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
};
|
||||
|
||||
/// Table-name slot variant that populates
|
||||
@@ -75,10 +73,10 @@ const TABLE_NAME_INSERT: Node = Node::Ident {
|
||||
highlight_override: None,
|
||||
writes_table: true,
|
||||
writes_column: false,
|
||||
writes_user_listed_column: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
writes_user_listed_column: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
};
|
||||
|
||||
// =================================================================
|
||||
@@ -95,10 +93,7 @@ const SHOW_DATA_NODES: &[Node] = &[
|
||||
];
|
||||
const SHOW_DATA: Node = Node::Seq(SHOW_DATA_NODES);
|
||||
|
||||
const SHOW_TABLE_NODES: &[Node] = &[
|
||||
Node::Word(Word::keyword("table")),
|
||||
TABLE_NAME_EXISTING,
|
||||
];
|
||||
const SHOW_TABLE_NODES: &[Node] = &[Node::Word(Word::keyword("table")), TABLE_NAME_EXISTING];
|
||||
const SHOW_TABLE: Node = Node::Seq(SHOW_TABLE_NODES);
|
||||
|
||||
// `show tables` / `show relationships` / `show indexes` — the
|
||||
@@ -144,8 +139,7 @@ const SHOW_INDEX_NAME: Node = Node::Ident {
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
};
|
||||
const SHOW_INDEX_NODES: &[Node] =
|
||||
&[Node::Word(Word::keyword("index")), SHOW_INDEX_NAME];
|
||||
const SHOW_INDEX_NODES: &[Node] = &[Node::Word(Word::keyword("index")), SHOW_INDEX_NAME];
|
||||
const SHOW_INDEX: Node = Node::Seq(SHOW_INDEX_NODES);
|
||||
|
||||
const SHOW_CHOICES: &[Node] = &[
|
||||
@@ -192,9 +186,9 @@ static FORM_A_COLUMN: Node = Node::Ident {
|
||||
writes_table: false,
|
||||
writes_column: false,
|
||||
writes_user_listed_column: true,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
};
|
||||
static INSERT_COMMA: Node = Node::Punct(',');
|
||||
|
||||
@@ -224,8 +218,7 @@ fn insert_first_paren(ctx: &WalkContext, source: &str, pos: usize) -> Node {
|
||||
/// 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,
|
||||
consume_ident, consume_number_literal, consume_string_literal, skip_whitespace,
|
||||
};
|
||||
let p = skip_whitespace(source, pos);
|
||||
if p >= source.len() {
|
||||
@@ -281,7 +274,11 @@ fn dsl_insert_value_list(ctx: &WalkContext, source: &str, pos: usize) -> Node {
|
||||
return FALLBACK_VALUE_LIST;
|
||||
};
|
||||
let (count, closed) = count_tuple_values(source, pos);
|
||||
let arity_ok = if closed { count == cols.len() } else { count <= cols.len() };
|
||||
let arity_ok = if closed {
|
||||
count == cols.len()
|
||||
} else {
|
||||
count <= cols.len()
|
||||
};
|
||||
if arity_ok {
|
||||
Node::DynamicSubgrammar(column_value_list)
|
||||
} else {
|
||||
@@ -320,8 +317,7 @@ const INSERT_VALUES_KEYWORD_FIRST_NODES: &[Node] = &[
|
||||
];
|
||||
const INSERT_VALUES_KEYWORD_FIRST: Node = Node::Seq(INSERT_VALUES_KEYWORD_FIRST_NODES);
|
||||
|
||||
const INSERT_AFTER_TABLE_CHOICES: &[Node] =
|
||||
&[INSERT_VALUES_KEYWORD_FIRST, INSERT_PAREN_FIRST];
|
||||
const INSERT_AFTER_TABLE_CHOICES: &[Node] = &[INSERT_VALUES_KEYWORD_FIRST, INSERT_PAREN_FIRST];
|
||||
const INSERT_AFTER_TABLE: Node = Node::Choice(INSERT_AFTER_TABLE_CHOICES);
|
||||
|
||||
const INSERT_NODES: &[Node] = &[
|
||||
@@ -349,10 +345,10 @@ const TABLE_NAME_WRITES: Node = Node::Ident {
|
||||
highlight_override: None,
|
||||
writes_table: true,
|
||||
writes_column: false,
|
||||
writes_user_listed_column: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
writes_user_listed_column: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
};
|
||||
|
||||
/// Column-name slot in `set col = …` — resolves the column's
|
||||
@@ -366,9 +362,9 @@ const SET_COLUMN: Node = Node::Ident {
|
||||
writes_table: false,
|
||||
writes_column: true,
|
||||
writes_user_listed_column: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
};
|
||||
|
||||
/// Value slot resolved at walk time from
|
||||
@@ -376,11 +372,7 @@ writes_projection_alias: false,
|
||||
/// value-literal choice when no current_column is bound.
|
||||
const PER_COLUMN_VALUE: Node = Node::DynamicSubgrammar(current_column_value);
|
||||
|
||||
const UPDATE_ASSIGNMENT_NODES: &[Node] = &[
|
||||
SET_COLUMN,
|
||||
Node::Punct('='),
|
||||
PER_COLUMN_VALUE,
|
||||
];
|
||||
const UPDATE_ASSIGNMENT_NODES: &[Node] = &[SET_COLUMN, Node::Punct('='), PER_COLUMN_VALUE];
|
||||
const UPDATE_ASSIGNMENT: Node = Node::Seq(UPDATE_ASSIGNMENT_NODES);
|
||||
const UPDATE_ASSIGNMENTS: Node = Node::Repeated {
|
||||
inner: &UPDATE_ASSIGNMENT,
|
||||
@@ -568,8 +560,7 @@ const SEED_OVERRIDES: Node = Node::Repeated {
|
||||
separator: Some(&Node::Punct(',')),
|
||||
min: 1,
|
||||
};
|
||||
const SEED_SET_CLAUSE_NODES: &[Node] =
|
||||
&[Node::Word(Word::keyword("set")), SEED_OVERRIDES];
|
||||
const SEED_SET_CLAUSE_NODES: &[Node] = &[Node::Word(Word::keyword("set")), SEED_OVERRIDES];
|
||||
const SEED_SET_CLAUSE: Node = Node::Seq(SEED_SET_CLAUSE_NODES);
|
||||
|
||||
const SEED_NODES: &[Node] = &[
|
||||
@@ -980,7 +971,10 @@ fn parse_seed_override_tail(
|
||||
MatchedKind::Word("in") => {
|
||||
*i += 1; // `in`
|
||||
// `(`
|
||||
if matches!(region.get(*i).map(|t| &t.kind), Some(MatchedKind::Punct('('))) {
|
||||
if matches!(
|
||||
region.get(*i).map(|t| &t.kind),
|
||||
Some(MatchedKind::Punct('('))
|
||||
) {
|
||||
*i += 1;
|
||||
}
|
||||
let mut values = Vec::new();
|
||||
@@ -1001,7 +995,10 @@ fn parse_seed_override_tail(
|
||||
MatchedKind::Word("between") => {
|
||||
*i += 1; // `between`
|
||||
let low = seed_take_value(region, i, column)?;
|
||||
if matches!(region.get(*i).map(|t| &t.kind), Some(MatchedKind::Word("and"))) {
|
||||
if matches!(
|
||||
region.get(*i).map(|t| &t.kind),
|
||||
Some(MatchedKind::Word("and"))
|
||||
) {
|
||||
*i += 1;
|
||||
}
|
||||
let high = seed_take_value(region, i, column)?;
|
||||
@@ -1011,7 +1008,15 @@ fn parse_seed_override_tail(
|
||||
*i += 1; // `as`
|
||||
let gen_item = region
|
||||
.get(*i)
|
||||
.filter(|t| matches!(t.kind, MatchedKind::Ident { role: "seed_generator", .. }))
|
||||
.filter(|t| {
|
||||
matches!(
|
||||
t.kind,
|
||||
MatchedKind::Ident {
|
||||
role: "seed_generator",
|
||||
..
|
||||
}
|
||||
)
|
||||
})
|
||||
.ok_or_else(|| seed_set_error(column))?;
|
||||
*i += 1;
|
||||
Ok(SeedOverrideKind::Generator(gen_item.text.clone()))
|
||||
@@ -1085,7 +1090,15 @@ fn build_insert(path: &MatchedPath, _source: &str) -> Result<Command, Validation
|
||||
let table_idx = path
|
||||
.items
|
||||
.iter()
|
||||
.position(|i| matches!(&i.kind, MatchedKind::Ident { role: "table_name", .. }))
|
||||
.position(|i| {
|
||||
matches!(
|
||||
&i.kind,
|
||||
MatchedKind::Ident {
|
||||
role: "table_name",
|
||||
..
|
||||
}
|
||||
)
|
||||
})
|
||||
.ok_or_else(|| ValidationError {
|
||||
message_key: "parse.error_wrapper",
|
||||
args: vec![("detail", "missing table".to_string())],
|
||||
@@ -1141,7 +1154,10 @@ fn build_insert(path: &MatchedPath, _source: &str) -> Result<Command, Validation
|
||||
if columns.is_empty() {
|
||||
return Err(ValidationError {
|
||||
message_key: "parse.error_wrapper",
|
||||
args: vec![("detail", "expected column names in `insert into T (…)`".to_string())],
|
||||
args: vec![(
|
||||
"detail",
|
||||
"expected column names in `insert into T (…)`".to_string(),
|
||||
)],
|
||||
});
|
||||
}
|
||||
// Find the `values` keyword and the next `(` — the values
|
||||
@@ -1247,9 +1263,7 @@ fn build_update(path: &MatchedPath, _source: &str) -> Result<Command, Validation
|
||||
})
|
||||
}
|
||||
|
||||
fn collect_assignments(
|
||||
path: &MatchedPath,
|
||||
) -> Result<Vec<(String, Value)>, ValidationError> {
|
||||
fn collect_assignments(path: &MatchedPath) -> Result<Vec<(String, Value)>, ValidationError> {
|
||||
let mut out = Vec::new();
|
||||
let mut iter = path.items.iter();
|
||||
while let Some(item) = iter.next() {
|
||||
@@ -1495,9 +1509,7 @@ fn build_sql_insert(path: &MatchedPath, source: &str) -> Result<Command, Validat
|
||||
let row_source = path
|
||||
.items
|
||||
.iter()
|
||||
.find(|item| {
|
||||
matches!(item.kind, MatchedKind::Word("values" | "select" | "with"))
|
||||
})
|
||||
.find(|item| matches!(item.kind, MatchedKind::Word("values" | "select" | "with")))
|
||||
.map(|item| {
|
||||
let end = tail_start.unwrap_or(source.len());
|
||||
source[item.span.0..end]
|
||||
@@ -1805,7 +1817,8 @@ pub static SHOW: CommandNode = CommandNode {
|
||||
"parse.usage.show_indexes",
|
||||
"parse.usage.show_relationship",
|
||||
"parse.usage.show_index",
|
||||
],};
|
||||
],
|
||||
};
|
||||
|
||||
pub static SEED: CommandNode = CommandNode {
|
||||
entry: Word::keyword("seed"),
|
||||
@@ -1823,7 +1836,8 @@ pub static INSERT: CommandNode = CommandNode {
|
||||
help_id: Some("data.insert"),
|
||||
// ADR-0053 Phase-B exemplar.
|
||||
hint_ids: &["insert"],
|
||||
usage_ids: &["parse.usage.insert"],};
|
||||
usage_ids: &["parse.usage.insert"],
|
||||
};
|
||||
|
||||
pub static UPDATE: CommandNode = CommandNode {
|
||||
entry: Word::keyword("update"),
|
||||
@@ -1831,7 +1845,8 @@ pub static UPDATE: CommandNode = CommandNode {
|
||||
ast_builder: build_update,
|
||||
help_id: Some("data.update"),
|
||||
hint_ids: &["update"],
|
||||
usage_ids: &["parse.usage.update"],};
|
||||
usage_ids: &["parse.usage.update"],
|
||||
};
|
||||
|
||||
pub static DELETE: CommandNode = CommandNode {
|
||||
entry: Word::keyword("delete"),
|
||||
@@ -1839,7 +1854,8 @@ pub static DELETE: CommandNode = CommandNode {
|
||||
ast_builder: build_delete,
|
||||
help_id: Some("data.delete"),
|
||||
hint_ids: &["delete"],
|
||||
usage_ids: &["parse.usage.delete"],};
|
||||
usage_ids: &["parse.usage.delete"],
|
||||
};
|
||||
|
||||
pub static REPLAY: CommandNode = CommandNode {
|
||||
entry: Word::keyword("replay"),
|
||||
@@ -1847,7 +1863,8 @@ pub static REPLAY: CommandNode = CommandNode {
|
||||
ast_builder: build_replay,
|
||||
help_id: Some("data.replay"),
|
||||
hint_ids: &["replay"],
|
||||
usage_ids: &["parse.usage.replay"],};
|
||||
usage_ids: &["parse.usage.replay"],
|
||||
};
|
||||
|
||||
pub static EXPLAIN: CommandNode = CommandNode {
|
||||
entry: Word::keyword("explain"),
|
||||
@@ -1855,7 +1872,8 @@ pub static EXPLAIN: CommandNode = CommandNode {
|
||||
ast_builder: build_explain,
|
||||
help_id: Some("data.explain"),
|
||||
hint_ids: &["explain"],
|
||||
usage_ids: &["parse.usage.explain"],};
|
||||
usage_ids: &["parse.usage.explain"],
|
||||
};
|
||||
|
||||
/// `explain` over advanced-mode SQL (ADR-0039).
|
||||
///
|
||||
@@ -1875,7 +1893,8 @@ pub static EXPLAIN_SQL: CommandNode = CommandNode {
|
||||
// precedent; otherwise `note_help` would print `explain` twice.
|
||||
help_id: None,
|
||||
hint_ids: &["explain_sql"],
|
||||
usage_ids: &[],};
|
||||
usage_ids: &[],
|
||||
};
|
||||
|
||||
/// SQL `SELECT` (ADR-0030 §6, ADR-0031, ADR-0032).
|
||||
///
|
||||
@@ -1891,7 +1910,8 @@ pub static SELECT: CommandNode = CommandNode {
|
||||
ast_builder: build_select,
|
||||
help_id: None,
|
||||
hint_ids: &["select"],
|
||||
usage_ids: &["parse.usage.select"],};
|
||||
usage_ids: &["parse.usage.select"],
|
||||
};
|
||||
|
||||
/// `WITH …` top-level statement (ADR-0032 §4 / sub-phase 2c).
|
||||
///
|
||||
@@ -1906,7 +1926,8 @@ pub static WITH: CommandNode = CommandNode {
|
||||
ast_builder: build_select,
|
||||
help_id: None,
|
||||
hint_ids: &["with"],
|
||||
usage_ids: &["parse.usage.with"],};
|
||||
usage_ids: &["parse.usage.with"],
|
||||
};
|
||||
|
||||
/// SQL `INSERT` — the `Advanced`-category node of the shared
|
||||
/// `insert` entry word (ADR-0033 §2, Amendment 1, sub-phase 3j).
|
||||
@@ -1993,7 +2014,11 @@ mod explain_tests {
|
||||
#[test]
|
||||
fn explain_show_data_carries_where_and_limit_through() {
|
||||
match explain_inner("explain show data Customers where id = 1 limit 5") {
|
||||
Command::ShowData { name, filter, limit } => {
|
||||
Command::ShowData {
|
||||
name,
|
||||
filter,
|
||||
limit,
|
||||
} => {
|
||||
assert_eq!(name, "Customers");
|
||||
assert!(filter.is_some(), "where clause should survive");
|
||||
assert_eq!(limit, Some(5));
|
||||
@@ -2052,9 +2077,7 @@ mod explain_tests {
|
||||
|
||||
/// Advanced-mode counterpart of `explain_inner`.
|
||||
fn explain_inner_adv(input: &str) -> Command {
|
||||
match parse_command_in_mode(input, Mode::Advanced)
|
||||
.expect("advanced explain should parse")
|
||||
{
|
||||
match parse_command_in_mode(input, Mode::Advanced).expect("advanced explain should parse") {
|
||||
Command::Explain { query } => *query,
|
||||
other => panic!("expected Command::Explain, got {other:?}"),
|
||||
}
|
||||
@@ -2085,7 +2108,9 @@ mod explain_tests {
|
||||
#[test]
|
||||
fn explain_sql_insert_wraps_a_sql_insert() {
|
||||
match explain_inner_adv("explain insert into Customers values (1, 'Bo')") {
|
||||
Command::SqlInsert { sql, target_table, .. } => {
|
||||
Command::SqlInsert {
|
||||
sql, target_table, ..
|
||||
} => {
|
||||
assert_eq!(target_table, "Customers");
|
||||
assert_eq!(sql, "insert into Customers values (1, 'Bo')");
|
||||
}
|
||||
@@ -2096,7 +2121,9 @@ mod explain_tests {
|
||||
#[test]
|
||||
fn explain_sql_update_wraps_a_sql_update_with_clean_sql() {
|
||||
match explain_inner_adv("explain update Customers set Name = 'Bo' where id = 1") {
|
||||
Command::SqlUpdate { sql, target_table, .. } => {
|
||||
Command::SqlUpdate {
|
||||
sql, target_table, ..
|
||||
} => {
|
||||
assert_eq!(target_table, "Customers");
|
||||
assert_eq!(sql, "update Customers set Name = 'Bo' where id = 1");
|
||||
}
|
||||
@@ -2107,7 +2134,9 @@ mod explain_tests {
|
||||
#[test]
|
||||
fn explain_sql_delete_wraps_a_sql_delete() {
|
||||
match explain_inner_adv("explain delete from Customers where id = 1") {
|
||||
Command::SqlDelete { sql, target_table, .. } => {
|
||||
Command::SqlDelete {
|
||||
sql, target_table, ..
|
||||
} => {
|
||||
assert_eq!(target_table, "Customers");
|
||||
assert_eq!(sql, "delete from Customers where id = 1");
|
||||
}
|
||||
@@ -2148,11 +2177,7 @@ mod explain_tests {
|
||||
fn explain_does_not_cover_ddl() {
|
||||
// EXPLAIN QUERY PLAN applies to DML/queries only (ADR-0039
|
||||
// out of scope); there is no SQL DDL branch under explain.
|
||||
assert!(parse_command_in_mode(
|
||||
"explain create table T (id int)",
|
||||
Mode::Advanced,
|
||||
)
|
||||
.is_err());
|
||||
assert!(parse_command_in_mode("explain create table T (id int)", Mode::Advanced,).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2165,9 +2190,8 @@ mod explain_tests {
|
||||
use crate::completion::candidates_at_cursor_in_mode;
|
||||
let schema = crate::completion::SchemaCache::default();
|
||||
let input = "explain ";
|
||||
let completion =
|
||||
candidates_at_cursor_in_mode(input, input.len(), &schema, Mode::Advanced)
|
||||
.expect("explain offers candidates");
|
||||
let completion = candidates_at_cursor_in_mode(input, input.len(), &schema, Mode::Advanced)
|
||||
.expect("explain offers candidates");
|
||||
let names: Vec<&str> = completion
|
||||
.candidates
|
||||
.iter()
|
||||
@@ -2178,4 +2202,3 @@ mod explain_tests {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+246
-166
@@ -16,11 +16,11 @@ use crate::dsl::command::{
|
||||
AlterTableAction, ChangeColumnMode, ColumnSpec, Command, Constraint, ConstraintKind, Expr,
|
||||
IndexSelector, RelationshipSelector, SqlForeignKey, TableConstraint,
|
||||
};
|
||||
use crate::dsl::value::Value;
|
||||
use crate::dsl::grammar::{
|
||||
CommandNode, HighlightClass, HintMode, IdentSource, Node, ValidationError, Word,
|
||||
shared::{REFERENTIAL_CLAUSES, TYPE_SLOT, TYPE_VALIDATOR},
|
||||
};
|
||||
use crate::dsl::value::Value;
|
||||
|
||||
/// `HintMode` annotation shared by every `NewName` ident slot:
|
||||
/// the user is inventing a name, so the hint panel forces the
|
||||
@@ -39,12 +39,12 @@ const TABLE_NAME_NEW_IDENT: Node = Node::Ident {
|
||||
role: "table_name",
|
||||
validator: None,
|
||||
highlight_override: None,
|
||||
writes_table: false,
|
||||
writes_column: false,
|
||||
writes_user_listed_column: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
writes_table: false,
|
||||
writes_column: false,
|
||||
writes_user_listed_column: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
};
|
||||
const TABLE_NAME_NEW: Node = Node::Hinted {
|
||||
mode: NEW_NAME_HINT,
|
||||
@@ -63,12 +63,12 @@ const TABLE_NAME_EXISTING: Node = Node::Ident {
|
||||
role: "table_name",
|
||||
validator: None,
|
||||
highlight_override: None,
|
||||
writes_table: true,
|
||||
writes_column: false,
|
||||
writes_user_listed_column: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
writes_table: true,
|
||||
writes_column: false,
|
||||
writes_user_listed_column: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
};
|
||||
|
||||
const COLUMN_NAME: Node = Node::Ident {
|
||||
@@ -76,12 +76,12 @@ const COLUMN_NAME: Node = Node::Ident {
|
||||
role: "column_name",
|
||||
validator: None,
|
||||
highlight_override: None,
|
||||
writes_table: false,
|
||||
writes_column: false,
|
||||
writes_user_listed_column: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
writes_table: false,
|
||||
writes_column: false,
|
||||
writes_user_listed_column: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
};
|
||||
|
||||
const COLUMN_NAME_NEW_IDENT: Node = Node::Ident {
|
||||
@@ -89,12 +89,12 @@ const COLUMN_NAME_NEW_IDENT: Node = Node::Ident {
|
||||
role: "column_name",
|
||||
validator: None,
|
||||
highlight_override: None,
|
||||
writes_table: false,
|
||||
writes_column: false,
|
||||
writes_user_listed_column: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
writes_table: false,
|
||||
writes_column: false,
|
||||
writes_user_listed_column: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
};
|
||||
const COLUMN_NAME_NEW: Node = Node::Hinted {
|
||||
mode: NEW_NAME_HINT,
|
||||
@@ -106,12 +106,12 @@ const RELATIONSHIP_NAME: Node = Node::Ident {
|
||||
role: "relationship_name",
|
||||
validator: None,
|
||||
highlight_override: None,
|
||||
writes_table: false,
|
||||
writes_column: false,
|
||||
writes_user_listed_column: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
writes_table: false,
|
||||
writes_column: false,
|
||||
writes_user_listed_column: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
};
|
||||
|
||||
const RELATIONSHIP_NAME_NEW_IDENT: Node = Node::Ident {
|
||||
@@ -119,12 +119,12 @@ const RELATIONSHIP_NAME_NEW_IDENT: Node = Node::Ident {
|
||||
role: "relationship_name",
|
||||
validator: None,
|
||||
highlight_override: None,
|
||||
writes_table: false,
|
||||
writes_column: false,
|
||||
writes_user_listed_column: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
writes_table: false,
|
||||
writes_column: false,
|
||||
writes_user_listed_column: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
};
|
||||
const RELATIONSHIP_NAME_NEW: Node = Node::Hinted {
|
||||
mode: NEW_NAME_HINT,
|
||||
@@ -139,9 +139,9 @@ const INDEX_NAME_EXISTING: Node = Node::Ident {
|
||||
writes_table: false,
|
||||
writes_column: false,
|
||||
writes_user_listed_column: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
};
|
||||
|
||||
const INDEX_NAME_NEW_IDENT: Node = Node::Ident {
|
||||
@@ -152,9 +152,9 @@ const INDEX_NAME_NEW_IDENT: Node = Node::Ident {
|
||||
writes_table: false,
|
||||
writes_column: false,
|
||||
writes_user_listed_column: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
};
|
||||
const INDEX_NAME_NEW: Node = Node::Hinted {
|
||||
mode: NEW_NAME_HINT,
|
||||
@@ -181,10 +181,7 @@ const TABLE_OPT: Node = Node::Optional(&Node::Word(Word::keyword("table")));
|
||||
// drop_table — `drop table <T>`
|
||||
// =================================================================
|
||||
|
||||
const DROP_TABLE_NODES: &[Node] = &[
|
||||
Node::Word(Word::keyword("table")),
|
||||
TABLE_NAME_EXISTING,
|
||||
];
|
||||
const DROP_TABLE_NODES: &[Node] = &[Node::Word(Word::keyword("table")), TABLE_NAME_EXISTING];
|
||||
const DROP_TABLE: Node = Node::Seq(DROP_TABLE_NODES);
|
||||
|
||||
// Advanced-mode SQL `DROP TABLE [IF EXISTS] <name> [;]` (ADR-0035 §4,
|
||||
@@ -192,8 +189,10 @@ const DROP_TABLE: Node = Node::Seq(DROP_TABLE_NODES);
|
||||
// plus the optional `IF EXISTS` no-op-with-note. The leading concrete
|
||||
// `table` keyword (not the Optional) keeps the element/dispatch
|
||||
// matching honest.
|
||||
static SQL_DROP_IF_EXISTS_NODES: &[Node] =
|
||||
&[Node::Word(Word::keyword("if")), Node::Word(Word::keyword("exists"))];
|
||||
static SQL_DROP_IF_EXISTS_NODES: &[Node] = &[
|
||||
Node::Word(Word::keyword("if")),
|
||||
Node::Word(Word::keyword("exists")),
|
||||
];
|
||||
const SQL_DROP_IF_EXISTS_OPT: Node = Node::Optional(&Node::Seq(SQL_DROP_IF_EXISTS_NODES));
|
||||
static SQL_DROP_TABLE_SHAPE_NODES: &[Node] = &[
|
||||
Node::Word(Word::keyword("table")),
|
||||
@@ -257,9 +256,9 @@ const DR_PARENT_NODES: &[Node] = &[
|
||||
writes_table: true,
|
||||
writes_column: false,
|
||||
writes_user_listed_column: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
},
|
||||
Node::Punct('.'),
|
||||
Node::Ident {
|
||||
@@ -270,9 +269,9 @@ const DR_PARENT_NODES: &[Node] = &[
|
||||
writes_table: false,
|
||||
writes_column: false,
|
||||
writes_user_listed_column: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
},
|
||||
];
|
||||
const DR_PARENT: Node = Node::Seq(DR_PARENT_NODES);
|
||||
@@ -286,9 +285,9 @@ const DR_CHILD_NODES: &[Node] = &[
|
||||
writes_table: true,
|
||||
writes_column: false,
|
||||
writes_user_listed_column: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
},
|
||||
Node::Punct('.'),
|
||||
Node::Ident {
|
||||
@@ -299,9 +298,9 @@ const DR_CHILD_NODES: &[Node] = &[
|
||||
writes_table: false,
|
||||
writes_column: false,
|
||||
writes_user_listed_column: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
},
|
||||
];
|
||||
const DR_CHILD: Node = Node::Seq(DR_CHILD_NODES);
|
||||
@@ -317,10 +316,7 @@ const DR_ENDPOINTS: Node = Node::Seq(DR_ENDPOINTS_NODES);
|
||||
const DR_SELECTOR_CHOICES: &[Node] = &[DR_ENDPOINTS, RELATIONSHIP_NAME];
|
||||
const DR_SELECTOR: Node = Node::Choice(DR_SELECTOR_CHOICES);
|
||||
|
||||
const DROP_RELATIONSHIP_NODES: &[Node] = &[
|
||||
Node::Word(Word::keyword("relationship")),
|
||||
DR_SELECTOR,
|
||||
];
|
||||
const DROP_RELATIONSHIP_NODES: &[Node] = &[Node::Word(Word::keyword("relationship")), DR_SELECTOR];
|
||||
const DROP_RELATIONSHIP: Node = Node::Seq(DROP_RELATIONSHIP_NODES);
|
||||
|
||||
// =================================================================
|
||||
@@ -341,18 +337,20 @@ const DI_POSITIONAL: Node = Node::Seq(DI_POSITIONAL_NODES);
|
||||
const DI_SELECTOR_CHOICES: &[Node] = &[DI_POSITIONAL, INDEX_NAME_EXISTING];
|
||||
const DI_SELECTOR: Node = Node::Choice(DI_SELECTOR_CHOICES);
|
||||
|
||||
const DROP_INDEX_NODES: &[Node] = &[
|
||||
Node::Word(Word::keyword("index")),
|
||||
DI_SELECTOR,
|
||||
];
|
||||
const DROP_INDEX_NODES: &[Node] = &[Node::Word(Word::keyword("index")), DI_SELECTOR];
|
||||
const DROP_INDEX: Node = Node::Seq(DROP_INDEX_NODES);
|
||||
|
||||
// =================================================================
|
||||
// drop entry — `drop (table|column|relationship|index) ...`
|
||||
// =================================================================
|
||||
|
||||
const DROP_CHOICES: &[Node] =
|
||||
&[DROP_COLUMN, DROP_RELATIONSHIP, DROP_TABLE, DROP_INDEX, DROP_CONSTRAINT];
|
||||
const DROP_CHOICES: &[Node] = &[
|
||||
DROP_COLUMN,
|
||||
DROP_RELATIONSHIP,
|
||||
DROP_TABLE,
|
||||
DROP_INDEX,
|
||||
DROP_CONSTRAINT,
|
||||
];
|
||||
const DROP_SHAPE: Node = Node::Choice(DROP_CHOICES);
|
||||
|
||||
// =================================================================
|
||||
@@ -450,8 +448,7 @@ const AR_CHILD_COL_LIST: Node = Node::Repeated {
|
||||
separator: Some(&Node::Punct(',')),
|
||||
min: 1,
|
||||
};
|
||||
const AR_CHILD_COLS_PAREN_NODES: &[Node] =
|
||||
&[Node::Punct('('), AR_CHILD_COL_LIST, Node::Punct(')')];
|
||||
const AR_CHILD_COLS_PAREN_NODES: &[Node] = &[Node::Punct('('), AR_CHILD_COL_LIST, Node::Punct(')')];
|
||||
const AR_CHILD_COLS_PAREN: Node = Node::Seq(AR_CHILD_COLS_PAREN_NODES);
|
||||
const AR_CHILD_COLS_CHOICES: &[Node] = &[AR_CHILD_COLS_PAREN, AR_CHILD_COL];
|
||||
const AR_CHILD_COLS: Node = Node::Choice(AR_CHILD_COLS_CHOICES);
|
||||
@@ -474,10 +471,7 @@ const AR_CHILD_NODES: &[Node] = &[
|
||||
];
|
||||
const AR_CHILD: Node = Node::Seq(AR_CHILD_NODES);
|
||||
|
||||
const AR_AS_NAME_NODES: &[Node] = &[
|
||||
Node::Word(Word::keyword("as")),
|
||||
RELATIONSHIP_NAME_NEW,
|
||||
];
|
||||
const AR_AS_NAME_NODES: &[Node] = &[Node::Word(Word::keyword("as")), RELATIONSHIP_NAME_NEW];
|
||||
const AR_AS_NAME_OPT: Node = Node::Optional(&Node::Seq(AR_AS_NAME_NODES));
|
||||
|
||||
const AR_CREATE_FK_OPT: Node = Node::Optional(&Node::Flag("create-fk"));
|
||||
@@ -501,10 +495,7 @@ const ADD_RELATIONSHIP: Node = Node::Seq(ADD_RELATIONSHIP_NODES);
|
||||
// add_index — `add index [as <name>] on <T> (<col>, …)`
|
||||
// =================================================================
|
||||
|
||||
const AI_AS_NAME_NODES: &[Node] = &[
|
||||
Node::Word(Word::keyword("as")),
|
||||
INDEX_NAME_NEW,
|
||||
];
|
||||
const AI_AS_NAME_NODES: &[Node] = &[Node::Word(Word::keyword("as")), INDEX_NAME_NEW];
|
||||
const AI_AS_NAME_OPT: Node = Node::Optional(&Node::Seq(AI_AS_NAME_NODES));
|
||||
|
||||
const ADD_INDEX_NODES: &[Node] = &[
|
||||
@@ -537,9 +528,9 @@ const NEW_COLUMN_NAME_IDENT: Node = Node::Ident {
|
||||
writes_table: false,
|
||||
writes_column: false,
|
||||
writes_user_listed_column: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
};
|
||||
const NEW_COLUMN_NAME: Node = Node::Hinted {
|
||||
mode: NEW_NAME_HINT,
|
||||
@@ -563,10 +554,7 @@ const RENAME_COLUMN: Node = Node::Seq(RENAME_COLUMN_NODES);
|
||||
// ( <type> ) [--force-conversion | --dont-convert]`
|
||||
// =================================================================
|
||||
|
||||
const CHANGE_FLAG_CHOICES: &[Node] = &[
|
||||
Node::Flag("force-conversion"),
|
||||
Node::Flag("dont-convert"),
|
||||
];
|
||||
const CHANGE_FLAG_CHOICES: &[Node] = &[Node::Flag("force-conversion"), Node::Flag("dont-convert")];
|
||||
const CHANGE_FLAG_OPT: Node = Node::Repeated {
|
||||
inner: &Node::Choice(CHANGE_FLAG_CHOICES),
|
||||
separator: None,
|
||||
@@ -732,8 +720,7 @@ fn build_add(path: &MatchedPath, _source: &str) -> Result<Command, ValidationErr
|
||||
message_key: "parse.error_wrapper",
|
||||
args: vec![("detail", "unknown type".to_string())],
|
||||
})?;
|
||||
let (not_null, unique, default, check) =
|
||||
collect_column_constraints(path)?;
|
||||
let (not_null, unique, default, check) = collect_column_constraints(path)?;
|
||||
Ok(Command::AddColumn {
|
||||
table: require_ident(path, "table_name")?,
|
||||
column: require_ident(path, "column_name")?,
|
||||
@@ -949,7 +936,10 @@ fn build_drop_constraint(path: &MatchedPath, _source: &str) -> Result<Command, V
|
||||
} else {
|
||||
return Err(ValidationError {
|
||||
message_key: "parse.error_wrapper",
|
||||
args: vec![("detail", "drop constraint needs a constraint kind".to_string())],
|
||||
args: vec![(
|
||||
"detail",
|
||||
"drop constraint needs a constraint kind".to_string(),
|
||||
)],
|
||||
});
|
||||
};
|
||||
Ok(Command::DropConstraint {
|
||||
@@ -981,7 +971,8 @@ pub static DROP: CommandNode = CommandNode {
|
||||
"parse.usage.drop_relationship",
|
||||
"parse.usage.drop_index",
|
||||
"parse.usage.drop_constraint",
|
||||
],};
|
||||
],
|
||||
};
|
||||
|
||||
pub static ADD: CommandNode = CommandNode {
|
||||
entry: Word::keyword("add"),
|
||||
@@ -1003,7 +994,8 @@ pub static ADD: CommandNode = CommandNode {
|
||||
"parse.usage.add_relationship",
|
||||
"parse.usage.add_index",
|
||||
"parse.usage.add_constraint",
|
||||
],};
|
||||
],
|
||||
};
|
||||
|
||||
pub static RENAME: CommandNode = CommandNode {
|
||||
entry: Word::keyword("rename"),
|
||||
@@ -1011,7 +1003,8 @@ pub static RENAME: CommandNode = CommandNode {
|
||||
ast_builder: build_rename_column,
|
||||
help_id: Some("ddl.rename"),
|
||||
hint_ids: &["rename_column"],
|
||||
usage_ids: &["parse.usage.rename_column"],};
|
||||
usage_ids: &["parse.usage.rename_column"],
|
||||
};
|
||||
|
||||
pub static CHANGE: CommandNode = CommandNode {
|
||||
entry: Word::keyword("change"),
|
||||
@@ -1019,7 +1012,8 @@ pub static CHANGE: CommandNode = CommandNode {
|
||||
ast_builder: build_change_column,
|
||||
help_id: Some("ddl.change"),
|
||||
hint_ids: &["change_column"],
|
||||
usage_ids: &["parse.usage.change_column"],};
|
||||
usage_ids: &["parse.usage.change_column"],
|
||||
};
|
||||
|
||||
// =================================================================
|
||||
// create_table — `create table <Name> [with pk [<col>(<type>)[, ...]]]`
|
||||
@@ -1034,9 +1028,9 @@ const COL_NAME_IDENT: Node = Node::Ident {
|
||||
writes_table: false,
|
||||
writes_column: false,
|
||||
writes_user_listed_column: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
};
|
||||
const COL_NAME: Node = Node::Hinted {
|
||||
mode: NEW_NAME_HINT,
|
||||
@@ -1074,8 +1068,12 @@ const CHECK_CONSTRAINT_NODES: &[Node] = &[
|
||||
];
|
||||
const CHECK_CONSTRAINT: Node = Node::Seq(CHECK_CONSTRAINT_NODES);
|
||||
|
||||
const COLUMN_CONSTRAINT_CHOICES: &[Node] =
|
||||
&[NOT_NULL_CONSTRAINT, UNIQUE_CONSTRAINT, DEFAULT_CONSTRAINT, CHECK_CONSTRAINT];
|
||||
const COLUMN_CONSTRAINT_CHOICES: &[Node] = &[
|
||||
NOT_NULL_CONSTRAINT,
|
||||
UNIQUE_CONSTRAINT,
|
||||
DEFAULT_CONSTRAINT,
|
||||
CHECK_CONSTRAINT,
|
||||
];
|
||||
const COLUMN_CONSTRAINT: Node = Node::Choice(COLUMN_CONSTRAINT_CHOICES);
|
||||
|
||||
/// Zero-or-more constraints — the suffix after a column's
|
||||
@@ -1114,8 +1112,7 @@ const DROP_CONSTRAINT_KIND: Node = Node::Choice(DROP_CONSTRAINT_KIND_CHOICES);
|
||||
// `writes_table: true` on the table ident (via `TABLE_NAME_
|
||||
// EXISTING`) narrows the `.<column>` slot's completion
|
||||
// candidates to that table's columns.
|
||||
const CONSTRAINT_TARGET_NODES: &[Node] =
|
||||
&[TABLE_NAME_EXISTING, Node::Punct('.'), COLUMN_NAME];
|
||||
const CONSTRAINT_TARGET_NODES: &[Node] = &[TABLE_NAME_EXISTING, Node::Punct('.'), COLUMN_NAME];
|
||||
const CONSTRAINT_TARGET: Node = Node::Seq(CONSTRAINT_TARGET_NODES);
|
||||
|
||||
const ADD_CONSTRAINT_NODES: &[Node] = &[
|
||||
@@ -1145,9 +1142,9 @@ const COL_SPEC_NODES: &[Node] = &[
|
||||
writes_table: false,
|
||||
writes_column: false,
|
||||
writes_user_listed_column: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
},
|
||||
Node::Punct(')'),
|
||||
COLUMN_CONSTRAINT_SUFFIX,
|
||||
@@ -1275,10 +1272,14 @@ fn build_create_table(path: &MatchedPath, _source: &str) -> Result<Command, Vali
|
||||
let mut items = path.items.iter().peekable();
|
||||
while let Some(item) = items.next() {
|
||||
match &item.kind {
|
||||
MatchedKind::Ident { role: "col_name", .. } => {
|
||||
MatchedKind::Ident {
|
||||
role: "col_name", ..
|
||||
} => {
|
||||
pending_name = Some(item.text.clone());
|
||||
}
|
||||
MatchedKind::Ident { role: "col_type", .. } => {
|
||||
MatchedKind::Ident {
|
||||
role: "col_type", ..
|
||||
} => {
|
||||
let ty = item.text.parse::<Type>().map_err(|_| ValidationError {
|
||||
message_key: "parse.error_wrapper",
|
||||
args: vec![("detail", "unknown type".to_string())],
|
||||
@@ -1380,7 +1381,8 @@ pub static CREATE: CommandNode = CommandNode {
|
||||
ast_builder: build_create_table,
|
||||
help_id: Some("ddl.create"),
|
||||
hint_ids: &["create_table"],
|
||||
usage_ids: &["parse.usage.create_table"],};
|
||||
usage_ids: &["parse.usage.create_table"],
|
||||
};
|
||||
|
||||
// =================================================================
|
||||
// create_m2n — `create m:n relationship from <T1> to <T2> [as <name>]`
|
||||
@@ -1506,11 +1508,15 @@ fn build_sql_create_table(path: &MatchedPath, source: &str) -> Result<Command, V
|
||||
while let Some(item) = items.next() {
|
||||
match &item.kind {
|
||||
// A column name stashes until its type finalises the spec.
|
||||
MatchedKind::Ident { role: "col_name", .. } => {
|
||||
MatchedKind::Ident {
|
||||
role: "col_name", ..
|
||||
} => {
|
||||
pending_name = Some(item.text.clone());
|
||||
}
|
||||
// Single-word type — resolve through the SQL alias map.
|
||||
MatchedKind::Ident { role: "col_type", .. } => {
|
||||
MatchedKind::Ident {
|
||||
role: "col_type", ..
|
||||
} => {
|
||||
let ty = Type::from_sql_name(&item.text).ok_or_else(|| ValidationError {
|
||||
message_key: "parse.error_wrapper",
|
||||
args: vec![("detail", "unknown type".to_string())],
|
||||
@@ -1533,7 +1539,9 @@ fn build_sql_create_table(path: &MatchedPath, source: &str) -> Result<Command, V
|
||||
column_open = true;
|
||||
}
|
||||
// A table-level `PRIMARY KEY (col, …)` column reference.
|
||||
MatchedKind::Ident { role: "pk_column", .. } => {
|
||||
MatchedKind::Ident {
|
||||
role: "pk_column", ..
|
||||
} => {
|
||||
primary_key.push(item.text.clone());
|
||||
}
|
||||
// `not null` column constraint (only once a column exists;
|
||||
@@ -1557,7 +1565,10 @@ fn build_sql_create_table(path: &MatchedPath, source: &str) -> Result<Command, V
|
||||
let mut cols: Vec<String> = Vec::new();
|
||||
while let Some(it) = items.peek() {
|
||||
match &it.kind {
|
||||
MatchedKind::Ident { role: "unique_column", .. } => {
|
||||
MatchedKind::Ident {
|
||||
role: "unique_column",
|
||||
..
|
||||
} => {
|
||||
cols.push(it.text.clone());
|
||||
items.next();
|
||||
}
|
||||
@@ -1575,7 +1586,10 @@ fn build_sql_create_table(path: &MatchedPath, source: &str) -> Result<Command, V
|
||||
// column's flag (round-trips via the single-column
|
||||
// path); composite (or a name not among the
|
||||
// columns) becomes a constraint.
|
||||
match columns.iter_mut().find(|c| cols.len() == 1 && c.name == cols[0]) {
|
||||
match columns
|
||||
.iter_mut()
|
||||
.find(|c| cols.len() == 1 && c.name == cols[0])
|
||||
{
|
||||
Some(c) => c.unique = true,
|
||||
None if !cols.is_empty() => unique_constraints.push(cols),
|
||||
None => {}
|
||||
@@ -1588,16 +1602,17 @@ fn build_sql_create_table(path: &MatchedPath, source: &str) -> Result<Command, V
|
||||
// the most recent column) or the table-level clause (whose
|
||||
// `pk_column` idents follow and are collected above).
|
||||
MatchedKind::Word("primary") => {
|
||||
if matches!(items.peek().map(|i| &i.kind), Some(MatchedKind::Word("key"))) {
|
||||
if matches!(
|
||||
items.peek().map(|i| &i.kind),
|
||||
Some(MatchedKind::Word("key"))
|
||||
) {
|
||||
items.next();
|
||||
// Table-level `PRIMARY KEY (…)` is followed by `(`
|
||||
// (then `pk_column` idents, collected above);
|
||||
// column-level `PRIMARY KEY` is not, and marks the
|
||||
// most-recent column.
|
||||
let table_level = matches!(
|
||||
items.peek().map(|i| &i.kind),
|
||||
Some(MatchedKind::Punct('('))
|
||||
);
|
||||
let table_level =
|
||||
matches!(items.peek().map(|i| &i.kind), Some(MatchedKind::Punct('(')));
|
||||
if !table_level && let Some(last) = columns.last() {
|
||||
primary_key.push(last.name.clone());
|
||||
}
|
||||
@@ -1647,12 +1662,20 @@ fn build_sql_create_table(path: &MatchedPath, source: &str) -> Result<Command, V
|
||||
// Inline FK is single-column (the column it sits on);
|
||||
// a compound FK uses the table-level form (ADR-0043 D4).
|
||||
let child_column = columns.last().map_or_else(String::new, |c| c.name.clone());
|
||||
foreign_keys.push(consume_fk_reference(&mut items, None, vec![child_column], true));
|
||||
foreign_keys.push(consume_fk_reference(
|
||||
&mut items,
|
||||
None,
|
||||
vec![child_column],
|
||||
true,
|
||||
));
|
||||
}
|
||||
// Table-level `[constraint <name>] foreign key (<col>)
|
||||
// references <parent> [(<col>)] [on …]` (ADR-0035 §5, 4b).
|
||||
MatchedKind::Word("foreign") => {
|
||||
if matches!(items.peek().map(|i| &i.kind), Some(MatchedKind::Word("key"))) {
|
||||
if matches!(
|
||||
items.peek().map(|i| &i.kind),
|
||||
Some(MatchedKind::Word("key"))
|
||||
) {
|
||||
items.next(); // `key`
|
||||
}
|
||||
// `( <child column> [, <child column>]* )` — a compound
|
||||
@@ -1674,7 +1697,10 @@ fn build_sql_create_table(path: &MatchedPath, source: &str) -> Result<Command, V
|
||||
items.next();
|
||||
}
|
||||
// `references <parent> …`
|
||||
if matches!(items.peek().map(|i| &i.kind), Some(MatchedKind::Word("references"))) {
|
||||
if matches!(
|
||||
items.peek().map(|i| &i.kind),
|
||||
Some(MatchedKind::Word("references"))
|
||||
) {
|
||||
items.next();
|
||||
}
|
||||
let fk =
|
||||
@@ -1859,13 +1885,19 @@ where
|
||||
Some(MatchedKind::Word("cascade")) => ReferentialAction::Cascade,
|
||||
Some(MatchedKind::Word("restrict")) => ReferentialAction::Restrict,
|
||||
Some(MatchedKind::Word("set")) => {
|
||||
if matches!(items.peek().map(|i| &i.kind), Some(MatchedKind::Word("null"))) {
|
||||
if matches!(
|
||||
items.peek().map(|i| &i.kind),
|
||||
Some(MatchedKind::Word("null"))
|
||||
) {
|
||||
items.next();
|
||||
}
|
||||
ReferentialAction::SetNull
|
||||
}
|
||||
Some(MatchedKind::Word("no")) => {
|
||||
if matches!(items.peek().map(|i| &i.kind), Some(MatchedKind::Word("action"))) {
|
||||
if matches!(
|
||||
items.peek().map(|i| &i.kind),
|
||||
Some(MatchedKind::Word("action"))
|
||||
) {
|
||||
items.next();
|
||||
}
|
||||
ReferentialAction::NoAction
|
||||
@@ -1933,11 +1965,12 @@ pub static SQL_DROP_INDEX: CommandNode = CommandNode {
|
||||
// concrete keyword (`unique index` | `index`) — the trap-safe form (the
|
||||
// §3 rule forbids a leading *Optional*, not a leading `Choice`). The
|
||||
// builder reads `unique` presence via `contains_word("unique")`.
|
||||
static SQL_CI_UNIQUE_INDEX_NODES: &[Node] =
|
||||
&[Node::Word(Word::keyword("unique")), Node::Word(Word::keyword("index"))];
|
||||
static SQL_CI_UNIQUE_INDEX_NODES: &[Node] = &[
|
||||
Node::Word(Word::keyword("unique")),
|
||||
Node::Word(Word::keyword("index")),
|
||||
];
|
||||
const SQL_CI_UNIQUE_INDEX: Node = Node::Seq(SQL_CI_UNIQUE_INDEX_NODES);
|
||||
static SQL_CI_LEAD_CHOICES: &[Node] =
|
||||
&[SQL_CI_UNIQUE_INDEX, Node::Word(Word::keyword("index"))];
|
||||
static SQL_CI_LEAD_CHOICES: &[Node] = &[SQL_CI_UNIQUE_INDEX, Node::Word(Word::keyword("index"))];
|
||||
const SQL_CI_LEAD: Node = Node::Choice(SQL_CI_LEAD_CHOICES);
|
||||
|
||||
static SQL_CI_IF_NOT_EXISTS_NODES: &[Node] = &[
|
||||
@@ -2104,8 +2137,7 @@ static AT_RENAME_COLUMN_TAIL_NODES: &[Node] = &[
|
||||
NEW_COLUMN_NAME,
|
||||
];
|
||||
const AT_RENAME_COLUMN_TAIL: Node = Node::Seq(AT_RENAME_COLUMN_TAIL_NODES);
|
||||
static AT_RENAME_TABLE_TAIL_NODES: &[Node] =
|
||||
&[Node::Word(Word::keyword("to")), NEW_TABLE_NAME];
|
||||
static AT_RENAME_TABLE_TAIL_NODES: &[Node] = &[Node::Word(Word::keyword("to")), NEW_TABLE_NAME];
|
||||
const AT_RENAME_TABLE_TAIL: Node = Node::Seq(AT_RENAME_TABLE_TAIL_NODES);
|
||||
static AT_RENAME_TAIL_CHOICES: &[Node] = &[AT_RENAME_COLUMN_TAIL, AT_RENAME_TABLE_TAIL];
|
||||
const AT_RENAME_TAIL: Node = Node::Choice(AT_RENAME_TAIL_CHOICES);
|
||||
@@ -2132,8 +2164,10 @@ static AT_AC_TYPE_NODES: &[Node] = &[
|
||||
super::sql_create_table::SQL_TYPE,
|
||||
];
|
||||
const AT_AC_TYPE: Node = Node::Seq(AT_AC_TYPE_NODES);
|
||||
static AT_AC_NOT_NULL_NODES: &[Node] =
|
||||
&[Node::Word(Word::keyword("not")), Node::Word(Word::keyword("null"))];
|
||||
static AT_AC_NOT_NULL_NODES: &[Node] = &[
|
||||
Node::Word(Word::keyword("not")),
|
||||
Node::Word(Word::keyword("null")),
|
||||
];
|
||||
const AT_AC_NOT_NULL: Node = Node::Seq(AT_AC_NOT_NULL_NODES);
|
||||
static AT_AC_SET_DATA_TYPE_NODES: &[Node] = &[
|
||||
Node::Word(Word::keyword("data")),
|
||||
@@ -2149,8 +2183,7 @@ static AT_AC_SET_TAIL_CHOICES: &[Node] = &[
|
||||
const AT_AC_SET_TAIL: Node = Node::Choice(AT_AC_SET_TAIL_CHOICES);
|
||||
static AT_AC_SET_NODES: &[Node] = &[Node::Word(Word::keyword("set")), AT_AC_SET_TAIL];
|
||||
const AT_AC_SET: Node = Node::Seq(AT_AC_SET_NODES);
|
||||
static AT_AC_DROP_TAIL_CHOICES: &[Node] =
|
||||
&[AT_AC_NOT_NULL, Node::Word(Word::keyword("default"))];
|
||||
static AT_AC_DROP_TAIL_CHOICES: &[Node] = &[AT_AC_NOT_NULL, Node::Word(Word::keyword("default"))];
|
||||
const AT_AC_DROP_TAIL: Node = Node::Choice(AT_AC_DROP_TAIL_CHOICES);
|
||||
static AT_AC_DROP_NODES: &[Node] = &[Node::Word(Word::keyword("drop")), AT_AC_DROP_TAIL];
|
||||
const AT_AC_DROP: Node = Node::Seq(AT_AC_DROP_NODES);
|
||||
@@ -2258,10 +2291,14 @@ fn build_alter_add_column_spec(
|
||||
let mut items = path.items.iter().peekable();
|
||||
while let Some(item) = items.next() {
|
||||
match &item.kind {
|
||||
MatchedKind::Ident { role: "col_name", .. } => {
|
||||
MatchedKind::Ident {
|
||||
role: "col_name", ..
|
||||
} => {
|
||||
pending_name = Some(item.text.clone());
|
||||
}
|
||||
MatchedKind::Ident { role: "col_type", .. } => {
|
||||
MatchedKind::Ident {
|
||||
role: "col_type", ..
|
||||
} => {
|
||||
let ty = Type::from_sql_name(&item.text).ok_or_else(|| ValidationError {
|
||||
message_key: "parse.error_wrapper",
|
||||
args: vec![("detail", "unknown type".to_string())],
|
||||
@@ -2280,7 +2317,10 @@ fn build_alter_add_column_spec(
|
||||
spec = Some(ColumnSpec::new(name, Type::Real));
|
||||
}
|
||||
MatchedKind::Word("not") => {
|
||||
if matches!(items.peek().map(|i| &i.kind), Some(MatchedKind::Word("null"))) {
|
||||
if matches!(
|
||||
items.peek().map(|i| &i.kind),
|
||||
Some(MatchedKind::Word("null"))
|
||||
) {
|
||||
items.next();
|
||||
if let Some(s) = spec.as_mut() {
|
||||
s.not_null = true;
|
||||
@@ -2326,11 +2366,15 @@ fn build_alter_column_type(path: &MatchedPath) -> Result<AlterTableAction, Valid
|
||||
let mut items = path.items.iter().peekable();
|
||||
while let Some(item) = items.next() {
|
||||
match &item.kind {
|
||||
MatchedKind::Ident { role: "col_type", .. } => {
|
||||
ty = Some(Type::from_sql_name(&item.text).ok_or_else(|| ValidationError {
|
||||
message_key: "parse.error_wrapper",
|
||||
args: vec![("detail", "unknown type".to_string())],
|
||||
})?);
|
||||
MatchedKind::Ident {
|
||||
role: "col_type", ..
|
||||
} => {
|
||||
ty = Some(
|
||||
Type::from_sql_name(&item.text).ok_or_else(|| ValidationError {
|
||||
message_key: "parse.error_wrapper",
|
||||
args: vec![("detail", "unknown type".to_string())],
|
||||
})?,
|
||||
);
|
||||
}
|
||||
MatchedKind::Word("double") => {
|
||||
if matches!(
|
||||
@@ -2379,7 +2423,10 @@ fn build_alter_column_attr(
|
||||
message_key: "parse.error_wrapper",
|
||||
args: vec![("detail", "set default needs a value".to_string())],
|
||||
})?;
|
||||
AlterTableAction::SetColumnDefault { column, default_sql }
|
||||
AlterTableAction::SetColumnDefault {
|
||||
column,
|
||||
default_sql,
|
||||
}
|
||||
}
|
||||
(false, true) => AlterTableAction::DropColumnDefault { column },
|
||||
(true, false) => AlterTableAction::SetColumnNotNull { column },
|
||||
@@ -2495,10 +2542,7 @@ fn build_alter_add_table_constraint(
|
||||
/// Capture the raw SQL text of an `ADD … CHECK (<expr>)` (ADR-0035 §4g).
|
||||
/// `sql_expr` is validate-only, so the expression is captured by byte
|
||||
/// span — the 4a.2 / 4e mechanism.
|
||||
fn capture_table_check_sql(
|
||||
path: &MatchedPath,
|
||||
source: &str,
|
||||
) -> Result<String, ValidationError> {
|
||||
fn capture_table_check_sql(path: &MatchedPath, source: &str) -> Result<String, ValidationError> {
|
||||
let mut items = path.items.iter().peekable();
|
||||
while let Some(item) = items.next() {
|
||||
if matches!(item.kind, MatchedKind::Word("check"))
|
||||
@@ -2528,7 +2572,10 @@ fn build_alter_fk(path: &MatchedPath) -> SqlForeignKey {
|
||||
items.next();
|
||||
}
|
||||
items.next(); // `foreign`
|
||||
if matches!(items.peek().map(|i| &i.kind), Some(MatchedKind::Word("key"))) {
|
||||
if matches!(
|
||||
items.peek().map(|i| &i.kind),
|
||||
Some(MatchedKind::Word("key"))
|
||||
) {
|
||||
items.next();
|
||||
}
|
||||
if matches!(items.peek().map(|i| &i.kind), Some(MatchedKind::Punct('('))) {
|
||||
@@ -2548,7 +2595,10 @@ fn build_alter_fk(path: &MatchedPath) -> SqlForeignKey {
|
||||
if matches!(items.peek().map(|i| &i.kind), Some(MatchedKind::Punct(')'))) {
|
||||
items.next();
|
||||
}
|
||||
if matches!(items.peek().map(|i| &i.kind), Some(MatchedKind::Word("references"))) {
|
||||
if matches!(
|
||||
items.peek().map(|i| &i.kind),
|
||||
Some(MatchedKind::Word("references"))
|
||||
) {
|
||||
items.next();
|
||||
}
|
||||
// `ALTER TABLE … ADD FOREIGN KEY (…)` is the table-level form.
|
||||
@@ -2626,7 +2676,10 @@ mod constraint_tests {
|
||||
fn an_unconstrained_create_table_still_parses() {
|
||||
let cols = create_columns("create table T with pk id(serial), name(text)");
|
||||
assert_eq!(cols.len(), 2);
|
||||
assert!(cols.iter().all(|c| !c.not_null && !c.unique && c.default.is_none()));
|
||||
assert!(
|
||||
cols.iter()
|
||||
.all(|c| !c.not_null && !c.unique && c.default.is_none())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2651,7 +2704,9 @@ mod constraint_tests {
|
||||
#[test]
|
||||
fn add_column_parses_a_unique_constraint() {
|
||||
match parse_command("add column to T: email (text) unique").expect("parse") {
|
||||
Command::AddColumn { unique, not_null, .. } => {
|
||||
Command::AddColumn {
|
||||
unique, not_null, ..
|
||||
} => {
|
||||
assert!(unique);
|
||||
assert!(!not_null);
|
||||
}
|
||||
@@ -2682,9 +2737,7 @@ mod constraint_tests {
|
||||
fn check_with_a_parenthesised_sub_expression_parses() {
|
||||
// The check's own parens plus a nested group — the
|
||||
// builder's paren-depth scan must pair them correctly.
|
||||
let cols = create_columns(
|
||||
"create table T with pk n(int) check ((n > 0) or (n < -10))",
|
||||
);
|
||||
let cols = create_columns("create table T with pk n(int) check ((n > 0) or (n < -10))");
|
||||
assert!(cols[0].check.is_some());
|
||||
}
|
||||
|
||||
@@ -2731,8 +2784,7 @@ mod constraint_tests {
|
||||
|
||||
#[test]
|
||||
fn add_constraint_check_parses() {
|
||||
match parse_command("add constraint check (age >= 0) to Users.age").expect("parse")
|
||||
{
|
||||
match parse_command("add constraint check (age >= 0) to Users.age").expect("parse") {
|
||||
Command::AddConstraint {
|
||||
column, constraint, ..
|
||||
} => {
|
||||
@@ -2826,8 +2878,11 @@ mod sql_drop_table_tests {
|
||||
Command::DropColumn { .. }
|
||||
));
|
||||
assert!(matches!(
|
||||
parse_command_in_mode("drop relationship Customers_id_to_Orders_CustId", Mode::Advanced)
|
||||
.expect("parses"),
|
||||
parse_command_in_mode(
|
||||
"drop relationship Customers_id_to_Orders_CustId",
|
||||
Mode::Advanced
|
||||
)
|
||||
.expect("parses"),
|
||||
Command::DropRelationship { .. }
|
||||
));
|
||||
}
|
||||
@@ -2932,7 +2987,13 @@ mod sql_create_index_tests {
|
||||
columns,
|
||||
unique,
|
||||
if_not_exists,
|
||||
} => Ci { name, table, columns, unique, if_not_exists },
|
||||
} => Ci {
|
||||
name,
|
||||
table,
|
||||
columns,
|
||||
unique,
|
||||
if_not_exists,
|
||||
},
|
||||
other => panic!("expected SqlCreateIndex, got {other:?}"),
|
||||
}
|
||||
}
|
||||
@@ -3134,7 +3195,9 @@ mod sql_alter_table_tests {
|
||||
// The target slot carries the `reject_internal_table` validator
|
||||
// (mirroring CREATE TABLE), so an `__rdbms_*` target is refused
|
||||
// before submit — engine-neutral, not a raw engine error.
|
||||
assert!(parse_command_in_mode("alter table T rename to __rdbms_evil", Mode::Advanced).is_err());
|
||||
assert!(
|
||||
parse_command_in_mode("alter table T rename to __rdbms_evil", Mode::Advanced).is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -3213,7 +3276,10 @@ mod sql_alter_table_tests {
|
||||
// alias map still applies through the synonym
|
||||
assert!(matches!(
|
||||
alter("alter table T alter column n set data type double precision").1,
|
||||
AlterTableAction::AlterColumnType { ty: crate::dsl::types::Type::Real, .. }
|
||||
AlterTableAction::AlterColumnType {
|
||||
ty: crate::dsl::types::Type::Real,
|
||||
..
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
@@ -3238,7 +3304,10 @@ mod sql_alter_table_tests {
|
||||
#[test]
|
||||
fn alter_column_set_default_captures_raw_expr() {
|
||||
match alter("alter table T alter column qty set default 0").1 {
|
||||
AlterTableAction::SetColumnDefault { column, default_sql } => {
|
||||
AlterTableAction::SetColumnDefault {
|
||||
column,
|
||||
default_sql,
|
||||
} => {
|
||||
assert_eq!(column, "qty");
|
||||
assert_eq!(default_sql, "0");
|
||||
}
|
||||
@@ -3317,7 +3386,9 @@ mod sql_alter_table_tests {
|
||||
match alter("alter table T add check (a < b)").1 {
|
||||
AlterTableAction::AddTableConstraint { name, constraint } => {
|
||||
assert_eq!(name, None);
|
||||
assert!(matches!(*constraint, TableConstraint::Check { ref expr_sql } if expr_sql == "a < b"));
|
||||
assert!(
|
||||
matches!(*constraint, TableConstraint::Check { ref expr_sql } if expr_sql == "a < b")
|
||||
);
|
||||
}
|
||||
other => panic!("expected AddTableConstraint/Check, got {other:?}"),
|
||||
}
|
||||
@@ -3335,7 +3406,9 @@ mod sql_alter_table_tests {
|
||||
match alter("alter table T add unique (a, b)").1 {
|
||||
AlterTableAction::AddTableConstraint { name, constraint } => {
|
||||
assert_eq!(name, None);
|
||||
assert!(matches!(*constraint, TableConstraint::Unique { ref columns } if columns == &["a".to_string(), "b".to_string()]));
|
||||
assert!(
|
||||
matches!(*constraint, TableConstraint::Unique { ref columns } if columns == &["a".to_string(), "b".to_string()])
|
||||
);
|
||||
}
|
||||
other => panic!("expected AddTableConstraint/Unique, got {other:?}"),
|
||||
}
|
||||
@@ -3352,7 +3425,9 @@ mod sql_alter_table_tests {
|
||||
)
|
||||
.expect_err("a named UNIQUE constraint is refused");
|
||||
assert!(
|
||||
err.to_string().to_lowercase().contains("unique constraint cannot be named"),
|
||||
err.to_string()
|
||||
.to_lowercase()
|
||||
.contains("unique constraint cannot be named"),
|
||||
"expected the builder's named-UNIQUE refusal, got: {err}"
|
||||
);
|
||||
}
|
||||
@@ -3364,7 +3439,9 @@ mod sql_alter_table_tests {
|
||||
let err = parse_command_in_mode("alter table T add primary key (id)", Mode::Advanced)
|
||||
.expect_err("ADD PRIMARY KEY is refused");
|
||||
assert!(
|
||||
err.to_string().to_lowercase().contains("primary key is fixed at creation"),
|
||||
err.to_string()
|
||||
.to_lowercase()
|
||||
.contains("primary key is fixed at creation"),
|
||||
"expected the builder's ADD-PRIMARY-KEY refusal, got: {err}"
|
||||
);
|
||||
}
|
||||
@@ -3392,7 +3469,10 @@ mod sql_alter_table_tests {
|
||||
assert_eq!(name.as_deref(), Some("fk_p"));
|
||||
match *constraint {
|
||||
TableConstraint::ForeignKey(fk) => {
|
||||
assert_eq!(fk.parent_columns, None, "bare reference resolves at execution");
|
||||
assert_eq!(
|
||||
fk.parent_columns, None,
|
||||
"bare reference resolves at execution"
|
||||
);
|
||||
}
|
||||
other => panic!("expected ForeignKey, got {other:?}"),
|
||||
}
|
||||
|
||||
+24
-35
@@ -79,9 +79,9 @@ const EXPR_COLUMN: Node = Node::Ident {
|
||||
writes_table: false,
|
||||
writes_column: true,
|
||||
writes_user_listed_column: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
};
|
||||
|
||||
/// Operand alternatives. The literal keywords (`null` / `true`
|
||||
@@ -126,8 +126,7 @@ fn where_rhs_operand(ctx: &WalkContext) -> Node {
|
||||
// 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());
|
||||
let leaked: &'static str = Box::leak(col.name.clone().into_boxed_str());
|
||||
Node::TypedValueSlot {
|
||||
ty: col.user_type,
|
||||
column_name: Some(leaked),
|
||||
@@ -260,10 +259,8 @@ static PAREN_GROUP_NODES: &[Node] = &[
|
||||
Node::Subgrammar(&OR_EXPR),
|
||||
Node::Punct(')'),
|
||||
];
|
||||
static BOOL_PRIMARY_CHOICES: &[Node] = &[
|
||||
Node::Seq(PAREN_GROUP_NODES),
|
||||
Node::Subgrammar(&PREDICATE),
|
||||
];
|
||||
static BOOL_PRIMARY_CHOICES: &[Node] =
|
||||
&[Node::Seq(PAREN_GROUP_NODES), Node::Subgrammar(&PREDICATE)];
|
||||
static BOOL_PRIMARY: Node = Node::Choice(BOOL_PRIMARY_CHOICES);
|
||||
|
||||
/// `not_expr := NOT not_expr | bool_primary`.
|
||||
@@ -271,10 +268,7 @@ static NOT_FORM_NODES: &[Node] = &[
|
||||
Node::Word(Word::keyword("not")),
|
||||
Node::Subgrammar(&NOT_EXPR),
|
||||
];
|
||||
static NOT_EXPR_CHOICES: &[Node] = &[
|
||||
Node::Seq(NOT_FORM_NODES),
|
||||
Node::Subgrammar(&BOOL_PRIMARY),
|
||||
];
|
||||
static NOT_EXPR_CHOICES: &[Node] = &[Node::Seq(NOT_FORM_NODES), Node::Subgrammar(&BOOL_PRIMARY)];
|
||||
static NOT_EXPR: Node = Node::Choice(NOT_EXPR_CHOICES);
|
||||
|
||||
/// `and_expr := not_expr ( AND not_expr )*`.
|
||||
@@ -296,10 +290,7 @@ static AND_EXPR: Node = Node::Seq(AND_EXPR_NODES);
|
||||
/// `or_expr := and_expr ( OR and_expr )*` — the fragment entry
|
||||
/// point. `update` / `delete` / `show data` reference this
|
||||
/// through `Node::Subgrammar(&OR_EXPR)`.
|
||||
static OR_TAIL_NODES: &[Node] = &[
|
||||
Node::Word(Word::keyword("or")),
|
||||
Node::Subgrammar(&AND_EXPR),
|
||||
];
|
||||
static OR_TAIL_NODES: &[Node] = &[Node::Word(Word::keyword("or")), Node::Subgrammar(&AND_EXPR)];
|
||||
static OR_TAIL: Node = Node::Seq(OR_TAIL_NODES);
|
||||
static OR_EXPR_NODES: &[Node] = &[
|
||||
Node::Subgrammar(&AND_EXPR),
|
||||
@@ -534,18 +525,18 @@ impl<'a> ExprParser<'a> {
|
||||
let span = item.span;
|
||||
let literal = |value: Value| Operand::Literal { value, span };
|
||||
match &item.kind {
|
||||
MatchedKind::Ident { role: "expr_column", .. } => {
|
||||
Ok(Operand::Column { name: item.text.clone(), span })
|
||||
}
|
||||
MatchedKind::Ident {
|
||||
role: "expr_column",
|
||||
..
|
||||
} => Ok(Operand::Column {
|
||||
name: item.text.clone(),
|
||||
span,
|
||||
}),
|
||||
MatchedKind::Word("null") => Ok(literal(Value::Null)),
|
||||
MatchedKind::Word("true") => Ok(literal(Value::Bool(true))),
|
||||
MatchedKind::Word("false") => Ok(literal(Value::Bool(false))),
|
||||
MatchedKind::NumberLit => {
|
||||
Ok(literal(Value::Number(item.text.clone())))
|
||||
}
|
||||
MatchedKind::StringLit => {
|
||||
Ok(literal(Value::Text(item.text.clone())))
|
||||
}
|
||||
MatchedKind::NumberLit => Ok(literal(Value::Number(item.text.clone()))),
|
||||
MatchedKind::StringLit => Ok(literal(Value::Text(item.text.clone()))),
|
||||
_ => Err(drift_error("expected a column or literal operand")),
|
||||
}
|
||||
}
|
||||
@@ -591,8 +582,7 @@ mod tests {
|
||||
let mut ctx = WalkContext::new();
|
||||
let mut path = MatchedPath::new();
|
||||
let mut per_byte = Vec::new();
|
||||
let result =
|
||||
walk_node(input, 0, &OR_EXPR, &mut ctx, &mut path, &mut per_byte);
|
||||
let result = walk_node(input, 0, &OR_EXPR, &mut ctx, &mut path, &mut per_byte);
|
||||
match result {
|
||||
NodeWalkResult::Matched { end, .. } => {
|
||||
assert!(
|
||||
@@ -730,8 +720,7 @@ mod tests {
|
||||
negated: false,
|
||||
}),
|
||||
);
|
||||
let Expr::Predicate(Predicate::Like { negated, .. }) =
|
||||
parse_expr("Name not like 'A%'")
|
||||
let Expr::Predicate(Predicate::Like { negated, .. }) = parse_expr("Name not like 'A%'")
|
||||
else {
|
||||
panic!("expected a negated Like");
|
||||
};
|
||||
@@ -794,16 +783,16 @@ mod tests {
|
||||
fn nested_parentheses_round_trip() {
|
||||
// Exercises the Subgrammar recursion a few levels deep.
|
||||
let expr = parse_expr("((a = 1 and b = 2) or (c = 3))");
|
||||
assert!(matches!(expr, Expr::Or(_) | Expr::And(_) | Expr::Predicate(_)));
|
||||
assert!(matches!(
|
||||
expr,
|
||||
Expr::Or(_) | Expr::And(_) | Expr::Predicate(_)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn case_insensitive_keywords() {
|
||||
// Keywords fold case; the built tree is identical.
|
||||
assert_eq!(
|
||||
parse_expr("a = 1 AND b = 2"),
|
||||
parse_expr("a = 1 and b = 2"),
|
||||
);
|
||||
assert_eq!(parse_expr("a = 1 AND b = 2"), parse_expr("a = 1 and b = 2"),);
|
||||
assert_eq!(
|
||||
parse_expr("Email IS NOT NULL"),
|
||||
parse_expr("Email is not null"),
|
||||
|
||||
+20
-24
@@ -27,9 +27,9 @@ pub mod data;
|
||||
pub mod ddl;
|
||||
pub mod expr;
|
||||
pub mod shared;
|
||||
pub mod sql_expr;
|
||||
pub mod sql_create_table;
|
||||
pub mod sql_delete;
|
||||
pub mod sql_expr;
|
||||
pub mod sql_insert;
|
||||
pub mod sql_select;
|
||||
pub mod sql_update;
|
||||
@@ -328,9 +328,7 @@ pub enum Node {
|
||||
/// A number literal. The optional `validator` runs against
|
||||
/// the matched text (used by Phase D value slots to enforce
|
||||
/// per-type integer/decimal rules).
|
||||
NumberLit {
|
||||
validator: Option<NumberValidator>,
|
||||
},
|
||||
NumberLit { validator: Option<NumberValidator> },
|
||||
/// A literal byte sequence at this position — matches
|
||||
/// bytes verbatim (whitespace-skipped) with a lookahead so
|
||||
/// `1` doesn't half-match `12` and `n` doesn't half-match
|
||||
@@ -701,7 +699,11 @@ fn selected_nodes_for_input_in_mode(
|
||||
.filter(|(_, _, c)| *c == CommandCategory::Simple)
|
||||
.collect()
|
||||
};
|
||||
if selected.is_empty() { candidates } else { selected }
|
||||
if selected.is_empty() {
|
||||
candidates
|
||||
} else {
|
||||
selected
|
||||
}
|
||||
}
|
||||
|
||||
/// The single usage template most relevant to `source`, when
|
||||
@@ -724,10 +726,7 @@ pub fn usage_key_for_input(source: &str) -> Option<&'static str> {
|
||||
/// disambiguates the single most-relevant usage key from the
|
||||
/// mode-selected key set.
|
||||
#[must_use]
|
||||
pub fn usage_key_for_input_in_mode(
|
||||
source: &str,
|
||||
mode: crate::mode::Mode,
|
||||
) -> Option<&'static str> {
|
||||
pub fn usage_key_for_input_in_mode(source: &str, mode: crate::mode::Mode) -> Option<&'static str> {
|
||||
let (_entry, keys) = usage_keys_for_input_in_mode(source, mode)?;
|
||||
pick_form_key(source, &keys)
|
||||
}
|
||||
@@ -755,7 +754,10 @@ fn pick_form_key<'a>(source: &str, keys: &[&'a str]) -> Option<&'a str> {
|
||||
}
|
||||
// The `create m:n relationship` form (ADR-0045) opens with `m:n`
|
||||
// — a letter, so the digit branch misses it; its key ends `…m2n`.
|
||||
if source[after..].get(..3).is_some_and(|s| s.eq_ignore_ascii_case("m:n")) {
|
||||
if source[after..]
|
||||
.get(..3)
|
||||
.is_some_and(|s| s.eq_ignore_ascii_case("m:n"))
|
||||
{
|
||||
return keys.iter().copied().find(|k| k.ends_with("m2n"));
|
||||
}
|
||||
// Otherwise the form word is an identifier — `column`, `index`,
|
||||
@@ -770,8 +772,7 @@ fn pick_form_key<'a>(source: &str, keys: &[&'a str]) -> Option<&'a str> {
|
||||
/// which read the same data through the legacy `usage::REGISTRY`.
|
||||
#[must_use]
|
||||
pub fn entry_words_alphabetised() -> Vec<&'static str> {
|
||||
let mut words: Vec<&'static str> =
|
||||
REGISTRY.iter().map(|(c, _)| c.entry.primary).collect();
|
||||
let mut words: Vec<&'static str> = REGISTRY.iter().map(|(c, _)| c.entry.primary).collect();
|
||||
words.sort_unstable();
|
||||
words.dedup();
|
||||
words
|
||||
@@ -905,9 +906,7 @@ pub fn command_for_entry_word(word: &str) -> Option<(usize, &'static CommandNode
|
||||
/// returns its `Simple` DSL node and `Advanced` SQL node. The
|
||||
/// dispatcher picks among them by the active input mode.
|
||||
#[must_use]
|
||||
pub fn commands_for_entry_word(
|
||||
word: &str,
|
||||
) -> Vec<(usize, &'static CommandNode, CommandCategory)> {
|
||||
pub fn commands_for_entry_word(word: &str) -> Vec<(usize, &'static CommandNode, CommandCategory)> {
|
||||
REGISTRY
|
||||
.iter()
|
||||
.enumerate()
|
||||
@@ -1010,7 +1009,10 @@ mod hint_key_tests {
|
||||
];
|
||||
for c in classes {
|
||||
let key = format!("hint.err.{c}.what");
|
||||
assert!(cat.get(&key).is_some(), "missing tier-3 error block `{key}`");
|
||||
assert!(
|
||||
cat.get(&key).is_some(),
|
||||
"missing tier-3 error block `{key}`"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1098,10 +1100,7 @@ mod usage_key_tests {
|
||||
let cases = [
|
||||
("add column to T: c (int)", "parse.usage.add_column"),
|
||||
("add index on T (c)", "parse.usage.add_index"),
|
||||
(
|
||||
"add constraint unique to T.c",
|
||||
"parse.usage.add_constraint",
|
||||
),
|
||||
("add constraint unique to T.c", "parse.usage.add_constraint"),
|
||||
(
|
||||
"drop constraint check from T.c",
|
||||
"parse.usage.drop_constraint",
|
||||
@@ -1118,10 +1117,7 @@ mod usage_key_tests {
|
||||
("drop table T", "parse.usage.drop_table"),
|
||||
("drop column from table T: c", "parse.usage.drop_column"),
|
||||
("drop index i", "parse.usage.drop_index"),
|
||||
(
|
||||
"drop relationship r",
|
||||
"parse.usage.drop_relationship",
|
||||
),
|
||||
("drop relationship r", "parse.usage.drop_relationship"),
|
||||
("show data T", "parse.usage.show_data"),
|
||||
("show table T", "parse.usage.show_table"),
|
||||
// `create` is multi-form (table vs m:n, ADR-0045): each typed
|
||||
|
||||
+17
-24
@@ -7,8 +7,8 @@
|
||||
|
||||
use crate::completion::TableColumn;
|
||||
use crate::dsl::grammar::{
|
||||
HighlightClass, HintMode, IdentSource, IdentValidator, Node,
|
||||
NumberValidator, ValidationError, Word,
|
||||
HighlightClass, HintMode, IdentSource, IdentValidator, Node, NumberValidator, ValidationError,
|
||||
Word,
|
||||
};
|
||||
use crate::dsl::types::Type;
|
||||
use crate::dsl::walker::context::WalkContext;
|
||||
@@ -32,10 +32,7 @@ pub fn validate_type_name(value: &str) -> Result<(), ValidationError> {
|
||||
.join(", ");
|
||||
Err(ValidationError {
|
||||
message_key: "parse.custom.unknown_type",
|
||||
args: vec![
|
||||
("found", value.to_string()),
|
||||
("expected", expected),
|
||||
],
|
||||
args: vec![("found", value.to_string()), ("expected", expected)],
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -51,12 +48,12 @@ pub const TYPE_SLOT: Node = Node::Ident {
|
||||
role: "type",
|
||||
validator: Some(TYPE_VALIDATOR),
|
||||
highlight_override: Some(HighlightClass::Type),
|
||||
writes_table: false,
|
||||
writes_column: false,
|
||||
writes_user_listed_column: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
writes_table: false,
|
||||
writes_column: false,
|
||||
writes_user_listed_column: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
};
|
||||
|
||||
// --- Qualified column reference (`<Table>.<Column>`) --------------
|
||||
@@ -70,9 +67,9 @@ const QUALIFIED_COLUMN_NODES: &[Node] = &[
|
||||
writes_table: false,
|
||||
writes_column: false,
|
||||
writes_user_listed_column: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
},
|
||||
Node::Punct('.'),
|
||||
Node::Ident {
|
||||
@@ -83,9 +80,9 @@ const QUALIFIED_COLUMN_NODES: &[Node] = &[
|
||||
writes_table: false,
|
||||
writes_column: false,
|
||||
writes_user_listed_column: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
},
|
||||
];
|
||||
pub const QUALIFIED_COLUMN: Node = Node::Seq(QUALIFIED_COLUMN_NODES);
|
||||
@@ -313,9 +310,7 @@ const fn slot_inner_for_type(ty: Type) -> &'static Node {
|
||||
Type::Real => &REAL_SLOT_INNER,
|
||||
Type::Decimal => &DECIMAL_SLOT_INNER,
|
||||
Type::Bool => &BOOL_SLOT_INNER,
|
||||
Type::Text | Type::Date | Type::DateTime | Type::Blob | Type::ShortId => {
|
||||
&TEXT_SLOT_INNER
|
||||
}
|
||||
Type::Text | Type::Date | Type::DateTime | Type::Blob | Type::ShortId => &TEXT_SLOT_INNER,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -397,9 +392,7 @@ pub(crate) const FALLBACK_VALUE_LIST: Node = Node::Repeated {
|
||||
/// This is the single source of truth shared by [`column_value_list`]
|
||||
/// (which builds the typed slots) and the `data.rs` arity gate (which
|
||||
/// counts them) so the two never disagree (issue #17).
|
||||
pub fn insert_target_columns<'c>(
|
||||
ctx: &'c WalkContext<'_>,
|
||||
) -> Option<Vec<&'c TableColumn>> {
|
||||
pub fn insert_target_columns<'c>(ctx: &'c WalkContext<'_>) -> Option<Vec<&'c TableColumn>> {
|
||||
let table_cols = ctx.current_table_columns.as_ref()?;
|
||||
if table_cols.is_empty() {
|
||||
return None;
|
||||
|
||||
@@ -405,8 +405,14 @@ const TABLE_FK_NAMED: Node = Node::Seq(TABLE_FK_NAMED_NODES);
|
||||
// / `foreign`) that disambiguates it from a column name. (A column
|
||||
// literally named with one of those keywords is therefore unavailable,
|
||||
// the same trade real SQL makes with its reserved words.)
|
||||
static ELEMENT_CHOICES: &[Node] =
|
||||
&[TABLE_PK, TABLE_UNIQUE, TABLE_CHECK, TABLE_FK_NAMED, TABLE_FK, COLUMN_DEF];
|
||||
static ELEMENT_CHOICES: &[Node] = &[
|
||||
TABLE_PK,
|
||||
TABLE_UNIQUE,
|
||||
TABLE_CHECK,
|
||||
TABLE_FK_NAMED,
|
||||
TABLE_FK,
|
||||
COLUMN_DEF,
|
||||
];
|
||||
const ELEMENT_INNER: Node = Node::Choice(ELEMENT_CHOICES);
|
||||
// Issue #4: wrap the element slot in `IntroProse` so a fresh element
|
||||
// position (`create table T (` and after every `,`) surfaces a prose
|
||||
@@ -495,18 +501,31 @@ mod tests {
|
||||
let mut ctx = WalkContext::new();
|
||||
let mut path = MatchedPath::new();
|
||||
let mut per_byte = Vec::new();
|
||||
match walk_node(input, 0, &SQL_CREATE_TABLE_SHAPE, &mut ctx, &mut path, &mut per_byte) {
|
||||
match walk_node(
|
||||
input,
|
||||
0,
|
||||
&SQL_CREATE_TABLE_SHAPE,
|
||||
&mut ctx,
|
||||
&mut path,
|
||||
&mut per_byte,
|
||||
) {
|
||||
NodeWalkResult::Matched { end, .. } => input[end..].trim().is_empty(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn good(input: &str) {
|
||||
assert!(walks(input), "{input:?} should be a valid CREATE TABLE tail");
|
||||
assert!(
|
||||
walks(input),
|
||||
"{input:?} should be a valid CREATE TABLE tail"
|
||||
);
|
||||
}
|
||||
|
||||
fn bad(input: &str) {
|
||||
assert!(!walks(input), "{input:?} should NOT walk as a complete CREATE TABLE tail");
|
||||
assert!(
|
||||
!walks(input),
|
||||
"{input:?} should NOT walk as a complete CREATE TABLE tail"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -638,7 +657,9 @@ mod tests {
|
||||
good("table t (id int, ref int references other(id))");
|
||||
good("table t (id int, ref int references other)"); // bare ref
|
||||
good("table t (id int, ref int references other(id) on delete cascade)");
|
||||
good("table t (id int, ref int references other(id) on update set null on delete restrict)");
|
||||
good(
|
||||
"table t (id int, ref int references other(id) on update set null on delete restrict)",
|
||||
);
|
||||
good("table t (id int, ref int, foreign key (ref) references other(id))");
|
||||
good("table t (id int, ref int, constraint fk_x foreign key (ref) references other(id))");
|
||||
good(
|
||||
@@ -691,7 +712,10 @@ mod builder_tests {
|
||||
assert_eq!(name, "t");
|
||||
assert_eq!(
|
||||
cols,
|
||||
vec![("id".to_string(), Type::Int), ("name".to_string(), Type::Text)]
|
||||
vec![
|
||||
("id".to_string(), Type::Int),
|
||||
("name".to_string(), Type::Text)
|
||||
]
|
||||
);
|
||||
assert!(pk.is_empty(), "no PK declared");
|
||||
assert!(!ine);
|
||||
@@ -740,7 +764,10 @@ mod builder_tests {
|
||||
let (_, cols, _, _) = sct("create table t (a varchar(255), b numeric(10, 2))");
|
||||
assert_eq!(
|
||||
cols,
|
||||
vec![("a".to_string(), Type::Text), ("b".to_string(), Type::Decimal)]
|
||||
vec![
|
||||
("a".to_string(), Type::Text),
|
||||
("b".to_string(), Type::Decimal)
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -780,8 +807,7 @@ mod builder_tests {
|
||||
fn redundant_constraints_deduped_off_sole_pk_column() {
|
||||
// ADR-0035 §6.5: advanced mode accepts the redundant spelling
|
||||
// and silently drops the flags off the sole PK column.
|
||||
match parse_command("create table t (id int primary key not null unique)")
|
||||
.expect("parses")
|
||||
match parse_command("create table t (id int primary key not null unique)").expect("parses")
|
||||
{
|
||||
Command::SqlCreateTable {
|
||||
columns,
|
||||
@@ -944,8 +970,7 @@ mod builder_tests {
|
||||
// depth 2, not an element boundary, so the following `check`
|
||||
// is still column-level. A naive "reset on any comma" would
|
||||
// misclassify it as table-level (the §4.2 probe).
|
||||
let (cols, checks) =
|
||||
parse_sct_checks("create table t (n numeric(10, 2) check (n > 0))");
|
||||
let (cols, checks) = parse_sct_checks("create table t (n numeric(10, 2) check (n > 0))");
|
||||
assert_eq!(col(&cols, "n").check_sql.as_deref(), Some("n > 0"));
|
||||
assert!(checks.is_empty(), "no table-level CHECK was produced");
|
||||
}
|
||||
@@ -977,8 +1002,7 @@ mod builder_tests {
|
||||
fn table_check_before_a_later_column_is_table_level() {
|
||||
// A CHECK element that appears between columns (not after a
|
||||
// column's type) is table-level even though more columns follow.
|
||||
let (cols, checks) =
|
||||
parse_sct_checks("create table t (a int, check (a > 0), b int)");
|
||||
let (cols, checks) = parse_sct_checks("create table t (a int, check (a > 0), b int)");
|
||||
assert_eq!(checks, vec!["a > 0".to_string()]);
|
||||
assert!(col(&cols, "a").check_sql.is_none() && col(&cols, "b").check_sql.is_none());
|
||||
}
|
||||
@@ -1004,7 +1028,10 @@ mod builder_tests {
|
||||
assert_eq!(fk.parent_columns, Some(vec!["id".to_string()]));
|
||||
assert_eq!(fk.on_delete, ReferentialAction::NoAction);
|
||||
assert_eq!(fk.on_update, ReferentialAction::NoAction);
|
||||
assert!(fk.inline, "a column-level `references` is an inline FK (ADR-0043 D4)");
|
||||
assert!(
|
||||
fk.inline,
|
||||
"a column-level `references` is an inline FK (ADR-0043 D4)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1012,14 +1039,19 @@ mod builder_tests {
|
||||
// The table-level `FOREIGN KEY (...)` form is not inline, so it can
|
||||
// carry a multi-column reference and never triggers the inline
|
||||
// "use the table-level form" hint (ADR-0043 D4).
|
||||
let fks = parse_sct_fks("create table t (id int, pid int, foreign key (pid) references parent(id))");
|
||||
let fks = parse_sct_fks(
|
||||
"create table t (id int, pid int, foreign key (pid) references parent(id))",
|
||||
);
|
||||
assert!(!fks[0].inline, "table-level FOREIGN KEY is not inline");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bare_inline_reference_has_no_parent_column() {
|
||||
let fks = parse_sct_fks("create table t (id int, pid int references parent)");
|
||||
assert_eq!(fks[0].parent_columns, None, "bare REFERENCES — resolved at execution");
|
||||
assert_eq!(
|
||||
fks[0].parent_columns, None,
|
||||
"bare REFERENCES — resolved at execution"
|
||||
);
|
||||
assert_eq!(fks[0].parent_table, "parent");
|
||||
assert_eq!(fks[0].child_columns, vec!["pid".to_string()]);
|
||||
}
|
||||
@@ -1047,8 +1079,9 @@ mod builder_tests {
|
||||
|
||||
#[test]
|
||||
fn table_level_foreign_key_captured() {
|
||||
let fks =
|
||||
parse_sct_fks("create table t (id int, pid int, foreign key (pid) references parent(id))");
|
||||
let fks = parse_sct_fks(
|
||||
"create table t (id int, pid int, foreign key (pid) references parent(id))",
|
||||
);
|
||||
assert_eq!(fks.len(), 1);
|
||||
assert_eq!(fks[0].name, None);
|
||||
assert_eq!(fks[0].child_columns, vec!["pid".to_string()]);
|
||||
@@ -1073,8 +1106,20 @@ mod builder_tests {
|
||||
foreign key (a) references p(id), foreign key (b) references q(id))",
|
||||
);
|
||||
assert_eq!(fks.len(), 2);
|
||||
assert_eq!((fks[0].child_columns[0].as_str(), fks[0].parent_table.as_str()), ("a", "p"));
|
||||
assert_eq!((fks[1].child_columns[0].as_str(), fks[1].parent_table.as_str()), ("b", "q"));
|
||||
assert_eq!(
|
||||
(
|
||||
fks[0].child_columns[0].as_str(),
|
||||
fks[0].parent_table.as_str()
|
||||
),
|
||||
("a", "p")
|
||||
);
|
||||
assert_eq!(
|
||||
(
|
||||
fks[1].child_columns[0].as_str(),
|
||||
fks[1].parent_table.as_str()
|
||||
),
|
||||
("b", "q")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1108,7 +1153,12 @@ mod builder_tests {
|
||||
assert_eq!(foreign_keys[0].child_columns, vec!["pid".to_string()]);
|
||||
// the column-level CHECK still attaches to `pid`
|
||||
assert_eq!(
|
||||
columns.iter().find(|c| c.name == "pid").unwrap().check_sql.as_deref(),
|
||||
columns
|
||||
.iter()
|
||||
.find(|c| c.name == "pid")
|
||||
.unwrap()
|
||||
.check_sql
|
||||
.as_deref(),
|
||||
Some("pid > 0")
|
||||
);
|
||||
// the table-level CHECK is captured separately
|
||||
|
||||
@@ -82,7 +82,14 @@ mod tests {
|
||||
let mut ctx = WalkContext::new();
|
||||
let mut path = MatchedPath::new();
|
||||
let mut per_byte = Vec::new();
|
||||
match walk_node(input, 0, &SQL_DELETE_SHAPE, &mut ctx, &mut path, &mut per_byte) {
|
||||
match walk_node(
|
||||
input,
|
||||
0,
|
||||
&SQL_DELETE_SHAPE,
|
||||
&mut ctx,
|
||||
&mut path,
|
||||
&mut per_byte,
|
||||
) {
|
||||
NodeWalkResult::Matched { end, .. } => input[end..].trim().is_empty(),
|
||||
_ => false,
|
||||
}
|
||||
@@ -93,7 +100,10 @@ mod tests {
|
||||
}
|
||||
|
||||
fn bad(input: &str) {
|
||||
assert!(!walks(input), "{input:?} should NOT walk as a complete DELETE tail");
|
||||
assert!(
|
||||
!walks(input),
|
||||
"{input:?} should NOT walk as a complete DELETE tail"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+31
-63
@@ -82,19 +82,16 @@ const EXPR_IDENT: Node = Node::Ident {
|
||||
writes_table: false,
|
||||
writes_column: false,
|
||||
writes_user_listed_column: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
};
|
||||
|
||||
// =================================================================
|
||||
// or_expr := and_expr ( OR and_expr )* — the fragment entry point
|
||||
// =================================================================
|
||||
|
||||
static OR_TAIL_NODES: &[Node] = &[
|
||||
Node::Word(Word::keyword("or")),
|
||||
Node::Subgrammar(&AND_EXPR),
|
||||
];
|
||||
static OR_TAIL_NODES: &[Node] = &[Node::Word(Word::keyword("or")), Node::Subgrammar(&AND_EXPR)];
|
||||
static OR_TAIL: Node = Node::Seq(OR_TAIL_NODES);
|
||||
static SQL_OR_EXPR_NODES: &[Node] = &[
|
||||
Node::Subgrammar(&AND_EXPR),
|
||||
@@ -140,10 +137,7 @@ static NOT_FORM_NODES: &[Node] = &[
|
||||
Node::Word(Word::keyword("not")),
|
||||
Node::Subgrammar(&NOT_EXPR),
|
||||
];
|
||||
static NOT_EXPR_CHOICES: &[Node] = &[
|
||||
Node::Seq(NOT_FORM_NODES),
|
||||
Node::Subgrammar(&PREDICATE),
|
||||
];
|
||||
static NOT_EXPR_CHOICES: &[Node] = &[Node::Seq(NOT_FORM_NODES), Node::Subgrammar(&PREDICATE)];
|
||||
static NOT_EXPR: Node = Node::Choice(NOT_EXPR_CHOICES);
|
||||
|
||||
// =================================================================
|
||||
@@ -156,10 +150,7 @@ static NOT_EXPR: Node = Node::Choice(NOT_EXPR_CHOICES);
|
||||
// needs. ADR-0026's DSL grammar made the tail mandatory because it
|
||||
// forbade a bare column as a boolean; SQL does not.
|
||||
|
||||
static PREDICATE_NODES: &[Node] = &[
|
||||
Node::Subgrammar(&ADDITIVE),
|
||||
Node::Optional(&PREDICATE_TAIL),
|
||||
];
|
||||
static PREDICATE_NODES: &[Node] = &[Node::Subgrammar(&ADDITIVE), Node::Optional(&PREDICATE_TAIL)];
|
||||
static PREDICATE: Node = Node::Seq(PREDICATE_NODES);
|
||||
|
||||
// ---- cmp_op := <= | <> | >= | != | < | > | = --------------------
|
||||
@@ -181,10 +172,7 @@ static CMP_OP_CHOICES: &[Node] = &[
|
||||
// ---- predicate_tail branches ------------------------------------
|
||||
|
||||
/// `cmp_op additive`.
|
||||
static COMPARE_FORM_NODES: &[Node] = &[
|
||||
Node::Choice(CMP_OP_CHOICES),
|
||||
Node::Subgrammar(&ADDITIVE),
|
||||
];
|
||||
static COMPARE_FORM_NODES: &[Node] = &[Node::Choice(CMP_OP_CHOICES), Node::Subgrammar(&ADDITIVE)];
|
||||
|
||||
/// `IS [NOT] NULL`.
|
||||
static IS_NULL_NODES: &[Node] = &[
|
||||
@@ -265,11 +253,7 @@ static PREDICATE_TAIL: Node = Node::Choice(PREDICATE_TAIL_CHOICES);
|
||||
// additive := multiplicative ( ( + | - | || ) multiplicative )*
|
||||
// =================================================================
|
||||
|
||||
static ADD_OP_CHOICES: &[Node] = &[
|
||||
Node::Punct('+'),
|
||||
Node::Punct('-'),
|
||||
Node::Literal("||"),
|
||||
];
|
||||
static ADD_OP_CHOICES: &[Node] = &[Node::Punct('+'), Node::Punct('-'), Node::Literal("||")];
|
||||
static ADD_TAIL_NODES: &[Node] = &[
|
||||
Node::Choice(ADD_OP_CHOICES),
|
||||
Node::Subgrammar(&MULTIPLICATIVE),
|
||||
@@ -289,15 +273,8 @@ static ADDITIVE: Node = Node::Seq(ADDITIVE_NODES);
|
||||
// multiplicative := unary ( ( * | / | % ) unary )*
|
||||
// =================================================================
|
||||
|
||||
static MUL_OP_CHOICES: &[Node] = &[
|
||||
Node::Punct('*'),
|
||||
Node::Punct('/'),
|
||||
Node::Punct('%'),
|
||||
];
|
||||
static MUL_TAIL_NODES: &[Node] = &[
|
||||
Node::Choice(MUL_OP_CHOICES),
|
||||
Node::Subgrammar(&UNARY),
|
||||
];
|
||||
static MUL_OP_CHOICES: &[Node] = &[Node::Punct('*'), Node::Punct('/'), Node::Punct('%')];
|
||||
static MUL_TAIL_NODES: &[Node] = &[Node::Choice(MUL_OP_CHOICES), Node::Subgrammar(&UNARY)];
|
||||
static MUL_TAIL: Node = Node::Seq(MUL_TAIL_NODES);
|
||||
static MULTIPLICATIVE_NODES: &[Node] = &[
|
||||
Node::Subgrammar(&UNARY),
|
||||
@@ -314,14 +291,8 @@ static MULTIPLICATIVE: Node = Node::Seq(MULTIPLICATIVE_NODES);
|
||||
// =================================================================
|
||||
|
||||
static SIGN_CHOICES: &[Node] = &[Node::Punct('-'), Node::Punct('+')];
|
||||
static UNARY_SIGN_NODES: &[Node] = &[
|
||||
Node::Choice(SIGN_CHOICES),
|
||||
Node::Subgrammar(&UNARY),
|
||||
];
|
||||
static UNARY_CHOICES: &[Node] = &[
|
||||
Node::Seq(UNARY_SIGN_NODES),
|
||||
Node::Subgrammar(&PRIMARY),
|
||||
];
|
||||
static UNARY_SIGN_NODES: &[Node] = &[Node::Choice(SIGN_CHOICES), Node::Subgrammar(&UNARY)];
|
||||
static UNARY_CHOICES: &[Node] = &[Node::Seq(UNARY_SIGN_NODES), Node::Subgrammar(&PRIMARY)];
|
||||
static UNARY: Node = Node::Choice(UNARY_CHOICES);
|
||||
|
||||
// =================================================================
|
||||
@@ -402,10 +373,7 @@ static SIMPLE_CASE_NODES: &[Node] = &[
|
||||
Node::Optional(&ELSE_CLAUSE),
|
||||
Node::Word(Word::keyword("end")),
|
||||
];
|
||||
static CASE_BODY_CHOICES: &[Node] = &[
|
||||
Node::Seq(SEARCHED_CASE_NODES),
|
||||
Node::Seq(SIMPLE_CASE_NODES),
|
||||
];
|
||||
static CASE_BODY_CHOICES: &[Node] = &[Node::Seq(SEARCHED_CASE_NODES), Node::Seq(SIMPLE_CASE_NODES)];
|
||||
static CASE_NODES: &[Node] = &[
|
||||
Node::Word(Word::keyword("case")),
|
||||
Node::Choice(CASE_BODY_CHOICES),
|
||||
@@ -467,14 +435,11 @@ const QUALIFIED_REF_IDENT: Node = Node::Ident {
|
||||
writes_table: false,
|
||||
writes_column: false,
|
||||
writes_user_listed_column: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
};
|
||||
static QUALIFIED_REF_TAIL_NODES: &[Node] = &[
|
||||
Node::Punct('.'),
|
||||
QUALIFIED_REF_IDENT,
|
||||
];
|
||||
static QUALIFIED_REF_TAIL_NODES: &[Node] = &[Node::Punct('.'), QUALIFIED_REF_IDENT];
|
||||
|
||||
static NAME_OR_CALL_TAIL_CHOICES: &[Node] = &[
|
||||
Node::Seq(QUALIFIED_REF_TAIL_NODES),
|
||||
@@ -531,7 +496,10 @@ mod tests {
|
||||
|
||||
/// Assert `input` is *not* a complete SQL expression.
|
||||
fn bad(input: &str) {
|
||||
assert!(!walks(input), "{input:?} should NOT walk as a complete expression");
|
||||
assert!(
|
||||
!walks(input),
|
||||
"{input:?} should NOT walk as a complete expression"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -643,13 +611,13 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn malformed_expressions_do_not_walk() {
|
||||
bad("a +"); // dangling operator
|
||||
bad("a in b"); // IN requires a parenthesised list
|
||||
bad("= 1"); // no left operand
|
||||
bad("a = "); // no right operand
|
||||
bad("case a end"); // CASE with no WHEN clause
|
||||
bad("and b"); // leading connective
|
||||
bad("upper("); // unclosed call
|
||||
bad("a +"); // dangling operator
|
||||
bad("a in b"); // IN requires a parenthesised list
|
||||
bad("= 1"); // no left operand
|
||||
bad("a = "); // no right operand
|
||||
bad("case a end"); // CASE with no WHEN clause
|
||||
bad("and b"); // leading connective
|
||||
bad("upper("); // unclosed call
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -680,9 +648,9 @@ mod tests {
|
||||
// The optional tail dispatches `.identifier` (qualified
|
||||
// ref) vs `(args)` (function call) by first token — a
|
||||
// bare ident remains a column ref.
|
||||
good("foo(x)"); // function call
|
||||
good("foo.bar"); // qualified ref
|
||||
good("foo"); // bare ref
|
||||
good("foo(x)"); // function call
|
||||
good("foo.bar"); // qualified ref
|
||||
good("foo"); // bare ref
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -120,7 +120,10 @@ fn target_value_columns(ctx: &WalkContext) -> Vec<TableColumn> {
|
||||
listed
|
||||
.iter()
|
||||
.filter_map(|name| {
|
||||
table_cols.iter().find(|c| c.name.eq_ignore_ascii_case(name)).cloned()
|
||||
table_cols
|
||||
.iter()
|
||||
.find(|c| c.name.eq_ignore_ascii_case(name))
|
||||
.cloned()
|
||||
})
|
||||
.collect()
|
||||
},
|
||||
@@ -148,7 +151,11 @@ fn target_value_columns(ctx: &WalkContext) -> Vec<TableColumn> {
|
||||
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() };
|
||||
let arity_ok = if closed {
|
||||
count == cols.len()
|
||||
} else {
|
||||
count <= cols.len()
|
||||
};
|
||||
if !cols.is_empty() && arity_ok {
|
||||
Node::DynamicSubgrammar(sql_value_list)
|
||||
} else {
|
||||
@@ -304,8 +311,10 @@ static DO_UPDATE_NODES: &[Node] = &[
|
||||
/// the enclosing Seq, each branch's FIRST token (`nothing` vs
|
||||
/// `update`) disambiguates, so a non-match of branch 0 is a clean
|
||||
/// `NoMatch` that falls through to branch 1.
|
||||
static DO_ACTION_CHOICES: &[Node] =
|
||||
&[Node::Word(Word::keyword("nothing")), Node::Seq(DO_UPDATE_NODES)];
|
||||
static DO_ACTION_CHOICES: &[Node] = &[
|
||||
Node::Word(Word::keyword("nothing")),
|
||||
Node::Seq(DO_UPDATE_NODES),
|
||||
];
|
||||
// `const` — used by value in `ON_CONFLICT_CLAUSE_NODES`.
|
||||
const DO_ACTION: Node = Node::Choice(DO_ACTION_CHOICES);
|
||||
|
||||
@@ -361,7 +370,14 @@ mod tests {
|
||||
let mut ctx = WalkContext::new();
|
||||
let mut path = MatchedPath::new();
|
||||
let mut per_byte = Vec::new();
|
||||
match walk_node(input, 0, &SQL_INSERT_SHAPE, &mut ctx, &mut path, &mut per_byte) {
|
||||
match walk_node(
|
||||
input,
|
||||
0,
|
||||
&SQL_INSERT_SHAPE,
|
||||
&mut ctx,
|
||||
&mut path,
|
||||
&mut per_byte,
|
||||
) {
|
||||
NodeWalkResult::Matched { end, .. } => input[end..].trim().is_empty(),
|
||||
_ => false,
|
||||
}
|
||||
@@ -372,7 +388,10 @@ mod tests {
|
||||
}
|
||||
|
||||
fn bad(input: &str) {
|
||||
assert!(!walks(input), "{input:?} should NOT walk as a complete INSERT tail");
|
||||
assert!(
|
||||
!walks(input),
|
||||
"{input:?} should NOT walk as a complete INSERT tail"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -418,8 +437,12 @@ mod tests {
|
||||
// 3h: ON CONFLICT … DO NOTHING / DO UPDATE (ADR-0033 §9).
|
||||
good("into t (id, name) values (1, 'x') on conflict (id) do nothing");
|
||||
good("into t (id, name) values (1, 'x') on conflict do nothing");
|
||||
good("into t (id, name) values (1, 'x') on conflict (id) do update set name = excluded.name");
|
||||
good("into t (id, name) values (1, 'x') on conflict (id) do update set name = 'y' where id > 0");
|
||||
good(
|
||||
"into t (id, name) values (1, 'x') on conflict (id) do update set name = excluded.name",
|
||||
);
|
||||
good(
|
||||
"into t (id, name) values (1, 'x') on conflict (id) do update set name = 'y' where id > 0",
|
||||
);
|
||||
// Multi-column conflict target + multi-assignment DO UPDATE.
|
||||
good("into t (a, b) values (1, 2) on conflict (a, b) do update set b = excluded.b, a = 9");
|
||||
// ON CONFLICT composes with RETURNING (order: row source,
|
||||
|
||||
@@ -141,8 +141,15 @@ static EMPTY_NOMATCH: Node = Node::Choice(&[]);
|
||||
/// suffix keywords. `as` is not listed — the AS-form alias is a
|
||||
/// separate `Choice` branch that fires before the lookahead.
|
||||
const PROJECTION_FOLLOW_SET: &[&str] = &[
|
||||
"from", "where", "group", "order", "having", "limit",
|
||||
"union", "intersect", "except",
|
||||
"from",
|
||||
"where",
|
||||
"group",
|
||||
"order",
|
||||
"having",
|
||||
"limit",
|
||||
"union",
|
||||
"intersect",
|
||||
"except",
|
||||
// `returning` belongs to an enclosing DML statement
|
||||
// (`INSERT … SELECT … RETURNING …`, ADR-0033 §5), never to a
|
||||
// projection item's bare alias — so a no-FROM SELECT row source
|
||||
@@ -158,9 +165,21 @@ const PROJECTION_FOLLOW_SET: &[&str] = &[
|
||||
/// only when `b` has no alias — `on` is not a base-table name a
|
||||
/// learner would type as an alias.
|
||||
const TABLE_SOURCE_FOLLOW_SET: &[&str] = &[
|
||||
"where", "group", "order", "having", "limit",
|
||||
"union", "intersect", "except",
|
||||
"inner", "left", "right", "full", "cross", "join", "on",
|
||||
"where",
|
||||
"group",
|
||||
"order",
|
||||
"having",
|
||||
"limit",
|
||||
"union",
|
||||
"intersect",
|
||||
"except",
|
||||
"inner",
|
||||
"left",
|
||||
"right",
|
||||
"full",
|
||||
"cross",
|
||||
"join",
|
||||
"on",
|
||||
// `returning` belongs to an enclosing DML statement
|
||||
// (`INSERT … SELECT … FROM t RETURNING …`, ADR-0033 §5), so the
|
||||
// SELECT row source must not read it as table `t`'s bare alias.
|
||||
@@ -172,15 +191,9 @@ fn peek_next_ident_lower(source: &str, pos: usize) -> Option<String> {
|
||||
consume_ident(source, p).map(|(s, e)| source[s..e].to_ascii_lowercase())
|
||||
}
|
||||
|
||||
fn projection_bare_alias_factory(
|
||||
_: &WalkContext,
|
||||
source: &str,
|
||||
pos: usize,
|
||||
) -> Node {
|
||||
fn projection_bare_alias_factory(_: &WalkContext, source: &str, pos: usize) -> Node {
|
||||
match peek_next_ident_lower(source, pos) {
|
||||
Some(word)
|
||||
if PROJECTION_FOLLOW_SET.iter().any(|k| *k == word) =>
|
||||
{
|
||||
Some(word) if PROJECTION_FOLLOW_SET.iter().any(|k| *k == word) => {
|
||||
Node::Subgrammar(&EMPTY_NOMATCH)
|
||||
}
|
||||
Some(_) => PROJECTION_BARE_ALIAS_IDENT,
|
||||
@@ -188,15 +201,9 @@ fn projection_bare_alias_factory(
|
||||
}
|
||||
}
|
||||
|
||||
fn table_source_bare_alias_factory(
|
||||
_: &WalkContext,
|
||||
source: &str,
|
||||
pos: usize,
|
||||
) -> Node {
|
||||
fn table_source_bare_alias_factory(_: &WalkContext, source: &str, pos: usize) -> Node {
|
||||
match peek_next_ident_lower(source, pos) {
|
||||
Some(word)
|
||||
if TABLE_SOURCE_FOLLOW_SET.iter().any(|k| *k == word) =>
|
||||
{
|
||||
Some(word) if TABLE_SOURCE_FOLLOW_SET.iter().any(|k| *k == word) => {
|
||||
Node::Subgrammar(&EMPTY_NOMATCH)
|
||||
}
|
||||
Some(_) => TABLE_SOURCE_BARE_ALIAS_IDENT,
|
||||
@@ -237,14 +244,12 @@ const TABLE_SOURCE_BARE_ALIAS_IDENT: Node = Node::Ident {
|
||||
writes_column: false,
|
||||
writes_user_listed_column: false,
|
||||
writes_table_alias: true,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
};
|
||||
|
||||
static PROJECTION_AS_ALIAS_NODES: &[Node] = &[
|
||||
Node::Word(Word::keyword("as")),
|
||||
PROJECTION_BARE_ALIAS_IDENT,
|
||||
];
|
||||
static PROJECTION_AS_ALIAS_NODES: &[Node] =
|
||||
&[Node::Word(Word::keyword("as")), PROJECTION_BARE_ALIAS_IDENT];
|
||||
static PROJECTION_AS_ALIAS: Node = Node::Seq(PROJECTION_AS_ALIAS_NODES);
|
||||
|
||||
static TABLE_SOURCE_AS_ALIAS_NODES: &[Node] = &[
|
||||
@@ -258,17 +263,14 @@ static PROJECTION_ALIAS_CHOICES: &[Node] = &[
|
||||
Node::Lookahead(projection_bare_alias_factory),
|
||||
];
|
||||
static PROJECTION_ALIAS_CHOICE: Node = Node::Choice(PROJECTION_ALIAS_CHOICES);
|
||||
static PROJECTION_ALIAS_OPTIONAL: Node =
|
||||
Node::Optional(&PROJECTION_ALIAS_CHOICE);
|
||||
static PROJECTION_ALIAS_OPTIONAL: Node = Node::Optional(&PROJECTION_ALIAS_CHOICE);
|
||||
|
||||
static TABLE_SOURCE_ALIAS_CHOICES: &[Node] = &[
|
||||
Node::Subgrammar(&TABLE_SOURCE_AS_ALIAS),
|
||||
Node::Lookahead(table_source_bare_alias_factory),
|
||||
];
|
||||
static TABLE_SOURCE_ALIAS_CHOICE: Node =
|
||||
Node::Choice(TABLE_SOURCE_ALIAS_CHOICES);
|
||||
static TABLE_SOURCE_ALIAS_OPTIONAL: Node =
|
||||
Node::Optional(&TABLE_SOURCE_ALIAS_CHOICE);
|
||||
static TABLE_SOURCE_ALIAS_CHOICE: Node = Node::Choice(TABLE_SOURCE_ALIAS_CHOICES);
|
||||
static TABLE_SOURCE_ALIAS_OPTIONAL: Node = Node::Optional(&TABLE_SOURCE_ALIAS_CHOICE);
|
||||
|
||||
// =================================================================
|
||||
// Projection item
|
||||
@@ -282,16 +284,13 @@ const QUALIFIED_STAR_QUALIFIER: Node = Node::Ident {
|
||||
writes_table: false,
|
||||
writes_column: false,
|
||||
writes_user_listed_column: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
};
|
||||
|
||||
static QUALIFIED_STAR_NODES: &[Node] = &[
|
||||
QUALIFIED_STAR_QUALIFIER,
|
||||
Node::Punct('.'),
|
||||
Node::Punct('*'),
|
||||
];
|
||||
static QUALIFIED_STAR_NODES: &[Node] =
|
||||
&[QUALIFIED_STAR_QUALIFIER, Node::Punct('.'), Node::Punct('*')];
|
||||
static QUALIFIED_STAR: Node = Node::Seq(QUALIFIED_STAR_NODES);
|
||||
|
||||
static PROJECTION_EXPR_ITEM_NODES: &[Node] = &[
|
||||
@@ -310,11 +309,7 @@ static PROJECTION_EXPR_ITEM: Node = Node::Seq(PROJECTION_EXPR_ITEM_NODES);
|
||||
/// ambiguity between `t.*` and `sql_expr` (which can match a
|
||||
/// bare `t`), since the walker's `Choice` doesn't backtrack on
|
||||
/// a committed match.
|
||||
fn projection_item_factory(
|
||||
_: &WalkContext,
|
||||
source: &str,
|
||||
pos: usize,
|
||||
) -> Node {
|
||||
fn projection_item_factory(_: &WalkContext, source: &str, pos: usize) -> Node {
|
||||
let p = skip_whitespace(source, pos);
|
||||
let bytes = source.as_bytes();
|
||||
if bytes.get(p) == Some(&b'*') {
|
||||
@@ -363,8 +358,7 @@ static DISTINCT_OR_ALL_CHOICES: &[Node] = &[
|
||||
Node::Word(Word::keyword("all")),
|
||||
];
|
||||
static DISTINCT_OR_ALL_CHOICE: Node = Node::Choice(DISTINCT_OR_ALL_CHOICES);
|
||||
static DISTINCT_OR_ALL_OPTIONAL: Node =
|
||||
Node::Optional(&DISTINCT_OR_ALL_CHOICE);
|
||||
static DISTINCT_OR_ALL_OPTIONAL: Node = Node::Optional(&DISTINCT_OR_ALL_CHOICE);
|
||||
|
||||
// =================================================================
|
||||
// Table source (FROM / JOIN target)
|
||||
@@ -379,8 +373,8 @@ const TABLE_NAME_IDENT: Node = Node::Ident {
|
||||
writes_column: false,
|
||||
writes_user_listed_column: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
};
|
||||
|
||||
static TABLE_SOURCE_NODES: &[Node] = &[
|
||||
@@ -395,8 +389,7 @@ static TABLE_SOURCE: Node = Node::Seq(TABLE_SOURCE_NODES);
|
||||
|
||||
const JOIN_WORD: Node = Node::Word(Word::keyword("join"));
|
||||
const ON_WORD: Node = Node::Word(Word::keyword("on"));
|
||||
static OUTER_OPTIONAL: Node =
|
||||
Node::Optional(&Node::Word(Word::keyword("outer")));
|
||||
static OUTER_OPTIONAL: Node = Node::Optional(&Node::Word(Word::keyword("outer")));
|
||||
|
||||
// `INNER JOIN` and bare `JOIN` are split into two Choice
|
||||
// branches so each branch has a distinct leading keyword
|
||||
@@ -585,8 +578,7 @@ static SET_OP_CHOICES: &[Node] = &[
|
||||
];
|
||||
static SET_OP: Node = Node::Choice(SET_OP_CHOICES);
|
||||
|
||||
static SET_OP_TAIL_NODES: &[Node] =
|
||||
&[Node::Subgrammar(&SET_OP), Node::Subgrammar(&SELECT_CORE)];
|
||||
static SET_OP_TAIL_NODES: &[Node] = &[Node::Subgrammar(&SET_OP), Node::Subgrammar(&SELECT_CORE)];
|
||||
static SET_OP_TAIL: Node = Node::Seq(SET_OP_TAIL_NODES);
|
||||
|
||||
static PLAIN_COMPOUND_NODES: &[Node] = &[
|
||||
@@ -619,8 +611,7 @@ static WITH_PREFIXED_COMPOUND_NODES: &[Node] = &[
|
||||
Node::Subgrammar(&WITH_CLAUSE),
|
||||
Node::Subgrammar(&PLAIN_COMPOUND),
|
||||
];
|
||||
static WITH_PREFIXED_COMPOUND: Node =
|
||||
Node::Seq(WITH_PREFIXED_COMPOUND_NODES);
|
||||
static WITH_PREFIXED_COMPOUND: Node = Node::Seq(WITH_PREFIXED_COMPOUND_NODES);
|
||||
|
||||
static COMPOUND_CHOICES: &[Node] = &[
|
||||
Node::Subgrammar(&WITH_PREFIXED_COMPOUND),
|
||||
@@ -659,9 +650,9 @@ const CTE_COLUMN_IDENT: Node = Node::Ident {
|
||||
writes_table: false,
|
||||
writes_column: false,
|
||||
writes_user_listed_column: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
};
|
||||
|
||||
static CTE_COLUMN_LIST_NODES: &[Node] = &[
|
||||
@@ -674,18 +665,13 @@ static CTE_COLUMN_LIST_NODES: &[Node] = &[
|
||||
RPAREN,
|
||||
];
|
||||
static CTE_COLUMN_LIST_SEQ: Node = Node::Seq(CTE_COLUMN_LIST_NODES);
|
||||
static CTE_COLUMN_LIST_OPTIONAL: Node =
|
||||
Node::Optional(&CTE_COLUMN_LIST_SEQ);
|
||||
static CTE_COLUMN_LIST_OPTIONAL: Node = Node::Optional(&CTE_COLUMN_LIST_SEQ);
|
||||
|
||||
// CTE body recursion pushes a fresh lexical scope frame (ADR-
|
||||
// 0032 §4 / §10.2). Subqueries in `sql_expr.rs` do the same;
|
||||
// the top-level statement's own COMPOUND embedding does not
|
||||
// (it shares the implicit bottom frame).
|
||||
static CTE_BODY_NODES: &[Node] = &[
|
||||
LPAREN,
|
||||
Node::ScopedSubgrammar(&SQL_SELECT_COMPOUND),
|
||||
RPAREN,
|
||||
];
|
||||
static CTE_BODY_NODES: &[Node] = &[LPAREN, Node::ScopedSubgrammar(&SQL_SELECT_COMPOUND), RPAREN];
|
||||
static CTE_BODY: Node = Node::Seq(CTE_BODY_NODES);
|
||||
|
||||
static CTE_DEF_NODES: &[Node] = &[
|
||||
@@ -807,9 +793,7 @@ mod tests {
|
||||
let mut path = MatchedPath::new();
|
||||
let mut per_byte = Vec::new();
|
||||
match walk_node(input, 0, fragment, &mut ctx, &mut path, &mut per_byte) {
|
||||
NodeWalkResult::Matched { end, .. } => {
|
||||
input[end..].trim().is_empty()
|
||||
}
|
||||
NodeWalkResult::Matched { end, .. } => input[end..].trim().is_empty(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
@@ -819,10 +803,7 @@ mod tests {
|
||||
}
|
||||
|
||||
fn good(input: &str) {
|
||||
assert!(
|
||||
walks(input),
|
||||
"{input:?} should be a valid SELECT statement"
|
||||
);
|
||||
assert!(walks(input), "{input:?} should be a valid SELECT statement");
|
||||
}
|
||||
|
||||
fn bad(input: &str) {
|
||||
@@ -1051,16 +1032,12 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn set_op_chain() {
|
||||
good(
|
||||
"select a from t union select b from u intersect select c from v",
|
||||
);
|
||||
good("select a from t union select b from u intersect select c from v");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_op_with_outer_order_by_and_limit() {
|
||||
good(
|
||||
"select a from t union select b from u order by a limit 10",
|
||||
);
|
||||
good("select a from t union select b from u order by a limit 10");
|
||||
}
|
||||
|
||||
// ----- ORDER BY / LIMIT / OFFSET -----
|
||||
@@ -1126,16 +1103,12 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn recursive_cte() {
|
||||
good(
|
||||
"with recursive r as (select 1 union all select 2) select * from r",
|
||||
);
|
||||
good("with recursive r as (select 1 union all select 2) select * from r");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_ctes() {
|
||||
good(
|
||||
"with a as (select 1), b as (select 2) select * from a union select * from b",
|
||||
);
|
||||
good("with a as (select 1), b as (select 2) select * from a union select * from b");
|
||||
}
|
||||
|
||||
// ----- subquery shapes (recursion through SQL_SELECT_COMPOUND) -----
|
||||
@@ -1147,9 +1120,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn nested_cte_body_with_union() {
|
||||
good(
|
||||
"with x as (select 1 union select 2) select * from x",
|
||||
);
|
||||
good("with x as (select 1 union select 2) select * from x");
|
||||
}
|
||||
|
||||
// ----- case insensitivity / spacing -----
|
||||
@@ -1363,9 +1334,7 @@ mod tests {
|
||||
#[test]
|
||||
fn in_subquery_in_where_clause() {
|
||||
good("select * from t where id in (select user_id from orders)");
|
||||
good(
|
||||
"select * from customers where id not in (select customer_id from blocklist)",
|
||||
);
|
||||
good("select * from customers where id not in (select customer_id from blocklist)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1378,9 +1347,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn nested_subqueries() {
|
||||
good(
|
||||
"select * from t where x in (select y from u where y in (select z from v))",
|
||||
);
|
||||
good("select * from t where x in (select y from u where y in (select z from v))");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1393,8 +1360,6 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn cte_body_references_qualified_columns() {
|
||||
good(
|
||||
"with x as (select t.name, t.age from t) select x.name from x",
|
||||
);
|
||||
good("with x as (select t.name, t.age from t) select x.name from x");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,7 +119,14 @@ mod tests {
|
||||
let mut ctx = WalkContext::new();
|
||||
let mut path = MatchedPath::new();
|
||||
let mut per_byte = Vec::new();
|
||||
match walk_node(input, 0, &SQL_UPDATE_SHAPE, &mut ctx, &mut path, &mut per_byte) {
|
||||
match walk_node(
|
||||
input,
|
||||
0,
|
||||
&SQL_UPDATE_SHAPE,
|
||||
&mut ctx,
|
||||
&mut path,
|
||||
&mut per_byte,
|
||||
) {
|
||||
NodeWalkResult::Matched { end, .. } => input[end..].trim().is_empty(),
|
||||
_ => false,
|
||||
}
|
||||
@@ -130,7 +137,10 @@ mod tests {
|
||||
}
|
||||
|
||||
fn bad(input: &str) {
|
||||
assert!(!walks(input), "{input:?} should NOT walk as a complete UPDATE tail");
|
||||
assert!(
|
||||
!walks(input),
|
||||
"{input:?} should NOT walk as a complete UPDATE tail"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+3
-3
@@ -21,9 +21,9 @@ pub mod walker;
|
||||
|
||||
pub use action::ReferentialAction;
|
||||
pub use command::{
|
||||
AlterTableAction, AppCommand, ChangeColumnMode, ColumnSpec, Command, CompareOp, CopyScope, Expr,
|
||||
IndexSelector, MessagesValue, ModeValue, Operand, Predicate, RelationshipSelector, RowFilter,
|
||||
ShowListKind, SqlForeignKey,
|
||||
AlterTableAction, AppCommand, ChangeColumnMode, ColumnSpec, Command, CompareOp, CopyScope,
|
||||
Expr, IndexSelector, MessagesValue, ModeValue, Operand, Predicate, RelationshipSelector,
|
||||
RowFilter, ShowListKind, SqlForeignKey,
|
||||
};
|
||||
pub use parser::{ParseError, parse_command};
|
||||
pub use types::Type;
|
||||
|
||||
+18
-25
@@ -55,10 +55,9 @@ pub enum ParseError {
|
||||
impl std::fmt::Display for ParseError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Invalid { message, .. } => f.write_str(&crate::t!(
|
||||
"parse.error_wrapper",
|
||||
detail = message,
|
||||
)),
|
||||
Self::Invalid { message, .. } => {
|
||||
f.write_str(&crate::t!("parse.error_wrapper", detail = message,))
|
||||
}
|
||||
Self::Empty => f.write_str(&crate::t!("parse.empty")),
|
||||
}
|
||||
}
|
||||
@@ -125,10 +124,7 @@ pub fn parse_command_with_schema(
|
||||
/// Schemaless, mode-aware parse (ADR-0030 §2). In `Mode::Simple`
|
||||
/// the walker gates SQL-only commands and produces the
|
||||
/// "this is SQL" hint instead of executing them.
|
||||
pub fn parse_command_in_mode(
|
||||
input: &str,
|
||||
mode: Mode,
|
||||
) -> Result<Command, ParseError> {
|
||||
pub fn parse_command_in_mode(input: &str, mode: Mode) -> Result<Command, ParseError> {
|
||||
parse_command_inner(input, None, mode)
|
||||
}
|
||||
|
||||
@@ -185,10 +181,8 @@ fn unknown_command_error(source: &str) -> ParseError {
|
||||
.collect();
|
||||
let joined = oxford_join(&entries);
|
||||
let start = skip_whitespace(source, 0);
|
||||
let (position, found_word) = consume_ident(source, start).map_or_else(
|
||||
|| (start, None),
|
||||
|(s, e)| (s, Some(&source[s..e])),
|
||||
);
|
||||
let (position, found_word) = consume_ident(source, start)
|
||||
.map_or_else(|| (start, None), |(s, e)| (s, Some(&source[s..e])));
|
||||
let message = found_word.map_or_else(
|
||||
|| format!("expected one of {joined}"),
|
||||
|w| format!("expected one of {joined}, found `{w}`"),
|
||||
@@ -1034,19 +1028,22 @@ mod tests {
|
||||
false,
|
||||
);
|
||||
assert_eq!(
|
||||
ok("add 1:n relationship from Customers.Id to Orders.CustId on delete cascade on update set null"),
|
||||
ok(
|
||||
"add 1:n relationship from Customers.Id to Orders.CustId on delete cascade on update set null"
|
||||
),
|
||||
expected
|
||||
);
|
||||
assert_eq!(
|
||||
ok("add 1:n relationship from Customers.Id to Orders.CustId on update set null on delete cascade"),
|
||||
ok(
|
||||
"add 1:n relationship from Customers.Id to Orders.CustId on update set null on delete cascade"
|
||||
),
|
||||
expected
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_relationship_repeated_clause_errors() {
|
||||
let e =
|
||||
err("add 1:n relationship from C.id to O.cid on delete cascade on delete restrict");
|
||||
let e = err("add 1:n relationship from C.id to O.cid on delete cascade on delete restrict");
|
||||
match e {
|
||||
ParseError::Invalid { message, .. } => {
|
||||
assert!(message.contains("specified twice"), "{message}");
|
||||
@@ -1073,7 +1070,9 @@ mod tests {
|
||||
#[test]
|
||||
fn add_relationship_with_name_actions_and_flag() {
|
||||
assert_eq!(
|
||||
ok("add 1:n relationship as cust_orders from Customers.Id to Orders.CustId on delete cascade on update no action --create-fk"),
|
||||
ok(
|
||||
"add 1:n relationship as cust_orders from Customers.Id to Orders.CustId on delete cascade on update no action --create-fk"
|
||||
),
|
||||
rel(
|
||||
Some("cust_orders"),
|
||||
("Customers", "Id"),
|
||||
@@ -1300,10 +1299,7 @@ mod tests {
|
||||
#[test]
|
||||
fn advanced_ambiguous_update_routes_to_sql() {
|
||||
assert!(matches!(
|
||||
parse_command_in_mode(
|
||||
"update Orders set total = 0 where id = 1",
|
||||
Mode::Advanced,
|
||||
),
|
||||
parse_command_in_mode("update Orders set total = 0 where id = 1", Mode::Advanced,),
|
||||
Ok(Command::SqlUpdate { .. })
|
||||
));
|
||||
}
|
||||
@@ -1399,10 +1395,7 @@ mod tests {
|
||||
// in advanced mode)" pointer is added at the hint layer
|
||||
// (input_render), not in the parsed command/error here.
|
||||
assert!(matches!(
|
||||
parse_command_in_mode(
|
||||
"delete from Orders where id = 1 returning *",
|
||||
Mode::Simple,
|
||||
),
|
||||
parse_command_in_mode("delete from Orders where id = 1 returning *", Mode::Simple,),
|
||||
Err(ParseError::Invalid { .. })
|
||||
));
|
||||
}
|
||||
|
||||
+1
-2
@@ -9,8 +9,7 @@ use rand::RngExt;
|
||||
|
||||
/// Base58 alphabet — Bitcoin-style. 0 / O / I / l are excluded
|
||||
/// because they are easily confused in print.
|
||||
const ALPHABET: &[u8; 58] =
|
||||
b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
||||
const ALPHABET: &[u8; 58] = b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
||||
|
||||
const DEFAULT_LEN: usize = 10;
|
||||
|
||||
|
||||
@@ -43,29 +43,9 @@
|
||||
/// - **Broader scalars:** `date`, `datetime`, `hex`, `ifnull`,
|
||||
/// `instr`, `nullif`, `random`, `replace`, `strftime`, `typeof`.
|
||||
pub const KNOWN_SQL_FUNCTIONS: &[&str] = &[
|
||||
"abs",
|
||||
"avg",
|
||||
"coalesce",
|
||||
"count",
|
||||
"date",
|
||||
"datetime",
|
||||
"hex",
|
||||
"ifnull",
|
||||
"instr",
|
||||
"length",
|
||||
"lower",
|
||||
"max",
|
||||
"min",
|
||||
"nullif",
|
||||
"random",
|
||||
"replace",
|
||||
"round",
|
||||
"strftime",
|
||||
"substr",
|
||||
"sum",
|
||||
"trim",
|
||||
"typeof",
|
||||
"upper",
|
||||
"abs", "avg", "coalesce", "count", "date", "datetime", "hex", "ifnull", "instr", "length",
|
||||
"lower", "max", "min", "nullif", "random", "replace", "round", "strftime", "substr", "sum",
|
||||
"trim", "typeof", "upper",
|
||||
];
|
||||
|
||||
/// Whether `partial` is a case-insensitive prefix of at least one
|
||||
@@ -80,9 +60,7 @@ pub const KNOWN_SQL_FUNCTIONS: &[&str] = &[
|
||||
#[must_use]
|
||||
pub fn is_known_function_prefix(partial: &str) -> bool {
|
||||
let lowered = partial.to_lowercase();
|
||||
KNOWN_SQL_FUNCTIONS
|
||||
.iter()
|
||||
.any(|f| f.starts_with(&lowered))
|
||||
KNOWN_SQL_FUNCTIONS.iter().any(|f| f.starts_with(&lowered))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
+2
-9
@@ -59,11 +59,7 @@ impl Type {
|
||||
#[must_use]
|
||||
pub const fn sqlite_strict_type(self) -> &'static str {
|
||||
match self {
|
||||
Self::Text
|
||||
| Self::ShortId
|
||||
| Self::Decimal
|
||||
| Self::Date
|
||||
| Self::DateTime => "TEXT",
|
||||
Self::Text | Self::ShortId | Self::Decimal | Self::Date | Self::DateTime => "TEXT",
|
||||
Self::Int | Self::Serial | Self::Bool => "INTEGER",
|
||||
Self::Real => "REAL",
|
||||
Self::Blob => "BLOB",
|
||||
@@ -107,10 +103,7 @@ impl Type {
|
||||
/// match against a numeric column (ADR-0027, Amendment 1).
|
||||
#[must_use]
|
||||
pub const fn is_numeric(self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::Int | Self::Real | Self::Decimal | Self::Serial
|
||||
)
|
||||
matches!(self, Self::Int | Self::Real | Self::Decimal | Self::Serial)
|
||||
}
|
||||
|
||||
/// The user-facing type that an FK column should use to
|
||||
|
||||
+37
-18
@@ -129,13 +129,14 @@ impl Value {
|
||||
|
||||
fn bind_int(&self, column: &str, ty: Type) -> Result<Bound, ValueError> {
|
||||
match self {
|
||||
Self::Number(n) => n
|
||||
.parse::<i64>()
|
||||
.map(Bound::Integer)
|
||||
.map_err(|_| ValueError::Format {
|
||||
column: column.to_string(),
|
||||
message: format!("`{n}` is not a valid {ty} (whole number expected)"),
|
||||
}),
|
||||
Self::Number(n) => {
|
||||
n.parse::<i64>()
|
||||
.map(Bound::Integer)
|
||||
.map_err(|_| ValueError::Format {
|
||||
column: column.to_string(),
|
||||
message: format!("`{n}` is not a valid {ty} (whole number expected)"),
|
||||
})
|
||||
}
|
||||
other => Err(ValueError::TypeMismatch {
|
||||
column: column.to_string(),
|
||||
expected_human: format!("a whole number for `{ty}`"),
|
||||
@@ -241,9 +242,7 @@ pub(crate) fn validate_date(s: &str) -> Result<(), String> {
|
||||
// Expect YYYY-MM-DD: 10 chars, two dashes at fixed positions.
|
||||
let bytes = s.as_bytes();
|
||||
if bytes.len() != 10 || bytes[4] != b'-' || bytes[7] != b'-' {
|
||||
return Err(format!(
|
||||
"`{s}` is not a date in `YYYY-MM-DD` form"
|
||||
));
|
||||
return Err(format!("`{s}` is not a date in `YYYY-MM-DD` form"));
|
||||
}
|
||||
let year = parse_digits(&s[0..4]).ok_or_else(|| format!("`{s}`: invalid year"))?;
|
||||
let month = parse_digits(&s[5..7]).ok_or_else(|| format!("`{s}`: invalid month"))?;
|
||||
@@ -272,7 +271,9 @@ pub(crate) fn validate_datetime(s: &str) -> Result<(), String> {
|
||||
validate_date(date_part)?;
|
||||
let bytes = s.as_bytes();
|
||||
if bytes[10] != b'T' {
|
||||
return Err(format!("`{s}`: missing `T` separator between date and time"));
|
||||
return Err(format!(
|
||||
"`{s}`: missing `T` separator between date and time"
|
||||
));
|
||||
}
|
||||
if bytes[13] != b':' || bytes[16] != b':' {
|
||||
return Err(format!("`{s}`: time portion must be `HH:MM:SS`"));
|
||||
@@ -326,8 +327,14 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn integer_for_int_column() {
|
||||
assert_eq!(n("42").bind_for_column("c", Type::Int).unwrap(), Bound::Integer(42));
|
||||
assert_eq!(n("-7").bind_for_column("c", Type::Int).unwrap(), Bound::Integer(-7));
|
||||
assert_eq!(
|
||||
n("42").bind_for_column("c", Type::Int).unwrap(),
|
||||
Bound::Integer(42)
|
||||
);
|
||||
assert_eq!(
|
||||
n("-7").bind_for_column("c", Type::Int).unwrap(),
|
||||
Bound::Integer(-7)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -355,7 +362,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn shortid_validation_runs_on_text_for_shortid_column() {
|
||||
let err = t("toolong_xyz_more").bind_for_column("c", Type::ShortId).unwrap_err();
|
||||
let err = t("toolong_xyz_more")
|
||||
.bind_for_column("c", Type::ShortId)
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, ValueError::Format { .. }));
|
||||
|
||||
// Well-formed shortid binds fine.
|
||||
@@ -367,8 +376,14 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn bool_for_bool_column_maps_to_zero_or_one() {
|
||||
assert_eq!(Value::Bool(true).bind_for_column("c", Type::Bool).unwrap(), Bound::Integer(1));
|
||||
assert_eq!(Value::Bool(false).bind_for_column("c", Type::Bool).unwrap(), Bound::Integer(0));
|
||||
assert_eq!(
|
||||
Value::Bool(true).bind_for_column("c", Type::Bool).unwrap(),
|
||||
Bound::Integer(1)
|
||||
);
|
||||
assert_eq!(
|
||||
Value::Bool(false).bind_for_column("c", Type::Bool).unwrap(),
|
||||
Bound::Integer(0)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -377,13 +392,17 @@ mod tests {
|
||||
t("2025-01-15").bind_for_column("c", Type::Date).unwrap(),
|
||||
Bound::Text("2025-01-15".to_string())
|
||||
);
|
||||
let err = t("2025/01/15").bind_for_column("c", Type::Date).unwrap_err();
|
||||
let err = t("2025/01/15")
|
||||
.bind_for_column("c", Type::Date)
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, ValueError::Format { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn date_range_check() {
|
||||
let err = t("2025-13-01").bind_for_column("c", Type::Date).unwrap_err();
|
||||
let err = t("2025-13-01")
|
||||
.bind_for_column("c", Type::Date)
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, ValueError::Format { message, .. } if message.contains("month")));
|
||||
}
|
||||
|
||||
|
||||
+119
-179
@@ -28,12 +28,10 @@ use crate::completion::TableColumn;
|
||||
use crate::dsl::grammar::{HighlightClass, Node, ValidationError};
|
||||
use crate::dsl::walker::context::WalkContext;
|
||||
use crate::dsl::walker::lex_helpers::{
|
||||
consume_bare_path, consume_flag, consume_ident, consume_number_literal,
|
||||
consume_string_literal, skip_whitespace,
|
||||
};
|
||||
use crate::dsl::walker::outcome::{
|
||||
ByteClass, Expectation, MatchedItem, MatchedKind, MatchedPath,
|
||||
consume_bare_path, consume_flag, consume_ident, consume_number_literal, consume_string_literal,
|
||||
skip_whitespace,
|
||||
};
|
||||
use crate::dsl::walker::outcome::{ByteClass, Expectation, MatchedItem, MatchedKind, MatchedPath};
|
||||
|
||||
/// Maximum nesting of `Node::Subgrammar` frames (ADR-0026 §1).
|
||||
///
|
||||
@@ -77,10 +75,7 @@ static DYNAMIC_CACHE: LazyLock<Mutex<HashMap<DynamicKey, &'static Node>>> =
|
||||
/// Resolve a `DynamicSubgrammar` factory to a `&'static Node`,
|
||||
/// reusing a previously-leaked Node when the factory's inputs
|
||||
/// match a cached entry.
|
||||
fn resolve_dynamic(
|
||||
factory: fn(&WalkContext) -> Node,
|
||||
ctx: &WalkContext,
|
||||
) -> &'static Node {
|
||||
fn resolve_dynamic(factory: fn(&WalkContext) -> Node, ctx: &WalkContext) -> &'static Node {
|
||||
let key = DynamicKey {
|
||||
factory: factory as usize,
|
||||
current_table_columns: ctx.current_table_columns.clone(),
|
||||
@@ -123,10 +118,7 @@ pub enum NodeWalkResult {
|
||||
expected: Vec<Expectation>,
|
||||
},
|
||||
/// Committed and hit a hard mismatch or validator failure.
|
||||
Failed {
|
||||
position: usize,
|
||||
kind: FailureKind,
|
||||
},
|
||||
Failed { position: usize, kind: FailureKind },
|
||||
}
|
||||
|
||||
const fn matched(end: usize) -> NodeWalkResult {
|
||||
@@ -218,9 +210,7 @@ fn walk_node_inner(
|
||||
kind: FailureKind::Mismatch { expected: vec![] },
|
||||
}
|
||||
}
|
||||
Node::Subgrammar(inner) => {
|
||||
walk_subgrammar(source, pos, inner, ctx, path, per_byte)
|
||||
}
|
||||
Node::Subgrammar(inner) => walk_subgrammar(source, pos, inner, ctx, path, per_byte),
|
||||
Node::ScopedSubgrammar(inner) => {
|
||||
walk_scoped_subgrammar(source, pos, inner, ctx, path, per_byte)
|
||||
}
|
||||
@@ -247,8 +237,7 @@ fn walk_node_inner(
|
||||
// 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)));
|
||||
let resolved: &'static Node = Box::leak(Box::new(factory(ctx, source, pos)));
|
||||
walk_node(source, pos, resolved, ctx, path, per_byte)
|
||||
}
|
||||
Node::SetColumn(col) => {
|
||||
@@ -262,7 +251,10 @@ fn walk_node_inner(
|
||||
let col: &crate::completion::TableColumn = col;
|
||||
ctx.current_column = Some(col.clone());
|
||||
ctx.pending_value_column = Some(col.name.clone());
|
||||
NodeWalkResult::Matched { end: pos, skipped: Vec::new() }
|
||||
NodeWalkResult::Matched {
|
||||
end: pos,
|
||||
skipped: Vec::new(),
|
||||
}
|
||||
}
|
||||
Node::TypedValueSlot {
|
||||
ty,
|
||||
@@ -342,7 +334,10 @@ fn walk_word(
|
||||
// Amendment 4). Plain keywords leave it `None`.
|
||||
class: word.highlight_override.unwrap_or(HighlightClass::Keyword),
|
||||
});
|
||||
NodeWalkResult::Matched { end, skipped: Vec::new() }
|
||||
NodeWalkResult::Matched {
|
||||
end,
|
||||
skipped: Vec::new(),
|
||||
}
|
||||
} else {
|
||||
NodeWalkResult::NoMatch {
|
||||
position,
|
||||
@@ -477,9 +472,7 @@ fn walk_ident(
|
||||
// ScopedSubgrammar (which is structurally guaranteed to be
|
||||
// the CTE body — no intervening scoped subgrammar in CTE
|
||||
// syntax) runs the harvest at body-frame exit.
|
||||
if writes_cte_name
|
||||
&& let Some(frame) = ctx.from_scope_stack.last_mut()
|
||||
{
|
||||
if writes_cte_name && let Some(frame) = ctx.from_scope_stack.last_mut() {
|
||||
frame
|
||||
.cte_bindings
|
||||
.push(crate::dsl::walker::context::CteBinding {
|
||||
@@ -487,13 +480,12 @@ fn walk_ident(
|
||||
columns: Vec::new(),
|
||||
});
|
||||
let placeholder_index = frame.cte_bindings.len() - 1;
|
||||
ctx.pending_cte_harvest =
|
||||
Some(crate::dsl::walker::context::PendingCteHarvest {
|
||||
placeholder_index,
|
||||
col_list: Vec::new(),
|
||||
cte_name: text.clone(),
|
||||
cte_name_span: (start, end),
|
||||
});
|
||||
ctx.pending_cte_harvest = Some(crate::dsl::walker::context::PendingCteHarvest {
|
||||
placeholder_index,
|
||||
col_list: Vec::new(),
|
||||
cte_name: text.clone(),
|
||||
cte_name_span: (start, end),
|
||||
});
|
||||
}
|
||||
// ADR-0032 §10.3: the optional `(c1, c2, …)` rename list
|
||||
// between the cte name and `AS`. Each `cte_column` ident
|
||||
@@ -507,9 +499,7 @@ fn walk_ident(
|
||||
}
|
||||
// ADR-0032 §10.4: projection-list alias accumulator for
|
||||
// ORDER BY completion candidates.
|
||||
if writes_projection_alias
|
||||
&& let Some(frame) = ctx.from_scope_stack.last_mut()
|
||||
{
|
||||
if writes_projection_alias && let Some(frame) = ctx.from_scope_stack.last_mut() {
|
||||
frame.projection_aliases.push(text.clone());
|
||||
}
|
||||
if writes_column && matches!(src, crate::dsl::grammar::IdentSource::Columns) {
|
||||
@@ -529,9 +519,7 @@ fn walk_ident(
|
||||
.map(|c| c.name.clone())
|
||||
.or_else(|| Some(text.clone()));
|
||||
}
|
||||
if writes_user_listed_column
|
||||
&& matches!(src, crate::dsl::grammar::IdentSource::Columns)
|
||||
{
|
||||
if writes_user_listed_column && matches!(src, crate::dsl::grammar::IdentSource::Columns) {
|
||||
// Form A: `insert into <T> (col1, col2, …)`. Append the
|
||||
// matched column name to user_listed_columns so the
|
||||
// inner `values (…)` slot list mirrors the user's
|
||||
@@ -564,7 +552,10 @@ fn walk_ident(
|
||||
// (issue #8 / ADR-0022 Amendment 4).
|
||||
class: highlight_override.unwrap_or(HighlightClass::Identifier),
|
||||
});
|
||||
NodeWalkResult::Matched { end, skipped: Vec::new() }
|
||||
NodeWalkResult::Matched {
|
||||
end,
|
||||
skipped: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn walk_string_lit(
|
||||
@@ -648,7 +639,10 @@ fn walk_literal(
|
||||
end,
|
||||
class,
|
||||
});
|
||||
NodeWalkResult::Matched { end, skipped: Vec::new() }
|
||||
NodeWalkResult::Matched {
|
||||
end,
|
||||
skipped: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn walk_number_lit(
|
||||
@@ -683,7 +677,10 @@ fn walk_number_lit(
|
||||
end,
|
||||
class: HighlightClass::Number,
|
||||
});
|
||||
NodeWalkResult::Matched { end, skipped: Vec::new() }
|
||||
NodeWalkResult::Matched {
|
||||
end,
|
||||
skipped: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn walk_flag(
|
||||
@@ -717,7 +714,10 @@ fn walk_flag(
|
||||
end,
|
||||
class: HighlightClass::Flag,
|
||||
});
|
||||
NodeWalkResult::Matched { end, skipped: Vec::new() }
|
||||
NodeWalkResult::Matched {
|
||||
end,
|
||||
skipped: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
@@ -784,7 +784,10 @@ fn walk_repeated(
|
||||
count += 1;
|
||||
last_item_skipped = skipped;
|
||||
}
|
||||
NodeWalkResult::NoMatch { expected, position: inner_pos } => {
|
||||
NodeWalkResult::NoMatch {
|
||||
expected,
|
||||
position: inner_pos,
|
||||
} => {
|
||||
// Mid-typing-the-next-item recovery: if the
|
||||
// separator just consumed and the inner failed
|
||||
// at EOF, the user is partway through typing the
|
||||
@@ -860,7 +863,10 @@ fn walk_bare_path(
|
||||
end,
|
||||
class: HighlightClass::String,
|
||||
});
|
||||
NodeWalkResult::Matched { end, skipped: Vec::new() }
|
||||
NodeWalkResult::Matched {
|
||||
end,
|
||||
skipped: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn walk_choice(
|
||||
@@ -1031,7 +1037,10 @@ fn walk_optional(
|
||||
skipped: expected,
|
||||
}
|
||||
}
|
||||
NodeWalkResult::Incomplete { position: p, expected } if !inner_committed => {
|
||||
NodeWalkResult::Incomplete {
|
||||
position: p,
|
||||
expected,
|
||||
} if !inner_committed => {
|
||||
// Inner reported Incomplete without consuming
|
||||
// anything — same as NoMatch from the user's
|
||||
// perspective. Roll back and skip.
|
||||
@@ -1156,9 +1165,7 @@ fn walk_scoped_subgrammar(
|
||||
// walks that NoMatch / Incomplete / Fail leave the placeholder
|
||||
// empty (the outer-frame state is also discarded in the
|
||||
// speculative path, so this is correct).
|
||||
if let (Some(req), NodeWalkResult::Matched { end, .. }) =
|
||||
(pending_cte, &result)
|
||||
{
|
||||
if let (Some(req), NodeWalkResult::Matched { end, .. }) = (pending_cte, &result) {
|
||||
run_cte_harvest(ctx, path, source, pos, *end, &req);
|
||||
}
|
||||
|
||||
@@ -1240,9 +1247,8 @@ fn run_cte_harvest(
|
||||
select_idx = Some(i + 1); // start of projection list
|
||||
}
|
||||
MatchedKind::Word(
|
||||
"from" | "where" | "group" | "having" | "order"
|
||||
| "limit" | "offset" | "union" | "intersect"
|
||||
| "except",
|
||||
"from" | "where" | "group" | "having" | "order" | "limit" | "offset" | "union"
|
||||
| "intersect" | "except",
|
||||
) if select_idx.is_some() => {
|
||||
end_idx = i;
|
||||
break;
|
||||
@@ -1281,12 +1287,7 @@ fn run_cte_harvest(
|
||||
// Classify each projection item per ADR-0032 §10.3.
|
||||
let mut derived: Vec<CteColumn> = Vec::new();
|
||||
for slice in item_slices {
|
||||
classify_projection_item(
|
||||
slice,
|
||||
body_frame,
|
||||
&ctx.from_scope_stack,
|
||||
&mut derived,
|
||||
);
|
||||
classify_projection_item(slice, body_frame, &ctx.from_scope_stack, &mut derived);
|
||||
}
|
||||
|
||||
// Apply (c1, c2, …) positional rename if provided. Types
|
||||
@@ -1339,8 +1340,7 @@ fn run_cte_harvest(
|
||||
let stack_len = ctx.from_scope_stack.len();
|
||||
if stack_len >= 2
|
||||
&& let Some(outer) = ctx.from_scope_stack.get_mut(stack_len - 2)
|
||||
&& let Some(placeholder) =
|
||||
outer.cte_bindings.get_mut(req.placeholder_index)
|
||||
&& let Some(placeholder) = outer.cte_bindings.get_mut(req.placeholder_index)
|
||||
{
|
||||
placeholder.columns = derived;
|
||||
}
|
||||
@@ -1368,9 +1368,7 @@ fn classify_projection_item(
|
||||
// empty because it wasn't a base-table lookup), resolve
|
||||
// through to the in-scope CteBinding so nested CTEs project
|
||||
// correctly.
|
||||
if expr_slice.len() == 1
|
||||
&& matches!(expr_slice[0].kind, MatchedKind::Punct('*'))
|
||||
{
|
||||
if expr_slice.len() == 1 && matches!(expr_slice[0].kind, MatchedKind::Punct('*')) {
|
||||
for binding in &body_frame.from_scope {
|
||||
for col in expand_binding(binding, scope_stack) {
|
||||
out.push(col);
|
||||
@@ -1383,7 +1381,10 @@ fn classify_projection_item(
|
||||
if expr_slice.len() == 3
|
||||
&& matches!(
|
||||
expr_slice[0].kind,
|
||||
MatchedKind::Ident { role: "qualified_star_qualifier", .. }
|
||||
MatchedKind::Ident {
|
||||
role: "qualified_star_qualifier",
|
||||
..
|
||||
}
|
||||
)
|
||||
&& matches!(expr_slice[1].kind, MatchedKind::Punct('.'))
|
||||
&& matches!(expr_slice[2].kind, MatchedKind::Punct('*'))
|
||||
@@ -1413,11 +1414,7 @@ fn classify_projection_item(
|
||||
)
|
||||
{
|
||||
let col_text = &expr_slice[0].text;
|
||||
let resolved_type = resolve_bare_column_type_in_frame(
|
||||
body_frame,
|
||||
scope_stack,
|
||||
col_text,
|
||||
);
|
||||
let resolved_type = resolve_bare_column_type_in_frame(body_frame, scope_stack, col_text);
|
||||
let name = alias.unwrap_or_else(|| col_text.clone());
|
||||
out.push(CteColumn {
|
||||
name: Some(name),
|
||||
@@ -1447,12 +1444,7 @@ fn classify_projection_item(
|
||||
{
|
||||
let qual = &expr_slice[0].text;
|
||||
let col_text = &expr_slice[2].text;
|
||||
let resolved_type = resolve_qualified_column_type(
|
||||
body_frame,
|
||||
scope_stack,
|
||||
qual,
|
||||
col_text,
|
||||
);
|
||||
let resolved_type = resolve_qualified_column_type(body_frame, scope_stack, qual, col_text);
|
||||
let name = alias.unwrap_or_else(|| col_text.clone());
|
||||
out.push(CteColumn {
|
||||
name: Some(name),
|
||||
@@ -1493,16 +1485,8 @@ fn strip_trailing_alias<'a>(
|
||||
}
|
||||
) {
|
||||
// Optional preceding `AS` keyword.
|
||||
if slice.len() >= 2
|
||||
&& matches!(
|
||||
slice[slice.len() - 2].kind,
|
||||
MatchedKind::Word("as")
|
||||
)
|
||||
{
|
||||
return (
|
||||
&slice[..slice.len() - 2],
|
||||
Some(last.text.clone()),
|
||||
);
|
||||
if slice.len() >= 2 && matches!(slice[slice.len() - 2].kind, MatchedKind::Word("as")) {
|
||||
return (&slice[..slice.len() - 2], Some(last.text.clone()));
|
||||
}
|
||||
return (&slice[..slice.len() - 1], Some(last.text.clone()));
|
||||
}
|
||||
@@ -1613,8 +1597,8 @@ fn merge_expected(dst: &mut Vec<Expectation>, src: Vec<Expectation>) {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
DYNAMIC_CACHE, FailureKind, MAX_SUBGRAMMAR_DEPTH, NodeWalkResult,
|
||||
resolve_dynamic, walk_node,
|
||||
DYNAMIC_CACHE, FailureKind, MAX_SUBGRAMMAR_DEPTH, NodeWalkResult, resolve_dynamic,
|
||||
walk_node,
|
||||
};
|
||||
use crate::dsl::grammar::{Node, Word};
|
||||
use crate::dsl::walker::context::WalkContext;
|
||||
@@ -1629,18 +1613,14 @@ mod tests {
|
||||
Node::Subgrammar(&NESTED),
|
||||
Node::Punct(')'),
|
||||
];
|
||||
static NESTED_CHOICES: &[Node] = &[
|
||||
Node::Seq(NESTED_GROUP),
|
||||
Node::Word(Word::keyword("x")),
|
||||
];
|
||||
static NESTED_CHOICES: &[Node] = &[Node::Seq(NESTED_GROUP), Node::Word(Word::keyword("x"))];
|
||||
static NESTED: Node = Node::Choice(NESTED_CHOICES);
|
||||
|
||||
fn walk_nested(input: &str) -> NodeWalkResult {
|
||||
let mut ctx = WalkContext::new();
|
||||
let mut path = MatchedPath::new();
|
||||
let mut per_byte = Vec::new();
|
||||
let result =
|
||||
walk_node(input, 0, &NESTED, &mut ctx, &mut path, &mut per_byte);
|
||||
let result = walk_node(input, 0, &NESTED, &mut ctx, &mut path, &mut per_byte);
|
||||
assert_eq!(
|
||||
ctx.subgrammar_depth, 0,
|
||||
"subgrammar_depth must be restored to 0 after the walk",
|
||||
@@ -1726,14 +1706,8 @@ mod tests {
|
||||
fn resolve_dynamic_cache_is_populated() {
|
||||
let ctx = WalkContext::new();
|
||||
let _ = resolve_dynamic(const_factory, &ctx);
|
||||
let populated = !DYNAMIC_CACHE
|
||||
.lock()
|
||||
.expect("cache lock")
|
||||
.is_empty();
|
||||
assert!(
|
||||
populated,
|
||||
"resolve_dynamic should populate the memo cache",
|
||||
);
|
||||
let populated = !DYNAMIC_CACHE.lock().expect("cache lock").is_empty();
|
||||
assert!(populated, "resolve_dynamic should populate the memo cache",);
|
||||
}
|
||||
|
||||
// ---- ScopedSubgrammar (ADR-0032 §10.2) -----------------------
|
||||
@@ -1758,14 +1732,7 @@ mod tests {
|
||||
let mut path = MatchedPath::new();
|
||||
let mut per_byte = Vec::new();
|
||||
let baseline_frames = ctx.from_scope_stack.len();
|
||||
let result = walk_node(
|
||||
input,
|
||||
0,
|
||||
&SCOPED_NESTED,
|
||||
&mut ctx,
|
||||
&mut path,
|
||||
&mut per_byte,
|
||||
);
|
||||
let result = walk_node(input, 0, &SCOPED_NESTED, &mut ctx, &mut path, &mut per_byte);
|
||||
assert_eq!(
|
||||
ctx.subgrammar_depth, 0,
|
||||
"subgrammar_depth must be restored to 0 after the walk",
|
||||
@@ -1801,9 +1768,9 @@ mod tests {
|
||||
kind: FailureKind::Validation(err),
|
||||
..
|
||||
} => assert_eq!(err.message_key, "parse.custom.expression_too_deep"),
|
||||
other => panic!(
|
||||
"expected expression_too_deep on pathological scoped nesting, got {other:?}",
|
||||
),
|
||||
other => {
|
||||
panic!("expected expression_too_deep on pathological scoped nesting, got {other:?}",)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1822,9 +1789,7 @@ mod tests {
|
||||
/// Walk a top-level SQL SELECT and return the bottom frame's
|
||||
/// `from_scope` after the walk completes. Used to verify that
|
||||
/// `writes_table` / `writes_table_alias` populate bindings.
|
||||
fn from_scope_after_walk(
|
||||
input: &str,
|
||||
) -> Vec<crate::dsl::walker::context::TableBinding> {
|
||||
fn from_scope_after_walk(input: &str) -> Vec<crate::dsl::walker::context::TableBinding> {
|
||||
let mut ctx = WalkContext::new();
|
||||
let mut path = MatchedPath::new();
|
||||
let mut per_byte = Vec::new();
|
||||
@@ -1871,9 +1836,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn join_pushes_a_second_binding() {
|
||||
let bindings = from_scope_after_walk(
|
||||
"select * from a join b on x = y",
|
||||
);
|
||||
let bindings = from_scope_after_walk("select * from a join b on x = y");
|
||||
assert_eq!(bindings.len(), 2);
|
||||
assert_eq!(bindings[0].table, "a");
|
||||
assert_eq!(bindings[1].table, "b");
|
||||
@@ -1881,9 +1844,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn join_with_aliases() {
|
||||
let bindings = from_scope_after_walk(
|
||||
"select * from a as x join b as y on x.id = y.id",
|
||||
);
|
||||
let bindings = from_scope_after_walk("select * from a as x join b as y on x.id = y.id");
|
||||
assert_eq!(bindings.len(), 2);
|
||||
assert_eq!(bindings[0].table, "a");
|
||||
assert_eq!(bindings[0].alias, Some("x".to_string()));
|
||||
@@ -1893,9 +1854,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn three_way_join_pushes_three_bindings() {
|
||||
let bindings = from_scope_after_walk(
|
||||
"select * from a join b on x = y left join c on y = z",
|
||||
);
|
||||
let bindings =
|
||||
from_scope_after_walk("select * from a join b on x = y left join c on y = z");
|
||||
assert_eq!(bindings.len(), 3);
|
||||
assert_eq!(bindings[0].table, "a");
|
||||
assert_eq!(bindings[1].table, "b");
|
||||
@@ -1908,9 +1868,8 @@ mod tests {
|
||||
// binding into the inner scope frame; on exit, the frame
|
||||
// pops and the inner binding is gone. The outer scope's
|
||||
// from_scope still contains only `outer_t`.
|
||||
let bindings = from_scope_after_walk(
|
||||
"select * from outer_t where id in (select id from inner_t)",
|
||||
);
|
||||
let bindings =
|
||||
from_scope_after_walk("select * from outer_t where id in (select id from inner_t)");
|
||||
assert_eq!(bindings.len(), 1);
|
||||
assert_eq!(bindings[0].table, "outer_t");
|
||||
}
|
||||
@@ -1921,9 +1880,8 @@ mod tests {
|
||||
// body's scope frame; on body-frame exit, the inner
|
||||
// binding goes away. The outer scope contains only
|
||||
// the CTE-name reference `cte_x`.
|
||||
let bindings = from_scope_after_walk(
|
||||
"with cte_x as (select * from base_table) select * from cte_x",
|
||||
);
|
||||
let bindings =
|
||||
from_scope_after_walk("with cte_x as (select * from base_table) select * from cte_x");
|
||||
assert_eq!(bindings.len(), 1);
|
||||
assert_eq!(bindings[0].table, "cte_x");
|
||||
}
|
||||
@@ -1940,10 +1898,7 @@ mod tests {
|
||||
/// `cte_bindings` and `projection_aliases` after the walk.
|
||||
fn frame_state_after_walk(
|
||||
input: &str,
|
||||
) -> (
|
||||
Vec<crate::dsl::walker::context::CteBinding>,
|
||||
Vec<String>,
|
||||
) {
|
||||
) -> (Vec<crate::dsl::walker::context::CteBinding>, Vec<String>) {
|
||||
let mut ctx = WalkContext::new();
|
||||
let mut path = MatchedPath::new();
|
||||
let mut per_byte = Vec::new();
|
||||
@@ -1968,9 +1923,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn cte_name_pushes_placeholder_binding() {
|
||||
let (ctes, _) = frame_state_after_walk(
|
||||
"with cte_x as (select 1) select * from cte_x",
|
||||
);
|
||||
let (ctes, _) = frame_state_after_walk("with cte_x as (select 1) select * from cte_x");
|
||||
assert_eq!(ctes.len(), 1);
|
||||
assert_eq!(ctes[0].name, "cte_x");
|
||||
// §10.3 stage-2 harvest produces one CteColumn per
|
||||
@@ -1984,9 +1937,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn multiple_ctes_push_in_order() {
|
||||
let (ctes, _) = frame_state_after_walk(
|
||||
"with a as (select 1), b as (select 2) select * from b",
|
||||
);
|
||||
let (ctes, _) =
|
||||
frame_state_after_walk("with a as (select 1), b as (select 2) select * from b");
|
||||
assert_eq!(ctes.len(), 2);
|
||||
assert_eq!(ctes[0].name, "a");
|
||||
assert_eq!(ctes[1].name, "b");
|
||||
@@ -2006,25 +1958,20 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn projection_aliases_captured_via_as_form() {
|
||||
let (_, aliases) = frame_state_after_walk(
|
||||
"select a as alpha, b as beta from t",
|
||||
);
|
||||
let (_, aliases) = frame_state_after_walk("select a as alpha, b as beta from t");
|
||||
assert_eq!(aliases, vec!["alpha".to_string(), "beta".to_string()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn projection_aliases_captured_via_bare_form() {
|
||||
let (_, aliases) = frame_state_after_walk(
|
||||
"select a alpha, b beta from t",
|
||||
);
|
||||
let (_, aliases) = frame_state_after_walk("select a alpha, b beta from t");
|
||||
assert_eq!(aliases, vec!["alpha".to_string(), "beta".to_string()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn projection_aliases_mixed_forms() {
|
||||
let (_, aliases) = frame_state_after_walk(
|
||||
"select a as alpha, b beta, c, d as delta from t",
|
||||
);
|
||||
let (_, aliases) =
|
||||
frame_state_after_walk("select a as alpha, b beta, c, d as delta from t");
|
||||
assert_eq!(
|
||||
aliases,
|
||||
vec!["alpha".to_string(), "beta".to_string(), "delta".to_string()]
|
||||
@@ -2033,8 +1980,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn projection_aliases_empty_when_no_aliases() {
|
||||
let (_, aliases) =
|
||||
frame_state_after_walk("select a, b from t");
|
||||
let (_, aliases) = frame_state_after_walk("select a, b from t");
|
||||
assert!(aliases.is_empty());
|
||||
}
|
||||
|
||||
@@ -2088,9 +2034,24 @@ mod tests {
|
||||
s.table_columns.insert(
|
||||
"users".to_string(),
|
||||
vec![
|
||||
TableColumn { name: "id".to_string(), user_type: Type::Int, not_null: false, has_default: false },
|
||||
TableColumn { name: "name".to_string(), user_type: Type::Text, not_null: false, has_default: false },
|
||||
TableColumn { name: "age".to_string(), user_type: Type::Int, not_null: false, has_default: false },
|
||||
TableColumn {
|
||||
name: "id".to_string(),
|
||||
user_type: Type::Int,
|
||||
not_null: false,
|
||||
has_default: false,
|
||||
},
|
||||
TableColumn {
|
||||
name: "name".to_string(),
|
||||
user_type: Type::Text,
|
||||
not_null: false,
|
||||
has_default: false,
|
||||
},
|
||||
TableColumn {
|
||||
name: "age".to_string(),
|
||||
user_type: Type::Int,
|
||||
not_null: false,
|
||||
has_default: false,
|
||||
},
|
||||
],
|
||||
);
|
||||
s
|
||||
@@ -2108,10 +2069,7 @@ mod tests {
|
||||
assert_eq!(ctes.len(), 1);
|
||||
assert_eq!(ctes[0].columns.len(), 3);
|
||||
assert_eq!(ctes[0].columns[0].name.as_deref(), Some("id"));
|
||||
assert_eq!(
|
||||
ctes[0].columns[0].type_,
|
||||
Some(crate::dsl::types::Type::Int),
|
||||
);
|
||||
assert_eq!(ctes[0].columns[0].type_, Some(crate::dsl::types::Type::Int),);
|
||||
assert_eq!(ctes[0].columns[1].name.as_deref(), Some("name"));
|
||||
assert_eq!(
|
||||
ctes[0].columns[1].type_,
|
||||
@@ -2161,10 +2119,7 @@ mod tests {
|
||||
);
|
||||
assert_eq!(ctes[0].columns.len(), 1);
|
||||
assert_eq!(ctes[0].columns[0].name.as_deref(), Some("age"));
|
||||
assert_eq!(
|
||||
ctes[0].columns[0].type_,
|
||||
Some(crate::dsl::types::Type::Int),
|
||||
);
|
||||
assert_eq!(ctes[0].columns[0].type_, Some(crate::dsl::types::Type::Int),);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2259,15 +2214,9 @@ mod tests {
|
||||
.expect("outer_cte binding");
|
||||
assert_eq!(outer.columns.len(), 2);
|
||||
assert_eq!(outer.columns[0].name.as_deref(), Some("id"));
|
||||
assert_eq!(
|
||||
outer.columns[0].type_,
|
||||
Some(crate::dsl::types::Type::Int),
|
||||
);
|
||||
assert_eq!(outer.columns[0].type_, Some(crate::dsl::types::Type::Int),);
|
||||
assert_eq!(outer.columns[1].name.as_deref(), Some("name"));
|
||||
assert_eq!(
|
||||
outer.columns[1].type_,
|
||||
Some(crate::dsl::types::Type::Text),
|
||||
);
|
||||
assert_eq!(outer.columns[1].type_, Some(crate::dsl::types::Type::Text),);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2287,15 +2236,9 @@ mod tests {
|
||||
let b = ctes.iter().find(|c| c.name == "b").expect("b binding");
|
||||
assert_eq!(b.columns.len(), 2);
|
||||
assert_eq!(b.columns[0].name.as_deref(), Some("id"));
|
||||
assert_eq!(
|
||||
b.columns[0].type_,
|
||||
Some(crate::dsl::types::Type::Int),
|
||||
);
|
||||
assert_eq!(b.columns[0].type_, Some(crate::dsl::types::Type::Int),);
|
||||
assert_eq!(b.columns[1].name.as_deref(), Some("name"));
|
||||
assert_eq!(
|
||||
b.columns[1].type_,
|
||||
Some(crate::dsl::types::Type::Text),
|
||||
);
|
||||
assert_eq!(b.columns[1].type_, Some(crate::dsl::types::Type::Text),);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2310,10 +2253,7 @@ mod tests {
|
||||
);
|
||||
assert_eq!(ctes[0].columns.len(), 3);
|
||||
assert_eq!(ctes[0].columns[0].name.as_deref(), Some("a"));
|
||||
assert_eq!(
|
||||
ctes[0].columns[0].type_,
|
||||
Some(crate::dsl::types::Type::Int),
|
||||
);
|
||||
assert_eq!(ctes[0].columns[0].type_, Some(crate::dsl::types::Type::Int),);
|
||||
assert_eq!(ctes[0].columns[1].name.as_deref(), Some("b"));
|
||||
assert_eq!(
|
||||
ctes[0].columns[1].type_,
|
||||
|
||||
+41
-28
@@ -24,8 +24,8 @@
|
||||
use crate::dsl::grammar::HighlightClass;
|
||||
use crate::dsl::walker::context::WalkContext;
|
||||
use crate::dsl::walker::lex_helpers::{
|
||||
consume_bare_path, consume_flag, consume_ident, consume_number_literal,
|
||||
consume_string_literal, skip_whitespace,
|
||||
consume_bare_path, consume_flag, consume_ident, consume_number_literal, consume_string_literal,
|
||||
skip_whitespace,
|
||||
};
|
||||
use crate::dsl::walker::outcome::{ByteClass, WalkBound};
|
||||
|
||||
@@ -47,16 +47,11 @@ pub fn highlight_runs(source: &str) -> Vec<ByteClass> {
|
||||
/// token, producing the keyword classes the renderer needs to
|
||||
/// colour `select` / `from` / `where` / `union` / `case` / etc.
|
||||
#[must_use]
|
||||
pub fn highlight_runs_in_mode(
|
||||
source: &str,
|
||||
mode: crate::mode::Mode,
|
||||
) -> Vec<ByteClass> {
|
||||
pub fn highlight_runs_in_mode(source: &str, mode: crate::mode::Mode) -> Vec<ByteClass> {
|
||||
let mut ctx = WalkContext::new();
|
||||
ctx.mode = mode;
|
||||
let (result, _cmd) = super::walk(source, WalkBound::EndOfInput, &mut ctx);
|
||||
let mut classes: Vec<ByteClass> = result
|
||||
.map(|r| r.per_byte_class)
|
||||
.unwrap_or_default();
|
||||
let mut classes: Vec<ByteClass> = result.map(|r| r.per_byte_class).unwrap_or_default();
|
||||
|
||||
let scan_start = classes.last().map_or(0, |c| c.end);
|
||||
scan_remainder(source, scan_start, &mut classes);
|
||||
@@ -133,9 +128,7 @@ fn scan_remainder(source: &str, start: usize, classes: &mut Vec<ByteClass>) {
|
||||
.get(pos + 1)
|
||||
.copied()
|
||||
.is_some_and(|c| c.is_ascii_digit()));
|
||||
if looks_like_number
|
||||
&& let Some((s, e)) = consume_number_literal(source, pos)
|
||||
{
|
||||
if looks_like_number && let Some((s, e)) = consume_number_literal(source, pos) {
|
||||
classes.push(ByteClass {
|
||||
start: s,
|
||||
end: e,
|
||||
@@ -222,8 +215,14 @@ mod tests {
|
||||
"no Error highlight on a valid m:n line: {runs:?}"
|
||||
);
|
||||
let kinds: Vec<HighlightClass> = runs.iter().map(|(_, _, c)| *c).collect();
|
||||
assert!(kinds.contains(&HighlightClass::Keyword), "keywords highlighted: {runs:?}");
|
||||
assert!(kinds.contains(&HighlightClass::Identifier), "table names highlighted: {runs:?}");
|
||||
assert!(
|
||||
kinds.contains(&HighlightClass::Keyword),
|
||||
"keywords highlighted: {runs:?}"
|
||||
);
|
||||
assert!(
|
||||
kinds.contains(&HighlightClass::Identifier),
|
||||
"table names highlighted: {runs:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -276,10 +275,7 @@ mod tests {
|
||||
#[test]
|
||||
fn flag_classified_via_fallback() {
|
||||
// Walker doesn't engage for a bare `--all-rows`.
|
||||
assert_eq!(
|
||||
run("--all-rows"),
|
||||
vec![(0, 10, HighlightClass::Flag)],
|
||||
);
|
||||
assert_eq!(run("--all-rows"), vec![(0, 10, HighlightClass::Flag)],);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -445,15 +441,13 @@ mod tests {
|
||||
// dispatcher, so only the entry word would highlight).
|
||||
let runs = run_advanced("select * from t");
|
||||
assert!(
|
||||
runs.iter().any(|(s, e, c)| {
|
||||
*c == HighlightClass::Keyword && (*s, *e) == (0, 6)
|
||||
}),
|
||||
runs.iter()
|
||||
.any(|(s, e, c)| { *c == HighlightClass::Keyword && (*s, *e) == (0, 6) }),
|
||||
"expected `select` keyword span 0..6; got {runs:?}",
|
||||
);
|
||||
assert!(
|
||||
runs.iter().any(|(s, e, c)| {
|
||||
*c == HighlightClass::Keyword && (*s, *e) == (9, 13)
|
||||
}),
|
||||
runs.iter()
|
||||
.any(|(s, e, c)| { *c == HighlightClass::Keyword && (*s, *e) == (9, 13) }),
|
||||
"expected `from` keyword span 9..13; got {runs:?}",
|
||||
);
|
||||
}
|
||||
@@ -514,18 +508,37 @@ mod tests {
|
||||
let insert = keywords_of(
|
||||
"insert into t (a) values (1) on conflict (a) do update set a = excluded.a returning a",
|
||||
);
|
||||
for kw in ["insert", "into", "values", "on", "conflict", "do", "update", "set", "returning"] {
|
||||
assert!(insert.contains(&kw), "INSERT/UPSERT: missing `{kw}`; got {insert:?}");
|
||||
for kw in [
|
||||
"insert",
|
||||
"into",
|
||||
"values",
|
||||
"on",
|
||||
"conflict",
|
||||
"do",
|
||||
"update",
|
||||
"set",
|
||||
"returning",
|
||||
] {
|
||||
assert!(
|
||||
insert.contains(&kw),
|
||||
"INSERT/UPSERT: missing `{kw}`; got {insert:?}"
|
||||
);
|
||||
}
|
||||
|
||||
let update = keywords_of("update t set a = 1 where id = 2 returning a");
|
||||
for kw in ["update", "set", "where", "returning"] {
|
||||
assert!(update.contains(&kw), "UPDATE: missing `{kw}`; got {update:?}");
|
||||
assert!(
|
||||
update.contains(&kw),
|
||||
"UPDATE: missing `{kw}`; got {update:?}"
|
||||
);
|
||||
}
|
||||
|
||||
let delete = keywords_of("delete from t where id = 1 returning *");
|
||||
for kw in ["delete", "from", "where", "returning"] {
|
||||
assert!(delete.contains(&kw), "DELETE: missing `{kw}`; got {delete:?}");
|
||||
assert!(
|
||||
delete.contains(&kw),
|
||||
"DELETE: missing `{kw}`; got {delete:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,9 +110,7 @@ pub fn consume_number_literal(source: &str, start: usize) -> Option<(usize, usiz
|
||||
return None;
|
||||
}
|
||||
let mut i = start;
|
||||
let leading_minus = bytes[i] == b'-'
|
||||
&& i + 1 < bytes.len()
|
||||
&& bytes[i + 1].is_ascii_digit();
|
||||
let leading_minus = bytes[i] == b'-' && i + 1 < bytes.len() && bytes[i + 1].is_ascii_digit();
|
||||
if leading_minus {
|
||||
i += 1;
|
||||
}
|
||||
|
||||
+430
-559
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user