sql_dialect_fmt_syntax/dialect.rs
1//! The SQL [`Dialect`] selector — the runtime seam for multi-dialect support.
2//!
3//! The engine (lossless lexer, never-fail parser, Doc-IR formatter) is dialect-agnostic; only the
4//! ~20% that differs between dialects — reserved keywords, lexer quoting/special tokens, which
5//! statements/operators the grammar accepts, and a few formatter rules — is gated on a [`Dialect`].
6//! Modeled on `sqlparser-rs`'s `Dialect` trait: rather than a trait object, a small `Copy` enum
7//! threads through the lexer, parser, and formatter, and the divergence points are expressed as
8//! `#[must_use]` predicate methods on it.
9//!
10//! Today every predicate returns the **Snowflake-correct** answer ([`Dialect::Snowflake`] is the
11//! [`Default`]), so adding the seam changes no behavior. The Databricks arms encode where the two
12//! dialects diverge and are refined in later phases.
13
14/// The SQL dialect a lex/parse/format request targets.
15///
16/// `#[non_exhaustive]` so further dialects can be added without it being a breaking change. Default
17/// is [`Dialect::Snowflake`].
18#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
19#[non_exhaustive]
20pub enum Dialect {
21 /// Snowflake SQL — the original and default dialect of this toolchain.
22 #[default]
23 Snowflake,
24 /// Databricks SQL / Spark SQL. Behavior is being filled in across later phases; today its
25 /// predicate answers describe the *intended* divergence but no Databricks-specific lexing,
26 /// parsing, or formatting is wired up yet.
27 Databricks,
28}
29
30impl Dialect {
31 /// Dollar quoting: `$$ ... $$` dollar-quoted bodies and `$1` / `$name` positional/variable
32 /// references. Snowflake only — Databricks has no `$$` body or `$n` reference.
33 #[must_use]
34 pub fn supports_dollar_quoting(self) -> bool {
35 matches!(self, Dialect::Snowflake)
36 }
37
38 /// The flow operator `->>` chaining statements into a pipeline. Snowflake only.
39 #[must_use]
40 pub fn supports_flow_operator(self) -> bool {
41 matches!(self, Dialect::Snowflake)
42 }
43
44 /// `COPY INTO <target> FROM <source>` bulk load/unload. Snowflake only.
45 #[must_use]
46 pub fn supports_copy_into(self) -> bool {
47 matches!(self, Dialect::Snowflake)
48 }
49
50 /// `// ...` line comments. Snowflake accepts them; Databricks/Spark uses `/` as an operator
51 /// and must not have a doubled slash consume the rest of the physical line.
52 #[must_use]
53 pub fn supports_double_slash_comments(self) -> bool {
54 matches!(self, Dialect::Snowflake)
55 }
56
57 /// Snowpipe DDL: `CREATE [OR REPLACE] PIPE <name> <properties> AS COPY INTO ...` and
58 /// `ALTER PIPE`. Snowflake only — the object does not exist in Databricks, where `pipe` stays an
59 /// ordinary identifier.
60 #[must_use]
61 pub fn supports_pipe_ddl(self) -> bool {
62 matches!(self, Dialect::Snowflake)
63 }
64
65 /// `CREATE SEMANTIC VIEW ...` semantic-layer DDL. Snowflake only.
66 #[must_use]
67 pub fn supports_semantic_view(self) -> bool {
68 matches!(self, Dialect::Snowflake)
69 }
70
71 /// SQL scripting blocks: `BEGIN ... END`, declarations, control-flow statements, and the `:=`
72 /// assignment operator. Snowflake and Databricks both support compound SQL blocks.
73 #[must_use]
74 pub fn supports_scripting_blocks(self) -> bool {
75 matches!(self, Dialect::Snowflake | Dialect::Databricks)
76 }
77
78 /// Stage references: `@stage` / `@~` / `@%table` paths in `FROM`, `COPY`, and `PUT`/`GET`.
79 /// Snowflake only.
80 #[must_use]
81 pub fn supports_stage_refs(self) -> bool {
82 matches!(self, Dialect::Snowflake)
83 }
84
85 /// Backtick-quoted identifiers: `` `col` ``. Databricks only (Snowflake quotes with `"`).
86 #[must_use]
87 pub fn supports_backtick_identifiers(self) -> bool {
88 matches!(self, Dialect::Databricks)
89 }
90
91 /// Databricks/Spark prefixed string literals such as raw strings (`r'...'`) and hex binary
92 /// literals (`X'1A'`).
93 #[must_use]
94 pub fn supports_prefixed_strings(self) -> bool {
95 matches!(self, Dialect::Databricks)
96 }
97
98 /// Databricks/Spark null-safe equality operator: `<=>`.
99 #[must_use]
100 pub fn supports_null_safe_eq(self) -> bool {
101 matches!(self, Dialect::Databricks)
102 }
103
104 /// `LATERAL VIEW explode(...)` table-generating clause. Databricks only.
105 #[must_use]
106 pub fn supports_lateral_view(self) -> bool {
107 matches!(self, Dialect::Databricks)
108 }
109
110 /// Delta/Spark table DDL options: `USING`, `LOCATION`, `TBLPROPERTIES`, `OPTIONS`, and
111 /// `PARTITIONED BY`. Databricks only.
112 #[must_use]
113 pub fn supports_delta_table_options(self) -> bool {
114 matches!(self, Dialect::Databricks)
115 }
116
117 /// Higher-order-function lambdas: `x -> expr` and `(x, y) -> expr`. Databricks only for now.
118 #[must_use]
119 pub fn supports_lambda_expr(self) -> bool {
120 matches!(self, Dialect::Databricks)
121 }
122
123 /// Time travel via `VERSION AS OF` / `TIMESTAMP AS OF`. Databricks only (Snowflake uses
124 /// `AT` / `BEFORE`).
125 #[must_use]
126 pub fn supports_as_of_travel(self) -> bool {
127 matches!(self, Dialect::Databricks)
128 }
129
130 /// Databricks/Spark query distribution clauses: `DISTRIBUTE BY`, `SORT BY`, and `CLUSTER BY`.
131 #[must_use]
132 pub fn supports_databricks_query_clauses(self) -> bool {
133 matches!(self, Dialect::Databricks)
134 }
135
136 /// Delta/Spark maintenance + cache statements — `VACUUM`, `OPTIMIZE … ZORDER BY`,
137 /// `INSERT OVERWRITE`, `CACHE`/`UNCACHE`/`REFRESH`, `DESCRIBE HISTORY`, and the
138 /// `WHEN NOT MATCHED BY SOURCE`/`INSERT *` MERGE extensions. Databricks only. The leading words
139 /// (`VACUUM`, `OPTIMIZE`, `CACHE`, …) are recognized **contextually** at statement start, so they
140 /// stay ordinary identifiers under Snowflake (and elsewhere under Databricks), and Snowflake
141 /// output is byte-identical.
142 #[must_use]
143 pub fn supports_delta_commands(self) -> bool {
144 matches!(self, Dialect::Databricks)
145 }
146}
147
148#[cfg(test)]
149mod tests {
150 use super::Dialect;
151
152 #[test]
153 fn default_is_snowflake() {
154 assert_eq!(Dialect::default(), Dialect::Snowflake);
155 }
156
157 #[test]
158 fn snowflake_only_predicates() {
159 let s = Dialect::Snowflake;
160 assert!(s.supports_dollar_quoting());
161 assert!(s.supports_flow_operator());
162 assert!(s.supports_copy_into());
163 assert!(s.supports_pipe_ddl());
164 assert!(s.supports_double_slash_comments());
165 assert!(s.supports_semantic_view());
166 assert!(s.supports_scripting_blocks());
167 assert!(s.supports_stage_refs());
168 assert!(!s.supports_backtick_identifiers());
169 assert!(!s.supports_prefixed_strings());
170 assert!(!s.supports_null_safe_eq());
171 assert!(!s.supports_lateral_view());
172 assert!(!s.supports_delta_table_options());
173 assert!(!s.supports_lambda_expr());
174 assert!(!s.supports_as_of_travel());
175 assert!(!s.supports_databricks_query_clauses());
176 assert!(!s.supports_delta_commands());
177 }
178
179 #[test]
180 fn databricks_only_predicates() {
181 let d = Dialect::Databricks;
182 assert!(!d.supports_dollar_quoting());
183 assert!(!d.supports_flow_operator());
184 assert!(!d.supports_copy_into());
185 assert!(!d.supports_pipe_ddl());
186 assert!(!d.supports_double_slash_comments());
187 assert!(!d.supports_semantic_view());
188 assert!(d.supports_scripting_blocks());
189 assert!(!d.supports_stage_refs());
190 assert!(d.supports_backtick_identifiers());
191 assert!(d.supports_prefixed_strings());
192 assert!(d.supports_null_safe_eq());
193 assert!(d.supports_lateral_view());
194 assert!(d.supports_delta_table_options());
195 assert!(d.supports_lambda_expr());
196 assert!(d.supports_as_of_travel());
197 assert!(d.supports_databricks_query_clauses());
198 assert!(d.supports_delta_commands());
199 }
200}