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}