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>,
81}
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
86#[serde(rename_all = "snake_case")]
87#[non_exhaustive]
88pub enum DeployScript {
89 Duckdb,
91 Sqlite3,
93 Sqlplus,
95 Hdbsql,
97 Mysql,
99 Psql,
101 InvokeSqlcmd,
103}
104
105pub 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 fn info(&self) -> DialectInfo;
128
129 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 fn engine_version(&self) -> Option<EngineVersion> {
160 self.info().versions.first().copied()
161 }
162}
163
164pub trait DialectHandle {
170 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#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
182#[error("{}", self.sentence())]
183pub struct UnsupportedVersion {
184 pub engine: String,
186 pub display_name: String,
188 pub asked: String,
190 pub answers_for: Vec<EngineVersion>,
192}
193
194impl UnsupportedVersion {
195 #[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 #[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
233fn 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 #[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 #[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
269pub struct PlatformRegistry {
274 dialects: HashMap<String, Arc<dyn PlatformDialect>>,
275}
276
277impl PlatformRegistry {
278 #[must_use]
280 pub fn new() -> Self {
281 Self { dialects: HashMap::new() }
282 }
283
284 pub fn register(&mut self, name: String, dialect: Arc<dyn PlatformDialect>) {
286 self.dialects.insert(name, dialect);
287 }
288
289 #[must_use]
291 pub fn get_by_name(&self, name: &str) -> Option<&Arc<dyn PlatformDialect>> {
292 self.dialects.get(name)
293 }
294
295 #[must_use]
297 pub fn resolve(&self, target: &str) -> Option<Arc<dyn PlatformDialect>> {
298 self.dialects.get(target).cloned()
299 }
300
301 #[must_use]
303 pub fn list(&self) -> Vec<DialectInfo> {
304 self.dialects.values().map(|d| d.info()).collect()
305 }
306
307 #[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 #[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 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 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 #[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 #[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 #[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 #[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 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 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}