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}