1use 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
22pub use razeri_plugin_contract;
24
25#[derive(Debug, Clone, serde::Serialize)]
32pub struct DialectInfo {
33 pub name: &'static str,
35 pub display_name: &'static str,
37 pub aliases: &'static [&'static str],
49 pub dialect_family: DialectFamily,
51 pub family: &'static str,
53 pub is_cloud_native: bool,
55 pub is_olap: bool,
57 pub is_postgres_compatible: bool,
59 pub versions: &'static [EngineVersion],
71 pub deploy_script: Option<DeployScript>,
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
88#[serde(rename_all = "snake_case")]
89#[non_exhaustive]
90pub enum DeployScript {
91 Duckdb,
93 Sqlite3,
95 Sqlplus,
97 Hdbsql,
99 Mysql,
101 Psql,
103 InvokeSqlcmd,
105 Client(DeployClient),
108}
109
110#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
139pub struct DeployClient {
140 pub command: &'static str,
142 pub run_file: &'static [&'static str],
150 pub read_rows: Option<&'static [&'static str]>,
158 pub block_terminator: Option<BlockTerminator>,
167}
168
169#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
181pub struct BlockTerminator {
182 pub open: &'static str,
184 pub end: &'static str,
186 pub close: &'static str,
188}
189
190pub trait PlatformDialect:
199 SqlRenderer
200 + ExpressionRenderer
201 + DdlRenderer
202 + DmlRenderer
203 + ProcedureRenderer
204 + FormulaRenderer
205 + TaskRenderer
206 + DialectCapabilities
207 + ReplicationRenderer
208 + StorageRenderer
209 + DialectHandle
210{
211 fn info(&self) -> DialectInfo;
213
214 fn at_version(
229 self: Arc<Self>,
230 version: &EngineVersion,
231 ) -> Result<Arc<dyn PlatformDialect>, UnsupportedVersion> {
232 if self.engine_version().as_ref() == Some(version) {
233 return Ok(self.into_dialect());
234 }
235 Err(UnsupportedVersion::of(&self.info(), &version.to_string()))
236 }
237
238 fn engine_version(&self) -> Option<EngineVersion> {
245 self.info().versions.first().copied()
246 }
247}
248
249pub trait DialectHandle {
255 fn into_dialect(self: Arc<Self>) -> Arc<dyn PlatformDialect>;
257}
258
259impl<T: PlatformDialect + 'static> DialectHandle for T {
260 fn into_dialect(self: Arc<Self>) -> Arc<dyn PlatformDialect> {
261 self
262 }
263}
264
265#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
267#[error("{}", self.sentence())]
268pub struct UnsupportedVersion {
269 pub engine: String,
271 pub display_name: String,
273 pub asked: String,
275 pub answers_for: Vec<EngineVersion>,
277}
278
279impl UnsupportedVersion {
280 #[must_use]
282 pub fn of(info: &DialectInfo, asked: &str) -> Self {
283 Self {
284 engine: info.name.to_owned(),
285 display_name: info.display_name.to_owned(),
286 asked: asked.to_owned(),
287 answers_for: info.versions.to_vec(),
288 }
289 }
290
291 #[must_use]
293 pub fn sentence(&self) -> String {
294 let Self { engine, display_name, asked, answers_for } = self;
295 let Some(floor) = answers_for.first() else {
296 return format!(
297 "razeri renders {display_name} for no version a design names, and not at \
298 {asked}: write `{engine}`"
299 );
300 };
301 let versions: Vec<String> = answers_for.iter().map(ToString::to_string).collect();
302 let names: Vec<String> = versions.iter().map(|v| format!("`{engine}@{v}`")).collect();
303 if versions.len() == 1 {
304 return format!(
305 "razeri renders {display_name} at {floor} alone, and not at {asked}: write \
306 `{engine}`"
307 );
308 }
309 format!(
310 "razeri renders {display_name} at {}, and not at {asked}: write {}, or `{engine}` \
311 for {floor}, the oldest",
312 in_words(&versions, "and"),
313 in_words(&names, "or"),
314 )
315 }
316}
317
318fn in_words(items: &[String], last: &str) -> String {
320 match items {
321 [] => String::new(),
322 [only] => only.clone(),
323 [rest @ .., tail] => format!("{} {last} {tail}", rest.join(", ")),
324 }
325}
326
327impl DialectInfo {
328 #[must_use]
334 pub fn spellings(&self) -> Vec<&'static str> {
335 let mut out = Vec::with_capacity(self.aliases.len() + 1);
336 out.push(self.name);
337 for alias in self.aliases {
338 if !out.contains(alias) {
339 out.push(alias);
340 }
341 }
342 out
343 }
344
345 #[must_use]
348 pub fn answers_to(&self, spelling: &str) -> bool {
349 let wanted = spelling.trim();
350 self.spellings().iter().any(|known| known.eq_ignore_ascii_case(wanted))
351 }
352}
353
354pub struct PlatformRegistry {
359 dialects: HashMap<String, Arc<dyn PlatformDialect>>,
360}
361
362impl PlatformRegistry {
363 #[must_use]
365 pub fn new() -> Self {
366 Self { dialects: HashMap::new() }
367 }
368
369 pub fn register(&mut self, name: String, dialect: Arc<dyn PlatformDialect>) {
371 self.dialects.insert(name, dialect);
372 }
373
374 #[must_use]
376 pub fn get_by_name(&self, name: &str) -> Option<&Arc<dyn PlatformDialect>> {
377 self.dialects.get(name)
378 }
379
380 #[must_use]
382 pub fn resolve(&self, target: &str) -> Option<Arc<dyn PlatformDialect>> {
383 self.dialects.get(target).cloned()
384 }
385
386 #[must_use]
388 pub fn list(&self) -> Vec<DialectInfo> {
389 self.dialects.values().map(|d| d.info()).collect()
390 }
391
392 #[must_use]
398 pub fn registered_names(&self) -> Vec<String> {
399 let mut names: Vec<String> = self.dialects.keys().cloned().collect();
400 names.sort();
401 names
402 }
403
404 #[must_use]
409 pub fn with_auto_discovered() -> Self {
410 use crate::builder::DialectRegistration;
411 let mut registry = Self::new();
412 for reg in inventory::iter::<DialectRegistration> {
413 let dialect = (reg.factory)();
414 let name = dialect.info().name.to_string();
415 registry.register(name, dialect);
416 }
417 for reg in inventory::iter::<crate::builder::PluginRegistration> {
420 let mut linked = crate::builder::PluginRegistryBuilder::new();
421 (reg.register)(&mut linked);
422 for (name, dialect) in linked.build().dialects.dialects {
423 registry.register(name, dialect);
424 }
425 }
426 registry
427 }
428}
429
430impl Default for PlatformRegistry {
431 fn default() -> Self {
432 Self::new()
433 }
434}
435
436#[cfg(test)]
437mod tests {
438 use super::*;
439 use razeri_plugin_contract::DataType;
440
441 struct TestDialect {
443 info: DialectInfo,
444 }
445
446 impl TestDialect {
447 fn postgres() -> Self {
448 Self {
449 info: DialectInfo {
450 name: "postgres",
451 display_name: "PostgreSQL",
452 aliases: &["postgresql", "pg"],
453 dialect_family: DialectFamily::PostgreSql,
454 family: "PostgreSQL",
455 is_cloud_native: false,
456 is_olap: false,
457 is_postgres_compatible: true,
458 versions: &[],
459 deploy_script: None,
460 },
461 }
462 }
463
464 fn redshift() -> Self {
465 Self {
466 info: DialectInfo {
467 name: "redshift",
468 display_name: "Amazon Redshift",
469 aliases: &["amazon_redshift", "rs"],
470 dialect_family: DialectFamily::PostgreSql,
471 family: "PostgreSQL",
472 is_cloud_native: true,
473 is_olap: true,
474 is_postgres_compatible: true,
475 versions: &[],
476 deploy_script: None,
477 },
478 }
479 }
480
481 fn snowflake() -> Self {
482 Self {
483 info: DialectInfo {
484 name: "snowflake",
485 display_name: "Snowflake",
486 aliases: &["sf"],
487 dialect_family: DialectFamily::Snowflake,
488 family: "Snowflake",
489 is_cloud_native: true,
490 is_olap: true,
491 is_postgres_compatible: false,
492 versions: &[],
493 deploy_script: None,
494 },
495 }
496 }
497 }
498
499 impl SqlRenderer for TestDialect {
500 fn render_data_type(&self, _dt: &DataType) -> String {
501 "TEXT".to_string()
502 }
503
504 fn quote_identifier(&self, name: &str) -> String {
505 format!("\"{name}\"")
506 }
507
508 fn qualify_name(&self, schema: Option<&str>, name: &str) -> String {
509 match schema {
510 Some(s) => format!("{}.{}", self.quote_identifier(s), self.quote_identifier(name)),
511 None => self.quote_identifier(name),
512 }
513 }
514
515 fn render_current_timestamp(&self) -> String {
516 "CURRENT_TIMESTAMP".to_string()
517 }
518 }
519
520 impl ExpressionRenderer for TestDialect {
521 fn render_concat(&self, parts: &[String]) -> String {
522 parts.join(" || ")
523 }
524 }
525
526 impl DdlRenderer for TestDialect {}
527 impl DmlRenderer for TestDialect {}
528 impl ProcedureRenderer for TestDialect {}
529 impl FormulaRenderer for TestDialect {}
530 impl TaskRenderer for TestDialect {}
531 impl ReplicationRenderer for TestDialect {}
532 impl StorageRenderer for TestDialect {}
533
534 impl DialectCapabilities for TestDialect {
535 fn supports_identity(&self) -> bool {
536 true
537 }
538 fn supports_sequences(&self) -> bool {
539 true
540 }
541 fn supports_stored_procedures(&self) -> bool {
542 true
543 }
544 }
545
546 impl PlatformDialect for TestDialect {
547 fn info(&self) -> DialectInfo {
548 self.info.clone()
549 }
550 }
551
552 const FLOOR_ONLY: &[EngineVersion] = &[EngineVersion::declared("15")];
553
554 #[test]
557 fn a_dialect_with_one_answer_is_itself_at_its_floor_and_refuses_another() {
558 let dialect: Arc<dyn PlatformDialect> = Arc::new(TestDialect {
559 info: DialectInfo { versions: FLOOR_ONLY, ..TestDialect::postgres().info() },
560 });
561 assert_eq!(dialect.engine_version(), Some(EngineVersion::declared("15")));
562 let at_floor =
563 Arc::clone(&dialect).at_version(&EngineVersion::declared("15")).expect("its floor");
564 assert!(Arc::ptr_eq(&at_floor, &dialect), "a dialect with one answer returns itself");
565
566 let Err(refused) = Arc::clone(&dialect).at_version(&EngineVersion::declared("17")) else {
567 panic!("17 is not declared")
568 };
569 assert_eq!(refused.answers_for, vec![EngineVersion::declared("15")]);
570 let said = refused.to_string();
571 assert!(said.contains("PostgreSQL"), "{said}");
572 assert!(said.contains("15"), "{said}");
573 assert!(said.contains("`postgres`"), "names the spelling that works: {said}");
574 }
575
576 #[test]
578 fn a_service_that_chooses_its_own_version_answers_for_none() {
579 let dialect = TestDialect::snowflake();
580 assert!(dialect.info().versions.is_empty());
581 assert_eq!(dialect.engine_version(), None);
582 }
583
584 #[test]
587 fn an_unsupported_version_names_every_version_and_how_to_write_each() {
588 let refusal = UnsupportedVersion {
589 engine: "postgres".to_owned(),
590 display_name: "PostgreSQL".to_owned(),
591 asked: "12".to_owned(),
592 answers_for: ["15", "16", "17"].iter().map(|v| EngineVersion::declared(v)).collect(),
593 };
594 let said = refusal.to_string();
595 assert!(said.contains("15, 16 and 17"), "{said}");
596 assert!(said.contains("not at 12"), "{said}");
597 assert!(said.contains("`postgres@17`"), "{said}");
598 assert!(said.contains("`postgres` for 15"), "the name alone is the floor: {said}");
599 }
600
601 #[test]
602 fn the_versions_ride_along_in_the_discovery_payload() {
603 let info = DialectInfo { versions: FLOOR_ONLY, ..TestDialect::postgres().info() };
604 let json = serde_json::to_value(&info).expect("DialectInfo serializes");
605 assert_eq!(json["versions"], serde_json::json!(["15"]));
606 }
607
608 #[test]
609 fn register_dialect_resolves_by_name() {
610 let mut registry = PlatformRegistry::new();
611 registry.register("postgres".to_string(), Arc::new(TestDialect::postgres()));
612 assert!(registry.get_by_name("postgres").is_some());
613 }
614
615 #[test]
616 fn register_extension_resolves_by_name() {
617 let mut registry = PlatformRegistry::new();
618 registry.register("redshift".to_string(), Arc::new(TestDialect::redshift()));
619 assert!(registry.get_by_name("redshift").is_some());
620 }
621
622 #[test]
623 fn resolve_tries_name_lookup() {
624 let mut registry = PlatformRegistry::new();
625 registry.register("postgres".to_string(), Arc::new(TestDialect::postgres()));
626 registry.register("redshift".to_string(), Arc::new(TestDialect::redshift()));
627
628 assert!(registry.resolve("postgres").is_some());
629 assert!(registry.resolve("redshift").is_some());
630 assert!(registry.resolve("nonexistent").is_none());
631 }
632
633 #[test]
634 fn list_returns_all_registered_dialects() {
635 let mut registry = PlatformRegistry::new();
636 registry.register("postgres".to_string(), Arc::new(TestDialect::postgres()));
637 registry.register("snowflake".to_string(), Arc::new(TestDialect::snowflake()));
638 registry.register("redshift".to_string(), Arc::new(TestDialect::redshift()));
639
640 let list = registry.list();
641 assert_eq!(list.len(), 3);
642 }
643
644 #[test]
645 fn duplicate_registration_overwrites() {
646 let mut registry = PlatformRegistry::new();
647 registry.register("postgres".to_string(), Arc::new(TestDialect::postgres()));
648 registry.register("postgres".to_string(), Arc::new(TestDialect::postgres()));
649
650 assert!(registry.get_by_name("postgres").is_some());
651 let list = registry.list();
652 assert_eq!(list.len(), 1);
653 }
654
655 #[test]
656 fn dialect_info_contains_expected_metadata() {
657 let dialect = TestDialect::postgres();
658 let info = dialect.info();
659 assert_eq!(info.name, "postgres");
660 assert_eq!(info.display_name, "PostgreSQL");
661 assert_eq!(info.dialect_family, DialectFamily::PostgreSql);
662 }
663
664 #[test]
665 fn classification_fields_reflect_dialect_characteristics() {
666 let pg = TestDialect::postgres();
667 let pg_info = pg.info();
668 assert_eq!(pg_info.family, "PostgreSQL");
669 assert!(pg_info.is_postgres_compatible);
670 assert!(!pg_info.is_cloud_native);
671 assert!(!pg_info.is_olap);
672
673 let sf = TestDialect::snowflake();
674 let sf_info = sf.info();
675 assert_eq!(sf_info.family, "Snowflake");
676 assert!(sf_info.is_cloud_native);
677 assert!(sf_info.is_olap);
678 assert!(!sf_info.is_postgres_compatible);
679
680 let rd = TestDialect::redshift();
681 let rd_info = rd.info();
682 assert!(rd_info.is_cloud_native);
683 assert!(rd_info.is_olap);
684 assert!(rd_info.is_postgres_compatible);
685 }
686
687 #[test]
688 fn empty_registry_returns_none() {
689 let registry = PlatformRegistry::new();
690 assert!(registry.get_by_name("postgres").is_none());
691 assert!(registry.resolve("postgres").is_none());
692 assert!(registry.list().is_empty());
693 }
694
695 #[test]
698 fn registered_names_are_sorted() {
699 let mut registry = PlatformRegistry::new();
700 registry.register("snowflake".to_string(), Arc::new(TestDialect::snowflake()));
701 registry.register("postgres".to_string(), Arc::new(TestDialect::postgres()));
702 registry.register("redshift".to_string(), Arc::new(TestDialect::redshift()));
703 assert_eq!(registry.registered_names(), vec!["postgres", "redshift", "snowflake"]);
704 }
705
706 #[test]
707 fn dialect_info_serializes_for_discovery() {
708 let info = TestDialect::snowflake().info();
711 let json = serde_json::to_value(&info).expect("DialectInfo serializes");
712 assert_eq!(json["name"], "snowflake");
713 assert_eq!(json["display_name"], "Snowflake");
714 assert_eq!(json["dialect_family"], "snowflake");
715 assert_eq!(json["is_cloud_native"], true);
716 assert_eq!(json["is_postgres_compatible"], false);
717 assert_eq!(json["aliases"], serde_json::json!(["sf"]));
720 }
721
722 #[test]
723 fn spellings_start_with_the_dialects_own_name() {
724 let info = TestDialect::postgres().info();
725 assert_eq!(info.spellings(), vec!["postgres", "postgresql", "pg"]);
726 }
727
728 #[test]
729 fn a_dialect_answers_to_its_name_and_its_aliases_in_any_case() {
730 let info = TestDialect::postgres().info();
731 assert!(info.answers_to("postgres"));
732 assert!(info.answers_to("pg"));
733 assert!(info.answers_to(" PostgreSQL "));
734 assert!(!info.answers_to("snowflake"));
735 }
736
737 #[test]
738 fn an_alias_repeating_the_name_is_listed_once() {
739 let info = DialectInfo { aliases: &["postgres", "pg"], ..TestDialect::postgres().info() };
740 assert_eq!(info.spellings(), vec!["postgres", "pg"]);
741 }
742}