Skip to main content

squonk_ast/dialect/
mssql.rs

1// SPDX-License-Identifier: MIT
2// Copyright (c) 2026 Moderately AI Inc.
3
4//! The MSSQL / T-SQL dialect preset (ANSI-derived, deliberately conservative).
5//!
6//! Microsoft SQL Server's T-SQL diverges widely across its type, function, and statement
7//! surface, and — unlike the five shipped oracle-compared presets — this workspace has **no
8//! MSSQL oracle**, so over-acceptance cannot be measured. Conservatism is therefore the
9//! honesty bar: this preset derives from [`FeatureSet::ANSI`], the strict standard baseline,
10//! and enables only the T-SQL surface that already has a modelled, tested parser gate and
11//! clear documentary evidence — in six cases the flag's *own* doc names T-SQL as the
12//! motivating dialect. Every other axis keeps its ANSI value; a reader can predict from this
13//! module exactly what MSSQL accepts beyond the standard, and unsupported T-SQL syntax is a
14//! clean reject routed to a focused follow-up ticket, never a silent over-accept.
15//!
16//! # What this preset adds over ANSI
17//!
18//! Seven gates, each documented (in the flag's own doc) as T-SQL surface:
19//!
20//! - [`apply_join`](JoinSyntax::apply_join) — the `CROSS APPLY` / `OUTER APPLY`
21//!   lateral-correlated join operators. This is the flag this preset exists to make real: it
22//!   shipped staged (Lenient-only) before this preset gave it an engine home. The leading
23//!   `CROSS`/`OUTER` keyword anchors the operator, so no reserved-word interplay is needed
24//!   (the preceding factor's alias can never swallow it).
25//! - [`table_hints`](TableExpressionSyntax::table_hints) — the `WITH ( <hint>, … )` table-hint
26//!   tail (`FROM t WITH (NOLOCK)`, `WITH (INDEX(ix), FORCESEEK)`), a T-SQL locking / optimizer
27//!   directive on a table factor. Reached only through the `WITH (` sequence at the
28//!   table-factor tail, where `WITH` is never otherwise consumed on a base table, so it never
29//!   contends with the leading-`WITH` CTE clause at statement start; when off (ANSI base) the
30//!   trailing `WITH` is left unconsumed and the construct is a clean reject. The common
31//!   documented hints ([`TableHintKeyword`](crate::ast::TableHintKeyword)) are typed for
32//!   planner consumers; an unrecognized word is preserved verbatim
33//!   ([`TableHint::Other`](crate::ast::TableHint)). Numeric index ids (`INDEX(0)`) and
34//!   the legacy no-`WITH` parenthesized form (`FROM t (NOLOCK)`) are conservative deferrals.
35//! - [`table_version`](TableExpressionSyntax::table_version) — the temporal-table
36//!   `FOR SYSTEM_TIME {AS OF | FROM..TO | BETWEEN..AND | CONTAINED IN | ALL}` query modifier
37//!   on a table factor, a typed [`TableVersion`](crate::ast::TableVersion) for planner
38//!   consumers. It sits at the table-factor position (right after the table name, before the
39//!   alias), so its `FOR SYSTEM_TIME` trigger never contends with the query-level `FOR XML` /
40//!   `FOR JSON` tail or the `FOR` locking clause — those are read only after the whole
41//!   `FROM`/`WHERE`, and the word after `FOR` (`SYSTEM_TIME` vs `XML`) partitions them.
42//! - Bracket identifiers `[name]` — T-SQL's signature delimiter, modelled by the
43//!   [`IdentifierQuote::Asymmetric`] `{ open: '[', close: ']' }` style whose own doc cites
44//!   T-SQL. Listed in [`MSSQL_IDENTIFIER_QUOTES`] alongside the standard `"…"`. Enabling the
45//!   `[` opener would contend with the `[`-punctuation expression grammar
46//!   ([`ExpressionSyntax::subscript`] / [`array_constructor`](ExpressionSyntax::array_constructor)
47//!   / [`collection_literals`](ExpressionSyntax::collection_literals), the
48//!   [`LexicalConflict::BracketIdentifierVersusArraySyntax`](super::LexicalConflict) hazard),
49//!   but all three are **off** in the ANSI base this preset keeps — and that is
50//!   behaviour-accurate: T-SQL genuinely lacks `[]` array subscripting, so the bracket is
51//!   unambiguously an identifier delimiter here. The lexical-consistency `const` assert below
52//!   enforces the single claimant.
53//! - [`named_at`](ParameterSyntax::named_at) — the `@name` parameter / local-variable sigil,
54//!   whose own doc names T-SQL. Its `@name` trigger contends with
55//!   [`SessionVariableSyntax::user_variables`] (MySQL's `@name` read), which stays off (ANSI's
56//!   value) so `@name` has a single claimant — the
57//!   [`AtNameParameterVersusUserVariable`](super::LexicalConflict) hazard the assert below
58//!   rules out. The `@@name` system-variable form is disjoint (its second `@` is not an
59//!   identifier byte) and stays off too.
60//! - [`national_strings`](StringLiteralSyntax::national_strings) — `N'…'` national-character
61//!   string constants, whose own doc names T-SQL. A pure lexical gate over the ANSI string
62//!   surface; `backslash_escapes` stays off (its doc reads "MySQL default; not T-SQL").
63//! - [`money_literals`](NumericLiteralSyntax::money_literals) — `$1234.56` / `$.5` money
64//!   literals (the `$` currency sigil prefixes a decimal), whose own doc names T-SQL. Its
65//!   `$`+digit trigger contends with [`ParameterSyntax::positional_dollar`] (PostgreSQL's
66//!   `$1`), which stays off (ANSI's value) so `$`+digit has a single claimant — the
67//!   [`MoneyVersusPositionalDollar`](super::LexicalConflict) hazard the assert below rules
68//!   out.
69//!
70//! # The two lexical facts over ANSI
71//!
72//! - **Dual identifier quoting.** T-SQL quotes identifiers with the bracket `[…]` **and** the
73//!   standard `"…"` (its default `QUOTED_IDENTIFIER ON`). [`MSSQL_IDENTIFIER_QUOTES`] lists
74//!   both; T-SQL has no MySQL-style backtick, so — unlike SQLite/Databricks — no `` ` `` opener
75//!   appears. `double_quoted_strings` stays off (ANSI's value) so `"x"` reads as an
76//!   identifier, keeping `QUOTED_IDENTIFIER ON` behaviour and the preset lexically consistent.
77//! - **Case folding.** T-SQL identifier resolution is case-insensitive (collation-dependent),
78//!   so [`identifier_casing`](FeatureSet::identifier_casing) is [`Casing::Lower`] — the
79//!   [`Casing`] doc names T-SQL as one of the two dialects [`Casing::Lower`] approximates
80//!   ("case-preserving storage, case-insensitive comparison"). The interned text still renders
81//!   exactly as written; the fold is identity-only and never affects acceptance. (The
82//!   per-identifier-kind table-vs-column sensitivity split is a documented `Casing` limitation
83//!   and a deliberate future extension, not modelled here.)
84//!
85//! # Deliberately deferred (conservative reject)
86//!
87//! `SELECT … INTO <table>` stays off: [`select_into`](SelectSyntax::select_into)'s own doc
88//! models only PostgreSQL's `SELECT … INTO [TEMP] <table>` create-table form and does not cite
89//! T-SQL, and with no MSSQL oracle the exact boundary (T-SQL has no `TEMP` keyword — it spells
90//! temp tables `#name` instead) cannot be verified, so shipping the PG-shaped gate would be an
91//! unmeasured guess. Likewise `TOP (n)`, `GO` batch separators,
92//! `#temp` tables, the `OUTPUT` clause, and the T-SQL `MERGE` variants have no modelled gate;
93//! several are already catalogued by the structural-extensibility spike. All are clean rejects
94//! routed to follow-up tickets, never silent over-accepts.
95
96use super::{
97    AccessControlSyntax, AggregateCallSyntax, CallSyntax, CaretOperator, Casing,
98    ColumnDefinitionSyntax, CommentSyntax, ConstraintSyntax, CreateTableClauseSyntax,
99    DoubleAmpersand, ExistenceGuards, ExpressionSyntax, FeatureSet, GroupingSyntax,
100    IdentifierQuote, IdentifierSyntax, IndexAlterSyntax, JoinSyntax, KeywordOperators, KeywordSet,
101    MaintenanceSyntax, MutationSyntax, NullOrdering, NumericLiteralSyntax, OperatorSyntax,
102    ParameterSyntax, PipeOperator, PredicateSyntax, QueryTailSyntax, RESERVED_BARE_ALIAS,
103    RESERVED_COLUMN_NAME, RESERVED_FUNCTION_NAME, RESERVED_TYPE_NAME, STANDARD_BYTE_CLASSES,
104    SelectSyntax, SessionVariableSyntax, ShowSyntax, StatementDdlGates, StringFuncForms,
105    StringLiteralSyntax, TableExpressionSyntax, TableFactorSyntax, TargetSpelling,
106    TransactionSyntax, TypeNameSyntax, UtilitySyntax, ViewSequenceClauseSyntax,
107};
108use crate::precedence::{STANDARD_BINDING_POWERS, STANDARD_SET_OPERATION_BINDING_POWERS};
109
110/// MSSQL / T-SQL identifier quoting: the bracket `[…]` **and** the SQL standard `"…"`, both at
111/// once. The two openers are distinct bytes, so their order is immaterial. T-SQL has no
112/// MySQL-style backtick, so — unlike the SQLite/Lenient bracket sets — no `` ` `` opener
113/// appears. `"` stays a quote here (and `double_quoted_strings` is correspondingly off in
114/// [`StringLiteralSyntax::ANSI`], which this preset keeps, matching T-SQL's default
115/// `QUOTED_IDENTIFIER ON`), so `"x"` is an identifier, never a string.
116pub const MSSQL_IDENTIFIER_QUOTES: &[IdentifierQuote] = &[
117    IdentifierQuote::Symmetric('"'),
118    IdentifierQuote::Asymmetric {
119        open: '[',
120        close: ']',
121    },
122];
123
124impl StringLiteralSyntax {
125    /// MSSQL string surface: the ANSI baseline plus `N'…'` national-character string constants.
126    /// `backslash_escapes` stays off (its doc reads "MySQL default; not T-SQL"). Every other
127    /// string knob is conservatively ANSI.
128    pub const MSSQL: Self = Self {
129        national_strings: true,
130        escape_strings: false,
131        dollar_quoted_strings: false,
132        double_quoted_strings: false,
133        backslash_escapes: false,
134        unicode_strings: false,
135        bit_string_literals: false,
136        blob_literals: false,
137        charset_introducers: false,
138        same_line_adjacent_concat: false,
139    };
140}
141
142impl NumericLiteralSyntax {
143    /// MSSQL numeric surface: the ANSI baseline plus `$1234.56` / `$.5` money literals. Every
144    /// other numeric knob is conservatively ANSI.
145    pub const MSSQL: Self = Self {
146        money_literals: true,
147        hex_integers: false,
148        octal_integers: false,
149        binary_integers: false,
150        underscore_separators: false,
151        radix_leading_underscore: false,
152        reject_trailing_junk: false,
153    };
154}
155
156impl ParameterSyntax {
157    /// MSSQL parameter surface: the ANSI baseline plus the `@name` parameter / local-variable
158    /// sigil. `positional_dollar` stays off (ANSI's value) so `$`+digit belongs solely to
159    /// [`NumericLiteralSyntax::money_literals`], and `user_variables` stays off (in
160    /// [`SessionVariableSyntax::ANSI`]) so `@name` belongs solely to this — both enforced by
161    /// the lexical assert below.
162    pub const MSSQL: Self = Self {
163        named_at: true,
164        positional_dollar: false,
165        positional_dollar_large: false,
166        anonymous_question: false,
167        named_colon: false,
168        named_dollar: false,
169        numbered_question: false,
170    };
171}
172
173impl TableExpressionSyntax {
174    /// MSSQL table-expression surface: the ANSI baseline plus the `WITH (...)` table-hint
175    /// tail and the temporal-table `FOR SYSTEM_TIME` modifier (all five forms). The
176    /// `CROSS APPLY` / `OUTER APPLY` join operators ride [`JoinSyntax`]; every other table
177    /// knob is conservatively ANSI.
178    pub const MSSQL: Self = Self {
179        // `WITH (NOLOCK)` / `WITH (INDEX(ix), FORCESEEK)` table hints — see the module docs.
180        table_hints: true,
181        // `FOR SYSTEM_TIME {AS OF | FROM..TO | BETWEEN..AND | CONTAINED IN | ALL}` — the
182        // temporal-table query modifier.
183        table_version: true,
184        only: false,
185        table_sample: false,
186        parenthesized_joins: true,
187        table_alias_column_lists: true,
188        join_using_alias: false,
189        index_hints: false,
190        partition_selection: false,
191        base_table_alias_column_lists: true,
192        string_literal_aliases: false,
193        aliased_parenthesized_join: true,
194        bare_table_alias_is_bare_label: false,
195        table_json_path: false,
196        indexed_by: false,
197        prefix_colon_alias: false,
198    };
199}
200
201impl JoinSyntax {
202    /// The `MSSQL` preset for join syntax.
203    pub const MSSQL: Self = Self {
204        apply_join: true,
205        stacked_join_qualifiers: true,
206        full_outer_join: true,
207        natural_cross_join: false,
208        straight_join: false,
209        asof_join: false,
210        positional_join: false,
211        semi_anti_join: false,
212        sided_semi_anti_join: false,
213        recursive_search_cycle: false,
214        recursive_union_rejects_order_limit: false,
215        recursive_using_key: false,
216    };
217}
218
219impl QueryTailSyntax {
220    /// MSSQL query-tail surface: the ANSI baseline plus the `FOR XML`/`FOR JSON`
221    /// result-shaping tail. MSSQL has no query-tail row-locking clause (it spells
222    /// isolation with `WITH (…)` table hints, [`TableExpressionSyntax::MSSQL`]), so
223    /// `locking_clauses` stays off and the `FOR` lead is unambiguously the
224    /// result-shaping clause here.
225    pub const MSSQL: Self = Self {
226        for_xml_json_clause: true,
227        fetch_first: true,
228        limit_offset_comma: false,
229        locking_clauses: false,
230        key_lock_strengths: false,
231        stacked_locking_clauses: false,
232        using_sample: false,
233        leading_offset: true,
234        limit_expressions: true,
235        limit_percent: false,
236        with_ties_requires_order_by: false,
237        pipe_syntax: false,
238        limit_by_clause: false,
239        settings_clause: false,
240        format_clause: false,
241    };
242}
243
244impl TableFactorSyntax {
245    /// The `MSSQL` preset for table factor syntax.
246    pub const MSSQL: Self = Self {
247        // SQL Server's `OPENJSON(<json> [, <path>]) [WITH (…)]` rowset-function table factor —
248        // the sole engine with this exact form (documented; no differential oracle here, so the
249        // gate follows the grammar on this conservative preset, the `apply_join` precedent).
250        open_json: true,
251        lateral: false,
252        table_functions: false,
253        rows_from: false,
254        unnest: false,
255        unnest_with_offset: false,
256        table_function_ordinality: false,
257        special_function_table_source: true,
258        pivot: false,
259        unpivot: false,
260        show_ref: false,
261        from_values: false,
262        json_table: false,
263        xml_table: false,
264        table_expr_factor: false,
265        pivot_value_sources: false,
266        match_recognize: false,
267    };
268}
269
270impl FeatureSet {
271    /// MSSQL / T-SQL as ANSI-derived dialect data (see the module docs for the full derivation
272    /// rationale and the conservatism bar).
273    pub const MSSQL: Self = Self {
274        // T-SQL identifier resolution is case-insensitive (collation-dependent); `Casing::Lower`
275        // is the closest single fit (the `Casing` doc names T-SQL). Identity only — the interned
276        // text still renders exactly as written, so this never affects acceptance.
277        identifier_casing: Casing::Lower,
278        // The lexical delta over ANSI: bracket `[…]` *and* double-quote identifier quoting.
279        identifier_quotes: MSSQL_IDENTIFIER_QUOTES,
280        default_null_ordering: NullOrdering::NullsLast,
281        // No reserved-set delta over ANSI — MSSQL adds no keyword reservation here.
282        reserved_column_name: RESERVED_COLUMN_NAME,
283        reserved_function_name: RESERVED_FUNCTION_NAME,
284        reserved_type_name: RESERVED_TYPE_NAME,
285        reserved_bare_alias: RESERVED_BARE_ALIAS,
286        reserved_as_label: KeywordSet::EMPTY,
287        catalog_qualified_names: true,
288        byte_classes: STANDARD_BYTE_CLASSES,
289        binding_powers: STANDARD_BINDING_POWERS,
290        set_operation_powers: STANDARD_SET_OPERATION_BINDING_POWERS,
291        // `N'…'` national-character strings.
292        string_literals: StringLiteralSyntax::MSSQL,
293        // `$1234.56` money literals.
294        numeric_literals: NumericLiteralSyntax::MSSQL,
295        // `@name` parameters / local variables.
296        parameters: ParameterSyntax::MSSQL,
297        session_variables: SessionVariableSyntax::ANSI,
298        identifier_syntax: IdentifierSyntax::ANSI,
299        // `CROSS APPLY` / `OUTER APPLY` — the capstone this preset exposes.
300        table_expressions: TableExpressionSyntax::MSSQL,
301        join_syntax: JoinSyntax::MSSQL,
302        table_factor_syntax: TableFactorSyntax::MSSQL,
303        expression_syntax: ExpressionSyntax::ANSI,
304        operator_syntax: OperatorSyntax::ANSI,
305        call_syntax: CallSyntax::ANSI,
306        string_func_forms: StringFuncForms::ANSI,
307        aggregate_call_syntax: AggregateCallSyntax::ANSI,
308        predicate_syntax: PredicateSyntax::ANSI,
309        pipe_operator: PipeOperator::StringConcat,
310        double_ampersand: DoubleAmpersand::Unsupported,
311        keyword_operators: KeywordOperators::Unsupported,
312        caret_operator: CaretOperator::Unsupported,
313        hash_bitwise_xor: false,
314        comment_syntax: CommentSyntax::ANSI,
315        mutation_syntax: MutationSyntax::ANSI,
316        statement_ddl_gates: StatementDdlGates::ANSI,
317        view_sequence_clause_syntax: ViewSequenceClauseSyntax::ANSI,
318        create_table_clause_syntax: CreateTableClauseSyntax::ANSI,
319        column_definition_syntax: ColumnDefinitionSyntax::ANSI,
320        constraint_syntax: ConstraintSyntax::ANSI,
321        index_alter_syntax: IndexAlterSyntax::ANSI,
322        existence_guards: ExistenceGuards::ANSI,
323        // `SELECT … INTO <table>` stays off (deferred — see the module docs); every other
324        // SELECT knob is conservatively ANSI.
325        select_syntax: SelectSyntax::ANSI,
326        query_tail_syntax: QueryTailSyntax::MSSQL,
327        grouping_syntax: GroupingSyntax::ANSI,
328        utility_syntax: UtilitySyntax::ANSI,
329        transaction_syntax: TransactionSyntax::ANSI,
330        show_syntax: ShowSyntax::ANSI,
331        maintenance_syntax: MaintenanceSyntax::ANSI,
332        access_control_syntax: AccessControlSyntax::ANSI,
333        type_name_syntax: TypeNameSyntax::ANSI,
334        // No MSSQL-specific Tier-1 output spelling yet; render the portable ANSI canonical type
335        // names (a `TargetSpelling::Mssql` is render work a later ticket owns).
336        target_spelling: TargetSpelling::Ansi,
337    };
338}
339
340/// Prefer [`FeatureSet::MSSQL`] for struct update.
341pub const MSSQL: FeatureSet = FeatureSet::MSSQL;
342
343// Compile-time proof the MSSQL preset claims no shared tokenizer trigger twice. Beyond ANSI it
344// adds three triggers — the `[` bracket identifier opener, the `@` named-at sigil, and the `$`
345// money sigil — each with a single claimant: no enabled expression grammar lexes `[` as
346// punctuation (`subscript`/`array_constructor`/`collection_literals` stay off, so T-SQL's
347// bracket is unambiguously an identifier delimiter), `user_variables` stays off so `@name`
348// belongs solely to `named_at`, and `positional_dollar` stays off so `$`+digit belongs solely
349// to `money_literals`. `"` stays the sole identifier quote it already was (`double_quoted_strings`
350// off). Kept as a ratchet so a future MSSQL delta that *does* add a contending trigger fails the
351// build here.
352const _: () = assert!(FeatureSet::MSSQL.is_lexically_consistent());
353// The two sibling self-consistency registries are ratcheted the same way, so the
354// parse-entry `debug_assert!` folds all three to dead code for this preset: no refinement
355// flag rides an unset base, and no two features contend for one parser-position head.
356const _: () = assert!(FeatureSet::MSSQL.has_satisfied_feature_dependencies());
357const _: () = assert!(FeatureSet::MSSQL.has_no_grammar_conflict());
358
359#[cfg(test)]
360mod tests {
361    use super::*;
362
363    #[test]
364    fn mssql_is_ansi_plus_the_six_gates_and_two_lexical_facts() {
365        // The preset is ANSI with a documented, closed set of divergent axes: the two lexical
366        // facts (case-folding, dual identifier quoting) and the five enabled sub-presets that
367        // carry the six gates (the `join_syntax` sub-preset carries `apply_join`, and the
368        // `table_expressions` sub-preset carries `table_hints` and `table_version`). Asserting
369        // the whole rest equals ANSI keeps the "ANSI-derived, every delta documented" claim
370        // honest against a future stray edit. Bind to locals so the const reads are not flagged by
371        // clippy's `assertions_on_constants`.
372        let ansi = FeatureSet::ANSI;
373        let mssql = FeatureSet::MSSQL;
374
375        // The two lexical facts.
376        assert_eq!(mssql.identifier_casing, Casing::Lower);
377        assert_ne!(mssql.identifier_casing, ansi.identifier_casing);
378        assert_eq!(mssql.identifier_quotes, MSSQL_IDENTIFIER_QUOTES);
379        assert_ne!(mssql.identifier_quotes, ansi.identifier_quotes);
380        // `"` stays an identifier quote, so MSSQL must keep `double_quoted_strings` off (the
381        // `QUOTED_IDENTIFIER ON` behaviour the preset depends on for lexical consistency).
382        assert!(!mssql.string_literals.double_quoted_strings);
383        // The single-claimant interplays the enabled sigils depend on.
384        assert!(!mssql.session_variables.user_variables);
385        assert!(!mssql.parameters.positional_dollar);
386        // T-SQL has no backtick identifier quote.
387        assert!(
388            !mssql
389                .identifier_quotes
390                .iter()
391                .any(|quote| quote.open() == '`')
392        );
393
394        // The five divergent sub-presets.
395        assert_eq!(mssql.string_literals, StringLiteralSyntax::MSSQL);
396        assert_ne!(mssql.string_literals, ansi.string_literals);
397        assert_eq!(mssql.numeric_literals, NumericLiteralSyntax::MSSQL);
398        assert_ne!(mssql.numeric_literals, ansi.numeric_literals);
399        assert_eq!(mssql.parameters, ParameterSyntax::MSSQL);
400        assert_ne!(mssql.parameters, ansi.parameters);
401        assert_eq!(mssql.table_expressions, TableExpressionSyntax::MSSQL);
402        assert_ne!(mssql.table_expressions, ansi.table_expressions);
403
404        // No reserved-set delta: every position is inherited verbatim from ANSI.
405        assert_eq!(mssql.reserved_column_name, ansi.reserved_column_name);
406        assert_eq!(mssql.reserved_function_name, ansi.reserved_function_name);
407        assert_eq!(mssql.reserved_type_name, ansi.reserved_type_name);
408        assert_eq!(mssql.reserved_bare_alias, ansi.reserved_bare_alias);
409        assert_eq!(mssql.reserved_as_label, KeywordSet::EMPTY);
410
411        // Everything else is inherited verbatim from ANSI — including SELECT (SELECT INTO is
412        // deferred, so it stays off).
413        assert_eq!(mssql.select_syntax, ansi.select_syntax);
414        assert_eq!(mssql.expression_syntax, ansi.expression_syntax);
415        assert_eq!(mssql.session_variables, ansi.session_variables);
416        assert_eq!(mssql.identifier_syntax, ansi.identifier_syntax);
417        assert_eq!(mssql.operator_syntax, ansi.operator_syntax);
418        assert_eq!(mssql.call_syntax, ansi.call_syntax);
419        assert_eq!(mssql.predicate_syntax, ansi.predicate_syntax);
420        assert_eq!(mssql.mutation_syntax, ansi.mutation_syntax);
421        assert_eq!(mssql.statement_ddl_gates, ansi.statement_ddl_gates);
422        assert_eq!(
423            mssql.create_table_clause_syntax,
424            ansi.create_table_clause_syntax
425        );
426        assert_eq!(
427            mssql.column_definition_syntax,
428            ansi.column_definition_syntax
429        );
430        assert_eq!(mssql.constraint_syntax, ansi.constraint_syntax);
431        assert_eq!(mssql.index_alter_syntax, ansi.index_alter_syntax);
432        assert_eq!(mssql.existence_guards, ansi.existence_guards);
433        assert_eq!(mssql.utility_syntax, ansi.utility_syntax);
434        assert_eq!(mssql.type_name_syntax, ansi.type_name_syntax);
435        assert_eq!(mssql.byte_classes, ansi.byte_classes);
436        assert_eq!(mssql.binding_powers, ansi.binding_powers);
437        assert_eq!(mssql.target_spelling, ansi.target_spelling);
438        assert_eq!(mssql.default_null_ordering, ansi.default_null_ordering);
439    }
440
441    #[test]
442    fn mssql_enables_exactly_the_six_staged_gates() {
443        // The capstone: CROSS/OUTER APPLY, `WITH (...)` table hints, `FOR SYSTEM_TIME` table
444        // versioning, bracket identifiers, `@name` parameters, `N'…'` national strings, and
445        // `$…` money literals are on, and each is off in the ANSI base it derives from.
446        // Forcing the flags back off recovers the ANSI sub-presets verbatim.
447        let ansi = FeatureSet::ANSI;
448        let mssql = FeatureSet::MSSQL;
449
450        assert!(mssql.join_syntax.apply_join);
451        assert!(!ansi.join_syntax.apply_join);
452        assert!(mssql.table_expressions.table_hints);
453        assert!(!ansi.table_expressions.table_hints);
454        assert!(mssql.parameters.named_at && !ansi.parameters.named_at);
455        assert!(mssql.string_literals.national_strings && !ansi.string_literals.national_strings);
456        assert!(mssql.numeric_literals.money_literals && !ansi.numeric_literals.money_literals);
457        // The bracket opener is the sixth gate (an identifier-quote delta, not a bool).
458        assert!(
459            mssql
460                .identifier_quotes
461                .iter()
462                .any(|quote| quote.open() == '[')
463        );
464        assert!(
465            !ansi
466                .identifier_quotes
467                .iter()
468                .any(|quote| quote.open() == '[')
469        );
470
471        assert_eq!(
472            TableExpressionSyntax {
473                table_hints: false,
474                table_version: false,
475                ..mssql.table_expressions
476            },
477            ansi.table_expressions,
478        );
479        assert_eq!(
480            JoinSyntax {
481                apply_join: false,
482                ..mssql.join_syntax
483            },
484            ansi.join_syntax,
485        );
486        assert_eq!(
487            ParameterSyntax {
488                named_at: false,
489                ..mssql.parameters
490            },
491            ansi.parameters,
492        );
493        assert_eq!(
494            StringLiteralSyntax {
495                national_strings: false,
496                ..mssql.string_literals
497            },
498            ansi.string_literals,
499        );
500        assert_eq!(
501            NumericLiteralSyntax {
502                money_literals: false,
503                ..mssql.numeric_literals
504            },
505            ansi.numeric_literals,
506        );
507    }
508
509    #[test]
510    fn mssql_is_lexically_consistent_and_dependency_clean() {
511        // Both self-consistency registries must be clean: the `[` bracket quote, the `@` named-at
512        // sigil, and the `$` money sigil each have a single claimant (array/subscript grammar off,
513        // `user_variables` off, `positional_dollar` off, `double_quoted_strings` off), and none of
514        // the six enabled gates rides an unset base flag.
515        let mssql = FeatureSet::MSSQL;
516        assert_eq!(mssql.lexical_conflict(), None);
517        assert!(mssql.is_lexically_consistent());
518        assert_eq!(mssql.feature_dependencies(), None);
519        assert!(mssql.has_satisfied_feature_dependencies());
520        assert_eq!(mssql.grammar_conflict(), None);
521        assert!(mssql.has_no_grammar_conflict());
522    }
523    #[test]
524    fn mssql_closed_delta_axes_match_documented_set() {
525        crate::dialect::closed_delta::assert_closed_delta(
526            &FeatureSet::ANSI,
527            &FeatureSet::MSSQL,
528            &[
529                "identifier_casing",
530                "identifier_quotes",
531                "string_literals",
532                "numeric_literals",
533                "parameters",
534                "table_expressions",
535                "join_syntax",
536                "table_factor_syntax",
537                "query_tail_syntax",
538            ],
539        );
540    }
541}