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, TypeNameSyntax,
106    UtilitySyntax,
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        ..StringLiteralSyntax::ANSI
131    };
132}
133
134impl NumericLiteralSyntax {
135    /// MSSQL numeric surface: the ANSI baseline plus `$1234.56` / `$.5` money literals. Every
136    /// other numeric knob is conservatively ANSI.
137    pub const MSSQL: Self = Self {
138        money_literals: true,
139        ..NumericLiteralSyntax::ANSI
140    };
141}
142
143impl ParameterSyntax {
144    /// MSSQL parameter surface: the ANSI baseline plus the `@name` parameter / local-variable
145    /// sigil. `positional_dollar` stays off (ANSI's value) so `$`+digit belongs solely to
146    /// [`NumericLiteralSyntax::money_literals`], and `user_variables` stays off (in
147    /// [`SessionVariableSyntax::ANSI`]) so `@name` belongs solely to this — both enforced by
148    /// the lexical assert below.
149    pub const MSSQL: Self = Self {
150        named_at: true,
151        ..ParameterSyntax::ANSI
152    };
153}
154
155impl TableExpressionSyntax {
156    /// MSSQL table-expression surface: the ANSI baseline plus the `WITH (...)` table-hint
157    /// tail and the temporal-table `FOR SYSTEM_TIME` modifier (all five forms). The
158    /// `CROSS APPLY` / `OUTER APPLY` join operators ride [`JoinSyntax`]; every other table
159    /// knob is conservatively ANSI.
160    pub const MSSQL: Self = Self {
161        // `WITH (NOLOCK)` / `WITH (INDEX(ix), FORCESEEK)` table hints — see the module docs.
162        table_hints: true,
163        // `FOR SYSTEM_TIME {AS OF | FROM..TO | BETWEEN..AND | CONTAINED IN | ALL}` — the
164        // temporal-table query modifier.
165        table_version: true,
166        ..TableExpressionSyntax::ANSI
167    };
168}
169
170impl JoinSyntax {
171    /// The `MSSQL` preset for join syntax.
172    pub const MSSQL: Self = Self {
173        apply_join: true,
174        ..JoinSyntax::ANSI
175    };
176}
177
178impl QueryTailSyntax {
179    /// MSSQL query-tail surface: the ANSI baseline plus the `FOR XML`/`FOR JSON`
180    /// result-shaping tail. MSSQL has no query-tail row-locking clause (it spells
181    /// isolation with `WITH (…)` table hints, [`TableExpressionSyntax::MSSQL`]), so
182    /// `locking_clauses` stays off and the `FOR` lead is unambiguously the
183    /// result-shaping clause here.
184    pub const MSSQL: Self = Self {
185        for_xml_json_clause: true,
186        ..QueryTailSyntax::ANSI
187    };
188}
189
190impl TableFactorSyntax {
191    /// The `MSSQL` preset for table factor syntax.
192    pub const MSSQL: Self = Self {
193        // SQL Server's `OPENJSON(<json> [, <path>]) [WITH (…)]` rowset-function table factor —
194        // the sole engine with this exact form (documented; no differential oracle here, so the
195        // gate follows the grammar on this conservative preset, the `apply_join` precedent).
196        open_json: true,
197        ..TableFactorSyntax::ANSI
198    };
199}
200
201impl FeatureSet {
202    /// MSSQL / T-SQL as ANSI-derived dialect data (see the module docs for the full derivation
203    /// rationale and the conservatism bar).
204    pub const MSSQL: Self = Self {
205        // T-SQL identifier resolution is case-insensitive (collation-dependent); `Casing::Lower`
206        // is the closest single fit (the `Casing` doc names T-SQL). Identity only — the interned
207        // text still renders exactly as written, so this never affects acceptance.
208        identifier_casing: Casing::Lower,
209        // The lexical delta over ANSI: bracket `[…]` *and* double-quote identifier quoting.
210        identifier_quotes: MSSQL_IDENTIFIER_QUOTES,
211        default_null_ordering: NullOrdering::NullsLast,
212        // No reserved-set delta over ANSI — MSSQL adds no keyword reservation here.
213        reserved_column_name: RESERVED_COLUMN_NAME,
214        reserved_function_name: RESERVED_FUNCTION_NAME,
215        reserved_type_name: RESERVED_TYPE_NAME,
216        reserved_bare_alias: RESERVED_BARE_ALIAS,
217        reserved_as_label: KeywordSet::EMPTY,
218        catalog_qualified_names: true,
219        byte_classes: STANDARD_BYTE_CLASSES,
220        binding_powers: STANDARD_BINDING_POWERS,
221        set_operation_powers: STANDARD_SET_OPERATION_BINDING_POWERS,
222        // `N'…'` national-character strings.
223        string_literals: StringLiteralSyntax::MSSQL,
224        // `$1234.56` money literals.
225        numeric_literals: NumericLiteralSyntax::MSSQL,
226        // `@name` parameters / local variables.
227        parameters: ParameterSyntax::MSSQL,
228        session_variables: SessionVariableSyntax::ANSI,
229        identifier_syntax: IdentifierSyntax::ANSI,
230        // `CROSS APPLY` / `OUTER APPLY` — the capstone this preset exposes.
231        table_expressions: TableExpressionSyntax::MSSQL,
232        join_syntax: JoinSyntax::MSSQL,
233        table_factor_syntax: TableFactorSyntax::MSSQL,
234        expression_syntax: ExpressionSyntax::ANSI,
235        operator_syntax: OperatorSyntax::ANSI,
236        call_syntax: CallSyntax::ANSI,
237        string_func_forms: StringFuncForms::ANSI,
238        aggregate_call_syntax: AggregateCallSyntax::ANSI,
239        predicate_syntax: PredicateSyntax::ANSI,
240        pipe_operator: PipeOperator::StringConcat,
241        double_ampersand: DoubleAmpersand::Unsupported,
242        keyword_operators: KeywordOperators::Unsupported,
243        caret_operator: CaretOperator::Unsupported,
244        hash_bitwise_xor: false,
245        comment_syntax: CommentSyntax::ANSI,
246        mutation_syntax: MutationSyntax::ANSI,
247        statement_ddl_gates: StatementDdlGates::ANSI,
248        create_table_clause_syntax: CreateTableClauseSyntax::ANSI,
249        column_definition_syntax: ColumnDefinitionSyntax::ANSI,
250        constraint_syntax: ConstraintSyntax::ANSI,
251        index_alter_syntax: IndexAlterSyntax::ANSI,
252        existence_guards: ExistenceGuards::ANSI,
253        // `SELECT … INTO <table>` stays off (deferred — see the module docs); every other
254        // SELECT knob is conservatively ANSI.
255        select_syntax: SelectSyntax::ANSI,
256        query_tail_syntax: QueryTailSyntax::MSSQL,
257        grouping_syntax: GroupingSyntax::ANSI,
258        utility_syntax: UtilitySyntax::ANSI,
259        show_syntax: ShowSyntax::ANSI,
260        maintenance_syntax: MaintenanceSyntax::ANSI,
261        access_control_syntax: AccessControlSyntax::ANSI,
262        type_name_syntax: TypeNameSyntax::ANSI,
263        // No MSSQL-specific Tier-1 output spelling yet; render the portable ANSI canonical type
264        // names (a `TargetSpelling::Mssql` is render work a later ticket owns).
265        target_spelling: TargetSpelling::Ansi,
266    };
267}
268
269/// Prefer [`FeatureSet::MSSQL`] for struct update.
270pub const MSSQL: FeatureSet = FeatureSet::MSSQL;
271
272// Compile-time proof the MSSQL preset claims no shared tokenizer trigger twice. Beyond ANSI it
273// adds three triggers — the `[` bracket identifier opener, the `@` named-at sigil, and the `$`
274// money sigil — each with a single claimant: no enabled expression grammar lexes `[` as
275// punctuation (`subscript`/`array_constructor`/`collection_literals` stay off, so T-SQL's
276// bracket is unambiguously an identifier delimiter), `user_variables` stays off so `@name`
277// belongs solely to `named_at`, and `positional_dollar` stays off so `$`+digit belongs solely
278// to `money_literals`. `"` stays the sole identifier quote it already was (`double_quoted_strings`
279// off). Kept as a ratchet so a future MSSQL delta that *does* add a contending trigger fails the
280// build here.
281const _: () = assert!(FeatureSet::MSSQL.is_lexically_consistent());
282// The two sibling self-consistency registries are ratcheted the same way, so the
283// parse-entry `debug_assert!` folds all three to dead code for this preset: no refinement
284// flag rides an unset base, and no two features contend for one parser-position head.
285const _: () = assert!(FeatureSet::MSSQL.has_satisfied_feature_dependencies());
286const _: () = assert!(FeatureSet::MSSQL.has_no_grammar_conflict());
287
288#[cfg(test)]
289mod tests {
290    use super::*;
291
292    #[test]
293    fn mssql_is_ansi_plus_the_six_gates_and_two_lexical_facts() {
294        // The preset is ANSI with a documented, closed set of divergent axes: the two lexical
295        // facts (case-folding, dual identifier quoting) and the four enabled sub-presets that
296        // carry the six gates (the `table_expressions` sub-preset carries two — `apply_join`
297        // and `table_hints`). Asserting the whole rest equals ANSI keeps the "ANSI-derived,
298        // every delta documented" claim honest against a future stray edit. Bind to locals so
299        // the const reads are not flagged by clippy's `assertions_on_constants`.
300        let ansi = FeatureSet::ANSI;
301        let mssql = FeatureSet::MSSQL;
302
303        // The two lexical facts.
304        assert_eq!(mssql.identifier_casing, Casing::Lower);
305        assert_ne!(mssql.identifier_casing, ansi.identifier_casing);
306        assert_eq!(mssql.identifier_quotes, MSSQL_IDENTIFIER_QUOTES);
307        assert_ne!(mssql.identifier_quotes, ansi.identifier_quotes);
308        // `"` stays an identifier quote, so MSSQL must keep `double_quoted_strings` off (the
309        // `QUOTED_IDENTIFIER ON` behaviour the preset depends on for lexical consistency).
310        assert!(!mssql.string_literals.double_quoted_strings);
311        // The single-claimant interplays the enabled sigils depend on.
312        assert!(!mssql.session_variables.user_variables);
313        assert!(!mssql.parameters.positional_dollar);
314        // T-SQL has no backtick identifier quote.
315        assert!(
316            !mssql
317                .identifier_quotes
318                .iter()
319                .any(|quote| quote.open() == '`')
320        );
321
322        // The four divergent sub-presets.
323        assert_eq!(mssql.string_literals, StringLiteralSyntax::MSSQL);
324        assert_ne!(mssql.string_literals, ansi.string_literals);
325        assert_eq!(mssql.numeric_literals, NumericLiteralSyntax::MSSQL);
326        assert_ne!(mssql.numeric_literals, ansi.numeric_literals);
327        assert_eq!(mssql.parameters, ParameterSyntax::MSSQL);
328        assert_ne!(mssql.parameters, ansi.parameters);
329        assert_eq!(mssql.table_expressions, TableExpressionSyntax::MSSQL);
330        assert_ne!(mssql.table_expressions, ansi.table_expressions);
331
332        // No reserved-set delta: every position is inherited verbatim from ANSI.
333        assert_eq!(mssql.reserved_column_name, ansi.reserved_column_name);
334        assert_eq!(mssql.reserved_function_name, ansi.reserved_function_name);
335        assert_eq!(mssql.reserved_type_name, ansi.reserved_type_name);
336        assert_eq!(mssql.reserved_bare_alias, ansi.reserved_bare_alias);
337        assert_eq!(mssql.reserved_as_label, KeywordSet::EMPTY);
338
339        // Everything else is inherited verbatim from ANSI — including SELECT (SELECT INTO is
340        // deferred, so it stays off).
341        assert_eq!(mssql.select_syntax, ansi.select_syntax);
342        assert_eq!(mssql.expression_syntax, ansi.expression_syntax);
343        assert_eq!(mssql.session_variables, ansi.session_variables);
344        assert_eq!(mssql.identifier_syntax, ansi.identifier_syntax);
345        assert_eq!(mssql.operator_syntax, ansi.operator_syntax);
346        assert_eq!(mssql.call_syntax, ansi.call_syntax);
347        assert_eq!(mssql.predicate_syntax, ansi.predicate_syntax);
348        assert_eq!(mssql.mutation_syntax, ansi.mutation_syntax);
349        assert_eq!(mssql.statement_ddl_gates, ansi.statement_ddl_gates);
350        assert_eq!(
351            mssql.create_table_clause_syntax,
352            ansi.create_table_clause_syntax
353        );
354        assert_eq!(
355            mssql.column_definition_syntax,
356            ansi.column_definition_syntax
357        );
358        assert_eq!(mssql.constraint_syntax, ansi.constraint_syntax);
359        assert_eq!(mssql.index_alter_syntax, ansi.index_alter_syntax);
360        assert_eq!(mssql.existence_guards, ansi.existence_guards);
361        assert_eq!(mssql.utility_syntax, ansi.utility_syntax);
362        assert_eq!(mssql.type_name_syntax, ansi.type_name_syntax);
363        assert_eq!(mssql.byte_classes, ansi.byte_classes);
364        assert_eq!(mssql.binding_powers, ansi.binding_powers);
365        assert_eq!(mssql.target_spelling, ansi.target_spelling);
366        assert_eq!(mssql.default_null_ordering, ansi.default_null_ordering);
367    }
368
369    #[test]
370    fn mssql_enables_exactly_the_six_staged_gates() {
371        // The capstone: CROSS/OUTER APPLY, `WITH (...)` table hints, bracket identifiers,
372        // `@name` parameters, `N'…'` national strings, and `$…` money literals are on, and each
373        // is off in the ANSI base it derives from. Forcing the flags back off recovers the ANSI
374        // sub-presets verbatim.
375        let ansi = FeatureSet::ANSI;
376        let mssql = FeatureSet::MSSQL;
377
378        assert!(mssql.join_syntax.apply_join);
379        assert!(!ansi.join_syntax.apply_join);
380        assert!(mssql.table_expressions.table_hints);
381        assert!(!ansi.table_expressions.table_hints);
382        assert!(mssql.parameters.named_at && !ansi.parameters.named_at);
383        assert!(mssql.string_literals.national_strings && !ansi.string_literals.national_strings);
384        assert!(mssql.numeric_literals.money_literals && !ansi.numeric_literals.money_literals);
385        // The bracket opener is the fifth gate (an identifier-quote delta, not a bool).
386        assert!(
387            mssql
388                .identifier_quotes
389                .iter()
390                .any(|quote| quote.open() == '[')
391        );
392        assert!(
393            !ansi
394                .identifier_quotes
395                .iter()
396                .any(|quote| quote.open() == '[')
397        );
398
399        assert_eq!(
400            TableExpressionSyntax {
401                table_hints: false,
402                table_version: false,
403                ..mssql.table_expressions
404            },
405            ansi.table_expressions,
406        );
407        assert_eq!(
408            JoinSyntax {
409                apply_join: false,
410                ..mssql.join_syntax
411            },
412            ansi.join_syntax,
413        );
414        assert_eq!(
415            ParameterSyntax {
416                named_at: false,
417                ..mssql.parameters
418            },
419            ansi.parameters,
420        );
421        assert_eq!(
422            StringLiteralSyntax {
423                national_strings: false,
424                ..mssql.string_literals
425            },
426            ansi.string_literals,
427        );
428        assert_eq!(
429            NumericLiteralSyntax {
430                money_literals: false,
431                ..mssql.numeric_literals
432            },
433            ansi.numeric_literals,
434        );
435    }
436
437    #[test]
438    fn mssql_is_lexically_consistent_and_dependency_clean() {
439        // Both self-consistency registries must be clean: the `[` bracket quote, the `@` named-at
440        // sigil, and the `$` money sigil each have a single claimant (array/subscript grammar off,
441        // `user_variables` off, `positional_dollar` off, `double_quoted_strings` off), and none of
442        // the five enabled gates rides an unset base flag.
443        let mssql = FeatureSet::MSSQL;
444        assert_eq!(mssql.lexical_conflict(), None);
445        assert!(mssql.is_lexically_consistent());
446        assert_eq!(mssql.feature_dependencies(), None);
447        assert!(mssql.has_satisfied_feature_dependencies());
448        assert_eq!(mssql.grammar_conflict(), None);
449        assert!(mssql.has_no_grammar_conflict());
450    }
451}