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)]
138pub struct DeployClient {
139 pub command: &'static str,
141 pub run_file: &'static [&'static str],
149 pub read_rows: Option<&'static [&'static str]>,
157}
158
159pub trait PlatformDialect:
168 SqlRenderer
169 + ExpressionRenderer
170 + DdlRenderer
171 + DmlRenderer
172 + ProcedureRenderer
173 + FormulaRenderer
174 + TaskRenderer
175 + DialectCapabilities
176 + ReplicationRenderer
177 + StorageRenderer
178 + DialectHandle
179{
180 fn info(&self) -> DialectInfo;
182
183 fn at_version(
198 self: Arc<Self>,
199 version: &EngineVersion,
200 ) -> Result<Arc<dyn PlatformDialect>, UnsupportedVersion> {
201 if self.engine_version().as_ref() == Some(version) {
202 return Ok(self.into_dialect());
203 }
204 Err(UnsupportedVersion::of(&self.info(), &version.to_string()))
205 }
206
207 fn engine_version(&self) -> Option<EngineVersion> {
214 self.info().versions.first().copied()
215 }
216}
217
218pub trait DialectHandle {
224 fn into_dialect(self: Arc<Self>) -> Arc<dyn PlatformDialect>;
226}
227
228impl<T: PlatformDialect + 'static> DialectHandle for T {
229 fn into_dialect(self: Arc<Self>) -> Arc<dyn PlatformDialect> {
230 self
231 }
232}
233
234#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
236#[error("{}", self.sentence())]
237pub struct UnsupportedVersion {
238 pub engine: String,
240 pub display_name: String,
242 pub asked: String,
244 pub answers_for: Vec<EngineVersion>,
246}
247
248impl UnsupportedVersion {
249 #[must_use]
251 pub fn of(info: &DialectInfo, asked: &str) -> Self {
252 Self {
253 engine: info.name.to_owned(),
254 display_name: info.display_name.to_owned(),
255 asked: asked.to_owned(),
256 answers_for: info.versions.to_vec(),
257 }
258 }
259
260 #[must_use]
262 pub fn sentence(&self) -> String {
263 let Self { engine, display_name, asked, answers_for } = self;
264 let Some(floor) = answers_for.first() else {
265 return format!(
266 "razeri renders {display_name} for no version a design names, and not at \
267 {asked}: write `{engine}`"
268 );
269 };
270 let versions: Vec<String> = answers_for.iter().map(ToString::to_string).collect();
271 let names: Vec<String> = versions.iter().map(|v| format!("`{engine}@{v}`")).collect();
272 if versions.len() == 1 {
273 return format!(
274 "razeri renders {display_name} at {floor} alone, and not at {asked}: write \
275 `{engine}`"
276 );
277 }
278 format!(
279 "razeri renders {display_name} at {}, and not at {asked}: write {}, or `{engine}` \
280 for {floor}, the oldest",
281 in_words(&versions, "and"),
282 in_words(&names, "or"),
283 )
284 }
285}
286
287fn in_words(items: &[String], last: &str) -> String {
289 match items {
290 [] => String::new(),
291 [only] => only.clone(),
292 [rest @ .., tail] => format!("{} {last} {tail}", rest.join(", ")),
293 }
294}
295
296impl DialectInfo {
297 #[must_use]
303 pub fn spellings(&self) -> Vec<&'static str> {
304 let mut out = Vec::with_capacity(self.aliases.len() + 1);
305 out.push(self.name);
306 for alias in self.aliases {
307 if !out.contains(alias) {
308 out.push(alias);
309 }
310 }
311 out
312 }
313
314 #[must_use]
317 pub fn answers_to(&self, spelling: &str) -> bool {
318 let wanted = spelling.trim();
319 self.spellings().iter().any(|known| known.eq_ignore_ascii_case(wanted))
320 }
321}
322
323pub struct PlatformRegistry {
328 dialects: HashMap<String, Arc<dyn PlatformDialect>>,
329}
330
331impl PlatformRegistry {
332 #[must_use]
334 pub fn new() -> Self {
335 Self { dialects: HashMap::new() }
336 }
337
338 pub fn register(&mut self, name: String, dialect: Arc<dyn PlatformDialect>) {
340 self.dialects.insert(name, dialect);
341 }
342
343 #[must_use]
345 pub fn get_by_name(&self, name: &str) -> Option<&Arc<dyn PlatformDialect>> {
346 self.dialects.get(name)
347 }
348
349 #[must_use]
351 pub fn resolve(&self, target: &str) -> Option<Arc<dyn PlatformDialect>> {
352 self.dialects.get(target).cloned()
353 }
354
355 #[must_use]
357 pub fn list(&self) -> Vec<DialectInfo> {
358 self.dialects.values().map(|d| d.info()).collect()
359 }
360
361 #[must_use]
367 pub fn registered_names(&self) -> Vec<String> {
368 let mut names: Vec<String> = self.dialects.keys().cloned().collect();
369 names.sort();
370 names
371 }
372
373 #[must_use]
378 pub fn with_auto_discovered() -> Self {
379 use crate::builder::DialectRegistration;
380 let mut registry = Self::new();
381 for reg in inventory::iter::<DialectRegistration> {
382 let dialect = (reg.factory)();
383 let name = dialect.info().name.to_string();
384 registry.register(name, dialect);
385 }
386 for reg in inventory::iter::<crate::builder::PluginRegistration> {
389 let mut linked = crate::builder::PluginRegistryBuilder::new();
390 (reg.register)(&mut linked);
391 for (name, dialect) in linked.build().dialects.dialects {
392 registry.register(name, dialect);
393 }
394 }
395 registry
396 }
397}
398
399impl Default for PlatformRegistry {
400 fn default() -> Self {
401 Self::new()
402 }
403}
404
405#[cfg(test)]
406mod tests {
407 use super::*;
408 use razeri_plugin_contract::DataType;
409
410 struct TestDialect {
412 info: DialectInfo,
413 }
414
415 impl TestDialect {
416 fn postgres() -> Self {
417 Self {
418 info: DialectInfo {
419 name: "postgres",
420 display_name: "PostgreSQL",
421 aliases: &["postgresql", "pg"],
422 dialect_family: DialectFamily::PostgreSql,
423 family: "PostgreSQL",
424 is_cloud_native: false,
425 is_olap: false,
426 is_postgres_compatible: true,
427 versions: &[],
428 deploy_script: None,
429 },
430 }
431 }
432
433 fn redshift() -> Self {
434 Self {
435 info: DialectInfo {
436 name: "redshift",
437 display_name: "Amazon Redshift",
438 aliases: &["amazon_redshift", "rs"],
439 dialect_family: DialectFamily::PostgreSql,
440 family: "PostgreSQL",
441 is_cloud_native: true,
442 is_olap: true,
443 is_postgres_compatible: true,
444 versions: &[],
445 deploy_script: None,
446 },
447 }
448 }
449
450 fn snowflake() -> Self {
451 Self {
452 info: DialectInfo {
453 name: "snowflake",
454 display_name: "Snowflake",
455 aliases: &["sf"],
456 dialect_family: DialectFamily::Snowflake,
457 family: "Snowflake",
458 is_cloud_native: true,
459 is_olap: true,
460 is_postgres_compatible: false,
461 versions: &[],
462 deploy_script: None,
463 },
464 }
465 }
466 }
467
468 impl SqlRenderer for TestDialect {
469 fn render_data_type(&self, _dt: &DataType) -> String {
470 "TEXT".to_string()
471 }
472
473 fn quote_identifier(&self, name: &str) -> String {
474 format!("\"{name}\"")
475 }
476
477 fn qualify_name(&self, schema: Option<&str>, name: &str) -> String {
478 match schema {
479 Some(s) => format!("{}.{}", self.quote_identifier(s), self.quote_identifier(name)),
480 None => self.quote_identifier(name),
481 }
482 }
483
484 fn render_current_timestamp(&self) -> String {
485 "CURRENT_TIMESTAMP".to_string()
486 }
487 }
488
489 impl ExpressionRenderer for TestDialect {
490 fn render_concat(&self, parts: &[String]) -> String {
491 parts.join(" || ")
492 }
493 }
494
495 impl DdlRenderer for TestDialect {}
496 impl DmlRenderer for TestDialect {}
497 impl ProcedureRenderer for TestDialect {}
498 impl FormulaRenderer for TestDialect {}
499 impl TaskRenderer for TestDialect {}
500 impl ReplicationRenderer for TestDialect {}
501 impl StorageRenderer for TestDialect {}
502
503 impl DialectCapabilities for TestDialect {
504 fn supports_identity(&self) -> bool {
505 true
506 }
507 fn supports_sequences(&self) -> bool {
508 true
509 }
510 fn supports_stored_procedures(&self) -> bool {
511 true
512 }
513 }
514
515 impl PlatformDialect for TestDialect {
516 fn info(&self) -> DialectInfo {
517 self.info.clone()
518 }
519 }
520
521 const FLOOR_ONLY: &[EngineVersion] = &[EngineVersion::declared("15")];
522
523 #[test]
526 fn a_dialect_with_one_answer_is_itself_at_its_floor_and_refuses_another() {
527 let dialect: Arc<dyn PlatformDialect> = Arc::new(TestDialect {
528 info: DialectInfo { versions: FLOOR_ONLY, ..TestDialect::postgres().info() },
529 });
530 assert_eq!(dialect.engine_version(), Some(EngineVersion::declared("15")));
531 let at_floor =
532 Arc::clone(&dialect).at_version(&EngineVersion::declared("15")).expect("its floor");
533 assert!(Arc::ptr_eq(&at_floor, &dialect), "a dialect with one answer returns itself");
534
535 let Err(refused) = Arc::clone(&dialect).at_version(&EngineVersion::declared("17")) else {
536 panic!("17 is not declared")
537 };
538 assert_eq!(refused.answers_for, vec![EngineVersion::declared("15")]);
539 let said = refused.to_string();
540 assert!(said.contains("PostgreSQL"), "{said}");
541 assert!(said.contains("15"), "{said}");
542 assert!(said.contains("`postgres`"), "names the spelling that works: {said}");
543 }
544
545 #[test]
547 fn a_service_that_chooses_its_own_version_answers_for_none() {
548 let dialect = TestDialect::snowflake();
549 assert!(dialect.info().versions.is_empty());
550 assert_eq!(dialect.engine_version(), None);
551 }
552
553 #[test]
556 fn an_unsupported_version_names_every_version_and_how_to_write_each() {
557 let refusal = UnsupportedVersion {
558 engine: "postgres".to_owned(),
559 display_name: "PostgreSQL".to_owned(),
560 asked: "12".to_owned(),
561 answers_for: ["15", "16", "17"].iter().map(|v| EngineVersion::declared(v)).collect(),
562 };
563 let said = refusal.to_string();
564 assert!(said.contains("15, 16 and 17"), "{said}");
565 assert!(said.contains("not at 12"), "{said}");
566 assert!(said.contains("`postgres@17`"), "{said}");
567 assert!(said.contains("`postgres` for 15"), "the name alone is the floor: {said}");
568 }
569
570 #[test]
571 fn the_versions_ride_along_in_the_discovery_payload() {
572 let info = DialectInfo { versions: FLOOR_ONLY, ..TestDialect::postgres().info() };
573 let json = serde_json::to_value(&info).expect("DialectInfo serializes");
574 assert_eq!(json["versions"], serde_json::json!(["15"]));
575 }
576
577 #[test]
578 fn register_dialect_resolves_by_name() {
579 let mut registry = PlatformRegistry::new();
580 registry.register("postgres".to_string(), Arc::new(TestDialect::postgres()));
581 assert!(registry.get_by_name("postgres").is_some());
582 }
583
584 #[test]
585 fn register_extension_resolves_by_name() {
586 let mut registry = PlatformRegistry::new();
587 registry.register("redshift".to_string(), Arc::new(TestDialect::redshift()));
588 assert!(registry.get_by_name("redshift").is_some());
589 }
590
591 #[test]
592 fn resolve_tries_name_lookup() {
593 let mut registry = PlatformRegistry::new();
594 registry.register("postgres".to_string(), Arc::new(TestDialect::postgres()));
595 registry.register("redshift".to_string(), Arc::new(TestDialect::redshift()));
596
597 assert!(registry.resolve("postgres").is_some());
598 assert!(registry.resolve("redshift").is_some());
599 assert!(registry.resolve("nonexistent").is_none());
600 }
601
602 #[test]
603 fn list_returns_all_registered_dialects() {
604 let mut registry = PlatformRegistry::new();
605 registry.register("postgres".to_string(), Arc::new(TestDialect::postgres()));
606 registry.register("snowflake".to_string(), Arc::new(TestDialect::snowflake()));
607 registry.register("redshift".to_string(), Arc::new(TestDialect::redshift()));
608
609 let list = registry.list();
610 assert_eq!(list.len(), 3);
611 }
612
613 #[test]
614 fn duplicate_registration_overwrites() {
615 let mut registry = PlatformRegistry::new();
616 registry.register("postgres".to_string(), Arc::new(TestDialect::postgres()));
617 registry.register("postgres".to_string(), Arc::new(TestDialect::postgres()));
618
619 assert!(registry.get_by_name("postgres").is_some());
620 let list = registry.list();
621 assert_eq!(list.len(), 1);
622 }
623
624 #[test]
625 fn dialect_info_contains_expected_metadata() {
626 let dialect = TestDialect::postgres();
627 let info = dialect.info();
628 assert_eq!(info.name, "postgres");
629 assert_eq!(info.display_name, "PostgreSQL");
630 assert_eq!(info.dialect_family, DialectFamily::PostgreSql);
631 }
632
633 #[test]
634 fn classification_fields_reflect_dialect_characteristics() {
635 let pg = TestDialect::postgres();
636 let pg_info = pg.info();
637 assert_eq!(pg_info.family, "PostgreSQL");
638 assert!(pg_info.is_postgres_compatible);
639 assert!(!pg_info.is_cloud_native);
640 assert!(!pg_info.is_olap);
641
642 let sf = TestDialect::snowflake();
643 let sf_info = sf.info();
644 assert_eq!(sf_info.family, "Snowflake");
645 assert!(sf_info.is_cloud_native);
646 assert!(sf_info.is_olap);
647 assert!(!sf_info.is_postgres_compatible);
648
649 let rd = TestDialect::redshift();
650 let rd_info = rd.info();
651 assert!(rd_info.is_cloud_native);
652 assert!(rd_info.is_olap);
653 assert!(rd_info.is_postgres_compatible);
654 }
655
656 #[test]
657 fn empty_registry_returns_none() {
658 let registry = PlatformRegistry::new();
659 assert!(registry.get_by_name("postgres").is_none());
660 assert!(registry.resolve("postgres").is_none());
661 assert!(registry.list().is_empty());
662 }
663
664 #[test]
667 fn registered_names_are_sorted() {
668 let mut registry = PlatformRegistry::new();
669 registry.register("snowflake".to_string(), Arc::new(TestDialect::snowflake()));
670 registry.register("postgres".to_string(), Arc::new(TestDialect::postgres()));
671 registry.register("redshift".to_string(), Arc::new(TestDialect::redshift()));
672 assert_eq!(registry.registered_names(), vec!["postgres", "redshift", "snowflake"]);
673 }
674
675 #[test]
676 fn dialect_info_serializes_for_discovery() {
677 let info = TestDialect::snowflake().info();
680 let json = serde_json::to_value(&info).expect("DialectInfo serializes");
681 assert_eq!(json["name"], "snowflake");
682 assert_eq!(json["display_name"], "Snowflake");
683 assert_eq!(json["dialect_family"], "snowflake");
684 assert_eq!(json["is_cloud_native"], true);
685 assert_eq!(json["is_postgres_compatible"], false);
686 assert_eq!(json["aliases"], serde_json::json!(["sf"]));
689 }
690
691 #[test]
692 fn spellings_start_with_the_dialects_own_name() {
693 let info = TestDialect::postgres().info();
694 assert_eq!(info.spellings(), vec!["postgres", "postgresql", "pg"]);
695 }
696
697 #[test]
698 fn a_dialect_answers_to_its_name_and_its_aliases_in_any_case() {
699 let info = TestDialect::postgres().info();
700 assert!(info.answers_to("postgres"));
701 assert!(info.answers_to("pg"));
702 assert!(info.answers_to(" PostgreSQL "));
703 assert!(!info.answers_to("snowflake"));
704 }
705
706 #[test]
707 fn an_alias_repeating_the_name_is_listed_once() {
708 let info = DialectInfo { aliases: &["postgres", "pg"], ..TestDialect::postgres().info() };
709 assert_eq!(info.spellings(), vec!["postgres", "pg"]);
710 }
711}