Skip to main content

squonk_ast/dialect/
redshift.rs

1// SPDX-License-Identifier: MIT
2// Copyright (c) 2026 Moderately AI Inc.
3
4//! The Amazon Redshift dialect preset (ANSI-derived, deliberately conservative).
5//!
6//! Redshift is genuinely a **PostgreSQL 8 fork** — the honest temptation is to derive it from
7//! [`FeatureSet::POSTGRES`]. This preset deliberately does *not*, and the reasoning is the
8//! project's evidence bar rather than convenience:
9//!
10//! - **No Redshift oracle exists.** Like the five other no-oracle presets
11//!   (BigQuery/ClickHouse/Snowflake/Databricks/MSSQL/Hive), over-acceptance here cannot be
12//!   *measured*. A `POSTGRES`-derived base would inherit our PostgreSQL preset's whole surface —
13//!   but that preset is oracle-fitted to **PostgreSQL 17**, decades past the PG-8 fork point, so
14//!   it carries modern features Redshift never had (dollar-quoting, `jsonb` operators, SQL/JSON
15//!   functions, `MERGE … RETURNING`, quantified `LIKE ANY (array)`, …). Deriving from `POSTGRES`
16//!   would silently over-accept every one of those, and each omission would then need its *own*
17//!   Redshift evidence to turn back off — a larger, less honest surface than starting strict.
18//! - **Conservatism is the honesty bar.** Deriving from [`FeatureSet::ANSI`], the strict standard
19//!   baseline, means every divergence from the standard is a documented, evidence-cited decision a
20//!   reader can audit from this one module, and unsupported Redshift syntax is a clean reject
21//!   routed to a focused follow-up ticket, never a silent over-accept.
22//! - **Our flag docs attribute the PG-isms to PostgreSQL, not Redshift.** The evidence bar for
23//!   turning a *dialect-attributed* grammar flag on is that our own flag doc names the dialect (as
24//!   the sided-join doc names Hive). A sweep of `dialect/mod.rs` finds **zero** Redshift
25//!   citations, and the candidate PG-heritage flags (`ilike`, `similar_to`, `distinct_on`,
26//!   `qualify`) are each documented as PostgreSQL/DuckDB/Teradata features. Those flags are
27//!   therefore conservative-off and deferred (see below) — not because Redshift lacks the
28//!   feature, but because turning them on here would assert an unmeasured equivalence.
29//!
30//! # What this preset adds over ANSI
31//!
32//! Exactly one axis, and it is *dialect-open* (a general folding model the [`Casing`] enum
33//! describes, not a flag our docs tie to one engine), so external Redshift documentation is the
34//! admissible evidence:
35//!
36//! - **Case folding to lowercase.** Redshift resolves unquoted identifiers case-insensitively and
37//!   folds them to lowercase (its default `enable_case_sensitive_identifier` is off — the
38//!   PostgreSQL-inherited behaviour, differing from PG only in fold *direction* being the same
39//!   lowercase), so [`identifier_casing`](FeatureSet::identifier_casing) is [`Casing::Lower`]
40//!   rather than ANSI's [`Casing::Upper`]. The fold is identity-only: the interned text still
41//!   renders exactly as written and acceptance never changes — this is a name-resolution fact, not
42//!   a parse boundary.
43//!
44//! Everything else is ANSI verbatim, including the lexis: Redshift quotes identifiers with the
45//! standard `"…"` (unlike Hive's backtick or MSSQL's bracket) and spells strings with `'…'`, so
46//! the ANSI [`STANDARD_IDENTIFIER_QUOTES`] and [`StringLiteralSyntax::ANSI`] are exact and this
47//! preset adds no new lexical trigger at all (the `const` assert below stays trivially clean).
48//!
49//! # Deliberately deferred (conservative reject)
50//!
51//! Redshift genuinely accepts each of these (it inherited most from PostgreSQL 8); each is a clean
52//! reject here, routed to a follow-up rather than guessed at without an oracle:
53//!
54//! - **`ILIKE` and `SIMILAR TO`.** Redshift ships both (AWS documents them), but our
55//!   [`ilike`](PredicateSyntax::ilike)/[`similar_to`](PredicateSyntax::similar_to) flag docs
56//!   attribute them to PostgreSQL and do not name Redshift — dialect-attributed, so external
57//!   Redshift evidence is not admissible under the bar. Deferred pending either a Redshift oracle
58//!   or a flag-doc citation update.
59//! - **`DISTINCT ON`.** Same shape: Redshift inherits PostgreSQL's `SELECT DISTINCT ON (…)`, but
60//!   [`distinct_on`](SelectSyntax::distinct_on) is documented as the PostgreSQL extension.
61//! - **`QUALIFY`.** Redshift added `QUALIFY`, but our [`qualify`](SelectSyntax::qualify) doc cites
62//!   DuckDB (Teradata-origin) and needs the reserved-keyword modelling Snowflake's preset does;
63//!   deferred rather than half-modelled.
64//! - **The large unmodelled Redshift surface.** `DISTKEY`/`SORTKEY`/`DISTSTYLE`/`ENCODE` table
65//!   attributes, `UNLOAD`/`COPY` bulk-load statements, the `SUPER` type with PartiQL
66//!   dot/subscript navigation, `INTERVAL` literal spellings, and Redshift's window-frame
67//!   differences all have no modelled gate and are clean rejects routed to follow-up tickets.
68//!
69//! A reader can predict from this module exactly what Redshift accepts beyond the standard: the
70//! lowercase identifier fold, and nothing else, until each deferred surface earns its own gate.
71
72use super::{
73    AccessControlSyntax, AggregateCallSyntax, CallSyntax, CaretOperator, Casing,
74    ColumnDefinitionSyntax, CommentSyntax, ConstraintSyntax, CreateTableClauseSyntax,
75    DoubleAmpersand, ExistenceGuards, ExpressionSyntax, FeatureSet, GroupingSyntax,
76    IdentifierSyntax, IndexAlterSyntax, JoinSyntax, KeywordOperators, KeywordSet,
77    MaintenanceSyntax, MutationSyntax, NullOrdering, NumericLiteralSyntax, OperatorSyntax,
78    ParameterSyntax, PipeOperator, PredicateSyntax, QueryTailSyntax, RESERVED_BARE_ALIAS,
79    RESERVED_COLUMN_NAME, RESERVED_FUNCTION_NAME, RESERVED_TYPE_NAME, STANDARD_BYTE_CLASSES,
80    STANDARD_IDENTIFIER_QUOTES, SelectSyntax, SessionVariableSyntax, ShowSyntax, StatementDdlGates,
81    StringFuncForms, StringLiteralSyntax, TableExpressionSyntax, TableFactorSyntax, TargetSpelling,
82    TypeNameSyntax, UtilitySyntax,
83};
84use crate::precedence::{STANDARD_BINDING_POWERS, STANDARD_SET_OPERATION_BINDING_POWERS};
85
86impl FeatureSet {
87    /// Amazon Redshift as ANSI-derived dialect data (see the module docs for the full derivation
88    /// rationale — including why a PostgreSQL-8 fork still derives from ANSI, not `POSTGRES` — and
89    /// the conservatism bar).
90    pub const REDSHIFT: Self = Self {
91        // The sole delta over ANSI: Redshift folds unquoted identifiers to lowercase (its default
92        // `enable_case_sensitive_identifier` off — the PostgreSQL-inherited lowercase model).
93        // Identity only: the interned text still renders exactly as written, so this never affects
94        // acceptance. `Casing` is dialect-open, so external Redshift docs are the admissible
95        // evidence here (unlike the dialect-attributed PG-heritage flags, which stay off).
96        identifier_casing: Casing::Lower,
97        // Standard `"…"` identifier quoting; Redshift adds no second delimiter (unlike Hive's
98        // backtick or MSSQL's bracket), so no lexical trigger is added over ANSI.
99        identifier_quotes: STANDARD_IDENTIFIER_QUOTES,
100        default_null_ordering: NullOrdering::NullsLast,
101        // No reserved-set delta over ANSI — this conservative preset reserves no extra keyword
102        // (the deferred `QUALIFY`/`DISTINCT ON` gates that would need reservation are off).
103        reserved_column_name: RESERVED_COLUMN_NAME,
104        reserved_function_name: RESERVED_FUNCTION_NAME,
105        reserved_type_name: RESERVED_TYPE_NAME,
106        reserved_bare_alias: RESERVED_BARE_ALIAS,
107        reserved_as_label: KeywordSet::EMPTY,
108        catalog_qualified_names: true,
109        byte_classes: STANDARD_BYTE_CLASSES,
110        binding_powers: STANDARD_BINDING_POWERS,
111        set_operation_powers: STANDARD_SET_OPERATION_BINDING_POWERS,
112        // Conservative ANSI string surface: Redshift spells strings with `'…'` (its `"…"` is a
113        // quoted identifier, exactly ANSI). Redshift's own extensions have no modelled gate here.
114        string_literals: StringLiteralSyntax::ANSI,
115        numeric_literals: NumericLiteralSyntax::ANSI,
116        parameters: ParameterSyntax::ANSI,
117        session_variables: SessionVariableSyntax::ANSI,
118        identifier_syntax: IdentifierSyntax::ANSI,
119        // ANSI table-expression surface plus the PartiQL / SUPER table-position JSON path
120        // (`FROM src[0].a`) navigating a SUPER column, sqlparser-rs's `supports_partiql`.
121        table_expressions: TableExpressionSyntax {
122            table_json_path: true,
123            ..TableExpressionSyntax::ANSI
124        },
125        join_syntax: JoinSyntax::ANSI,
126        table_factor_syntax: TableFactorSyntax::ANSI,
127        expression_syntax: ExpressionSyntax::ANSI,
128        operator_syntax: OperatorSyntax::ANSI,
129        call_syntax: CallSyntax::ANSI,
130        string_func_forms: StringFuncForms::ANSI,
131        aggregate_call_syntax: AggregateCallSyntax::ANSI,
132        // `ILIKE`/`SIMILAR TO` are deferred (dialect-attributed to PostgreSQL in our docs, no
133        // Redshift citation) — every predicate knob is conservatively ANSI.
134        predicate_syntax: PredicateSyntax::ANSI,
135        pipe_operator: PipeOperator::StringConcat,
136        double_ampersand: DoubleAmpersand::Unsupported,
137        keyword_operators: KeywordOperators::Unsupported,
138        caret_operator: CaretOperator::Unsupported,
139        hash_bitwise_xor: false,
140        comment_syntax: CommentSyntax::ANSI,
141        mutation_syntax: MutationSyntax::ANSI,
142        statement_ddl_gates: StatementDdlGates::ANSI,
143        create_table_clause_syntax: CreateTableClauseSyntax::ANSI,
144        column_definition_syntax: ColumnDefinitionSyntax::ANSI,
145        constraint_syntax: ConstraintSyntax::ANSI,
146        index_alter_syntax: IndexAlterSyntax::ANSI,
147        existence_guards: ExistenceGuards::ANSI,
148        // `DISTINCT ON`/`QUALIFY` are deferred (see the module docs) — every SELECT knob is
149        // conservatively ANSI.
150        select_syntax: SelectSyntax::ANSI,
151        query_tail_syntax: QueryTailSyntax::ANSI,
152        grouping_syntax: GroupingSyntax::ANSI,
153        utility_syntax: UtilitySyntax::ANSI,
154        show_syntax: ShowSyntax::ANSI,
155        maintenance_syntax: MaintenanceSyntax::ANSI,
156        access_control_syntax: AccessControlSyntax::ANSI,
157        type_name_syntax: TypeNameSyntax::ANSI,
158        // No Redshift-specific Tier-1 output spelling yet; render the portable ANSI canonical type
159        // names (a `TargetSpelling::Redshift` is render work a later ticket owns).
160        target_spelling: TargetSpelling::Ansi,
161    };
162}
163
164/// Prefer [`FeatureSet::REDSHIFT`] for struct update.
165pub const REDSHIFT: FeatureSet = FeatureSet::REDSHIFT;
166
167// Compile-time proof the Redshift preset claims no shared tokenizer trigger twice. It adds *no*
168// lexical trigger over ANSI (standard `"…"` identifier quoting, `'…'` strings — the same lexis as
169// the ANSI baseline), so this holds as trivially as ANSI's own assert. Kept as a ratchet so a
170// future Redshift delta that *does* add a contending trigger (e.g. `$$…$$` dollar-quoting) fails
171// the build here rather than silently shadowing a meaning.
172const _: () = assert!(FeatureSet::REDSHIFT.is_lexically_consistent());
173// The two sibling self-consistency registries are ratcheted the same way, so the
174// parse-entry `debug_assert!` folds all three to dead code for this preset: no refinement
175// flag rides an unset base, and no two features contend for one parser-position head.
176const _: () = assert!(FeatureSet::REDSHIFT.has_satisfied_feature_dependencies());
177const _: () = assert!(FeatureSet::REDSHIFT.has_no_grammar_conflict());
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182
183    #[test]
184    fn redshift_is_ansi_plus_only_the_lowercase_fold() {
185        // The strategic claim made auditable: despite Redshift being a PostgreSQL-8 fork, this
186        // preset is ANSI with a *single* divergent axis — the lowercase identifier fold. Asserting
187        // the whole rest equals ANSI keeps the "ANSI-derived, every delta documented, the PG-isms
188        // deferred" claim honest against a future stray edit. Bind to locals so the const reads are
189        // not flagged by clippy's `assertions_on_constants`.
190        let ansi = FeatureSet::ANSI;
191        let redshift = FeatureSet::REDSHIFT;
192
193        // The one delta: lowercase folding (vs ANSI's uppercase).
194        assert_eq!(redshift.identifier_casing, Casing::Lower);
195        assert_ne!(redshift.identifier_casing, ansi.identifier_casing);
196        assert_eq!(ansi.identifier_casing, Casing::Upper);
197
198        // The lexis is ANSI verbatim — standard `"…"` quoting, no new delimiter, no
199        // double-quoted strings (Redshift's `"…"` is an identifier, exactly ANSI).
200        assert_eq!(redshift.identifier_quotes, STANDARD_IDENTIFIER_QUOTES);
201        assert_eq!(redshift.identifier_quotes, ansi.identifier_quotes);
202        assert_eq!(redshift.string_literals, ansi.string_literals);
203        assert!(!redshift.string_literals.double_quoted_strings);
204
205        // No reserved-set delta: every position is inherited verbatim from ANSI.
206        assert_eq!(redshift.reserved_column_name, ansi.reserved_column_name);
207        assert_eq!(redshift.reserved_function_name, ansi.reserved_function_name);
208        assert_eq!(redshift.reserved_type_name, ansi.reserved_type_name);
209        assert_eq!(redshift.reserved_bare_alias, ansi.reserved_bare_alias);
210        assert_eq!(redshift.reserved_as_label, KeywordSet::EMPTY);
211
212        // The deferred PG-heritage flags are all OFF — the conservative-off decisions the module
213        // docs record, pinned so a future edit cannot silently turn one on without evidence.
214        assert!(!redshift.predicate_syntax.ilike);
215        assert!(!redshift.predicate_syntax.similar_to);
216        assert!(!redshift.select_syntax.distinct_on);
217        assert!(!redshift.select_syntax.qualify);
218
219        // Everything else is inherited verbatim from ANSI.
220        assert_eq!(redshift.select_syntax, ansi.select_syntax);
221        assert_eq!(redshift.predicate_syntax, ansi.predicate_syntax);
222        assert_eq!(redshift.numeric_literals, ansi.numeric_literals);
223        assert_eq!(redshift.parameters, ansi.parameters);
224        // Redshift diverges from ANSI on exactly the PartiQL / SUPER table-position path.
225        assert_eq!(
226            TableExpressionSyntax {
227                table_json_path: false,
228                ..redshift.table_expressions
229            },
230            ansi.table_expressions,
231        );
232        assert!(redshift.table_expressions.table_json_path);
233        assert_eq!(redshift.expression_syntax, ansi.expression_syntax);
234        assert_eq!(redshift.session_variables, ansi.session_variables);
235        assert_eq!(redshift.identifier_syntax, ansi.identifier_syntax);
236        assert_eq!(redshift.operator_syntax, ansi.operator_syntax);
237        assert_eq!(redshift.call_syntax, ansi.call_syntax);
238        assert_eq!(redshift.mutation_syntax, ansi.mutation_syntax);
239        assert_eq!(redshift.statement_ddl_gates, ansi.statement_ddl_gates);
240        assert_eq!(
241            redshift.create_table_clause_syntax,
242            ansi.create_table_clause_syntax
243        );
244        assert_eq!(
245            redshift.column_definition_syntax,
246            ansi.column_definition_syntax
247        );
248        assert_eq!(redshift.constraint_syntax, ansi.constraint_syntax);
249        assert_eq!(redshift.index_alter_syntax, ansi.index_alter_syntax);
250        assert_eq!(redshift.existence_guards, ansi.existence_guards);
251        assert_eq!(redshift.utility_syntax, ansi.utility_syntax);
252        assert_eq!(redshift.type_name_syntax, ansi.type_name_syntax);
253        assert_eq!(redshift.byte_classes, ansi.byte_classes);
254        assert_eq!(redshift.binding_powers, ansi.binding_powers);
255        assert_eq!(redshift.target_spelling, ansi.target_spelling);
256        assert_eq!(redshift.default_null_ordering, ansi.default_null_ordering);
257    }
258
259    #[test]
260    fn redshift_recovers_ansi_when_the_fold_is_forced_back() {
261        // The two deltas isolated: forcing the fold direction back to ANSI's `Upper` and
262        // dropping the PartiQL / SUPER table-position path recovers the ANSI FeatureSet
263        // verbatim, proving the lowercase fold and the `table_json_path` grant are the *only*
264        // divergences.
265        let ansi = FeatureSet::ANSI;
266        let redshift = FeatureSet::REDSHIFT;
267        assert_eq!(
268            FeatureSet {
269                identifier_casing: Casing::Upper,
270                table_expressions: TableExpressionSyntax {
271                    table_json_path: false,
272                    ..redshift.table_expressions
273                },
274                ..redshift
275            },
276            ansi,
277        );
278    }
279
280    #[test]
281    fn redshift_is_lexically_consistent_and_dependency_clean() {
282        // Both self-consistency registries must be clean: adding no lexical trigger over ANSI, the
283        // conflict registry is trivially empty, and riding no dependent grammar flag, the
284        // dependency registry is empty too.
285        let redshift = FeatureSet::REDSHIFT;
286        assert_eq!(redshift.lexical_conflict(), None);
287        assert!(redshift.is_lexically_consistent());
288        assert_eq!(redshift.feature_dependencies(), None);
289        assert!(redshift.has_satisfied_feature_dependencies());
290        assert_eq!(redshift.grammar_conflict(), None);
291        assert!(redshift.has_no_grammar_conflict());
292    }
293}