Skip to main content

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}