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//! Two axes (both evidence-backed; the rest stays ANSI verbatim):
33//!
34//! - **Case folding to lowercase.** Redshift resolves unquoted identifiers case-insensitively and
35//! folds them to lowercase (its default `enable_case_sensitive_identifier` is off — the
36//! PostgreSQL-inherited behaviour, differing from PG only in fold *direction* being the same
37//! lowercase), so [`identifier_casing`](FeatureSet::identifier_casing) is [`Casing::Lower`]
38//! rather than ANSI's [`Casing::Upper`]. The fold is identity-only: the interned text still
39//! renders exactly as written and acceptance never changes — this is a name-resolution fact, not
40//! a parse boundary. `Casing` is *dialect-open* (a general folding model, not a flag our docs
41//! tie to one engine), so external Redshift documentation is the admissible evidence.
42//! - **Table-position PartiQL / SUPER JSON path**
43//! ([`table_json_path`](TableExpressionSyntax::table_json_path)) — `FROM src[0].a` navigation
44//! of a SUPER column (sqlparser-rs's `supports_partiql` surface). A parse boundary, not just
45//! identity.
46//!
47//! Lexis remains ANSI: Redshift quotes identifiers with the standard `"…"` (unlike Hive's
48//! backtick or MSSQL's bracket) and spells strings with `'…'`, so the ANSI
49//! [`STANDARD_IDENTIFIER_QUOTES`] and [`StringLiteralSyntax::ANSI`] are exact and this preset
50//! adds no new lexical trigger (the `const` assert below stays clean).
51//!
52//! # Deliberately deferred (conservative reject)
53//!
54//! Redshift genuinely accepts each of these (it inherited most from PostgreSQL 8); each is a clean
55//! reject here, routed to a follow-up rather than guessed at without an oracle:
56//!
57//! - **`ILIKE` and `SIMILAR TO`.** Redshift ships both (AWS documents them), but our
58//! [`ilike`](PredicateSyntax::ilike)/[`similar_to`](PredicateSyntax::similar_to) flag docs
59//! attribute them to PostgreSQL and do not name Redshift — dialect-attributed, so external
60//! Redshift evidence is not admissible under the bar. Deferred pending either a Redshift oracle
61//! or a flag-doc citation update.
62//! - **`DISTINCT ON`.** Same shape: Redshift inherits PostgreSQL's `SELECT DISTINCT ON (…)`, but
63//! [`distinct_on`](SelectSyntax::distinct_on) is documented as the PostgreSQL extension.
64//! - **`QUALIFY`.** Redshift added `QUALIFY`, but our [`qualify`](SelectSyntax::qualify) doc cites
65//! DuckDB (Teradata-origin) and needs the reserved-keyword modelling Snowflake's preset does;
66//! deferred rather than half-modelled.
67//! - **The large unmodelled Redshift surface.** `DISTKEY`/`SORTKEY`/`DISTSTYLE`/`ENCODE` table
68//! attributes, `UNLOAD`/`COPY` bulk-load statements, full `SUPER` type DDL (beyond the table-
69//! position JSON path above), `INTERVAL` literal spellings, and Redshift's window-frame
70//! differences all have no modelled gate and are clean rejects routed to follow-up tickets.
71//!
72//! A reader can predict from this module exactly what Redshift accepts beyond the standard: the
73//! lowercase identifier fold and table-position `table_json_path`, and nothing else, until each
74//! deferred surface earns its own gate.
75
76use super::{
77 AccessControlSyntax, AggregateCallSyntax, CallSyntax, CaretOperator, Casing,
78 ColumnDefinitionSyntax, CommentSyntax, ConstraintSyntax, CreateTableClauseSyntax,
79 DoubleAmpersand, ExistenceGuards, ExpressionSyntax, FeatureSet, GroupingSyntax,
80 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 STANDARD_IDENTIFIER_QUOTES, SelectSyntax, SessionVariableSyntax, ShowSyntax, StatementDdlGates,
85 StringFuncForms, StringLiteralSyntax, TableExpressionSyntax, TableFactorSyntax, TargetSpelling,
86 TransactionSyntax, TypeNameSyntax, UtilitySyntax, ViewSequenceClauseSyntax,
87};
88use crate::precedence::{STANDARD_BINDING_POWERS, STANDARD_SET_OPERATION_BINDING_POWERS};
89
90impl FeatureSet {
91 /// Amazon Redshift as ANSI-derived dialect data (see the module docs for the full derivation
92 /// rationale — including why a PostgreSQL-8 fork still derives from ANSI, not `POSTGRES` — and
93 /// the conservatism bar).
94 pub const REDSHIFT: Self = Self {
95 // Delta 1/2 over ANSI: Redshift folds unquoted identifiers to lowercase (its default
96 // `enable_case_sensitive_identifier` off — the PostgreSQL-inherited lowercase model).
97 // Identity only: the interned text still renders exactly as written, so this never affects
98 // acceptance. `Casing` is dialect-open, so external Redshift docs are the admissible
99 // evidence here (unlike the dialect-attributed PG-heritage flags, which stay off).
100 identifier_casing: Casing::Lower,
101 // Standard `"…"` identifier quoting; Redshift adds no second delimiter (unlike Hive's
102 // backtick or MSSQL's bracket), so no lexical trigger is added over ANSI.
103 identifier_quotes: STANDARD_IDENTIFIER_QUOTES,
104 default_null_ordering: NullOrdering::NullsLast,
105 // No reserved-set delta over ANSI — this conservative preset reserves no extra keyword
106 // (the deferred `QUALIFY`/`DISTINCT ON` gates that would need reservation are off).
107 reserved_column_name: RESERVED_COLUMN_NAME,
108 reserved_function_name: RESERVED_FUNCTION_NAME,
109 reserved_type_name: RESERVED_TYPE_NAME,
110 reserved_bare_alias: RESERVED_BARE_ALIAS,
111 reserved_as_label: KeywordSet::EMPTY,
112 catalog_qualified_names: true,
113 byte_classes: STANDARD_BYTE_CLASSES,
114 binding_powers: STANDARD_BINDING_POWERS,
115 set_operation_powers: STANDARD_SET_OPERATION_BINDING_POWERS,
116 // Conservative ANSI string surface: Redshift spells strings with `'…'` (its `"…"` is a
117 // quoted identifier, exactly ANSI). Redshift's own extensions have no modelled gate here.
118 string_literals: StringLiteralSyntax::ANSI,
119 numeric_literals: NumericLiteralSyntax::ANSI,
120 parameters: ParameterSyntax::ANSI,
121 session_variables: SessionVariableSyntax::ANSI,
122 identifier_syntax: IdentifierSyntax::ANSI,
123 // ANSI table-expression surface plus the PartiQL / SUPER table-position JSON path
124 // (`FROM src[0].a`) navigating a SUPER column, sqlparser-rs's `supports_partiql`.
125 table_expressions: TableExpressionSyntax {
126 table_json_path: true,
127 only: false,
128 table_sample: false,
129 parenthesized_joins: true,
130 table_alias_column_lists: true,
131 join_using_alias: false,
132 index_hints: false,
133 table_hints: false,
134 partition_selection: false,
135 base_table_alias_column_lists: true,
136 string_literal_aliases: false,
137 aliased_parenthesized_join: true,
138 bare_table_alias_is_bare_label: false,
139 table_version: false,
140 indexed_by: false,
141 prefix_colon_alias: false,
142 },
143 join_syntax: JoinSyntax::ANSI,
144 table_factor_syntax: TableFactorSyntax::ANSI,
145 expression_syntax: ExpressionSyntax::ANSI,
146 operator_syntax: OperatorSyntax::ANSI,
147 call_syntax: CallSyntax::ANSI,
148 string_func_forms: StringFuncForms::ANSI,
149 aggregate_call_syntax: AggregateCallSyntax::ANSI,
150 // `ILIKE`/`SIMILAR TO` are deferred (dialect-attributed to PostgreSQL in our docs, no
151 // Redshift citation) — every predicate knob is conservatively ANSI.
152 predicate_syntax: PredicateSyntax::ANSI,
153 pipe_operator: PipeOperator::StringConcat,
154 double_ampersand: DoubleAmpersand::Unsupported,
155 keyword_operators: KeywordOperators::Unsupported,
156 caret_operator: CaretOperator::Unsupported,
157 hash_bitwise_xor: false,
158 comment_syntax: CommentSyntax::ANSI,
159 mutation_syntax: MutationSyntax::ANSI,
160 statement_ddl_gates: StatementDdlGates::ANSI,
161 view_sequence_clause_syntax: ViewSequenceClauseSyntax::ANSI,
162 create_table_clause_syntax: CreateTableClauseSyntax::ANSI,
163 column_definition_syntax: ColumnDefinitionSyntax::ANSI,
164 constraint_syntax: ConstraintSyntax::ANSI,
165 index_alter_syntax: IndexAlterSyntax::ANSI,
166 existence_guards: ExistenceGuards::ANSI,
167 // `DISTINCT ON`/`QUALIFY` are deferred (see the module docs) — every SELECT knob is
168 // conservatively ANSI.
169 select_syntax: SelectSyntax::ANSI,
170 query_tail_syntax: QueryTailSyntax::ANSI,
171 grouping_syntax: GroupingSyntax::ANSI,
172 utility_syntax: UtilitySyntax::ANSI,
173 transaction_syntax: TransactionSyntax::ANSI,
174 show_syntax: ShowSyntax::ANSI,
175 maintenance_syntax: MaintenanceSyntax::ANSI,
176 access_control_syntax: AccessControlSyntax::ANSI,
177 type_name_syntax: TypeNameSyntax::ANSI,
178 // No Redshift-specific Tier-1 output spelling yet; render the portable ANSI canonical type
179 // names (a `TargetSpelling::Redshift` is render work a later ticket owns).
180 target_spelling: TargetSpelling::Ansi,
181 };
182}
183
184/// Prefer [`FeatureSet::REDSHIFT`] for struct update.
185pub const REDSHIFT: FeatureSet = FeatureSet::REDSHIFT;
186
187// Compile-time proof the Redshift preset claims no shared tokenizer trigger twice. It adds *no*
188// lexical trigger over ANSI (standard `"…"` identifier quoting, `'…'` strings — the same lexis as
189// the ANSI baseline), so this holds as trivially as ANSI's own assert. Kept as a ratchet so a
190// future Redshift delta that *does* add a contending trigger (e.g. `$$…$$` dollar-quoting) fails
191// the build here rather than silently shadowing a meaning.
192const _: () = assert!(FeatureSet::REDSHIFT.is_lexically_consistent());
193// The two sibling self-consistency registries are ratcheted the same way, so the
194// parse-entry `debug_assert!` folds all three to dead code for this preset: no refinement
195// flag rides an unset base, and no two features contend for one parser-position head.
196const _: () = assert!(FeatureSet::REDSHIFT.has_satisfied_feature_dependencies());
197const _: () = assert!(FeatureSet::REDSHIFT.has_no_grammar_conflict());
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202
203 #[test]
204 fn redshift_is_ansi_plus_only_the_lowercase_fold() {
205 // Closed-delta honesty: despite Redshift being a PostgreSQL-8 fork, this preset is
206 // ANSI plus exactly two divergent top-level axes (casing + table_json_path).
207 let ansi = FeatureSet::ANSI;
208 let redshift = FeatureSet::REDSHIFT;
209
210 assert_eq!(redshift.identifier_casing, Casing::Lower);
211 assert!(redshift.table_expressions.table_json_path);
212 // Deferred PG-heritage flags stay off.
213 assert!(!redshift.predicate_syntax.ilike);
214 assert!(!redshift.predicate_syntax.similar_to);
215 assert!(!redshift.select_syntax.distinct_on);
216 assert!(!redshift.select_syntax.qualify);
217
218 crate::dialect::closed_delta::assert_closed_delta(
219 &ansi,
220 &redshift,
221 &["identifier_casing", "table_expressions"],
222 );
223 }
224
225 #[test]
226 fn redshift_recovers_ansi_when_the_fold_is_forced_back() {
227 // The two deltas isolated: forcing the fold direction back to ANSI's `Upper` and
228 // dropping the PartiQL / SUPER table-position path recovers the ANSI FeatureSet
229 // verbatim, proving the lowercase fold and the `table_json_path` grant are the *only*
230 // divergences.
231 let ansi = FeatureSet::ANSI;
232 let redshift = FeatureSet::REDSHIFT;
233 assert_eq!(
234 FeatureSet {
235 identifier_casing: Casing::Upper,
236 table_expressions: TableExpressionSyntax {
237 table_json_path: false,
238 ..redshift.table_expressions
239 },
240 ..redshift
241 },
242 ansi,
243 );
244 }
245
246 #[test]
247 fn redshift_is_lexically_consistent_and_dependency_clean() {
248 // Both self-consistency registries must be clean: adding no lexical trigger over ANSI, the
249 // conflict registry is trivially empty, and riding no dependent grammar flag, the
250 // dependency registry is empty too.
251 let redshift = FeatureSet::REDSHIFT;
252 assert_eq!(redshift.lexical_conflict(), None);
253 assert!(redshift.is_lexically_consistent());
254 assert_eq!(redshift.feature_dependencies(), None);
255 assert!(redshift.has_satisfied_feature_dependencies());
256 assert_eq!(redshift.grammar_conflict(), None);
257 assert!(redshift.has_no_grammar_conflict());
258 }
259}