Skip to main content

razeri_plugin_api/
dialect.rs

1// Copyright (C) 2026 Gabriel Shea
2// Licensed under either of the Apache License, Version 2.0 or the MIT
3// license, at your option.
4
5//! Platform dialect trait and registry for database SQL rendering.
6//!
7//! [`PlatformDialect`] extends the rendering trait hierarchy from
8//! `razeri-plugin-contract` with plugin metadata and registration support.
9//! [`PlatformRegistry`] resolves dialects by canonical string name,
10//! so built-in and third-party plugins are looked up the same way — there is no
11//! closed platform enum, and an unknown name is an error, never a fallback.
12
13use razeri_plugin_contract::DialectFamily;
14use razeri_plugin_contract::EngineVersion;
15use razeri_plugin_contract::{
16    DdlRenderer, DialectCapabilities, DmlRenderer, ExpressionRenderer, FormulaRenderer,
17    ProcedureRenderer, ReplicationRenderer, SqlRenderer, StorageRenderer, TaskRenderer,
18};
19use std::collections::HashMap;
20use std::sync::Arc;
21
22// Re-export rendering traits so dialect crates can import from one place.
23pub use razeri_plugin_contract;
24
25/// Metadata describing a dialect implementation.
26///
27/// Serializes to the discovery payload returned by `list_platforms()` across
28/// every surface (and consumed by the typed-constants generator), so hosts learn
29/// the registered platforms — including third-party plugins — at runtime instead
30/// of hard-coding them.
31#[derive(Debug, Clone, serde::Serialize)]
32pub struct DialectInfo {
33    /// Machine-readable name (e.g., "postgres", "redshift").
34    pub name: &'static str,
35    /// Human-readable display name (e.g., "`PostgreSQL`", "Amazon Redshift").
36    pub display_name: &'static str,
37    /// The other spellings this dialect answers to (e.g. `["pg", "postgresql"]`).
38    ///
39    /// A dialect owns its own names. [`name`](Self::name) always resolves; this
40    /// is everything else a design or a `--target` may say for the same
41    /// platform. Matching ignores case and surrounding whitespace, so
42    /// `"PostgreSQL"` needs no separate entry. Declare `&[]` to answer to
43    /// `name` alone.
44    ///
45    /// Two loaded dialects must not claim the same spelling: a spelling that
46    /// names two platforms names neither, and resolving it is refused rather
47    /// than guessed.
48    pub aliases: &'static [&'static str],
49    /// Dialect family for shared behavior (e.g., `PostgreSQL`, `TSql`).
50    pub dialect_family: DialectFamily,
51    /// SQL family label (e.g., "`PostgreSQL`", "T-SQL", "`ClickHouse` SQL").
52    pub family: &'static str,
53    /// Whether this is a cloud-native platform (e.g. Snowflake, Databricks).
54    pub is_cloud_native: bool,
55    /// Whether this is primarily an OLAP engine (e.g. `ClickHouse`).
56    pub is_olap: bool,
57    /// Whether this dialect is PostgreSQL-wire-compatible.
58    pub is_postgres_compatible: bool,
59    /// **The versions of the engine this dialect answers for**, the floor
60    /// first, then each version above it at which an answer changes.
61    ///
62    /// The floor is the oldest version the dialect is supported at, and a
63    /// platform named with no version (`postgres`) is the engine at its floor.
64    /// A design opts into a newer server's forms by naming one of the others
65    /// (`postgres@17`). Declare `&[]` for a managed service that chooses its
66    /// own version, where a version named in a design is refused.
67    ///
68    /// Each version is one a dialect has been measured at. A dialect whose
69    /// answers are the same at every version declares one: its floor.
70    pub versions: &'static [EngineVersion],
71    /// **Which of razeri's deploy scripts drives this engine's client**, or
72    /// `None` where razeri has written no script for it.
73    ///
74    /// A bundle carries one script, written for one client: `psql`, `mysql`,
75    /// `sqlplus`. Razeri asks the dialect which one its engine speaks rather
76    /// than recognising the engine by name, so an engine that speaks a client
77    /// razeri already drives — a PostgreSQL-wire engine and `psql` — gets that
78    /// script with no change to razeri. `None` gets a script that refuses at
79    /// the door and prints the run order by hand.
80    pub deploy_script: Option<DeployScript>,
81}
82
83/// **The client a generated deploy script drives**, one per script razeri
84/// writes.
85#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
86#[serde(rename_all = "snake_case")]
87#[non_exhaustive]
88pub enum DeployScript {
89    /// `deploy.sh` driving the `duckdb` client.
90    Duckdb,
91    /// `deploy.sh` driving `sqlite3`.
92    Sqlite3,
93    /// `deploy.sh` driving `sqlplus`, local or in the engine's container.
94    Sqlplus,
95    /// `deploy.sh` driving `hdbsql`, local or in the engine's container.
96    Hdbsql,
97    /// `deploy.sh` driving `mysql` or `mariadb`, whichever the engine ships.
98    Mysql,
99    /// `deploy.sh` driving `psql`, local or in the engine's container.
100    Psql,
101    /// `deploy.ps1` driving `Invoke-Sqlcmd`.
102    InvokeSqlcmd,
103}
104
105/// Full plugin trait for database platform dialects.
106///
107/// Extends the rendering trait hierarchy (`SqlRenderer`, `ExpressionRenderer`,
108/// `DdlRenderer`, `DmlRenderer`, `ProcedureRenderer`, `FormulaRenderer`,
109/// `TaskRenderer`, `ReplicationRenderer`, `StorageRenderer`, `DialectCapabilities`)
110/// with plugin-specific metadata and registration support.
111///
112/// Dialect authors implement the rendering traits plus `PlatformDialect::info()`.
113pub trait PlatformDialect:
114    SqlRenderer
115    + ExpressionRenderer
116    + DdlRenderer
117    + DmlRenderer
118    + ProcedureRenderer
119    + FormulaRenderer
120    + TaskRenderer
121    + DialectCapabilities
122    + ReplicationRenderer
123    + StorageRenderer
124    + DialectHandle
125{
126    /// Returns metadata about this dialect.
127    fn info(&self) -> DialectInfo;
128
129    /// **This dialect, answering for `version`.**
130    ///
131    /// `version` is one of [`DialectInfo::versions`]; the resolver calls this
132    /// after matching the version a design names against that list, so a
133    /// version the dialect does not declare never reaches it. A dialect whose
134    /// answers change between its versions returns an instance that answers
135    /// for `version`, and says so from [`Self::engine_version`].
136    ///
137    /// The default is a dialect with one answer: it returns itself for a
138    /// version it declares, and refuses any other.
139    ///
140    /// # Errors
141    ///
142    /// [`UnsupportedVersion`], naming the versions this dialect answers for.
143    fn at_version(
144        self: Arc<Self>,
145        version: &EngineVersion,
146    ) -> Result<Arc<dyn PlatformDialect>, UnsupportedVersion> {
147        if self.engine_version().as_ref() == Some(version) {
148            return Ok(self.into_dialect());
149        }
150        Err(UnsupportedVersion::of(&self.info(), &version.to_string()))
151    }
152
153    /// **The version this dialect answers for**, or `None` for a managed
154    /// service that chooses its own.
155    ///
156    /// The default is the floor, the first of [`DialectInfo::versions`]: right
157    /// for a dialect with one answer. A dialect that overrides
158    /// [`Self::at_version`] overrides this too.
159    fn engine_version(&self) -> Option<EngineVersion> {
160        self.info().versions.first().copied()
161    }
162}
163
164/// **A dialect as a shared handle**, so [`PlatformDialect::at_version`] can
165/// hand back the dialect it was called on.
166///
167/// Every dialect has it, through the implementation below; there is nothing
168/// to write.
169pub trait DialectHandle {
170    /// This dialect, as the shared handle the registry holds.
171    fn into_dialect(self: Arc<Self>) -> Arc<dyn PlatformDialect>;
172}
173
174impl<T: PlatformDialect + 'static> DialectHandle for T {
175    fn into_dialect(self: Arc<Self>) -> Arc<dyn PlatformDialect> {
176        self
177    }
178}
179
180/// **A version this dialect does not answer for**, with the ones it does.
181#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
182#[error("{}", self.sentence())]
183pub struct UnsupportedVersion {
184    /// The engine's name, as a design writes it (`postgres`).
185    pub engine: String,
186    /// The engine's name as its vendor writes it (`PostgreSQL`).
187    pub display_name: String,
188    /// The version that was asked for, as it was written.
189    pub asked: String,
190    /// The versions the dialect answers for, the floor first.
191    pub answers_for: Vec<EngineVersion>,
192}
193
194impl UnsupportedVersion {
195    /// The refusal of `asked`, for the dialect `info` describes.
196    #[must_use]
197    pub fn of(info: &DialectInfo, asked: &str) -> Self {
198        Self {
199            engine: info.name.to_owned(),
200            display_name: info.display_name.to_owned(),
201            asked: asked.to_owned(),
202            answers_for: info.versions.to_vec(),
203        }
204    }
205
206    /// The refusal, in one sentence that names the versions to write instead.
207    #[must_use]
208    pub fn sentence(&self) -> String {
209        let Self { engine, display_name, asked, answers_for } = self;
210        let Some(floor) = answers_for.first() else {
211            return format!(
212                "razeri renders {display_name} for no version a design names, and not at \
213                 {asked}: write `{engine}`"
214            );
215        };
216        let versions: Vec<String> = answers_for.iter().map(ToString::to_string).collect();
217        let names: Vec<String> = versions.iter().map(|v| format!("`{engine}@{v}`")).collect();
218        if versions.len() == 1 {
219            return format!(
220                "razeri renders {display_name} at {floor} alone, and not at {asked}: write \
221                 `{engine}`"
222            );
223        }
224        format!(
225            "razeri renders {display_name} at {}, and not at {asked}: write {}, or `{engine}` \
226             for {floor}, the oldest",
227            in_words(&versions, "and"),
228            in_words(&names, "or"),
229        )
230    }
231}
232
233/// `15, 16 and 17`: a list in a sentence.
234fn in_words(items: &[String], last: &str) -> String {
235    match items {
236        [] => String::new(),
237        [only] => only.clone(),
238        [rest @ .., tail] => format!("{} {last} {tail}", rest.join(", ")),
239    }
240}
241
242impl DialectInfo {
243    /// Every spelling this dialect answers to: its own name, then its aliases.
244    ///
245    /// This is what the resolver reads, so it is also the honest answer to
246    /// "what may a design call this platform?". An alias repeating the name is
247    /// listed once.
248    #[must_use]
249    pub fn spellings(&self) -> Vec<&'static str> {
250        let mut out = Vec::with_capacity(self.aliases.len() + 1);
251        out.push(self.name);
252        for alias in self.aliases {
253            if !out.contains(alias) {
254                out.push(alias);
255            }
256        }
257        out
258    }
259
260    /// Whether this dialect answers to `spelling`, ignoring case and
261    /// surrounding whitespace.
262    #[must_use]
263    pub fn answers_to(&self, spelling: &str) -> bool {
264        let wanted = spelling.trim();
265        self.spellings().iter().any(|known| known.eq_ignore_ascii_case(wanted))
266    }
267}
268
269/// String-keyed registry for platform dialects.
270///
271/// All dialects are indexed by their canonical string name
272/// (e.g. `"postgres"`, `"snowflake"`, `"mysql"`).
273pub struct PlatformRegistry {
274    dialects: HashMap<String, Arc<dyn PlatformDialect>>,
275}
276
277impl PlatformRegistry {
278    /// Create an empty registry.
279    #[must_use]
280    pub fn new() -> Self {
281        Self { dialects: HashMap::new() }
282    }
283
284    /// Register a dialect by its canonical string name.
285    pub fn register(&mut self, name: String, dialect: Arc<dyn PlatformDialect>) {
286        self.dialects.insert(name, dialect);
287    }
288
289    /// Resolve by string name (canonical path for all dialects).
290    #[must_use]
291    pub fn get_by_name(&self, name: &str) -> Option<&Arc<dyn PlatformDialect>> {
292        self.dialects.get(name)
293    }
294
295    /// Resolve by string — same as [`get_by_name`](Self::get_by_name).
296    #[must_use]
297    pub fn resolve(&self, target: &str) -> Option<Arc<dyn PlatformDialect>> {
298        self.dialects.get(target).cloned()
299    }
300
301    /// List all registered dialects.
302    #[must_use]
303    pub fn list(&self) -> Vec<DialectInfo> {
304        self.dialects.values().map(|d| d.info()).collect()
305    }
306
307    /// Sorted machine-readable names of every registered dialect.
308    ///
309    /// The stable, ordered form of [`list`](Self::list); the
310    /// [`resolve`](crate::resolve) module uses it for error messages and the
311    /// typed-constants generator reads it for deterministic output.
312    #[must_use]
313    pub fn registered_names(&self) -> Vec<String> {
314        let mut names: Vec<String> = self.dialects.keys().cloned().collect();
315        names.sort();
316        names
317    }
318
319    /// Build a registry pre-populated with all auto-discovered dialect crates.
320    ///
321    /// Uses `inventory` to find every [`crate::builder::DialectRegistration`]
322    /// submitted at link time.
323    #[must_use]
324    pub fn with_auto_discovered() -> Self {
325        use crate::builder::DialectRegistration;
326        let mut registry = Self::new();
327        for reg in inventory::iter::<DialectRegistration> {
328            let dialect = (reg.factory)();
329            let name = dialect.info().name.to_string();
330            registry.register(name, dialect);
331        }
332        // Plugin crates linked into this binary with `export_plugin!`
333        // register through a builder; the dialects are what is kept here.
334        for reg in inventory::iter::<crate::builder::PluginRegistration> {
335            let mut linked = crate::builder::PluginRegistryBuilder::new();
336            (reg.register)(&mut linked);
337            for (name, dialect) in linked.build().dialects.dialects {
338                registry.register(name, dialect);
339            }
340        }
341        registry
342    }
343}
344
345impl Default for PlatformRegistry {
346    fn default() -> Self {
347        Self::new()
348    }
349}
350
351#[cfg(test)]
352mod tests {
353    use super::*;
354    use razeri_plugin_contract::DataType;
355
356    /// Test dialect for unit tests.
357    struct TestDialect {
358        info: DialectInfo,
359    }
360
361    impl TestDialect {
362        fn postgres() -> Self {
363            Self {
364                info: DialectInfo {
365                    name: "postgres",
366                    display_name: "PostgreSQL",
367                    aliases: &["postgresql", "pg"],
368                    dialect_family: DialectFamily::PostgreSql,
369                    family: "PostgreSQL",
370                    is_cloud_native: false,
371                    is_olap: false,
372                    is_postgres_compatible: true,
373                    versions: &[],
374                    deploy_script: None,
375                },
376            }
377        }
378
379        fn redshift() -> Self {
380            Self {
381                info: DialectInfo {
382                    name: "redshift",
383                    display_name: "Amazon Redshift",
384                    aliases: &["amazon_redshift", "rs"],
385                    dialect_family: DialectFamily::PostgreSql,
386                    family: "PostgreSQL",
387                    is_cloud_native: true,
388                    is_olap: true,
389                    is_postgres_compatible: true,
390                    versions: &[],
391                    deploy_script: None,
392                },
393            }
394        }
395
396        fn snowflake() -> Self {
397            Self {
398                info: DialectInfo {
399                    name: "snowflake",
400                    display_name: "Snowflake",
401                    aliases: &["sf"],
402                    dialect_family: DialectFamily::Snowflake,
403                    family: "Snowflake",
404                    is_cloud_native: true,
405                    is_olap: true,
406                    is_postgres_compatible: false,
407                    versions: &[],
408                    deploy_script: None,
409                },
410            }
411        }
412    }
413
414    impl SqlRenderer for TestDialect {
415        fn render_data_type(&self, _dt: &DataType) -> String {
416            "TEXT".to_string()
417        }
418
419        fn quote_identifier(&self, name: &str) -> String {
420            format!("\"{name}\"")
421        }
422
423        fn qualify_name(&self, schema: Option<&str>, name: &str) -> String {
424            match schema {
425                Some(s) => format!("{}.{}", self.quote_identifier(s), self.quote_identifier(name)),
426                None => self.quote_identifier(name),
427            }
428        }
429
430        fn render_current_timestamp(&self) -> String {
431            "CURRENT_TIMESTAMP".to_string()
432        }
433    }
434
435    impl ExpressionRenderer for TestDialect {
436        fn render_concat(&self, parts: &[String]) -> String {
437            parts.join(" || ")
438        }
439    }
440
441    impl DdlRenderer for TestDialect {}
442    impl DmlRenderer for TestDialect {}
443    impl ProcedureRenderer for TestDialect {}
444    impl FormulaRenderer for TestDialect {}
445    impl TaskRenderer for TestDialect {}
446    impl ReplicationRenderer for TestDialect {}
447    impl StorageRenderer for TestDialect {}
448
449    impl DialectCapabilities for TestDialect {
450        fn supports_identity(&self) -> bool {
451            true
452        }
453        fn supports_sequences(&self) -> bool {
454            true
455        }
456        fn supports_stored_procedures(&self) -> bool {
457            true
458        }
459    }
460
461    impl PlatformDialect for TestDialect {
462        fn info(&self) -> DialectInfo {
463            self.info.clone()
464        }
465    }
466
467    const FLOOR_ONLY: &[EngineVersion] = &[EngineVersion::declared("15")];
468
469    /// **A dialect with one answer is itself at its one version**, and the
470    /// default refuses any other, naming the one it answers for.
471    #[test]
472    fn a_dialect_with_one_answer_is_itself_at_its_floor_and_refuses_another() {
473        let dialect: Arc<dyn PlatformDialect> = Arc::new(TestDialect {
474            info: DialectInfo { versions: FLOOR_ONLY, ..TestDialect::postgres().info() },
475        });
476        assert_eq!(dialect.engine_version(), Some(EngineVersion::declared("15")));
477        let at_floor =
478            Arc::clone(&dialect).at_version(&EngineVersion::declared("15")).expect("its floor");
479        assert!(Arc::ptr_eq(&at_floor, &dialect), "a dialect with one answer returns itself");
480
481        let Err(refused) = Arc::clone(&dialect).at_version(&EngineVersion::declared("17")) else {
482            panic!("17 is not declared")
483        };
484        assert_eq!(refused.answers_for, vec![EngineVersion::declared("15")]);
485        let said = refused.to_string();
486        assert!(said.contains("PostgreSQL"), "{said}");
487        assert!(said.contains("15"), "{said}");
488        assert!(said.contains("`postgres`"), "names the spelling that works: {said}");
489    }
490
491    /// **A service that chooses its own version answers for none.**
492    #[test]
493    fn a_service_that_chooses_its_own_version_answers_for_none() {
494        let dialect = TestDialect::snowflake();
495        assert!(dialect.info().versions.is_empty());
496        assert_eq!(dialect.engine_version(), None);
497    }
498
499    /// **The refusal lists every version, the floor first, and the names to
500    /// write.**
501    #[test]
502    fn an_unsupported_version_names_every_version_and_how_to_write_each() {
503        let refusal = UnsupportedVersion {
504            engine: "postgres".to_owned(),
505            display_name: "PostgreSQL".to_owned(),
506            asked: "12".to_owned(),
507            answers_for: ["15", "16", "17"].iter().map(|v| EngineVersion::declared(v)).collect(),
508        };
509        let said = refusal.to_string();
510        assert!(said.contains("15, 16 and 17"), "{said}");
511        assert!(said.contains("not at 12"), "{said}");
512        assert!(said.contains("`postgres@17`"), "{said}");
513        assert!(said.contains("`postgres` for 15"), "the name alone is the floor: {said}");
514    }
515
516    #[test]
517    fn the_versions_ride_along_in_the_discovery_payload() {
518        let info = DialectInfo { versions: FLOOR_ONLY, ..TestDialect::postgres().info() };
519        let json = serde_json::to_value(&info).expect("DialectInfo serializes");
520        assert_eq!(json["versions"], serde_json::json!(["15"]));
521    }
522
523    #[test]
524    fn register_dialect_resolves_by_name() {
525        let mut registry = PlatformRegistry::new();
526        registry.register("postgres".to_string(), Arc::new(TestDialect::postgres()));
527        assert!(registry.get_by_name("postgres").is_some());
528    }
529
530    #[test]
531    fn register_extension_resolves_by_name() {
532        let mut registry = PlatformRegistry::new();
533        registry.register("redshift".to_string(), Arc::new(TestDialect::redshift()));
534        assert!(registry.get_by_name("redshift").is_some());
535    }
536
537    #[test]
538    fn resolve_tries_name_lookup() {
539        let mut registry = PlatformRegistry::new();
540        registry.register("postgres".to_string(), Arc::new(TestDialect::postgres()));
541        registry.register("redshift".to_string(), Arc::new(TestDialect::redshift()));
542
543        assert!(registry.resolve("postgres").is_some());
544        assert!(registry.resolve("redshift").is_some());
545        assert!(registry.resolve("nonexistent").is_none());
546    }
547
548    #[test]
549    fn list_returns_all_registered_dialects() {
550        let mut registry = PlatformRegistry::new();
551        registry.register("postgres".to_string(), Arc::new(TestDialect::postgres()));
552        registry.register("snowflake".to_string(), Arc::new(TestDialect::snowflake()));
553        registry.register("redshift".to_string(), Arc::new(TestDialect::redshift()));
554
555        let list = registry.list();
556        assert_eq!(list.len(), 3);
557    }
558
559    #[test]
560    fn duplicate_registration_overwrites() {
561        let mut registry = PlatformRegistry::new();
562        registry.register("postgres".to_string(), Arc::new(TestDialect::postgres()));
563        registry.register("postgres".to_string(), Arc::new(TestDialect::postgres()));
564
565        assert!(registry.get_by_name("postgres").is_some());
566        let list = registry.list();
567        assert_eq!(list.len(), 1);
568    }
569
570    #[test]
571    fn dialect_info_contains_expected_metadata() {
572        let dialect = TestDialect::postgres();
573        let info = dialect.info();
574        assert_eq!(info.name, "postgres");
575        assert_eq!(info.display_name, "PostgreSQL");
576        assert_eq!(info.dialect_family, DialectFamily::PostgreSql);
577    }
578
579    #[test]
580    fn classification_fields_reflect_dialect_characteristics() {
581        let pg = TestDialect::postgres();
582        let pg_info = pg.info();
583        assert_eq!(pg_info.family, "PostgreSQL");
584        assert!(pg_info.is_postgres_compatible);
585        assert!(!pg_info.is_cloud_native);
586        assert!(!pg_info.is_olap);
587
588        let sf = TestDialect::snowflake();
589        let sf_info = sf.info();
590        assert_eq!(sf_info.family, "Snowflake");
591        assert!(sf_info.is_cloud_native);
592        assert!(sf_info.is_olap);
593        assert!(!sf_info.is_postgres_compatible);
594
595        let rd = TestDialect::redshift();
596        let rd_info = rd.info();
597        assert!(rd_info.is_cloud_native);
598        assert!(rd_info.is_olap);
599        assert!(rd_info.is_postgres_compatible);
600    }
601
602    #[test]
603    fn empty_registry_returns_none() {
604        let registry = PlatformRegistry::new();
605        assert!(registry.get_by_name("postgres").is_none());
606        assert!(registry.resolve("postgres").is_none());
607        assert!(registry.list().is_empty());
608    }
609
610    // ── Discovery + canonical names ─────────────────────────────────────────
611
612    #[test]
613    fn registered_names_are_sorted() {
614        let mut registry = PlatformRegistry::new();
615        registry.register("snowflake".to_string(), Arc::new(TestDialect::snowflake()));
616        registry.register("postgres".to_string(), Arc::new(TestDialect::postgres()));
617        registry.register("redshift".to_string(), Arc::new(TestDialect::redshift()));
618        assert_eq!(registry.registered_names(), vec!["postgres", "redshift", "snowflake"]);
619    }
620
621    #[test]
622    fn dialect_info_serializes_for_discovery() {
623        // The discovery payload (list_platforms) + the constants generator both
624        // read serialized DialectInfo, so it must round-trip to JSON.
625        let info = TestDialect::snowflake().info();
626        let json = serde_json::to_value(&info).expect("DialectInfo serializes");
627        assert_eq!(json["name"], "snowflake");
628        assert_eq!(json["display_name"], "Snowflake");
629        assert_eq!(json["dialect_family"], "snowflake");
630        assert_eq!(json["is_cloud_native"], true);
631        assert_eq!(json["is_postgres_compatible"], false);
632        // The spellings ride along, so a host learns what a design may call
633        // this platform from the same payload that lists it.
634        assert_eq!(json["aliases"], serde_json::json!(["sf"]));
635    }
636
637    #[test]
638    fn spellings_start_with_the_dialects_own_name() {
639        let info = TestDialect::postgres().info();
640        assert_eq!(info.spellings(), vec!["postgres", "postgresql", "pg"]);
641    }
642
643    #[test]
644    fn a_dialect_answers_to_its_name_and_its_aliases_in_any_case() {
645        let info = TestDialect::postgres().info();
646        assert!(info.answers_to("postgres"));
647        assert!(info.answers_to("pg"));
648        assert!(info.answers_to("  PostgreSQL "));
649        assert!(!info.answers_to("snowflake"));
650    }
651
652    #[test]
653    fn an_alias_repeating_the_name_is_listed_once() {
654        let info = DialectInfo { aliases: &["postgres", "pg"], ..TestDialect::postgres().info() };
655        assert_eq!(info.spellings(), vec!["postgres", "pg"]);
656    }
657}