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,
48    TransactionSyntax, TypeNameSyntax, UtilitySyntax, ViewSequenceClauseSyntax,
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        distinct_on: false,
72        select_into: false,
73        empty_target_list: false,
74        qualify: false,
75        alias_string_literals: false,
76        bare_alias_string_literals: false,
77        union_by_name: false,
78        wildcard_modifiers: false,
79        wildcard_replace: false,
80        intersect_all: true,
81        except_all: true,
82        qualified_wildcard_alias: false,
83        from_first: false,
84        explicit_table: true,
85        parenthesized_query_operands: true,
86        values_rows_require_equal_arity: false,
87        values_row_constructor: true,
88        as_alias_rejects_reserved: false,
89        trailing_comma: false,
90        prefix_colon_alias: false,
91        lateral_view_clause: false,
92        connect_by_clause: false,
93    };
94}
95
96impl QueryTailSyntax {
97    /// The `CLICKHOUSE` preset for query tail syntax.
98    pub const CLICKHOUSE: Self = Self {
99        limit_by_clause: true,
100        settings_clause: true,
101        format_clause: true,
102        fetch_first: true,
103        limit_offset_comma: false,
104        locking_clauses: false,
105        key_lock_strengths: false,
106        stacked_locking_clauses: false,
107        using_sample: false,
108        leading_offset: true,
109        limit_expressions: true,
110        limit_percent: false,
111        with_ties_requires_order_by: false,
112        pipe_syntax: false,
113        for_xml_json_clause: false,
114    };
115}
116
117impl GroupingSyntax {
118    /// The `CLICKHOUSE` preset for grouping syntax.
119    pub const CLICKHOUSE: Self = Self {
120        grouping_sets: true,
121        with_rollup: false,
122        order_by_using: false,
123        group_by_all: false,
124        group_by_set_quantifier: false,
125        order_by_all: false,
126    };
127}
128
129impl TypeNameSyntax {
130    /// ClickHouse type surface: the ANSI baseline plus the six ClickHouse type
131    /// constructors, each a contextual keyword + `(`-lookahead form (a bare `Nullable`
132    /// or `Int256` with no `(` stays an ordinary type/column name), so none reserves a
133    /// word. Every other type knob is conservatively ANSI: ClickHouse types with no
134    /// modelled gate (`Array(T)`, `Map(K, V)`, `Tuple(...)`, `Enum8`/`Enum16`,
135    /// `Decimal32`, `AggregateFunction`, …) are deferred to focused tickets.
136    pub const CLICKHOUSE: Self = Self {
137        nullable_type: true,
138        low_cardinality_type: true,
139        fixed_string_type: true,
140        datetime64_type: true,
141        nested_type: true,
142        bit_width_integer_names: true,
143        extended_scalar_type_names: false,
144        enum_type: false,
145        set_type: false,
146        numeric_modifiers: false,
147        integer_display_width: false,
148        composite_types: false,
149        varchar_requires_length: false,
150        zoned_temporal_types: true,
151        empty_type_parens: false,
152        character_set_annotation: false,
153        signed_type_modifier: false,
154        liberal_type_names: false,
155        string_type_modifiers: false,
156        angle_bracket_types: false,
157    };
158}
159
160impl FeatureSet {
161    /// ClickHouse as ANSI-derived dialect data (see the module docs for the full
162    /// derivation rationale and the conservatism bar).
163    pub const CLICKHOUSE: Self = Self {
164        // ClickHouse is case-sensitive: an unquoted identifier keeps its exact text, so
165        // this diverges from ANSI's upper-fold. Identity only — never affects acceptance.
166        identifier_casing: Casing::Preserve,
167        // The one lexical delta over ANSI: backtick *and* double-quote identifier quoting.
168        identifier_quotes: CLICKHOUSE_IDENTIFIER_QUOTES,
169        default_null_ordering: NullOrdering::NullsLast,
170        // The ANSI/PostgreSQL position-aware reserved model. ClickHouse's own reservation
171        // profile is unmodelled here (no oracle to fit it), so the conservative standard
172        // sets stand; the nine enabled gates are all contextual and reserve nothing.
173        reserved_column_name: RESERVED_COLUMN_NAME,
174        reserved_function_name: RESERVED_FUNCTION_NAME,
175        reserved_type_name: RESERVED_TYPE_NAME,
176        reserved_bare_alias: RESERVED_BARE_ALIAS,
177        reserved_as_label: KeywordSet::EMPTY,
178        catalog_qualified_names: true,
179        byte_classes: STANDARD_BYTE_CLASSES,
180        binding_powers: STANDARD_BINDING_POWERS,
181        set_operation_powers: STANDARD_SET_OPERATION_BINDING_POWERS,
182        // Conservative ANSI string/number/parameter surface: ClickHouse's own forms
183        // (backslash escapes, `0x` radix, `{name:Type}` parameters) have no modelled
184        // gate here and are deferred rather than guessed at without an oracle.
185        string_literals: StringLiteralSyntax::ANSI,
186        numeric_literals: NumericLiteralSyntax::ANSI,
187        parameters: ParameterSyntax::ANSI,
188        session_variables: SessionVariableSyntax::ANSI,
189        identifier_syntax: IdentifierSyntax::ANSI,
190        table_expressions: TableExpressionSyntax::ANSI,
191        join_syntax: JoinSyntax::ANSI,
192        table_factor_syntax: TableFactorSyntax::ANSI,
193        expression_syntax: ExpressionSyntax::ANSI,
194        operator_syntax: OperatorSyntax::ANSI,
195        call_syntax: CallSyntax::ANSI,
196        string_func_forms: StringFuncForms::ANSI,
197        aggregate_call_syntax: AggregateCallSyntax::ANSI,
198        predicate_syntax: PredicateSyntax::ANSI,
199        pipe_operator: PipeOperator::StringConcat,
200        double_ampersand: DoubleAmpersand::Unsupported,
201        keyword_operators: KeywordOperators::Unsupported,
202        caret_operator: CaretOperator::Unsupported,
203        hash_bitwise_xor: false,
204        comment_syntax: CommentSyntax::ANSI,
205        mutation_syntax: MutationSyntax::ANSI,
206        statement_ddl_gates: StatementDdlGates::ANSI,
207        view_sequence_clause_syntax: ViewSequenceClauseSyntax::ANSI,
208        create_table_clause_syntax: CreateTableClauseSyntax::ANSI,
209        column_definition_syntax: ColumnDefinitionSyntax::ANSI,
210        constraint_syntax: ConstraintSyntax::ANSI,
211        index_alter_syntax: IndexAlterSyntax::ANSI,
212        existence_guards: ExistenceGuards::ANSI,
213        // The two divergent sub-presets: the three query tails and the six type
214        // constructors (the capstone this preset exists to expose).
215        select_syntax: SelectSyntax::CLICKHOUSE,
216        query_tail_syntax: QueryTailSyntax::CLICKHOUSE,
217        grouping_syntax: GroupingSyntax::CLICKHOUSE,
218        utility_syntax: UtilitySyntax::ANSI,
219        transaction_syntax: TransactionSyntax::ANSI,
220        show_syntax: ShowSyntax::ANSI,
221        maintenance_syntax: MaintenanceSyntax::ANSI,
222        access_control_syntax: AccessControlSyntax::ANSI,
223        type_name_syntax: TypeNameSyntax::CLICKHOUSE,
224        // No ClickHouse-specific Tier-1 output spelling yet; render the portable ANSI
225        // canonical type names (a `TargetSpelling::ClickHouse` is render work a later
226        // ticket owns).
227        target_spelling: TargetSpelling::Ansi,
228    };
229}
230
231/// Prefer [`FeatureSet::CLICKHOUSE`] for struct update.
232pub const CLICKHOUSE: FeatureSet = FeatureSet::CLICKHOUSE;
233
234// Compile-time proof the ClickHouse preset claims no shared tokenizer trigger twice.
235// Beyond ANSI's lexical surface it adds exactly one trigger — the backtick identifier
236// quote — which has a single claimant (no ClickHouse feature enabled here lexes a
237// backtick otherwise), and it keeps `double_quoted_strings` off so `"` stays the sole
238// identifier quote it already was. Every other delta is a contextual grammar gate with
239// no tokenizer trigger. Kept as a ratchet so a future ClickHouse delta that *does* add a
240// contending trigger fails the build here.
241const _: () = assert!(FeatureSet::CLICKHOUSE.is_lexically_consistent());
242// The two sibling self-consistency registries are ratcheted the same way, so the
243// parse-entry `debug_assert!` folds all three to dead code for this preset: no refinement
244// flag rides an unset base, and no two features contend for one parser-position head.
245const _: () = assert!(FeatureSet::CLICKHOUSE.has_satisfied_feature_dependencies());
246const _: () = assert!(FeatureSet::CLICKHOUSE.has_no_grammar_conflict());
247
248#[cfg(test)]
249mod tests {
250    use super::*;
251
252    #[test]
253    fn clickhouse_is_ansi_plus_the_nine_gates_and_two_lexical_facts() {
254        // The preset is ANSI with a documented, closed set of divergent axes: the two
255        // lexical facts (case-sensitivity, dual identifier quoting) and the two enabled
256        // sub-presets (the query tails and the type constructors). Asserting the whole
257        // rest equals ANSI keeps the "ANSI-derived, every delta documented" claim honest
258        // against a future stray edit. Bind to locals so the const reads are not flagged
259        // by clippy's `assertions_on_constants`.
260        let ansi = FeatureSet::ANSI;
261        let ch = FeatureSet::CLICKHOUSE;
262
263        // The two lexical facts.
264        assert_eq!(ch.identifier_casing, Casing::Preserve);
265        assert_ne!(ch.identifier_casing, ansi.identifier_casing);
266        assert_eq!(ch.identifier_quotes, CLICKHOUSE_IDENTIFIER_QUOTES);
267        assert_ne!(ch.identifier_quotes, ansi.identifier_quotes);
268        // `"` stays an identifier quote, so ClickHouse must keep `double_quoted_strings`
269        // off (the interplay the preset depends on for lexical consistency).
270        assert!(!ch.string_literals.double_quoted_strings);
271
272        // The two divergent sub-presets.
273        assert_eq!(ch.query_tail_syntax, QueryTailSyntax::CLICKHOUSE);
274        assert_ne!(ch.query_tail_syntax, ansi.query_tail_syntax);
275        assert_eq!(ch.type_name_syntax, TypeNameSyntax::CLICKHOUSE);
276        assert_ne!(ch.type_name_syntax, ansi.type_name_syntax);
277
278        // Everything else is inherited verbatim from ANSI.
279        assert_eq!(ch.string_literals, ansi.string_literals);
280        assert_eq!(ch.numeric_literals, ansi.numeric_literals);
281        assert_eq!(ch.parameters, ansi.parameters);
282        assert_eq!(ch.session_variables, ansi.session_variables);
283        assert_eq!(ch.identifier_syntax, ansi.identifier_syntax);
284        assert_eq!(ch.table_expressions, ansi.table_expressions);
285        assert_eq!(ch.expression_syntax, ansi.expression_syntax);
286        assert_eq!(ch.operator_syntax, ansi.operator_syntax);
287        assert_eq!(ch.call_syntax, ansi.call_syntax);
288        assert_eq!(ch.predicate_syntax, ansi.predicate_syntax);
289        assert_eq!(ch.mutation_syntax, ansi.mutation_syntax);
290        assert_eq!(ch.statement_ddl_gates, ansi.statement_ddl_gates);
291        assert_eq!(
292            ch.create_table_clause_syntax,
293            ansi.create_table_clause_syntax
294        );
295        assert_eq!(ch.column_definition_syntax, ansi.column_definition_syntax);
296        assert_eq!(ch.constraint_syntax, ansi.constraint_syntax);
297        assert_eq!(ch.index_alter_syntax, ansi.index_alter_syntax);
298        assert_eq!(ch.existence_guards, ansi.existence_guards);
299        assert_eq!(ch.utility_syntax, ansi.utility_syntax);
300        assert_eq!(ch.reserved_column_name, ansi.reserved_column_name);
301        assert_eq!(ch.reserved_function_name, ansi.reserved_function_name);
302        assert_eq!(ch.reserved_type_name, ansi.reserved_type_name);
303        assert_eq!(ch.reserved_bare_alias, ansi.reserved_bare_alias);
304        assert_eq!(ch.byte_classes, ansi.byte_classes);
305        assert_eq!(ch.binding_powers, ansi.binding_powers);
306        assert_eq!(ch.target_spelling, ansi.target_spelling);
307    }
308
309    #[test]
310    fn clickhouse_enables_exactly_the_nine_staged_gates() {
311        // The capstone: the three query tails and the six type constructors are on, and
312        // each is off in the ANSI base it derives from — so the ClickHouse preset is the
313        // shipped home for the nine feature gates, not the Lenient tooling union alone.
314        let ansi = FeatureSet::ANSI;
315        let ch = FeatureSet::CLICKHOUSE;
316
317        assert!(ch.query_tail_syntax.limit_by_clause && !ansi.query_tail_syntax.limit_by_clause);
318        assert!(ch.query_tail_syntax.settings_clause && !ansi.query_tail_syntax.settings_clause);
319        assert!(ch.query_tail_syntax.format_clause && !ansi.query_tail_syntax.format_clause);
320
321        assert!(ch.type_name_syntax.nullable_type && !ansi.type_name_syntax.nullable_type);
322        assert!(
323            ch.type_name_syntax.low_cardinality_type && !ansi.type_name_syntax.low_cardinality_type
324        );
325        assert!(ch.type_name_syntax.fixed_string_type && !ansi.type_name_syntax.fixed_string_type);
326        assert!(ch.type_name_syntax.datetime64_type && !ansi.type_name_syntax.datetime64_type);
327        assert!(ch.type_name_syntax.nested_type && !ansi.type_name_syntax.nested_type);
328        assert!(
329            ch.type_name_syntax.bit_width_integer_names
330                && !ansi.type_name_syntax.bit_width_integer_names
331        );
332
333        // The enabled sub-presets are ANSI plus exactly those gates and nothing else —
334        // forcing the nine back off recovers the ANSI sub-presets verbatim.
335        assert_eq!(
336            QueryTailSyntax {
337                limit_by_clause: false,
338                settings_clause: false,
339                format_clause: false,
340                ..ch.query_tail_syntax
341            },
342            ansi.query_tail_syntax,
343        );
344        assert_eq!(
345            TypeNameSyntax {
346                nullable_type: false,
347                low_cardinality_type: false,
348                fixed_string_type: false,
349                datetime64_type: false,
350                nested_type: false,
351                bit_width_integer_names: false,
352                ..ch.type_name_syntax
353            },
354            ansi.type_name_syntax,
355        );
356    }
357
358    #[test]
359    fn clickhouse_is_lexically_consistent_and_dependency_clean() {
360        // Both self-consistency registries must be clean: the dual identifier quoting
361        // and off `double_quoted_strings` introduce no lexical conflict, and none of the
362        // nine contextual gates rides an unset base flag.
363        let ch = FeatureSet::CLICKHOUSE;
364        assert_eq!(ch.lexical_conflict(), None);
365        assert!(ch.is_lexically_consistent());
366        assert_eq!(ch.feature_dependencies(), None);
367        assert!(ch.has_satisfied_feature_dependencies());
368        assert_eq!(ch.grammar_conflict(), None);
369        assert!(ch.has_no_grammar_conflict());
370    }
371    #[test]
372    fn clickhouse_closed_delta_axes_match_documented_set() {
373        crate::dialect::closed_delta::assert_closed_delta(
374            &FeatureSet::ANSI,
375            &FeatureSet::CLICKHOUSE,
376            &[
377                "identifier_casing",
378                "identifier_quotes",
379                "query_tail_syntax",
380                "type_name_syntax",
381            ],
382        );
383    }
384}