use super::FeatureSet;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum HeadResolution {
MeceLookahead,
DispatchOrderUnion,
OneReadingExclusion,
Route,
}
#[derive(Clone, Copy, Debug)]
pub struct MultiClaimantHead {
pub heads: &'static [&'static str],
pub claimants: &'static [&'static str],
pub resolution: HeadResolution,
pub lenient_excludes: &'static [&'static str],
pub lenient_reading: &'static str,
pub doc: &'static str,
}
pub const MULTI_CLAIMANT_STATEMENT_HEADS: &[MultiClaimantHead] = &[
MultiClaimantHead {
heads: &["DO"],
claimants: &["do_statement", "do_expression_list"],
resolution: HeadResolution::OneReadingExclusion,
lenient_excludes: &["do_expression_list"],
lenient_reading: "PostgreSQL's `DO [LANGUAGE <lang>] $$…$$` anonymous code block; \
MySQL's `DO <expr-list>` collides on inputs like `DO 'x'` and cannot express the \
`LANGUAGE` clause, so it is forgone.",
doc: "squonk/src/parser/query.rs `DO` dispatch arms; \
util.rs `parse_do_statement` / `parse_do_expressions_statement`.",
},
MultiClaimantHead {
heads: &["PREPARE", "EXECUTE", "DEALLOCATE"],
claimants: &["prepared_statements", "prepared_statements_from"],
resolution: HeadResolution::OneReadingExclusion,
lenient_excludes: &["prepared_statements_from"],
lenient_reading: "DuckDB's typed-`AS` lifecycle (`PREPARE p AS <stmt>`, \
`EXECUTE name(<args>)`, `DEALLOCATE`); MySQL's `PREPARE … FROM` / \
`EXECUTE … USING @var` is a different grammar on the same three keywords with no \
positional-argument spelling, so it is forgone.",
doc: "squonk/src/parser/query.rs `PREPARE`/`EXECUTE`/`DEALLOCATE` dispatch arms; \
util.rs `parse_prepare_statement` / `parse_prepare_from_statement` / \
`parse_deallocate_statement`.",
},
MultiClaimantHead {
heads: &["COPY"],
claimants: &["copy", "copy_into"],
resolution: HeadResolution::MeceLookahead,
lenient_excludes: &[],
lenient_reading: "both kept — PostgreSQL's `COPY <table> {FROM|TO}` and Snowflake's `COPY INTO` split on the `INTO` lookahead: `COPY` routes to `copy` while `COPY INTO` routes to `copy_into`.",
doc: "squonk/src/parser/query.rs `COPY` dispatch and util.rs `parse_copy_or_copy_into_statement`.",
},
MultiClaimantHead {
heads: &["DESCRIBE", "DESC", "SUMMARIZE"],
claimants: &["describe", "describe_summarize"],
resolution: HeadResolution::DispatchOrderUnion,
lenient_excludes: &[],
lenient_reading: "both kept — `DESCRIBE`/`DESC` and `SUMMARIZE` remain exposed with Lenient's documented dispatch split by parser arm and shape.",
doc: "squonk/src/parser/query.rs `describe`/`describe_summarize` dispatch and from.rs `parse_describe_summarize_statement`.",
},
MultiClaimantHead {
heads: &["GRANT", "REVOKE"],
claimants: &[
"access_control_account_grants",
"access_control_extended_objects",
],
resolution: HeadResolution::Route,
lenient_excludes: &["access_control_account_grants"],
lenient_reading: "the extended standard/PostgreSQL object-and-role grammar (schema \
objects, `GRANTED BY`, `CASCADE`, routine signatures, the `{GRANT|ADMIN} OPTION \
FOR` REVOKE prefix); MySQL's account-based grant route structurally replaces (does \
not extend) it, so the route is forgone.",
doc: "squonk/src/parser/dcl.rs `parse_grant_kind` / `parse_revoke_kind` \
(the `access_control_account_grants` route branches to \
`parse_account_grant` / `parse_account_revoke`).",
},
MultiClaimantHead {
heads: &["LOCK", "UNLOCK"],
claimants: &["lock_tables", "lock_instance"],
resolution: HeadResolution::MeceLookahead,
lenient_excludes: &[],
lenient_reading: "both MySQL grammars — `LOCK/UNLOCK {TABLES|TABLE}` and `LOCK \
INSTANCE FOR BACKUP` / `UNLOCK INSTANCE` — kept, split on the second word. The \
(unimplemented) PostgreSQL statement-level mode-list reading of `LOCK` will take \
its own future gate, owing the same one-reading decision `DO` got; that gate does \
not exist yet, so nothing is resolved away today.",
doc: "squonk/src/parser/query.rs `LOCK`/`UNLOCK` dispatch arms \
(`peek_nth_starts_table_or_tables` vs `INSTANCE` second-word split).",
},
MultiClaimantHead {
heads: &["LOAD"],
claimants: &["load_data", "load_extension", "key_cache_statements"],
resolution: HeadResolution::MeceLookahead,
lenient_excludes: &[],
lenient_reading: "all three readings kept, split on the follow token: MySQL `LOAD \
{DATA|XML}` (second word), MySQL `LOAD INDEX INTO CACHE` (the `key_cache_statements` \
`LOAD INDEX` two-token lookahead), and PostgreSQL/DuckDB `LOAD <extension>` (the \
fall-through bare `LOAD`).",
doc: "squonk/src/parser/query.rs `LOAD DATA` / `LOAD INDEX` / `LOAD` dispatch arms \
(ordered so the two-word forms are checked before the bare `load_extension` arm); \
util.rs `parse_load_statement`.",
},
MultiClaimantHead {
heads: &["VACUUM"],
claimants: &["vacuum", "vacuum_analyze"],
resolution: HeadResolution::DispatchOrderUnion,
lenient_excludes: &[],
lenient_reading: "both tails kept; on the overlapping `VACUUM <name>` operand the \
DuckDB `vacuum_analyze` reading takes precedence (a *qualified* table name via \
`parse_object_name`) over the SQLite `vacuum` bare-schema `parse_ident`, and the \
SQLite `INTO <expr>` clause is still admitted afterwards. This is a one-reading \
precedence on the shared operand, NOT a pure addition.",
doc: "squonk/src/parser/util.rs `parse_vacuum_statement` (the `duck`-branch \
precedence on the name operand; see the in-function comment).",
},
MultiClaimantHead {
heads: &["ANALYZE"],
claimants: &["analyze", "table_maintenance"],
resolution: HeadResolution::MeceLookahead,
lenient_excludes: &[],
lenient_reading: "both kept — MySQL `ANALYZE {TABLE|TABLES} …` (the \
`table_maintenance` verb family, which always requires the `TABLE`/`TABLES` \
lookahead) and the SQLite/DuckDB bare `ANALYZE [<table> [(<cols>)]]`; a bare \
`ANALYZE` falls through to the SQLite/DuckDB reading.",
doc: "squonk/src/parser/query.rs `table_maintenance` arm placed before the bare \
`ANALYZE` arm (`peek_starts_table_maintenance` insists on `TABLE`/`TABLES`).",
},
MultiClaimantHead {
heads: &["ALTER", "VIEW"],
claimants: &["view_definition_options", "alter_object_set_schema"],
resolution: HeadResolution::MeceLookahead,
lenient_excludes: &[],
lenient_reading: "both kept — MySQL view redefinition (`ALGORITHM`/`SQL SECURITY` \
prefix, or a `(cols)` / `AS <query>` tail) vs DuckDB's `SET SCHEMA` relocation, \
split by lookahead on the bare `ALTER VIEW <name>` head (an `IF EXISTS` guard or a \
`SET SCHEMA` tail routes to the relocation; a `(`/`AS` tail to the redefinition).",
doc: "squonk/src/parser/ddl.rs `parse_alter` `view_definition_options` block \
(the `relocates` lookahead split when `alter_object_set_schema` is also on).",
},
MultiClaimantHead {
heads: &["ALTER", "DATABASE", "SCHEMA"],
claimants: &["alter_database", "alter_database_options"],
resolution: HeadResolution::MeceLookahead,
lenient_excludes: &[],
lenient_reading: "both kept — DuckDB's `SET ALIAS TO` relocation vs MySQL's option \
list, split by lookahead on the shared `ALTER {DATABASE|SCHEMA}` head (an `IF \
EXISTS` guard or a `SET` tail after the name is the relocation; an option keyword \
leading with no name, or a name followed by a non-`SET` tail, is the option list).",
doc: "squonk/src/parser/ddl.rs `parse_alter_database_head` (the both-on \
disambiguator).",
},
MultiClaimantHead {
heads: &["DROP"],
claimants: &["drop_database", "index_drop_on_table"],
resolution: HeadResolution::OneReadingExclusion,
lenient_excludes: &["drop_database", "index_drop_on_table"],
lenient_reading: "the shared name-list drop grammar kept for both sub-heads: `DROP \
{DATABASE|SCHEMA}` stays the PostgreSQL/DuckDB name-list-plus-`CASCADE` form \
(MySQL's single-name `drop_database` synonym would displace it), and `DROP INDEX \
<name>[, …]` stays the bare-name form (MySQL's mandatory-`ON` `index_drop_on_table` \
would displace it). Both MySQL displacements are forgone.",
doc: "squonk/src/parser/ddl.rs `parse_drop` dispatch (the `drop_database` and \
`index_drop_on_table` interceptors before `parse_drop_object_kind`).",
},
MultiClaimantHead {
heads: &["IMPORT"],
claimants: &["import_table", "export_import_database"],
resolution: HeadResolution::MeceLookahead,
lenient_excludes: &[],
lenient_reading: "both kept — MySQL `IMPORT TABLE FROM …` vs DuckDB `IMPORT DATABASE`, \
split on the second keyword (`TABLE` vs `DATABASE`), the `import_table` arm checked \
before the `export_import_database` arm.",
doc: "squonk/src/parser/query.rs `IMPORT TABLE` / `IMPORT` dispatch arms.",
},
];
pub type FeatureGatePredicate = fn(&FeatureSet) -> bool;
pub type NamedFeatureGate = (&'static str, FeatureGatePredicate);
#[derive(Clone, Copy, Debug)]
#[allow(dead_code)]
pub struct BaseVsFeatureStatementHead {
pub heads: &'static [&'static str],
pub feature: &'static str,
pub is_on: FeatureGatePredicate,
pub base_gate: Option<NamedFeatureGate>,
pub doc: &'static str,
}
#[allow(dead_code)]
pub const BASE_VS_FEATURE_STATEMENT_HEADS: &[BaseVsFeatureStatementHead] = &[
BaseVsFeatureStatementHead {
heads: &["SET"],
feature: "variable_assignment",
is_on: |f| f.session_variables.variable_assignment,
base_gate: Some(("show_syntax.session_statements", |f| {
f.show_syntax.session_statements
})),
doc: "squonk/src/parser/query.rs `session` dispatch to `parse_set`, and dcl.rs parse_set's `parse_mysql_set_variables` branch.",
},
BaseVsFeatureStatementHead {
heads: &["UPDATE"],
feature: "update_extensions",
is_on: |f| f.utility_syntax.update_extensions,
base_gate: None,
doc: "squonk/src/parser/query.rs `UPDATE` dispatch branch with `peek_starts_update_extensions` before `parse_update_statement_with`.",
},
BaseVsFeatureStatementHead {
heads: &["CACHE", "LOAD INDEX"],
feature: "key_cache_statements",
is_on: |f| f.utility_syntax.key_cache_statements,
base_gate: None,
doc: "squonk/src/parser/query.rs `CACHE` / `LOAD INDEX` dispatch arms, plus util.rs `parse_cache_index_statement` and `parse_load_index_statement`.",
},
];
#[cfg(test)]
mod tests {
use super::super::FeatureSet;
use super::*;
struct HeadGateValueRow {
flag: &'static str,
get: fn(&FeatureSet) -> bool,
expected: [bool; 5],
}
const T: bool = true;
const F: bool = false;
const CORE_PRESETS: [(&str, FeatureSet); 5] = [
("ANSI", FeatureSet::ANSI),
("POSTGRES", FeatureSet::POSTGRES),
("SQLITE", FeatureSet::SQLITE),
("MYSQL", FeatureSet::MYSQL),
("DUCKDB", FeatureSet::DUCKDB),
];
const STATEMENT_HEAD_GATE_VALUES: &[HeadGateValueRow] = &[
HeadGateValueRow {
flag: "do_statement",
get: |f| f.utility_syntax.do_statement,
expected: [F, T, F, F, F],
},
HeadGateValueRow {
flag: "do_expression_list",
get: |f| f.utility_syntax.do_expression_list,
expected: [F, F, F, T, F],
},
HeadGateValueRow {
flag: "prepared_statements",
get: |f| f.utility_syntax.prepared_statements,
expected: [F, T, F, F, T],
},
HeadGateValueRow {
flag: "prepared_statements_from",
get: |f| f.utility_syntax.prepared_statements_from,
expected: [F, F, F, T, F],
},
HeadGateValueRow {
flag: "access_control_account_grants",
get: |f| f.access_control_syntax.access_control_account_grants,
expected: [F, F, F, T, F],
},
HeadGateValueRow {
flag: "access_control_extended_objects",
get: |f| f.access_control_syntax.access_control_extended_objects,
expected: [T, T, F, F, T],
},
HeadGateValueRow {
flag: "copy",
get: |f| f.utility_syntax.copy,
expected: [F, T, F, F, T],
},
HeadGateValueRow {
flag: "copy_into",
get: |f| f.utility_syntax.copy_into,
expected: [F, F, F, F, F],
},
HeadGateValueRow {
flag: "describe",
get: |f| f.show_syntax.describe,
expected: [F, F, F, T, F],
},
HeadGateValueRow {
flag: "describe_summarize",
get: |f| f.show_syntax.describe_summarize,
expected: [F, F, F, F, T],
},
HeadGateValueRow {
flag: "variable_assignment",
get: |f| f.session_variables.variable_assignment,
expected: [F, F, F, T, F],
},
HeadGateValueRow {
flag: "lock_tables",
get: |f| f.utility_syntax.lock_tables,
expected: [F, F, F, T, F],
},
HeadGateValueRow {
flag: "lock_instance",
get: |f| f.utility_syntax.lock_instance,
expected: [F, F, F, T, F],
},
HeadGateValueRow {
flag: "load_data",
get: |f| f.utility_syntax.load_data,
expected: [F, F, F, T, F],
},
HeadGateValueRow {
flag: "load_extension",
get: |f| f.utility_syntax.load_extension,
expected: [F, T, F, F, T],
},
HeadGateValueRow {
flag: "key_cache_statements",
get: |f| f.utility_syntax.key_cache_statements,
expected: [F, F, F, T, F],
},
HeadGateValueRow {
flag: "vacuum",
get: |f| f.maintenance_syntax.vacuum,
expected: [F, F, T, F, F],
},
HeadGateValueRow {
flag: "vacuum_analyze",
get: |f| f.maintenance_syntax.vacuum_analyze,
expected: [F, F, F, F, T],
},
HeadGateValueRow {
flag: "analyze",
get: |f| f.maintenance_syntax.analyze,
expected: [F, F, T, F, T],
},
HeadGateValueRow {
flag: "table_maintenance",
get: |f| f.maintenance_syntax.table_maintenance,
expected: [F, F, F, T, F],
},
HeadGateValueRow {
flag: "update_extensions",
get: |f| f.utility_syntax.update_extensions,
expected: [F, F, F, F, T],
},
HeadGateValueRow {
flag: "view_definition_options",
get: |f| f.view_sequence_clause_syntax.view_definition_options,
expected: [F, F, F, T, F],
},
HeadGateValueRow {
flag: "alter_object_set_schema",
get: |f| f.statement_ddl_gates.alter_object_set_schema,
expected: [F, F, F, F, T],
},
HeadGateValueRow {
flag: "alter_database",
get: |f| f.statement_ddl_gates.alter_database,
expected: [F, F, F, F, T],
},
HeadGateValueRow {
flag: "alter_database_options",
get: |f| f.statement_ddl_gates.alter_database_options,
expected: [F, F, F, T, F],
},
HeadGateValueRow {
flag: "drop_database",
get: |f| f.statement_ddl_gates.drop_database,
expected: [F, F, F, T, F],
},
HeadGateValueRow {
flag: "index_drop_on_table",
get: |f| f.index_alter_syntax.index_drop_on_table,
expected: [F, F, F, T, F],
},
HeadGateValueRow {
flag: "import_table",
get: |f| f.utility_syntax.import_table,
expected: [F, F, F, T, F],
},
HeadGateValueRow {
flag: "export_import_database",
get: |f| f.utility_syntax.export_import_database,
expected: [F, F, F, F, T],
},
];
#[test]
fn statement_head_gate_values_are_pinned_per_preset() {
for row in STATEMENT_HEAD_GATE_VALUES {
for ((name, preset), &expected) in CORE_PRESETS.iter().zip(row.expected.iter()) {
assert_eq!(
(row.get)(preset),
expected,
"{}: expected {expected} in {name} — a statement-head gate value drifted \
(a base-preset flip may have silently propagated through a struct spread)",
row.flag,
);
}
}
}
#[test]
fn value_pinned_gates_cover_every_ledger_claimant() {
let mut from_ledger: Vec<&str> = MULTI_CLAIMANT_STATEMENT_HEADS
.iter()
.flat_map(|head| head.claimants.iter().copied())
.chain(
BASE_VS_FEATURE_STATEMENT_HEADS
.iter()
.map(|head| head.feature),
)
.collect();
from_ledger.sort_unstable();
from_ledger.dedup();
let mut valued: Vec<&str> = STATEMENT_HEAD_GATE_VALUES
.iter()
.map(|row| row.flag)
.collect();
valued.sort_unstable();
valued.dedup();
assert_eq!(
from_ledger, valued,
"the value pin must value exactly the ledger's claimant flags — add or remove a row in STATEMENT_HEAD_GATE_VALUES to match a ledger or base-vs-feature change",
);
}
#[test]
fn ledger_rows_are_internally_coherent() {
for head in MULTI_CLAIMANT_STATEMENT_HEADS {
match head.resolution {
HeadResolution::MeceLookahead | HeadResolution::DispatchOrderUnion => {
assert!(
head.lenient_excludes.is_empty(),
"{:?}: union kinds keep every claimant, so lenient_excludes must be empty",
head.heads,
);
}
HeadResolution::OneReadingExclusion | HeadResolution::Route => {
assert!(
!head.lenient_excludes.is_empty(),
"{:?}: exclusion kinds must forgo at least one claimant",
head.heads,
);
}
}
for excluded in head.lenient_excludes {
assert!(
head.claimants.contains(excluded),
"{:?}: excluded flag {excluded:?} is not among the row's claimants",
head.heads,
);
}
}
}
#[test]
fn multi_claimant_rows_are_purely_multi_claimant() {
assert!(
MULTI_CLAIMANT_STATEMENT_HEADS
.iter()
.all(|head| head.claimants.len() >= 2),
"single-claimant entries belong in BASE_VS_FEATURE_STATEMENT_HEADS, not the multi table",
);
}
#[test]
fn single_claimant_heads_are_registered_once() {
let mut from_base: Vec<&str> = BASE_VS_FEATURE_STATEMENT_HEADS
.iter()
.map(|row| row.feature)
.collect();
from_base.sort_unstable();
from_base.dedup();
let mut from_multi: Vec<&str> = MULTI_CLAIMANT_STATEMENT_HEADS
.iter()
.flat_map(|head| head.claimants.iter().copied())
.collect();
from_multi.sort_unstable();
from_multi.dedup();
let mut base_off_lenient: Vec<&str> = BASE_VS_FEATURE_STATEMENT_HEADS
.iter()
.filter(|row| !(row.is_on)(&FeatureSet::LENIENT))
.map(|row| row.feature)
.collect();
base_off_lenient.sort_unstable();
assert_eq!(
from_base,
[
"key_cache_statements",
"update_extensions",
"variable_assignment"
],
"unexpected base-vs-feature registration set",
);
assert!(
!from_multi.contains(&"variable_assignment"),
"`variable_assignment` should stay out of the multi table",
);
assert!(
!from_multi.contains(&"update_extensions"),
"`update_extensions` should stay out of the multi table",
);
assert!(
from_multi.contains(&"key_cache_statements"),
"`key_cache_statements` should remain in the multi table due `LOAD` sharing",
);
assert_eq!(
base_off_lenient,
["variable_assignment"],
"base-vs-feature lenient-off feature set changed; update LENIENT expectations if intentional",
);
}
#[cfg(feature = "lenient")]
#[test]
fn lenient_exclusions_match_the_ledger() {
let lenient = FeatureSet::LENIENT;
let stays_off: [(&str, bool); 5] = [
(
"do_expression_list",
lenient.utility_syntax.do_expression_list,
),
(
"prepared_statements_from",
lenient.utility_syntax.prepared_statements_from,
),
("drop_database", lenient.statement_ddl_gates.drop_database),
(
"index_drop_on_table",
lenient.index_alter_syntax.index_drop_on_table,
),
(
"access_control_account_grants",
lenient.access_control_syntax.access_control_account_grants,
),
];
for (name, is_on) in stays_off {
assert!(
!is_on,
"Lenient documents {name:?} as off, but the preset has it on"
);
}
let mut from_ledger: Vec<&str> = MULTI_CLAIMANT_STATEMENT_HEADS
.iter()
.flat_map(|head| head.lenient_excludes.iter().copied())
.collect();
from_ledger.sort_unstable();
let deduped = {
let mut d = from_ledger.clone();
d.dedup();
d
};
assert_eq!(
from_ledger, deduped,
"a flag is excluded by two ledger rows"
);
let mut expected: Vec<&str> = stays_off.iter().map(|(name, _)| *name).collect();
expected.sort_unstable();
assert_eq!(
from_ledger, expected,
"the ledger's exclusion columns must be exactly the statement-head flags Lenient turns off",
);
}
}