Skip to main content

squonk/dialect/
builtin.rs

1// SPDX-License-Identifier: MIT
2// Copyright (c) 2026 Moderately AI Inc.
3
4//! Runtime built-in dialect selection.
5//!
6//! [`Dialect::Ext`](crate::parser::Dialect::Ext) makes `&dyn Dialect` non-object-safe,
7//! so a config/CLI string cannot pick a compile-time dialect the way
8//! `parse_with(src, Postgres)` does. This module is the designed escape: a
9//! [`BuiltinDialect`] value-enum over the stock dialects — all of which share
10//! `Ext = NoExt` — plus a [`parse_with_builtin`] dispatcher that matches the value to
11//! the monomorphized parser and returns a plain [`Parsed`]. Each non-ANSI arm is
12//! gated by its cargo feature, so a name for a disabled (or unknown) dialect resolves
13//! to `None` rather than panicking.
14
15use std::fmt;
16use std::str::FromStr;
17
18use crate::ast::dialect::FeatureSet;
19use crate::dialect::Ansi;
20#[cfg(feature = "bigquery")]
21use crate::dialect::BigQuery;
22#[cfg(feature = "clickhouse")]
23use crate::dialect::ClickHouse;
24#[cfg(feature = "databricks")]
25use crate::dialect::Databricks;
26#[cfg(feature = "duckdb")]
27use crate::dialect::DuckDb;
28#[cfg(feature = "hive")]
29use crate::dialect::Hive;
30#[cfg(feature = "lenient")]
31use crate::dialect::Lenient;
32#[cfg(feature = "mssql")]
33use crate::dialect::Mssql;
34#[cfg(feature = "mysql")]
35use crate::dialect::MySql;
36#[cfg(feature = "postgres")]
37use crate::dialect::Postgres;
38#[cfg(feature = "redshift")]
39use crate::dialect::Redshift;
40#[cfg(feature = "snowflake")]
41use crate::dialect::Snowflake;
42#[cfg(feature = "sqlite")]
43use crate::dialect::Sqlite;
44use crate::error::ParseResult;
45use crate::parser::{
46    ParseOptions, Parsed, Recovered, parse_recovering_with_options, parse_with_options,
47};
48use crate::render::RenderDialect;
49use crate::tokenizer::{LexError, Token, TriviaIndex, tokenize_with, tokenize_with_trivia};
50
51/// A stock dialect chosen at runtime by value or name.
52///
53/// The arms present in a given build are exactly the dialects compiled in: [`Ansi`]
54/// always, every other dialect only with its cargo feature. It is `#[non_exhaustive]`
55/// because the variant set grows with new dialects and varies by feature, so downstream
56/// `match`es must carry a wildcard.
57///
58/// # Parsing and formatting
59///
60/// [`FromStr`] and [`Display`](fmt::Display) mirror the inherent
61/// [`from_name`](Self::from_name)/[`name`](Self::name) pair, so the type drops straight
62/// into config- and CLI-driven selection (clap's `value_parser!`, serde's string forms)
63/// without a hand-written adapter. Parsing an unknown or feature-disabled name is a typed
64/// [`ParseBuiltinDialectError`], never a panic; [`Default`] is [`Ansi`], the
65/// always-compiled baseline `parse` itself defaults to.
66///
67/// ```
68/// use squonk::dialect::BuiltinDialect;
69///
70/// let dialect: BuiltinDialect = "ansi".parse().expect("ansi is always built in");
71/// assert_eq!(dialect, BuiltinDialect::Ansi);
72/// assert_eq!(dialect, BuiltinDialect::default());
73/// assert_eq!(dialect.to_string(), "ansi"); // Display == canonical name
74/// assert!("no-such-dialect".parse::<BuiltinDialect>().is_err());
75/// ```
76#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
77#[non_exhaustive]
78pub enum BuiltinDialect {
79    /// The ANSI/standard baseline ([`Ansi`]).
80    Ansi,
81    /// PostgreSQL ([`Postgres`]); present only with the `postgres` feature.
82    #[cfg(feature = "postgres")]
83    Postgres,
84    /// MySQL ([`MySql`]); present only with the `mysql` feature.
85    #[cfg(feature = "mysql")]
86    MySql,
87    /// SQLite ([`Sqlite`]); present only with the `sqlite` feature.
88    #[cfg(feature = "sqlite")]
89    Sqlite,
90    /// DuckDB ([`DuckDb`]); present only with the `duckdb` feature.
91    #[cfg(feature = "duckdb")]
92    DuckDb,
93    /// BigQuery / ZetaSQL ([`BigQuery`]); present only with the `bigquery` feature.
94    #[cfg(feature = "bigquery")]
95    BigQuery,
96    /// Hive / HiveQL ([`Hive`]); present only with the `hive` feature.
97    #[cfg(feature = "hive")]
98    Hive,
99    /// ClickHouse ([`ClickHouse`]); present only with the `clickhouse` feature.
100    #[cfg(feature = "clickhouse")]
101    ClickHouse,
102    /// Databricks ([`Databricks`]); present only with the `databricks` feature.
103    #[cfg(feature = "databricks")]
104    Databricks,
105    /// MSSQL / T-SQL ([`Mssql`]); present only with the `mssql` feature.
106    #[cfg(feature = "mssql")]
107    Mssql,
108    /// Snowflake ([`Snowflake`]); present only with the `snowflake` feature.
109    #[cfg(feature = "snowflake")]
110    Snowflake,
111    /// Amazon Redshift ([`Redshift`]); present only with the `redshift` feature.
112    #[cfg(feature = "redshift")]
113    Redshift,
114    /// The permissive "parse anything" tooling union ([`Lenient`]); present only with
115    /// the `lenient` feature.
116    #[cfg(feature = "lenient")]
117    Lenient,
118}
119
120impl BuiltinDialect {
121    /// Every built-in dialect compiled into this build, ANSI first.
122    ///
123    /// A consumer enumerating selectable dialects (a CLI `--dialect` help text, a
124    /// config validator) reads this rather than hard-coding names that may be gated
125    /// out of the current build.
126    pub const ALL: &'static [BuiltinDialect] = &[
127        BuiltinDialect::Ansi,
128        #[cfg(feature = "postgres")]
129        BuiltinDialect::Postgres,
130        #[cfg(feature = "mysql")]
131        BuiltinDialect::MySql,
132        #[cfg(feature = "sqlite")]
133        BuiltinDialect::Sqlite,
134        #[cfg(feature = "duckdb")]
135        BuiltinDialect::DuckDb,
136        #[cfg(feature = "bigquery")]
137        BuiltinDialect::BigQuery,
138        #[cfg(feature = "hive")]
139        BuiltinDialect::Hive,
140        #[cfg(feature = "clickhouse")]
141        BuiltinDialect::ClickHouse,
142        #[cfg(feature = "databricks")]
143        BuiltinDialect::Databricks,
144        #[cfg(feature = "mssql")]
145        BuiltinDialect::Mssql,
146        #[cfg(feature = "snowflake")]
147        BuiltinDialect::Snowflake,
148        #[cfg(feature = "redshift")]
149        BuiltinDialect::Redshift,
150        #[cfg(feature = "lenient")]
151        BuiltinDialect::Lenient,
152    ];
153
154    /// Resolve a builtin by case-insensitive name and common aliases, returning
155    /// `None` for an unknown name *or* one whose dialect is not compiled into this
156    /// build (e.g. `"postgres"` without the `postgres` feature). Never panics — the
157    /// clean-error contract for config/CLI-driven selection.
158    ///
159    /// # Migrating from `datafusion-sqlparser-rs`
160    ///
161    /// `"generic"` is an alias for strict [`Ansi`] (the SQL:2016
162    /// standard), matching that crate's `AnsiDialect` strictness — **not** its
163    /// `GenericDialect`, a permissive catch-all that accepts non-standard surface such
164    /// as `COPY`. A `GenericDialect` consumer wanting that permissive behaviour should
165    /// select `"lenient"` (`Lenient`, the `lenient` feature) — our documented
166    /// parse-anything union and the true catch-all (see the preset spectrum on
167    /// [`FeatureSet`]). Selecting `"generic"` here is
168    /// therefore *stricter* than their `GenericDialect`: input it accepted (e.g. `COPY`)
169    /// now rejects.
170    pub fn from_name(name: &str) -> Option<Self> {
171        if name.eq_ignore_ascii_case("ansi") || name.eq_ignore_ascii_case("generic") {
172            return Some(Self::Ansi);
173        }
174        #[cfg(feature = "postgres")]
175        if name.eq_ignore_ascii_case("postgres")
176            || name.eq_ignore_ascii_case("postgresql")
177            || name.eq_ignore_ascii_case("pg")
178        {
179            return Some(Self::Postgres);
180        }
181        #[cfg(feature = "mysql")]
182        if name.eq_ignore_ascii_case("mysql") || name.eq_ignore_ascii_case("mariadb") {
183            return Some(Self::MySql);
184        }
185        #[cfg(feature = "sqlite")]
186        if name.eq_ignore_ascii_case("sqlite") || name.eq_ignore_ascii_case("sqlite3") {
187            return Some(Self::Sqlite);
188        }
189        #[cfg(feature = "duckdb")]
190        if name.eq_ignore_ascii_case("duckdb") || name.eq_ignore_ascii_case("duck") {
191            return Some(Self::DuckDb);
192        }
193        #[cfg(feature = "bigquery")]
194        if name.eq_ignore_ascii_case("bigquery")
195            || name.eq_ignore_ascii_case("bq")
196            || name.eq_ignore_ascii_case("zetasql")
197        {
198            return Some(Self::BigQuery);
199        }
200        #[cfg(feature = "hive")]
201        if name.eq_ignore_ascii_case("hive") || name.eq_ignore_ascii_case("hiveql") {
202            return Some(Self::Hive);
203        }
204        #[cfg(feature = "clickhouse")]
205        if name.eq_ignore_ascii_case("clickhouse") || name.eq_ignore_ascii_case("ch") {
206            return Some(Self::ClickHouse);
207        }
208        #[cfg(feature = "databricks")]
209        if name.eq_ignore_ascii_case("databricks") || name.eq_ignore_ascii_case("dbx") {
210            return Some(Self::Databricks);
211        }
212        #[cfg(feature = "mssql")]
213        if name.eq_ignore_ascii_case("mssql")
214            || name.eq_ignore_ascii_case("tsql")
215            || name.eq_ignore_ascii_case("sqlserver")
216        {
217            return Some(Self::Mssql);
218        }
219        #[cfg(feature = "snowflake")]
220        if name.eq_ignore_ascii_case("snowflake") || name.eq_ignore_ascii_case("sf") {
221            return Some(Self::Snowflake);
222        }
223        // Canonical `redshift` plus the unambiguous `amazonredshift`. The bare abbreviation `rs`
224        // is deliberately *not* accepted — too generic (Rust source extension, "right side", a
225        // common column name) to claim as a dialect selector.
226        #[cfg(feature = "redshift")]
227        if name.eq_ignore_ascii_case("redshift") || name.eq_ignore_ascii_case("amazonredshift") {
228            return Some(Self::Redshift);
229        }
230        #[cfg(feature = "lenient")]
231        if name.eq_ignore_ascii_case("lenient") || name.eq_ignore_ascii_case("permissive") {
232            return Some(Self::Lenient);
233        }
234        None
235    }
236
237    /// The canonical lower-case name of this builtin — the primary spelling
238    /// [`from_name`](Self::from_name) accepts, so `from_name(d.name()) == Some(d)`.
239    pub const fn name(self) -> &'static str {
240        match self {
241            Self::Ansi => "ansi",
242            #[cfg(feature = "postgres")]
243            Self::Postgres => "postgres",
244            #[cfg(feature = "mysql")]
245            Self::MySql => "mysql",
246            #[cfg(feature = "sqlite")]
247            Self::Sqlite => "sqlite",
248            #[cfg(feature = "duckdb")]
249            Self::DuckDb => "duckdb",
250            #[cfg(feature = "bigquery")]
251            Self::BigQuery => "bigquery",
252            #[cfg(feature = "hive")]
253            Self::Hive => "hive",
254            #[cfg(feature = "clickhouse")]
255            Self::ClickHouse => "clickhouse",
256            #[cfg(feature = "databricks")]
257            Self::Databricks => "databricks",
258            #[cfg(feature = "mssql")]
259            Self::Mssql => "mssql",
260            #[cfg(feature = "snowflake")]
261            Self::Snowflake => "snowflake",
262            #[cfg(feature = "redshift")]
263            Self::Redshift => "redshift",
264            #[cfg(feature = "lenient")]
265            Self::Lenient => "lenient",
266        }
267    }
268
269    /// Every case-insensitive name accepted for this builtin in this build, with the
270    /// canonical spelling first.
271    pub const fn aliases(self) -> &'static [&'static str] {
272        match self {
273            Self::Ansi => &["ansi", "generic"],
274            #[cfg(feature = "postgres")]
275            Self::Postgres => &["postgres", "postgresql", "pg"],
276            #[cfg(feature = "mysql")]
277            Self::MySql => &["mysql", "mariadb"],
278            #[cfg(feature = "sqlite")]
279            Self::Sqlite => &["sqlite", "sqlite3"],
280            #[cfg(feature = "duckdb")]
281            Self::DuckDb => &["duckdb", "duck"],
282            #[cfg(feature = "bigquery")]
283            Self::BigQuery => &["bigquery", "bq", "zetasql"],
284            #[cfg(feature = "hive")]
285            Self::Hive => &["hive", "hiveql"],
286            #[cfg(feature = "clickhouse")]
287            Self::ClickHouse => &["clickhouse", "ch"],
288            #[cfg(feature = "databricks")]
289            Self::Databricks => &["databricks", "dbx"],
290            #[cfg(feature = "mssql")]
291            Self::Mssql => &["mssql", "tsql", "sqlserver"],
292            #[cfg(feature = "snowflake")]
293            Self::Snowflake => &["snowflake", "sf"],
294            #[cfg(feature = "redshift")]
295            Self::Redshift => &["redshift", "amazonredshift"],
296            #[cfg(feature = "lenient")]
297            Self::Lenient => &["lenient", "permissive"],
298        }
299    }
300
301    /// The const [`FeatureSet`] preset this builtin parses and renders with — the
302    /// runtime analogue of `Dialect::features` for code that needs the dialect data
303    /// without constructing a `Parser`.
304    pub fn features(self) -> &'static FeatureSet {
305        match self {
306            Self::Ansi => &FeatureSet::ANSI,
307            #[cfg(feature = "postgres")]
308            Self::Postgres => &FeatureSet::POSTGRES,
309            #[cfg(feature = "mysql")]
310            Self::MySql => &FeatureSet::MYSQL,
311            #[cfg(feature = "sqlite")]
312            Self::Sqlite => &FeatureSet::SQLITE,
313            #[cfg(feature = "duckdb")]
314            Self::DuckDb => &FeatureSet::DUCKDB,
315            #[cfg(feature = "bigquery")]
316            Self::BigQuery => &FeatureSet::BIGQUERY,
317            #[cfg(feature = "hive")]
318            Self::Hive => &FeatureSet::HIVE,
319            #[cfg(feature = "clickhouse")]
320            Self::ClickHouse => &FeatureSet::CLICKHOUSE,
321            #[cfg(feature = "databricks")]
322            Self::Databricks => &FeatureSet::DATABRICKS,
323            #[cfg(feature = "mssql")]
324            Self::Mssql => &FeatureSet::MSSQL,
325            #[cfg(feature = "snowflake")]
326            Self::Snowflake => &FeatureSet::SNOWFLAKE,
327            #[cfg(feature = "redshift")]
328            Self::Redshift => &FeatureSet::REDSHIFT,
329            #[cfg(feature = "lenient")]
330            Self::Lenient => &FeatureSet::LENIENT,
331        }
332    }
333}
334
335// The std-trait surface below (`Display`/`FromStr`/`Default` plus the new public
336// `ParseBuiltinDialectError`) is a purely additive extension of `BuiltinDialect`: no
337// existing item changes shape, so it is semver-compatible against the
338// `release/semver-baseline.toml` contract (C-COMMON-TRAITS — a string-selected config
339// enum implements the std string traits directly rather than via inherent methods alone).
340
341impl fmt::Display for BuiltinDialect {
342    /// Writes the canonical [`name`](Self::name) — the spelling
343    /// [`from_name`](Self::from_name) round-trips.
344    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
345        f.write_str(self.name())
346    }
347}
348
349impl FromStr for BuiltinDialect {
350    type Err = ParseBuiltinDialectError;
351
352    /// Resolves a case-insensitive name or alias via [`from_name`](Self::from_name),
353    /// yielding a [`ParseBuiltinDialectError`] for a name that maps to no built-in
354    /// dialect in this build.
355    fn from_str(s: &str) -> Result<Self, Self::Err> {
356        Self::from_name(s).ok_or_else(|| ParseBuiltinDialectError { name: s.into() })
357    }
358}
359
360impl Default for BuiltinDialect {
361    /// [`Ansi`] — the always-compiled SQL-standard baseline `parse` defaults to, so the
362    /// runtime selector defaults to the same dialect as the compile-time entry point.
363    fn default() -> Self {
364        Self::Ansi
365    }
366}
367
368/// The error [`BuiltinDialect`]'s [`FromStr`] returns for a name that resolves to no
369/// built-in dialect in this build.
370///
371/// The name is either genuinely unrecognized or names a real dialect whose cargo feature
372/// is disabled — [`BuiltinDialect::from_name`] draws exactly the same line with its
373/// `None`, and this error carries the offending [`name`](Self::name) for a diagnostic.
374#[derive(Clone, Debug, PartialEq, Eq)]
375pub struct ParseBuiltinDialectError {
376    name: Box<str>,
377}
378
379impl ParseBuiltinDialectError {
380    /// The unrecognized name as supplied to [`FromStr`].
381    pub fn name(&self) -> &str {
382        &self.name
383    }
384}
385
386impl fmt::Display for ParseBuiltinDialectError {
387    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
388        write!(
389            f,
390            "unknown built-in dialect {:?}: not a recognized name or alias, or its dialect \
391             is not compiled into this build",
392            self.name,
393        )
394    }
395}
396
397impl std::error::Error for ParseBuiltinDialectError {}
398
399/// Parse `src` under a runtime-selected built-in `dialect` into an owned [`Parsed`].
400///
401/// Dispatches by value to the monomorphized [`parse_with`](crate::parse_with): every
402/// builtin's `Ext` is [`NoExt`](crate::ast::NoExt), so all arms share the single
403/// [`Parsed`] return type — the designed object-safe runtime path, without a
404/// `dyn Dialect`. To select the dialect from a string, resolve it with
405/// [`BuiltinDialect::from_name`] first; an unknown or disabled name is a clean `None`
406/// there, never a panic here.
407///
408/// # Errors
409///
410/// Returns the first [`ParseError`](crate::error::ParseError), exactly as
411/// [`parse_with`](crate::parse_with) does.
412///
413/// ```
414/// use squonk::dialect::{BuiltinDialect, parse_with_builtin};
415///
416/// let dialect = BuiltinDialect::from_name("ansi").expect("ansi is always built in");
417/// let parsed = parse_with_builtin("SELECT 1", dialect).expect("`SELECT 1` parses");
418/// assert_eq!(parsed.statements().len(), 1);
419///
420/// assert!(BuiltinDialect::from_name("nope").is_none());
421/// ```
422pub fn parse_with_builtin(src: &str, dialect: BuiltinDialect) -> ParseResult<Parsed> {
423    parse_with_builtin_options(src, dialect, ParseOptions::default())
424}
425
426/// Parse `src` under a runtime-selected built-in `dialect`, honouring
427/// [`ParseOptions`].
428///
429/// This is the optioned counterpart to [`parse_with_builtin`]. It keeps runtime
430/// dialect dispatch centralized so language bindings do not have to hand-copy the
431/// same feature-gated match.
432pub fn parse_with_builtin_options(
433    src: &str,
434    dialect: BuiltinDialect,
435    options: ParseOptions,
436) -> ParseResult<Parsed> {
437    match dialect {
438        BuiltinDialect::Ansi => parse_with_options(src, Ansi, options),
439        #[cfg(feature = "postgres")]
440        BuiltinDialect::Postgres => parse_with_options(src, Postgres, options),
441        #[cfg(feature = "mysql")]
442        BuiltinDialect::MySql => parse_with_options(src, MySql, options),
443        #[cfg(feature = "sqlite")]
444        BuiltinDialect::Sqlite => parse_with_options(src, Sqlite, options),
445        #[cfg(feature = "duckdb")]
446        BuiltinDialect::DuckDb => parse_with_options(src, DuckDb, options),
447        #[cfg(feature = "bigquery")]
448        BuiltinDialect::BigQuery => parse_with_options(src, BigQuery, options),
449        #[cfg(feature = "hive")]
450        BuiltinDialect::Hive => parse_with_options(src, Hive, options),
451        #[cfg(feature = "clickhouse")]
452        BuiltinDialect::ClickHouse => parse_with_options(src, ClickHouse, options),
453        #[cfg(feature = "databricks")]
454        BuiltinDialect::Databricks => parse_with_options(src, Databricks, options),
455        #[cfg(feature = "mssql")]
456        BuiltinDialect::Mssql => parse_with_options(src, Mssql, options),
457        #[cfg(feature = "snowflake")]
458        BuiltinDialect::Snowflake => parse_with_options(src, Snowflake, options),
459        #[cfg(feature = "redshift")]
460        BuiltinDialect::Redshift => parse_with_options(src, Redshift, options),
461        #[cfg(feature = "lenient")]
462        BuiltinDialect::Lenient => parse_with_options(src, Lenient, options),
463    }
464}
465
466/// Parse `src` under a runtime-selected built-in `dialect`, recovering past
467/// statement errors to return the partial tree plus diagnostics.
468pub fn parse_recovering_with_builtin(src: &str, dialect: BuiltinDialect) -> ParseResult<Recovered> {
469    parse_recovering_with_builtin_options(src, dialect, ParseOptions::default())
470}
471
472/// [`parse_recovering_with_builtin`] honouring [`ParseOptions`].
473pub fn parse_recovering_with_builtin_options(
474    src: &str,
475    dialect: BuiltinDialect,
476    options: ParseOptions,
477) -> ParseResult<Recovered> {
478    match dialect {
479        BuiltinDialect::Ansi => parse_recovering_with_options(src, Ansi, options),
480        #[cfg(feature = "postgres")]
481        BuiltinDialect::Postgres => parse_recovering_with_options(src, Postgres, options),
482        #[cfg(feature = "mysql")]
483        BuiltinDialect::MySql => parse_recovering_with_options(src, MySql, options),
484        #[cfg(feature = "sqlite")]
485        BuiltinDialect::Sqlite => parse_recovering_with_options(src, Sqlite, options),
486        #[cfg(feature = "duckdb")]
487        BuiltinDialect::DuckDb => parse_recovering_with_options(src, DuckDb, options),
488        #[cfg(feature = "bigquery")]
489        BuiltinDialect::BigQuery => parse_recovering_with_options(src, BigQuery, options),
490        #[cfg(feature = "hive")]
491        BuiltinDialect::Hive => parse_recovering_with_options(src, Hive, options),
492        #[cfg(feature = "clickhouse")]
493        BuiltinDialect::ClickHouse => parse_recovering_with_options(src, ClickHouse, options),
494        #[cfg(feature = "databricks")]
495        BuiltinDialect::Databricks => parse_recovering_with_options(src, Databricks, options),
496        #[cfg(feature = "mssql")]
497        BuiltinDialect::Mssql => parse_recovering_with_options(src, Mssql, options),
498        #[cfg(feature = "snowflake")]
499        BuiltinDialect::Snowflake => parse_recovering_with_options(src, Snowflake, options),
500        #[cfg(feature = "redshift")]
501        BuiltinDialect::Redshift => parse_recovering_with_options(src, Redshift, options),
502        #[cfg(feature = "lenient")]
503        BuiltinDialect::Lenient => parse_recovering_with_options(src, Lenient, options),
504    }
505}
506
507/// Tokenize `src` under a runtime-selected built-in `dialect`.
508pub fn tokenize_with_builtin(src: &str, dialect: BuiltinDialect) -> Result<Vec<Token>, LexError> {
509    tokenize_with(src, dialect.features())
510}
511
512/// Tokenize `src` under a runtime-selected built-in `dialect`, capturing trivia.
513pub fn tokenize_with_builtin_trivia(
514    src: &str,
515    dialect: BuiltinDialect,
516) -> Result<(Vec<Token>, TriviaIndex), LexError> {
517    tokenize_with_trivia(src, dialect.features())
518}
519
520impl RenderDialect for BuiltinDialect {
521    fn render_features(&self) -> FeatureSet {
522        self.features().clone()
523    }
524}
525
526#[cfg(test)]
527mod tests {
528    use super::*;
529
530    #[test]
531    fn ansi_is_always_built_in_and_round_trips_its_name() {
532        let ansi = BuiltinDialect::from_name("ansi").expect("ansi resolves");
533        assert_eq!(ansi, BuiltinDialect::Ansi);
534        // Case-insensitive, with the `generic` alias.
535        assert_eq!(
536            BuiltinDialect::from_name("ANSI"),
537            Some(BuiltinDialect::Ansi)
538        );
539        assert_eq!(
540            BuiltinDialect::from_name("Generic"),
541            Some(BuiltinDialect::Ansi)
542        );
543        // name() is the inverse of the canonical from_name spelling.
544        for dialect in BuiltinDialect::ALL {
545            assert_eq!(BuiltinDialect::from_name(dialect.name()), Some(*dialect));
546        }
547        assert_eq!(BuiltinDialect::Ansi.features(), &FeatureSet::ANSI);
548    }
549
550    #[test]
551    fn unknown_name_is_a_clean_none() {
552        // The clean-error contract: never a panic for an unrecognized name. `duckdb`,
553        // `snowflake`, `databricks`, and `mssql`/`tsql` are real dialects behind their cargo
554        // features, so they are deliberately absent from this unknown-name set — their positive
555        // resolution is proven by `duckdb_builtin_resolves_and_parses_its_surface` /
556        // `snowflake_builtin_resolves_and_parses_its_surface` /
557        // `databricks_builtin_resolves_and_parses_its_surface` /
558        // `mssql_builtin_resolves_and_parses_its_surface`.
559        for name in ["", "oracle", "postgre", "my sql"] {
560            assert_eq!(BuiltinDialect::from_name(name), None, "{name:?}");
561        }
562    }
563
564    #[test]
565    fn display_and_fromstr_round_trip_over_all_including_aliases_and_case() {
566        for &dialect in BuiltinDialect::ALL {
567            // Display == canonical name, and FromStr inverts both the canonical name and
568            // the `Display` string.
569            assert_eq!(dialect.to_string(), dialect.name());
570            assert_eq!(dialect.name().parse::<BuiltinDialect>(), Ok(dialect));
571            assert_eq!(dialect.to_string().parse::<BuiltinDialect>(), Ok(dialect));
572            // Every accepted alias resolves through `FromStr`, case-insensitively.
573            for alias in dialect.aliases() {
574                assert_eq!(alias.parse::<BuiltinDialect>(), Ok(dialect), "{alias:?}");
575                assert_eq!(
576                    alias.to_uppercase().parse::<BuiltinDialect>(),
577                    Ok(dialect),
578                    "{alias:?} upper",
579                );
580            }
581        }
582        assert_eq!(BuiltinDialect::default(), BuiltinDialect::Ansi);
583    }
584
585    #[test]
586    fn fromstr_error_carries_the_name_and_is_a_std_error() {
587        let err = "no-such-dialect".parse::<BuiltinDialect>().unwrap_err();
588        assert_eq!(err.name(), "no-such-dialect");
589        // C-GOOD-ERR: a real `Error`, not `()`, with a non-empty diagnostic Display.
590        let as_error: &dyn std::error::Error = &err;
591        assert!(as_error.to_string().contains("no-such-dialect"));
592    }
593
594    #[test]
595    fn parse_with_builtin_parses_under_ansi() {
596        let parsed = parse_with_builtin("SELECT 1", BuiltinDialect::Ansi).expect("parses");
597        assert_eq!(parsed.statements().len(), 1);
598    }
599
600    #[cfg(feature = "postgres")]
601    #[test]
602    fn postgres_builtin_resolves_and_parses_its_surface() {
603        assert_eq!(
604            BuiltinDialect::from_name("postgresql"),
605            Some(BuiltinDialect::Postgres)
606        );
607        assert_eq!(
608            BuiltinDialect::from_name("PG"),
609            Some(BuiltinDialect::Postgres)
610        );
611        assert_eq!(BuiltinDialect::Postgres.features(), &FeatureSet::POSTGRES);
612        // `$1` positional parameters are a PostgreSQL-only lexical form: it parses
613        // under the runtime-selected Postgres builtin and is rejected under ANSI.
614        assert!(parse_with_builtin("SELECT $1", BuiltinDialect::Postgres).is_ok());
615        assert!(parse_with_builtin("SELECT $1", BuiltinDialect::Ansi).is_err());
616    }
617
618    #[cfg(feature = "mysql")]
619    #[test]
620    fn mysql_builtin_resolves_and_parses_its_surface() {
621        assert_eq!(
622            BuiltinDialect::from_name("MySQL"),
623            Some(BuiltinDialect::MySql)
624        );
625        assert_eq!(BuiltinDialect::MySql.features(), &FeatureSet::MYSQL);
626        // The `LIMIT <offset>, <count>` comma form is MySQL-only: parses under the
627        // runtime-selected MySQL builtin, rejected under ANSI.
628        assert!(parse_with_builtin("SELECT a FROM t LIMIT 5, 10", BuiltinDialect::MySql).is_ok());
629        assert!(parse_with_builtin("SELECT a FROM t LIMIT 5, 10", BuiltinDialect::Ansi).is_err());
630    }
631
632    #[cfg(feature = "sqlite")]
633    #[test]
634    fn sqlite_builtin_resolves_and_parses_its_surface() {
635        assert_eq!(
636            BuiltinDialect::from_name("SQLite"),
637            Some(BuiltinDialect::Sqlite)
638        );
639        assert_eq!(
640            BuiltinDialect::from_name("sqlite3"),
641            Some(BuiltinDialect::Sqlite)
642        );
643        assert_eq!(BuiltinDialect::Sqlite.features(), &FeatureSet::SQLITE);
644        // The `==` equality spelling and `$name` placeholder are SQLite-only lexical
645        // forms: they parse under the runtime-selected SQLite builtin, rejected under ANSI.
646        assert!(parse_with_builtin("SELECT 1 == 1", BuiltinDialect::Sqlite).is_ok());
647        assert!(parse_with_builtin("SELECT 1 == 1", BuiltinDialect::Ansi).is_err());
648        assert!(parse_with_builtin("SELECT $value", BuiltinDialect::Sqlite).is_ok());
649    }
650
651    #[cfg(feature = "duckdb")]
652    #[test]
653    fn duckdb_builtin_resolves_and_parses_its_surface() {
654        assert_eq!(
655            BuiltinDialect::from_name("DuckDB"),
656            Some(BuiltinDialect::DuckDb)
657        );
658        assert_eq!(
659            BuiltinDialect::from_name("duck"),
660            Some(BuiltinDialect::DuckDb)
661        );
662        assert_eq!(BuiltinDialect::DuckDb.features(), &FeatureSet::DUCKDB);
663        // The `0x` radix integer form is a DuckDB-only lexical widening over ANSI: with
664        // the trailing `+ 1` forcing the hex reading it parses under the runtime-selected
665        // DuckDB builtin and rejects under ANSI (which lexes `0 AS xFF` then trailing `+`).
666        assert!(parse_with_builtin("SELECT 0xFF + 1", BuiltinDialect::DuckDb).is_ok());
667        assert!(parse_with_builtin("SELECT 0xFF + 1", BuiltinDialect::Ansi).is_err());
668        // The bare `SELECT` empty-target list is the DuckDB tightening: rejected under
669        // DuckDB, accepted under the runtime-selected Postgres builtin.
670        assert!(parse_with_builtin("SELECT", BuiltinDialect::DuckDb).is_err());
671    }
672
673    #[cfg(feature = "bigquery")]
674    #[test]
675    fn bigquery_builtin_resolves_and_parses_its_surface() {
676        assert_eq!(
677            BuiltinDialect::from_name("BigQuery"),
678            Some(BuiltinDialect::BigQuery)
679        );
680        // Both alternate names resolve — `bq` and `zetasql` — alongside canonical `bigquery`.
681        assert_eq!(
682            BuiltinDialect::from_name("bq"),
683            Some(BuiltinDialect::BigQuery)
684        );
685        assert_eq!(
686            BuiltinDialect::from_name("ZetaSQL"),
687            Some(BuiltinDialect::BigQuery)
688        );
689        assert_eq!(BuiltinDialect::BigQuery.features(), &FeatureSet::BIGQUERY);
690        // `UNNEST(...) WITH OFFSET` is the BigQuery-only surface this preset makes real: it
691        // parses under the runtime-selected BigQuery builtin and is rejected under ANSI (which
692        // has no first-class UNNEST factor at all).
693        assert!(
694            parse_with_builtin(
695                "SELECT * FROM UNNEST(arr) WITH OFFSET AS pos",
696                BuiltinDialect::BigQuery
697            )
698            .is_ok()
699        );
700        assert!(
701            parse_with_builtin(
702                "SELECT * FROM UNNEST(arr) WITH OFFSET AS pos",
703                BuiltinDialect::Ansi
704            )
705            .is_err()
706        );
707    }
708
709    #[cfg(feature = "hive")]
710    #[test]
711    fn hive_builtin_resolves_and_parses_its_surface() {
712        assert_eq!(
713            BuiltinDialect::from_name("Hive"),
714            Some(BuiltinDialect::Hive)
715        );
716        // The alternate name resolves — `hiveql` — alongside canonical `hive`.
717        assert_eq!(
718            BuiltinDialect::from_name("HiveQL"),
719            Some(BuiltinDialect::Hive)
720        );
721        assert_eq!(BuiltinDialect::Hive.features(), &FeatureSet::HIVE);
722        // The sided `LEFT SEMI JOIN` is the Hive-originated surface this preset makes real: it
723        // parses under the runtime-selected Hive builtin and is rejected under ANSI (which has
724        // no sided semi-/anti-join spelling).
725        assert!(
726            parse_with_builtin(
727                "SELECT * FROM a LEFT SEMI JOIN b ON a.x = b.x",
728                BuiltinDialect::Hive
729            )
730            .is_ok()
731        );
732        assert!(
733            parse_with_builtin(
734                "SELECT * FROM a LEFT SEMI JOIN b ON a.x = b.x",
735                BuiltinDialect::Ansi
736            )
737            .is_err()
738        );
739    }
740
741    #[cfg(feature = "clickhouse")]
742    #[test]
743    fn clickhouse_builtin_resolves_and_parses_its_surface() {
744        assert_eq!(
745            BuiltinDialect::from_name("ClickHouse"),
746            Some(BuiltinDialect::ClickHouse)
747        );
748        assert_eq!(
749            BuiltinDialect::from_name("ch"),
750            Some(BuiltinDialect::ClickHouse)
751        );
752        assert_eq!(
753            BuiltinDialect::ClickHouse.features(),
754            &FeatureSet::CLICKHOUSE
755        );
756        // The ClickHouse `SETTINGS` query tail is a ClickHouse-only clause: it parses
757        // under the runtime-selected ClickHouse builtin and is rejected under ANSI (which
758        // leaves the trailing `SETTINGS …` unconsumed).
759        assert!(
760            parse_with_builtin(
761                "SELECT a FROM t SETTINGS max_threads = 8",
762                BuiltinDialect::ClickHouse
763            )
764            .is_ok()
765        );
766        assert!(
767            parse_with_builtin(
768                "SELECT a FROM t SETTINGS max_threads = 8",
769                BuiltinDialect::Ansi
770            )
771            .is_err()
772        );
773    }
774
775    #[cfg(feature = "snowflake")]
776    #[test]
777    fn snowflake_builtin_resolves_and_parses_its_surface() {
778        assert_eq!(
779            BuiltinDialect::from_name("Snowflake"),
780            Some(BuiltinDialect::Snowflake)
781        );
782        assert_eq!(
783            BuiltinDialect::from_name("sf"),
784            Some(BuiltinDialect::Snowflake)
785        );
786        assert_eq!(BuiltinDialect::Snowflake.features(), &FeatureSet::SNOWFLAKE);
787        // The `base:key` semi-structured path is the Snowflake-only accessor this preset
788        // exposes: it parses under the runtime-selected Snowflake builtin and is rejected
789        // under ANSI (where `:` after an identifier is unexpected).
790        assert!(
791            parse_with_builtin("SELECT src:customer[0].name", BuiltinDialect::Snowflake).is_ok()
792        );
793        assert!(parse_with_builtin("SELECT src:customer[0].name", BuiltinDialect::Ansi).is_err());
794    }
795
796    #[cfg(feature = "redshift")]
797    #[test]
798    fn redshift_builtin_resolves_and_defers_its_pg_heritage_surface() {
799        assert_eq!(
800            BuiltinDialect::from_name("Redshift"),
801            Some(BuiltinDialect::Redshift)
802        );
803        // The alternate name resolves — `amazonredshift` — alongside canonical `redshift`.
804        assert_eq!(
805            BuiltinDialect::from_name("AmazonRedshift"),
806            Some(BuiltinDialect::Redshift)
807        );
808        // `rs` is deliberately not a Redshift alias (too generic).
809        assert_eq!(BuiltinDialect::from_name("rs"), None);
810        assert_eq!(BuiltinDialect::Redshift.features(), &FeatureSet::REDSHIFT);
811        // Redshift's ANSI-verbatim grammar parses the shared surface, and its lexis is standard —
812        // `"…"` is a quoted identifier just like ANSI (unlike Hive/BigQuery).
813        assert!(parse_with_builtin("SELECT \"Col\" FROM t", BuiltinDialect::Redshift).is_ok());
814        // The conservative-off deferral, pinned at the runtime layer: the PostgreSQL-heritage
815        // `ILIKE` Redshift genuinely accepts is deferred here, so it rejects under the
816        // runtime-selected Redshift builtin exactly as under ANSI — while Postgres accepts it.
817        assert!(
818            parse_with_builtin(
819                "SELECT * FROM t WHERE name ILIKE 'a%'",
820                BuiltinDialect::Redshift
821            )
822            .is_err()
823        );
824    }
825
826    #[cfg(feature = "databricks")]
827    #[test]
828    fn databricks_builtin_resolves_and_parses_its_surface() {
829        assert_eq!(
830            BuiltinDialect::from_name("Databricks"),
831            Some(BuiltinDialect::Databricks)
832        );
833        assert_eq!(
834            BuiltinDialect::from_name("dbx"),
835            Some(BuiltinDialect::Databricks)
836        );
837        assert_eq!(
838            BuiltinDialect::Databricks.features(),
839            &FeatureSet::DATABRICKS
840        );
841        // The sided `LEFT SEMI JOIN` is the Databricks-only join family this preset makes
842        // real: it parses under the runtime-selected Databricks builtin and is rejected
843        // under ANSI (where `SEMI` after `LEFT` is unexpected).
844        assert!(
845            parse_with_builtin(
846                "SELECT * FROM a LEFT SEMI JOIN b ON a.x = b.x",
847                BuiltinDialect::Databricks
848            )
849            .is_ok()
850        );
851        assert!(
852            parse_with_builtin(
853                "SELECT * FROM a LEFT SEMI JOIN b ON a.x = b.x",
854                BuiltinDialect::Ansi
855            )
856            .is_err()
857        );
858    }
859
860    #[cfg(feature = "mssql")]
861    #[test]
862    fn mssql_builtin_resolves_and_parses_its_surface() {
863        assert_eq!(
864            BuiltinDialect::from_name("MSSQL"),
865            Some(BuiltinDialect::Mssql)
866        );
867        // Both alternate names resolve — `tsql` and `sqlserver` — alongside canonical `mssql`.
868        assert_eq!(
869            BuiltinDialect::from_name("tsql"),
870            Some(BuiltinDialect::Mssql)
871        );
872        assert_eq!(
873            BuiltinDialect::from_name("SqlServer"),
874            Some(BuiltinDialect::Mssql)
875        );
876        assert_eq!(BuiltinDialect::Mssql.features(), &FeatureSet::MSSQL);
877        // `CROSS APPLY` is the MSSQL-only join family this preset makes real: it parses under
878        // the runtime-selected MSSQL builtin and is rejected under ANSI (where `APPLY` in join
879        // position is unexpected).
880        assert!(
881            parse_with_builtin(
882                "SELECT * FROM a CROSS APPLY (SELECT 1) AS t",
883                BuiltinDialect::Mssql
884            )
885            .is_ok()
886        );
887        assert!(
888            parse_with_builtin(
889                "SELECT * FROM a CROSS APPLY (SELECT 1) AS t",
890                BuiltinDialect::Ansi
891            )
892            .is_err()
893        );
894    }
895
896    #[cfg(feature = "lenient")]
897    #[test]
898    fn lenient_builtin_resolves_and_parses_its_union() {
899        assert_eq!(
900            BuiltinDialect::from_name("Lenient"),
901            Some(BuiltinDialect::Lenient)
902        );
903        assert_eq!(
904            BuiltinDialect::from_name("permissive"),
905            Some(BuiltinDialect::Lenient)
906        );
907        assert_eq!(BuiltinDialect::Lenient.features(), &FeatureSet::LENIENT);
908        // The multi-quote union is the distinctive capability: all three identifier
909        // styles parse under the runtime-selected Lenient builtin, where ANSI rejects the
910        // non-standard ones.
911        assert!(
912            parse_with_builtin(r#"SELECT "a", `b`, [c] FROM t"#, BuiltinDialect::Lenient).is_ok()
913        );
914        assert!(parse_with_builtin("SELECT `b` FROM t", BuiltinDialect::Ansi).is_err());
915    }
916}