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