squonk_ast/dialect/clickhouse.rs
1// SPDX-License-Identifier: MIT
2// Copyright (c) 2026 Moderately AI Inc.
3
4//! The ClickHouse dialect preset (ANSI-derived, deliberately conservative).
5//!
6//! ClickHouse is PostgreSQL-adjacent in its expression syntax but diverges widely
7//! across its statement, type, and function surface, and — unlike the five shipped
8//! oracle-compared presets — this workspace has **no ClickHouse oracle**, so
9//! over-acceptance cannot be measured. Conservatism is therefore the honesty bar: this
10//! preset derives from [`FeatureSet::ANSI`], the strict standard baseline, and enables
11//! only the ClickHouse surface that already has a modelled, tested parser gate. Every
12//! other axis keeps its ANSI value; a reader can predict from this module exactly what
13//! ClickHouse accepts beyond the standard, and unsupported ClickHouse syntax is a clean
14//! reject routed to a focused follow-up ticket, never a silent over-accept.
15//!
16//! # What this preset adds over ANSI
17//!
18//! Nine ClickHouse features, each staged (Lenient-only) behind its own parser gate
19//! and turned on here — the query tails [`limit_by_clause`](QueryTailSyntax::limit_by_clause),
20//! [`settings_clause`](QueryTailSyntax::settings_clause), and
21//! [`format_clause`](QueryTailSyntax::format_clause), and the type constructors
22//! [`nullable_type`](TypeNameSyntax::nullable_type),
23//! [`low_cardinality_type`](TypeNameSyntax::low_cardinality_type),
24//! [`fixed_string_type`](TypeNameSyntax::fixed_string_type),
25//! [`datetime64_type`](TypeNameSyntax::datetime64_type),
26//! [`nested_type`](TypeNameSyntax::nested_type), and
27//! [`bit_width_integer_names`](TypeNameSyntax::bit_width_integer_names) — plus two
28//! lexical facts with clear documentary evidence: ClickHouse quotes identifiers with
29//! **both** backticks and double quotes, and it is case-sensitive (no identity fold).
30//!
31//! All nine grammar gates are contextual — each type keyword is matched by spelling with
32//! a `(`-lookahead and each query tail by a contextual keyword at a clause boundary — so
33//! none reserves a word and none claims a new tokenizer trigger. The identifier-quote
34//! delta is the only lexical change, and `double_quoted_strings` stays off (ANSI's
35//! value) so `"…"` reads as an identifier, keeping the preset lexically consistent (the
36//! `const` assert below).
37
38use super::{
39 AccessControlSyntax, AggregateCallSyntax, CallSyntax, CaretOperator, Casing,
40 ColumnDefinitionSyntax, CommentSyntax, ConstraintSyntax, CreateTableClauseSyntax,
41 DoubleAmpersand, ExistenceGuards, ExpressionSyntax, FeatureSet, GroupingSyntax,
42 IdentifierQuote, IdentifierSyntax, IndexAlterSyntax, JoinSyntax, KeywordOperators, KeywordSet,
43 MaintenanceSyntax, MutationSyntax, NullOrdering, NumericLiteralSyntax, OperatorSyntax,
44 ParameterSyntax, PipeOperator, PredicateSyntax, QueryTailSyntax, RESERVED_BARE_ALIAS,
45 RESERVED_COLUMN_NAME, RESERVED_FUNCTION_NAME, RESERVED_TYPE_NAME, STANDARD_BYTE_CLASSES,
46 SelectSyntax, SessionVariableSyntax, ShowSyntax, StatementDdlGates, StringFuncForms,
47 StringLiteralSyntax, TableExpressionSyntax, TableFactorSyntax, TargetSpelling, TypeNameSyntax,
48 UtilitySyntax,
49};
50use crate::precedence::{STANDARD_BINDING_POWERS, STANDARD_SET_OPERATION_BINDING_POWERS};
51
52/// ClickHouse identifier quoting: the SQL-standard `"…"` **and** the MySQL-style
53/// backtick `` `…` ``, both at once. ClickHouse accepts either delimiter for a quoted
54/// identifier; the two openers are distinct bytes, so their order is immaterial. `"`
55/// stays a quote here (and `double_quoted_strings` is correspondingly off in
56/// [`StringLiteralSyntax::ANSI`], which this preset keeps), so `"x"` is an identifier,
57/// never a string.
58pub const CLICKHOUSE_IDENTIFIER_QUOTES: &[IdentifierQuote] = &[
59 IdentifierQuote::Symmetric('"'),
60 IdentifierQuote::Symmetric('`'),
61];
62
63impl SelectSyntax {
64 /// ClickHouse SELECT surface: the ANSI baseline plus the three ClickHouse query
65 /// tails, each a contextual clause at the query boundary that shadows no existing
66 /// reading (a plain `LIMIT n` still parses; `SETTINGS`/`FORMAT` are contextual, so
67 /// they divert only with the gate on). Every other SELECT knob is conservatively
68 /// ANSI: ClickHouse's further SELECT extensions (e.g. `WITH FILL`, `ARRAY JOIN`,
69 /// `SAMPLE`) have no modelled gate and are deferred to focused tickets.
70 pub const CLICKHOUSE: Self = Self {
71 ..SelectSyntax::ANSI
72 };
73}
74
75impl QueryTailSyntax {
76 /// The `CLICKHOUSE` preset for query tail syntax.
77 pub const CLICKHOUSE: Self = Self {
78 limit_by_clause: true,
79 settings_clause: true,
80 format_clause: true,
81 ..QueryTailSyntax::ANSI
82 };
83}
84
85impl GroupingSyntax {
86 /// The `CLICKHOUSE` preset for grouping syntax.
87 pub const CLICKHOUSE: Self = Self {
88 ..GroupingSyntax::ANSI
89 };
90}
91
92impl TypeNameSyntax {
93 /// ClickHouse type surface: the ANSI baseline plus the six ClickHouse type
94 /// constructors, each a contextual keyword + `(`-lookahead form (a bare `Nullable`
95 /// or `Int256` with no `(` stays an ordinary type/column name), so none reserves a
96 /// word. Every other type knob is conservatively ANSI: ClickHouse types with no
97 /// modelled gate (`Array(T)`, `Map(K, V)`, `Tuple(...)`, `Enum8`/`Enum16`,
98 /// `Decimal32`, `AggregateFunction`, …) are deferred to focused tickets.
99 pub const CLICKHOUSE: Self = Self {
100 nullable_type: true,
101 low_cardinality_type: true,
102 fixed_string_type: true,
103 datetime64_type: true,
104 nested_type: true,
105 bit_width_integer_names: true,
106 ..TypeNameSyntax::ANSI
107 };
108}
109
110impl FeatureSet {
111 /// ClickHouse as ANSI-derived dialect data (see the module docs for the full
112 /// derivation rationale and the conservatism bar).
113 pub const CLICKHOUSE: Self = Self {
114 // ClickHouse is case-sensitive: an unquoted identifier keeps its exact text, so
115 // this diverges from ANSI's upper-fold. Identity only — never affects acceptance.
116 identifier_casing: Casing::Preserve,
117 // The one lexical delta over ANSI: backtick *and* double-quote identifier quoting.
118 identifier_quotes: CLICKHOUSE_IDENTIFIER_QUOTES,
119 default_null_ordering: NullOrdering::NullsLast,
120 // The ANSI/PostgreSQL position-aware reserved model. ClickHouse's own reservation
121 // profile is unmodelled here (no oracle to fit it), so the conservative standard
122 // sets stand; the nine enabled gates are all contextual and reserve nothing.
123 reserved_column_name: RESERVED_COLUMN_NAME,
124 reserved_function_name: RESERVED_FUNCTION_NAME,
125 reserved_type_name: RESERVED_TYPE_NAME,
126 reserved_bare_alias: RESERVED_BARE_ALIAS,
127 reserved_as_label: KeywordSet::EMPTY,
128 catalog_qualified_names: true,
129 byte_classes: STANDARD_BYTE_CLASSES,
130 binding_powers: STANDARD_BINDING_POWERS,
131 set_operation_powers: STANDARD_SET_OPERATION_BINDING_POWERS,
132 // Conservative ANSI string/number/parameter surface: ClickHouse's own forms
133 // (backslash escapes, `0x` radix, `{name:Type}` parameters) have no modelled
134 // gate here and are deferred rather than guessed at without an oracle.
135 string_literals: StringLiteralSyntax::ANSI,
136 numeric_literals: NumericLiteralSyntax::ANSI,
137 parameters: ParameterSyntax::ANSI,
138 session_variables: SessionVariableSyntax::ANSI,
139 identifier_syntax: IdentifierSyntax::ANSI,
140 table_expressions: TableExpressionSyntax::ANSI,
141 join_syntax: JoinSyntax::ANSI,
142 table_factor_syntax: TableFactorSyntax::ANSI,
143 expression_syntax: ExpressionSyntax::ANSI,
144 operator_syntax: OperatorSyntax::ANSI,
145 call_syntax: CallSyntax::ANSI,
146 string_func_forms: StringFuncForms::ANSI,
147 aggregate_call_syntax: AggregateCallSyntax::ANSI,
148 predicate_syntax: PredicateSyntax::ANSI,
149 pipe_operator: PipeOperator::StringConcat,
150 double_ampersand: DoubleAmpersand::Unsupported,
151 keyword_operators: KeywordOperators::Unsupported,
152 caret_operator: CaretOperator::Unsupported,
153 hash_bitwise_xor: false,
154 comment_syntax: CommentSyntax::ANSI,
155 mutation_syntax: MutationSyntax::ANSI,
156 statement_ddl_gates: StatementDdlGates::ANSI,
157 create_table_clause_syntax: CreateTableClauseSyntax::ANSI,
158 column_definition_syntax: ColumnDefinitionSyntax::ANSI,
159 constraint_syntax: ConstraintSyntax::ANSI,
160 index_alter_syntax: IndexAlterSyntax::ANSI,
161 existence_guards: ExistenceGuards::ANSI,
162 // The two divergent sub-presets: the three query tails and the six type
163 // constructors (the capstone this preset exists to expose).
164 select_syntax: SelectSyntax::CLICKHOUSE,
165 query_tail_syntax: QueryTailSyntax::CLICKHOUSE,
166 grouping_syntax: GroupingSyntax::CLICKHOUSE,
167 utility_syntax: UtilitySyntax::ANSI,
168 show_syntax: ShowSyntax::ANSI,
169 maintenance_syntax: MaintenanceSyntax::ANSI,
170 access_control_syntax: AccessControlSyntax::ANSI,
171 type_name_syntax: TypeNameSyntax::CLICKHOUSE,
172 // No ClickHouse-specific Tier-1 output spelling yet; render the portable ANSI
173 // canonical type names (a `TargetSpelling::ClickHouse` is render work a later
174 // ticket owns).
175 target_spelling: TargetSpelling::Ansi,
176 };
177}
178
179/// Prefer [`FeatureSet::CLICKHOUSE`] for struct update.
180pub const CLICKHOUSE: FeatureSet = FeatureSet::CLICKHOUSE;
181
182// Compile-time proof the ClickHouse preset claims no shared tokenizer trigger twice.
183// Beyond ANSI's lexical surface it adds exactly one trigger — the backtick identifier
184// quote — which has a single claimant (no ClickHouse feature enabled here lexes a
185// backtick otherwise), and it keeps `double_quoted_strings` off so `"` stays the sole
186// identifier quote it already was. Every other delta is a contextual grammar gate with
187// no tokenizer trigger. Kept as a ratchet so a future ClickHouse delta that *does* add a
188// contending trigger fails the build here.
189const _: () = assert!(FeatureSet::CLICKHOUSE.is_lexically_consistent());
190// The two sibling self-consistency registries are ratcheted the same way, so the
191// parse-entry `debug_assert!` folds all three to dead code for this preset: no refinement
192// flag rides an unset base, and no two features contend for one parser-position head.
193const _: () = assert!(FeatureSet::CLICKHOUSE.has_satisfied_feature_dependencies());
194const _: () = assert!(FeatureSet::CLICKHOUSE.has_no_grammar_conflict());
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199
200 #[test]
201 fn clickhouse_is_ansi_plus_the_nine_gates_and_two_lexical_facts() {
202 // The preset is ANSI with a documented, closed set of divergent axes: the two
203 // lexical facts (case-sensitivity, dual identifier quoting) and the two enabled
204 // sub-presets (the query tails and the type constructors). Asserting the whole
205 // rest equals ANSI keeps the "ANSI-derived, every delta documented" claim honest
206 // against a future stray edit. Bind to locals so the const reads are not flagged
207 // by clippy's `assertions_on_constants`.
208 let ansi = FeatureSet::ANSI;
209 let ch = FeatureSet::CLICKHOUSE;
210
211 // The two lexical facts.
212 assert_eq!(ch.identifier_casing, Casing::Preserve);
213 assert_ne!(ch.identifier_casing, ansi.identifier_casing);
214 assert_eq!(ch.identifier_quotes, CLICKHOUSE_IDENTIFIER_QUOTES);
215 assert_ne!(ch.identifier_quotes, ansi.identifier_quotes);
216 // `"` stays an identifier quote, so ClickHouse must keep `double_quoted_strings`
217 // off (the interplay the preset depends on for lexical consistency).
218 assert!(!ch.string_literals.double_quoted_strings);
219
220 // The two divergent sub-presets.
221 assert_eq!(ch.query_tail_syntax, QueryTailSyntax::CLICKHOUSE);
222 assert_ne!(ch.query_tail_syntax, ansi.query_tail_syntax);
223 assert_eq!(ch.type_name_syntax, TypeNameSyntax::CLICKHOUSE);
224 assert_ne!(ch.type_name_syntax, ansi.type_name_syntax);
225
226 // Everything else is inherited verbatim from ANSI.
227 assert_eq!(ch.string_literals, ansi.string_literals);
228 assert_eq!(ch.numeric_literals, ansi.numeric_literals);
229 assert_eq!(ch.parameters, ansi.parameters);
230 assert_eq!(ch.session_variables, ansi.session_variables);
231 assert_eq!(ch.identifier_syntax, ansi.identifier_syntax);
232 assert_eq!(ch.table_expressions, ansi.table_expressions);
233 assert_eq!(ch.expression_syntax, ansi.expression_syntax);
234 assert_eq!(ch.operator_syntax, ansi.operator_syntax);
235 assert_eq!(ch.call_syntax, ansi.call_syntax);
236 assert_eq!(ch.predicate_syntax, ansi.predicate_syntax);
237 assert_eq!(ch.mutation_syntax, ansi.mutation_syntax);
238 assert_eq!(ch.statement_ddl_gates, ansi.statement_ddl_gates);
239 assert_eq!(
240 ch.create_table_clause_syntax,
241 ansi.create_table_clause_syntax
242 );
243 assert_eq!(ch.column_definition_syntax, ansi.column_definition_syntax);
244 assert_eq!(ch.constraint_syntax, ansi.constraint_syntax);
245 assert_eq!(ch.index_alter_syntax, ansi.index_alter_syntax);
246 assert_eq!(ch.existence_guards, ansi.existence_guards);
247 assert_eq!(ch.utility_syntax, ansi.utility_syntax);
248 assert_eq!(ch.reserved_column_name, ansi.reserved_column_name);
249 assert_eq!(ch.reserved_function_name, ansi.reserved_function_name);
250 assert_eq!(ch.reserved_type_name, ansi.reserved_type_name);
251 assert_eq!(ch.reserved_bare_alias, ansi.reserved_bare_alias);
252 assert_eq!(ch.byte_classes, ansi.byte_classes);
253 assert_eq!(ch.binding_powers, ansi.binding_powers);
254 assert_eq!(ch.target_spelling, ansi.target_spelling);
255 }
256
257 #[test]
258 fn clickhouse_enables_exactly_the_nine_staged_gates() {
259 // The capstone: the three query tails and the six type constructors are on, and
260 // each is off in the ANSI base it derives from — so the ClickHouse preset is the
261 // shipped home for the nine feature gates, not the Lenient tooling union alone.
262 let ansi = FeatureSet::ANSI;
263 let ch = FeatureSet::CLICKHOUSE;
264
265 assert!(ch.query_tail_syntax.limit_by_clause && !ansi.query_tail_syntax.limit_by_clause);
266 assert!(ch.query_tail_syntax.settings_clause && !ansi.query_tail_syntax.settings_clause);
267 assert!(ch.query_tail_syntax.format_clause && !ansi.query_tail_syntax.format_clause);
268
269 assert!(ch.type_name_syntax.nullable_type && !ansi.type_name_syntax.nullable_type);
270 assert!(
271 ch.type_name_syntax.low_cardinality_type && !ansi.type_name_syntax.low_cardinality_type
272 );
273 assert!(ch.type_name_syntax.fixed_string_type && !ansi.type_name_syntax.fixed_string_type);
274 assert!(ch.type_name_syntax.datetime64_type && !ansi.type_name_syntax.datetime64_type);
275 assert!(ch.type_name_syntax.nested_type && !ansi.type_name_syntax.nested_type);
276 assert!(
277 ch.type_name_syntax.bit_width_integer_names
278 && !ansi.type_name_syntax.bit_width_integer_names
279 );
280
281 // The enabled sub-presets are ANSI plus exactly those gates and nothing else —
282 // forcing the nine back off recovers the ANSI sub-presets verbatim.
283 assert_eq!(
284 QueryTailSyntax {
285 limit_by_clause: false,
286 settings_clause: false,
287 format_clause: false,
288 ..ch.query_tail_syntax
289 },
290 ansi.query_tail_syntax,
291 );
292 assert_eq!(
293 TypeNameSyntax {
294 nullable_type: false,
295 low_cardinality_type: false,
296 fixed_string_type: false,
297 datetime64_type: false,
298 nested_type: false,
299 bit_width_integer_names: false,
300 ..ch.type_name_syntax
301 },
302 ansi.type_name_syntax,
303 );
304 }
305
306 #[test]
307 fn clickhouse_is_lexically_consistent_and_dependency_clean() {
308 // Both self-consistency registries must be clean: the dual identifier quoting
309 // and off `double_quoted_strings` introduce no lexical conflict, and none of the
310 // nine contextual gates rides an unset base flag.
311 let ch = FeatureSet::CLICKHOUSE;
312 assert_eq!(ch.lexical_conflict(), None);
313 assert!(ch.is_lexically_consistent());
314 assert_eq!(ch.feature_dependencies(), None);
315 assert!(ch.has_satisfied_feature_dependencies());
316 assert_eq!(ch.grammar_conflict(), None);
317 assert!(ch.has_no_grammar_conflict());
318 }
319}