Skip to main content

squonk_ast/dialect/
hive.rs

1// SPDX-License-Identifier: MIT
2// Copyright (c) 2026 Moderately AI Inc.
3
4//! The Hive / HiveQL dialect preset (ANSI-derived, deliberately conservative).
5//!
6//! Apache Hive's HiveQL diverges widely across its type, function, and statement surface, and
7//! — unlike the five shipped oracle-compared presets — this workspace has **no Hive oracle**,
8//! so over-acceptance cannot be measured. Conservatism is therefore the honesty bar: this
9//! preset derives from [`FeatureSet::ANSI`], the strict standard baseline, and enables only the
10//! Hive surface that already has a modelled, tested parser gate and clear documentary evidence
11//! — in the headline case the flag's *own* doc names Hive as a motivating dialect. Every other
12//! axis keeps its ANSI value; a reader can predict from this module exactly what Hive accepts
13//! beyond the standard, and unsupported Hive syntax is a clean reject routed to a focused
14//! follow-up ticket, never a silent over-accept.
15//!
16//! # What this preset adds over ANSI
17//!
18//! Two grammar gates carry the headline Hive surface:
19//!
20//! - [`sided_semi_anti_join`](JoinSyntax::sided_semi_anti_join) — the
21//!   `{LEFT|RIGHT} {SEMI|ANTI} JOIN` sided semi-/anti-join spelling, whose flag doc names
22//!   Spark/Hive/Databricks as the motivating family. Hive is the dialect that *originated* the
23//!   `LEFT SEMI JOIN`; the Databricks preset already turned this flag on, and Hive is the
24//!   second engine preset to enable it. The leading `LEFT`/`RIGHT` is already a reserved join
25//!   side, so no reserved-word interplay is needed (the preceding factor's alias can never
26//!   swallow it). See the over-acceptance deferral note below.
27//! - [`lateral_view_clause`](SelectSyntax::lateral_view_clause) — the
28//!   `LATERAL VIEW [OUTER] explode(col) t [AS a, b]` table-generating clause Hive
29//!   originated (LanguageManual LateralView: `fromClause: FROM baseTable (lateralView)*`).
30//!   The derived-table `LATERAL` factor stays off, so `LATERAL` leads only this clause
31//!   under the preset. See the `AS`-optional over-acceptance deferral note below.
32//!
33//! # The two lexical facts over ANSI
34//!
35//! - **Backtick identifier quoting.** Hive quotes identifiers with the backtick `` `name` ``
36//!   alone (its default `hive.support.quoted.identifiers=column` mode), and its `"…"` and `'…'`
37//!   are *both* string literals — HiveQL string literals may be written with single or double
38//!   quotes. So [`HIVE_IDENTIFIER_QUOTES`] lists only the backtick (unlike the
39//!   SQLite/Databricks/MSSQL bracket-or-double-quote sets, and unlike ANSI's `"…"`), and
40//!   [`double_quoted_strings`](StringLiteralSyntax::double_quoted_strings) is correspondingly
41//!   **on** so `"x"` lexes as a string, never an identifier — exactly the MySQL default
42//!   (`ANSI_QUOTES` off) lexis, and the same shape the BigQuery preset ships. The two facts are
43//!   coupled: `"` has a single claimant (the string scanner) precisely because it is absent
44//!   from the quote set, the [`DoubleQuoteStringVersusIdentifier`](super::LexicalConflict)
45//!   hazard the `const` assert below rules out. The backtick likewise has a single claimant —
46//!   no enabled expression grammar lexes a backtick.
47//! - **Case folding.** Hive resolves unquoted identifiers case-insensitively (its metastore
48//!   lowercases table and column names), so [`identifier_casing`](FeatureSet::identifier_casing)
49//!   is [`Casing::Lower`] — the value the [`Casing`] doc prescribes for the "case-preserving
50//!   storage, case-insensitive comparison" model it describes for MySQL/T-SQL columns. Hive is
51//!   uniformly case-insensitive (it lacks even the case-sensitive-table split that paragraph
52//!   flags as the single-fold known limitation), so the fit is exact. The interned text still
53//!   renders exactly as written; the fold is identity-only and never affects acceptance.
54//!
55//! # Deliberately deferred (conservative reject)
56//!
57//! - **`RIGHT`/`ANTI` sided joins.** Classic Hive documents only `LEFT SEMI JOIN` (and later
58//!   `LEFT ANTI JOIN`); the atomic [`sided_semi_anti_join`](JoinSyntax::sided_semi_anti_join) flag also admits the `RIGHT`-sided
59//!   spelling and the `ANTI` flavour across both sides — a known conservative-direction
60//!   over-acceptance a future side-refinement (or a Hive oracle) would tighten. This mirrors
61//!   the deferral the Databricks preset recorded for the same atomic flag; captured on the
62//!   owning ticket rather than split into a narrower gate that no measured boundary yet
63//!   justifies.
64//! - **`AS`-less lateral-view column aliases.** Hive's grammar requires the `AS` before a
65//!   lateral view's column aliases; Spark's spells it `AS?`, and the one atomic
66//!   [`lateral_view_clause`](SelectSyntax::lateral_view_clause) gate accepts the wider
67//!   Spark bound — a known conservative-direction over-acceptance for this preset, the
68//!   same class as the sided-join deferral above (see the [`LateralView`](crate::ast::LateralView)
69//!   acceptance-bound doc); captured on the owning ticket.
70//!
71//! Everything else Hive (the `STRUCT`/`ARRAY<…>`/`MAP<…>` complex type surface,
72//! `TRANSFORM`/`MAP`/`REDUCE` script operators, `DISTRIBUTE BY`/`SORT BY`/`CLUSTER BY` clauses,
73//! `TABLESAMPLE` bucketing, backslash string escapes, …) has no modelled gate and is a clean
74//! reject routed to follow-up tickets, never a silent over-accept.
75
76use super::{
77    AccessControlSyntax, AggregateCallSyntax, CallSyntax, CaretOperator, Casing,
78    ColumnDefinitionSyntax, CommentSyntax, ConstraintSyntax, CreateTableClauseSyntax,
79    DoubleAmpersand, ExistenceGuards, ExpressionSyntax, FeatureSet, GroupingSyntax,
80    IdentifierQuote, IdentifierSyntax, IndexAlterSyntax, JoinSyntax, KeywordOperators, KeywordSet,
81    MaintenanceSyntax, MutationSyntax, NullOrdering, NumericLiteralSyntax, OperatorSyntax,
82    ParameterSyntax, PipeOperator, PredicateSyntax, QueryTailSyntax, RESERVED_BARE_ALIAS,
83    RESERVED_COLUMN_NAME, RESERVED_FUNCTION_NAME, RESERVED_TYPE_NAME, STANDARD_BYTE_CLASSES,
84    SelectSyntax, SessionVariableSyntax, ShowSyntax, StatementDdlGates, StringFuncForms,
85    StringLiteralSyntax, TableExpressionSyntax, TableFactorSyntax, TargetSpelling, TypeNameSyntax,
86    UtilitySyntax,
87};
88use crate::precedence::{STANDARD_BINDING_POWERS, STANDARD_SET_OPERATION_BINDING_POWERS};
89
90/// Hive identifier quoting: the backtick `` `…` `` alone. Hive spells a quoted identifier
91/// `` `a` `` (its default `hive.support.quoted.identifiers=column` mode); its `"a"` and `'a'`
92/// are *both* string constants, so `"` is deliberately absent here (and
93/// [`StringLiteralSyntax::HIVE`] turns `double_quoted_strings` on so `"` unambiguously lexes a
94/// string) — the same backtick-only lexis MySQL uses under its default `ANSI_QUOTES`-off mode.
95pub const HIVE_IDENTIFIER_QUOTES: &[IdentifierQuote] = &[IdentifierQuote::Symmetric('`')];
96
97impl StringLiteralSyntax {
98    /// Hive string surface: the ANSI baseline plus `"…"` double-quoted string constants (HiveQL
99    /// string literals may be written with single *or* double quotes, reserving the backtick
100    /// for identifiers). Enabling this is what lets [`HIVE_IDENTIFIER_QUOTES`] drop `"` from the
101    /// quote set without stranding the byte. Every other string knob is conservatively ANSI —
102    /// Hive's backslash escape sequences have no Hive-citing flag doc or oracle here and are
103    /// deferred rather than guessed at (`backslash_escapes` stays off).
104    pub const HIVE: Self = Self {
105        double_quoted_strings: true,
106        ..StringLiteralSyntax::ANSI
107    };
108}
109
110impl TableExpressionSyntax {
111    /// Hive table-expression surface: the ANSI baseline plus the sided
112    /// `{LEFT|RIGHT} {SEMI|ANTI} JOIN` family, whose flag doc names Hive as a motivating
113    /// dialect (Hive originated `LEFT SEMI JOIN`). The side-less DuckDB spelling
114    /// (`semi_anti_join`) stays off — it is a different engine family and needs `SEMI`/`ANTI`
115    /// bare-alias reservation modelling not done here. Every other table knob is conservatively
116    /// ANSI.
117    pub const HIVE: Self = Self {
118        ..TableExpressionSyntax::ANSI
119    };
120}
121
122impl JoinSyntax {
123    /// The `HIVE` preset for join syntax.
124    pub const HIVE: Self = Self {
125        sided_semi_anti_join: true,
126        ..JoinSyntax::ANSI
127    };
128}
129
130impl TableFactorSyntax {
131    /// The `HIVE` preset for table factor syntax.
132    pub const HIVE: Self = Self {
133        ..TableFactorSyntax::ANSI
134    };
135}
136
137impl SelectSyntax {
138    /// Hive SELECT surface: the ANSI baseline plus the `LATERAL VIEW` table-generating
139    /// clause Hive originated (LanguageManual LateralView). The derived-table `LATERAL`
140    /// factor ([`TableFactorSyntax::HIVE`]) stays off, so `LATERAL` leads only this
141    /// clause under the preset. Every other SELECT knob is conservatively ANSI.
142    pub const HIVE: Self = Self {
143        lateral_view_clause: true,
144        ..SelectSyntax::ANSI
145    };
146}
147
148impl FeatureSet {
149    /// Hive / HiveQL as ANSI-derived dialect data (see the module docs for the full derivation
150    /// rationale and the conservatism bar).
151    pub const HIVE: Self = Self {
152        // Hive resolves unquoted identifiers case-insensitively (its metastore lowercases table
153        // and column names); `Casing::Lower` is the exact fit (Hive lacks even the
154        // case-sensitive-table split the `Casing` known-limitation paragraph flags). Identity
155        // only — the interned text still renders exactly as written, so this never affects
156        // acceptance.
157        identifier_casing: Casing::Lower,
158        // The lexical delta over ANSI: backtick-only identifier quoting (with `"` handed to the
159        // string scanner via `double_quoted_strings` below).
160        identifier_quotes: HIVE_IDENTIFIER_QUOTES,
161        default_null_ordering: NullOrdering::NullsLast,
162        // No reserved-set delta over ANSI — this conservative preset adds no keyword
163        // reservation (the sided-join gate rides the already-reserved LEFT/RIGHT join sides).
164        reserved_column_name: RESERVED_COLUMN_NAME,
165        reserved_function_name: RESERVED_FUNCTION_NAME,
166        reserved_type_name: RESERVED_TYPE_NAME,
167        reserved_bare_alias: RESERVED_BARE_ALIAS,
168        reserved_as_label: KeywordSet::EMPTY,
169        catalog_qualified_names: true,
170        byte_classes: STANDARD_BYTE_CLASSES,
171        binding_powers: STANDARD_BINDING_POWERS,
172        set_operation_powers: STANDARD_SET_OPERATION_BINDING_POWERS,
173        // `"…"` double-quoted strings (the coupled half of the backtick-only identifier lexis).
174        string_literals: StringLiteralSyntax::HIVE,
175        numeric_literals: NumericLiteralSyntax::ANSI,
176        parameters: ParameterSyntax::ANSI,
177        session_variables: SessionVariableSyntax::ANSI,
178        identifier_syntax: IdentifierSyntax::ANSI,
179        // The sided `{LEFT|RIGHT} {SEMI|ANTI} JOIN` family — the capstone this preset exposes.
180        table_expressions: TableExpressionSyntax::HIVE,
181        join_syntax: JoinSyntax::HIVE,
182        table_factor_syntax: TableFactorSyntax::HIVE,
183        expression_syntax: ExpressionSyntax::ANSI,
184        operator_syntax: OperatorSyntax::ANSI,
185        call_syntax: CallSyntax::ANSI,
186        string_func_forms: StringFuncForms::ANSI,
187        aggregate_call_syntax: AggregateCallSyntax::ANSI,
188        predicate_syntax: PredicateSyntax::ANSI,
189        pipe_operator: PipeOperator::StringConcat,
190        double_ampersand: DoubleAmpersand::Unsupported,
191        keyword_operators: KeywordOperators::Unsupported,
192        caret_operator: CaretOperator::Unsupported,
193        hash_bitwise_xor: false,
194        comment_syntax: CommentSyntax::ANSI,
195        mutation_syntax: MutationSyntax::ANSI,
196        statement_ddl_gates: StatementDdlGates::ANSI,
197        create_table_clause_syntax: CreateTableClauseSyntax::ANSI,
198        column_definition_syntax: ColumnDefinitionSyntax::ANSI,
199        constraint_syntax: ConstraintSyntax::ANSI,
200        index_alter_syntax: IndexAlterSyntax::ANSI,
201        existence_guards: ExistenceGuards::ANSI,
202        // The `LATERAL VIEW` table-generating clause — the second capstone this preset
203        // exposes; every other SELECT knob is conservatively ANSI.
204        select_syntax: SelectSyntax::HIVE,
205        query_tail_syntax: QueryTailSyntax::ANSI,
206        grouping_syntax: GroupingSyntax::ANSI,
207        utility_syntax: UtilitySyntax::ANSI,
208        show_syntax: ShowSyntax::ANSI,
209        maintenance_syntax: MaintenanceSyntax::ANSI,
210        access_control_syntax: AccessControlSyntax::ANSI,
211        type_name_syntax: TypeNameSyntax::ANSI,
212        // No Hive-specific Tier-1 output spelling yet; render the portable ANSI canonical type
213        // names (a `TargetSpelling::Hive` is render work a later ticket owns).
214        target_spelling: TargetSpelling::Ansi,
215    };
216}
217
218/// Prefer [`FeatureSet::HIVE`] for struct update.
219pub const HIVE: FeatureSet = FeatureSet::HIVE;
220
221// Compile-time proof the Hive preset claims no shared tokenizer trigger twice. Beyond ANSI it
222// adds one lexical trigger — the backtick identifier opener — with a single claimant (no
223// enabled expression grammar lexes a backtick), and it hands `"` to the string scanner
224// (`double_quoted_strings` on) *while dropping `"` from the identifier quote set*, so `"` also
225// keeps a single claimant. The sided-join gate is contextual keyword grammar with no tokenizer
226// trigger. Kept as a ratchet so a future Hive delta that *does* add a contending trigger (e.g.
227// re-listing `"` as an identifier quote) fails the build here.
228const _: () = assert!(FeatureSet::HIVE.is_lexically_consistent());
229// The two sibling self-consistency registries are ratcheted the same way, so the
230// parse-entry `debug_assert!` folds all three to dead code for this preset: no refinement
231// flag rides an unset base, and no two features contend for one parser-position head.
232const _: () = assert!(FeatureSet::HIVE.has_satisfied_feature_dependencies());
233const _: () = assert!(FeatureSet::HIVE.has_no_grammar_conflict());
234
235#[cfg(test)]
236mod tests {
237    use super::*;
238
239    #[test]
240    fn hive_is_ansi_plus_the_two_gates_and_two_lexical_facts() {
241        // The preset is ANSI with a documented, closed set of divergent axes: the two lexical
242        // facts (case-folding, backtick-only quoting coupled with double-quoted strings), the
243        // enabled table-expression gate, and the enabled SELECT gate. Asserting the whole rest
244        // equals ANSI keeps the "ANSI-derived, every delta documented" claim honest against a
245        // future stray edit. Bind to locals so the const reads are not flagged by clippy's
246        // `assertions_on_constants`.
247        let ansi = FeatureSet::ANSI;
248        let hive = FeatureSet::HIVE;
249
250        // The two lexical facts.
251        assert_eq!(hive.identifier_casing, Casing::Lower);
252        assert_ne!(hive.identifier_casing, ansi.identifier_casing);
253        assert_eq!(hive.identifier_quotes, HIVE_IDENTIFIER_QUOTES);
254        assert_ne!(hive.identifier_quotes, ansi.identifier_quotes);
255        // The coupling: `"` is dropped from the quote set and handed to the string scanner.
256        assert!(hive.string_literals.double_quoted_strings);
257        assert!(
258            !hive
259                .identifier_quotes
260                .iter()
261                .any(|quote| quote.open() == '"')
262        );
263        // Backtick is the sole identifier quote; no bracket (unlike SQLite/MSSQL) and no `"`.
264        assert!(
265            hive.identifier_quotes
266                .iter()
267                .any(|quote| quote.open() == '`')
268        );
269        assert_eq!(hive.identifier_quotes.len(), 1);
270
271        // The one divergent string sub-preset (the double-quote coupling).
272        assert_eq!(hive.string_literals, StringLiteralSyntax::HIVE);
273        assert_ne!(hive.string_literals, ansi.string_literals);
274        // The divergent table-expression sub-preset.
275        assert_eq!(hive.table_expressions, TableExpressionSyntax::HIVE);
276        assert_eq!(hive.join_syntax, JoinSyntax::HIVE);
277        assert_ne!(hive.join_syntax, ansi.join_syntax);
278        // The divergent SELECT sub-preset (the LATERAL VIEW clause).
279        assert_eq!(hive.select_syntax, SelectSyntax::HIVE);
280        assert_ne!(hive.select_syntax, ansi.select_syntax);
281
282        // No reserved-set delta: every position is inherited verbatim from ANSI.
283        assert_eq!(hive.reserved_column_name, ansi.reserved_column_name);
284        assert_eq!(hive.reserved_function_name, ansi.reserved_function_name);
285        assert_eq!(hive.reserved_type_name, ansi.reserved_type_name);
286        assert_eq!(hive.reserved_bare_alias, ansi.reserved_bare_alias);
287        assert_eq!(hive.reserved_as_label, KeywordSet::EMPTY);
288
289        // Everything else is inherited verbatim from ANSI.
290        assert_eq!(hive.numeric_literals, ansi.numeric_literals);
291        assert_eq!(hive.parameters, ansi.parameters);
292        assert_eq!(hive.expression_syntax, ansi.expression_syntax);
293        assert_eq!(hive.session_variables, ansi.session_variables);
294        assert_eq!(hive.identifier_syntax, ansi.identifier_syntax);
295        assert_eq!(hive.operator_syntax, ansi.operator_syntax);
296        assert_eq!(hive.call_syntax, ansi.call_syntax);
297        assert_eq!(hive.predicate_syntax, ansi.predicate_syntax);
298        assert_eq!(hive.mutation_syntax, ansi.mutation_syntax);
299        assert_eq!(hive.statement_ddl_gates, ansi.statement_ddl_gates);
300        assert_eq!(
301            hive.create_table_clause_syntax,
302            ansi.create_table_clause_syntax
303        );
304        assert_eq!(hive.column_definition_syntax, ansi.column_definition_syntax);
305        assert_eq!(hive.constraint_syntax, ansi.constraint_syntax);
306        assert_eq!(hive.index_alter_syntax, ansi.index_alter_syntax);
307        assert_eq!(hive.existence_guards, ansi.existence_guards);
308        assert_eq!(hive.utility_syntax, ansi.utility_syntax);
309        assert_eq!(hive.type_name_syntax, ansi.type_name_syntax);
310        assert_eq!(hive.byte_classes, ansi.byte_classes);
311        assert_eq!(hive.binding_powers, ansi.binding_powers);
312        assert_eq!(hive.target_spelling, ansi.target_spelling);
313        assert_eq!(hive.default_null_ordering, ansi.default_null_ordering);
314    }
315
316    #[test]
317    fn hive_enables_exactly_the_two_gates_and_double_quote_string() {
318        // The capstones: the sided semi-/anti-join family, the LATERAL VIEW clause, and
319        // double-quoted strings are on, and each is off in the ANSI base it derives from.
320        // Forcing the flags back off recovers the ANSI sub-presets verbatim.
321        let ansi = FeatureSet::ANSI;
322        let hive = FeatureSet::HIVE;
323
324        assert!(hive.join_syntax.sided_semi_anti_join);
325        assert!(!ansi.join_syntax.sided_semi_anti_join);
326        // The side-less DuckDB spelling stays off (a different engine family).
327        assert!(!hive.join_syntax.semi_anti_join);
328        // The LATERAL VIEW clause; the derived-table LATERAL factor stays off, so
329        // `LATERAL` leads only the view clause under this preset.
330        assert!(hive.select_syntax.lateral_view_clause && !ansi.select_syntax.lateral_view_clause);
331        assert!(!hive.table_factor_syntax.lateral);
332        assert!(
333            hive.string_literals.double_quoted_strings
334                && !ansi.string_literals.double_quoted_strings
335        );
336        // The backtick opener is the lexical gate (an identifier-quote delta, not a bool).
337        assert!(
338            hive.identifier_quotes
339                .iter()
340                .any(|quote| quote.open() == '`')
341        );
342        assert!(
343            !ansi
344                .identifier_quotes
345                .iter()
346                .any(|quote| quote.open() == '`')
347        );
348
349        assert_eq!(
350            JoinSyntax {
351                sided_semi_anti_join: false,
352                ..hive.join_syntax
353            },
354            ansi.join_syntax,
355        );
356        assert_eq!(
357            SelectSyntax {
358                lateral_view_clause: false,
359                ..hive.select_syntax
360            },
361            ansi.select_syntax,
362        );
363        assert_eq!(
364            StringLiteralSyntax {
365                double_quoted_strings: false,
366                ..hive.string_literals
367            },
368            ansi.string_literals,
369        );
370    }
371
372    #[test]
373    fn hive_is_lexically_consistent_and_dependency_clean() {
374        // Both self-consistency registries must be clean: the backtick quote has a single
375        // claimant, `"` is handed to the string scanner (dropped from the quote set), and the
376        // sided-join gate rides no unset base flag.
377        let hive = FeatureSet::HIVE;
378        assert_eq!(hive.lexical_conflict(), None);
379        assert!(hive.is_lexically_consistent());
380        assert_eq!(hive.feature_dependencies(), None);
381        assert!(hive.has_satisfied_feature_dependencies());
382        assert_eq!(hive.grammar_conflict(), None);
383        assert!(hive.has_no_grammar_conflict());
384    }
385}