1use crate::PlatformDialect;
17use crate::dialect::{PlatformRegistry, UnsupportedVersion};
18use razeri_plugin_contract::{EngineVersion, EngineVersionError, split_platform};
19use std::sync::Arc;
20use thiserror::Error;
21
22#[derive(Debug, Error)]
24pub enum ResolveError {
25 #[error(
27 "unknown dialect: '{name}'. {suggestions_text}Registered: {registered}",
28 suggestions_text = if suggestions.is_empty() {
29 String::new()
30 } else {
31 format!("Did you mean: {}? ", suggestions.join(", "))
32 },
33 registered = registered.join(", ")
34 )]
35 UnknownDialect {
36 name: String,
38 suggestions: Vec<String>,
40 registered: Vec<String>,
42 },
43
44 #[error(
50 "the platform name '{name}' belongs to {claimed_by_text} at once — use one platform's \
51 own name, or load only one of them",
52 claimed_by_text = claimed_by.join(" and ")
53 )]
54 AmbiguousName {
55 name: String,
57 claimed_by: Vec<String>,
59 },
60
61 #[error("'{name}' is not a platform name: {reason}")]
64 NotAPlatformName {
65 name: String,
67 reason: EngineVersionError,
70 },
71
72 #[error(
75 "{display_name} {why}, so a platform name gives it no version: write `{engine}`, not \
76 '{name}'",
77 why = if *chosen_by_its_service {
78 "chooses its own version"
79 } else {
80 "is rendered the same for every version"
81 }
82 )]
83 VersionChosenByTheService {
84 name: String,
86 engine: String,
88 display_name: String,
90 chosen_by_its_service: bool,
93 },
94
95 #[error(transparent)]
97 UnsupportedVersion(#[from] UnsupportedVersion),
98}
99
100pub fn resolve_dialect_by_name(name: &str) -> Result<Arc<dyn PlatformDialect>, ResolveError> {
134 resolve_dialect_by_name_with_registry(name, crate::process::process_registry())
135}
136
137pub fn resolve_dialect_by_name_with_registry(
144 name: &str,
145 registry: &PlatformRegistry,
146) -> Result<Arc<dyn PlatformDialect>, ResolveError> {
147 let (engine, version) = split_platform(name).map_err(|reason| {
148 ResolveError::NotAPlatformName { name: name.trim().to_owned(), reason }
149 })?;
150 let dialect = resolve_engine(engine, name, registry)?;
151 at_named_version(dialect, version, name)
152}
153
154fn at_named_version(
157 dialect: Arc<dyn PlatformDialect>,
158 version: Option<EngineVersion>,
159 name: &str,
160) -> Result<Arc<dyn PlatformDialect>, ResolveError> {
161 let info = dialect.info();
162 let Some(asked) = version else {
163 return match info.versions.first() {
164 Some(floor) => Ok(dialect.at_version(floor)?),
165 None => Ok(dialect),
166 };
167 };
168 if info.versions.is_empty() {
169 return Err(ResolveError::VersionChosenByTheService {
170 name: name.trim().to_owned(),
171 engine: info.name.to_owned(),
172 display_name: info.display_name.to_owned(),
173 chosen_by_its_service: info.is_cloud_native,
174 });
175 }
176 match info.versions.iter().find(|declared| **declared == asked) {
177 Some(declared) => Ok(dialect.at_version(declared)?),
178 None => Err(UnsupportedVersion::of(&info, asked.as_str()).into()),
179 }
180}
181
182fn resolve_engine(
185 wanted: &str,
186 name: &str,
187 registry: &PlatformRegistry,
188) -> Result<Arc<dyn PlatformDialect>, ResolveError> {
189 let wanted = wanted.trim();
190 let mut claimants: Vec<String> = registry
191 .registered_names()
192 .into_iter()
193 .filter(|key| answers_to(registry, key, wanted))
194 .collect();
195 claimants.sort();
196 claimants.dedup();
197
198 match claimants.as_slice() {
199 [] => {
200 let all_names = registered_names(registry);
201 let suggestions = near_misses(&wanted.to_lowercase(), &all_names);
202 Err(ResolveError::UnknownDialect {
203 name: name.to_string(),
204 suggestions,
205 registered: all_names,
206 })
207 }
208 [only] => registry.get_by_name(only).map(Arc::clone).ok_or_else(|| {
209 let all_names = registered_names(registry);
210 ResolveError::UnknownDialect {
211 name: name.to_string(),
212 suggestions: Vec::new(),
213 registered: all_names,
214 }
215 }),
216 _ => Err(ResolveError::AmbiguousName { name: wanted.to_string(), claimed_by: claimants }),
217 }
218}
219
220#[must_use]
225pub fn accepted_names(registry: &PlatformRegistry) -> Vec<String> {
226 let mut names: Vec<String> = registry
227 .list()
228 .iter()
229 .flat_map(|info| info.spellings().into_iter().map(str::to_owned))
230 .collect();
231 names.extend(registry.registered_names());
234 names.sort();
235 names.dedup();
236 names
237}
238
239#[must_use]
241pub fn all_accepted_names() -> Vec<String> {
242 accepted_names(&PlatformRegistry::with_auto_discovered())
243}
244
245fn answers_to(registry: &PlatformRegistry, key: &str, wanted: &str) -> bool {
253 if key.trim().eq_ignore_ascii_case(wanted) {
254 return true;
255 }
256 registry.get_by_name(key).is_some_and(|dialect| dialect.info().answers_to(wanted))
257}
258
259fn registered_names(registry: &PlatformRegistry) -> Vec<String> {
261 let mut names: Vec<String> = registry.list().iter().map(|info| info.name.to_string()).collect();
262 names.sort();
263 names
264}
265
266fn near_misses(input: &str, candidates: &[String]) -> Vec<String> {
268 let mut scored: Vec<(&str, usize)> = candidates
269 .iter()
270 .filter_map(|c| {
271 let dist = levenshtein(input, c);
272 if dist <= 3 { Some((c.as_str(), dist)) } else { None }
273 })
274 .collect();
275 scored.sort_by_key(|(_, d)| *d);
276 scored.into_iter().map(|(name, _)| name.to_string()).collect()
277}
278
279fn levenshtein(a: &str, b: &str) -> usize {
281 let a_bytes = a.as_bytes();
282 let b_bytes = b.as_bytes();
283 let m = a_bytes.len();
284 let n = b_bytes.len();
285
286 let mut prev: Vec<usize> = (0..=n).collect();
287 let mut curr = vec![0; n + 1];
288
289 for i in 1..=m {
290 curr[0] = i;
291 for j in 1..=n {
292 let cost = usize::from(a_bytes[i - 1] != b_bytes[j - 1]);
293 curr[j] = (prev[j] + 1).min(curr[j - 1] + 1).min(prev[j - 1] + cost);
294 }
295 std::mem::swap(&mut prev, &mut curr);
296 }
297 prev[n]
298}
299
300#[cfg(test)]
301mod tests {
302 use super::*;
303 use crate::dialect::{DialectInfo, PlatformDialect, PlatformRegistry};
304 use razeri_plugin_contract::DataType;
305 use razeri_plugin_contract::DialectFamily;
306 use razeri_plugin_contract::{
307 DdlRenderer, DialectCapabilities, DmlRenderer, ExpressionRenderer, FormulaRenderer,
308 ProcedureRenderer, ReplicationRenderer, SqlRenderer, StorageRenderer, TaskRenderer,
309 };
310
311 struct StubDialect {
315 info: DialectInfo,
316 at: Option<EngineVersion>,
317 }
318
319 impl SqlRenderer for StubDialect {
320 fn render_data_type(&self, _dt: &DataType) -> String {
321 String::new()
322 }
323 fn quote_identifier(&self, name: &str) -> String {
324 format!("\"{name}\"")
325 }
326 fn qualify_name(&self, _schema: Option<&str>, name: &str) -> String {
327 self.quote_identifier(name)
328 }
329 fn render_current_timestamp(&self) -> String {
330 String::new()
331 }
332 }
333 impl ExpressionRenderer for StubDialect {}
334 impl DdlRenderer for StubDialect {}
335 impl DmlRenderer for StubDialect {}
336 impl ProcedureRenderer for StubDialect {}
337 impl FormulaRenderer for StubDialect {}
338 impl TaskRenderer for StubDialect {}
339 impl DialectCapabilities for StubDialect {}
340 impl ReplicationRenderer for StubDialect {}
341 impl StorageRenderer for StubDialect {}
342 impl PlatformDialect for StubDialect {
343 fn info(&self) -> DialectInfo {
344 self.info.clone()
345 }
346
347 fn at_version(
348 self: Arc<Self>,
349 version: &EngineVersion,
350 ) -> Result<Arc<dyn PlatformDialect>, UnsupportedVersion> {
351 Ok(Arc::new(StubDialect { info: self.info.clone(), at: Some(*version) }))
352 }
353
354 fn engine_version(&self) -> Option<EngineVersion> {
355 self.at
356 }
357 }
358
359 fn test_registry() -> PlatformRegistry {
362 let mut reg = PlatformRegistry::new();
363 let entries: &[(&str, &str, &[&str], DialectFamily, &str)] = &[
364 (
365 "postgres",
366 "PostgreSQL",
367 &["postgresql", "pg"],
368 DialectFamily::PostgreSql,
369 "PostgreSQL",
370 ),
371 ("snowflake", "Snowflake", &["sf"], DialectFamily::Snowflake, "Snowflake"),
372 (
373 "sqlserver",
374 "SQL Server",
375 &["tsql", "mssql", "sql_server"],
376 DialectFamily::TSql,
377 "T-SQL",
378 ),
379 ("databricks", "Databricks", &["dbx", "spark"], DialectFamily::SparkSql, "Spark SQL"),
380 ("clickhouse", "ClickHouse", &["ch"], DialectFamily::ClickHouse, "ClickHouse SQL"),
381 ("duckdb", "DuckDB", &["duck"], DialectFamily::PostgreSql, "PostgreSQL"),
382 ("citus", "Citus", &["citusdb"], DialectFamily::PostgreSql, "PostgreSQL"),
383 ("mariadb", "MariaDB", &["mariadb_columnstore"], DialectFamily::MariaDB, "MariaDB"),
384 ("mysql", "MySQL", &[], DialectFamily::MariaDB, "MariaDB"),
385 ("agnostic", "Agnostic", &[], DialectFamily::Razeri, "ANSI"),
386 ("sqlite", "SQLite", &[], DialectFamily::MariaDB, "SQLite"),
387 ];
388 for &(name, display, aliases, family, fam_str) in entries {
389 reg.register(name.to_string(), Arc::new(stub(name, display, aliases, family, fam_str)));
390 }
391 reg
392 }
393
394 fn stub(
395 name: &'static str,
396 display: &'static str,
397 aliases: &'static [&'static str],
398 family: DialectFamily,
399 fam_str: &'static str,
400 ) -> StubDialect {
401 StubDialect {
402 info: DialectInfo {
403 name,
404 display_name: display,
405 aliases,
406 dialect_family: family,
407 family: fam_str,
408 is_cloud_native: false,
409 is_olap: false,
410 is_postgres_compatible: false,
411 versions: &[],
412 deploy_script: None,
413 },
414 at: None,
415 }
416 }
417
418 const POSTGRES_VERSIONS: &[EngineVersion] = &[
419 EngineVersion::declared("15"),
420 EngineVersion::declared("16"),
421 EngineVersion::declared("17"),
422 ];
423 const ORACLE_VERSIONS: &[EngineVersion] = &[EngineVersion::declared("23ai")];
424
425 fn versioned_registry() -> PlatformRegistry {
428 let mut reg = PlatformRegistry::new();
429 for (name, display, aliases, versions, is_service) in [
430 ("postgres", "PostgreSQL", &["postgresql", "pg"][..], POSTGRES_VERSIONS, false),
431 ("oracle", "Oracle", &[][..], ORACLE_VERSIONS, false),
432 ("snowflake", "Snowflake", &["sf"][..], &[][..], true),
433 ] {
434 let mut dialect = stub(name, display, aliases, DialectFamily::Custom(name), display);
435 dialect.info.versions = versions;
436 dialect.info.is_cloud_native = is_service;
437 reg.register(name.to_string(), Arc::new(dialect));
438 }
439 reg
440 }
441
442 fn version_of(name: &str, registry: &PlatformRegistry) -> String {
443 resolve_dialect_by_name_with_registry(name, registry)
444 .unwrap_or_else(|e| panic!("'{name}' resolves: {e}"))
445 .engine_version()
446 .map_or_else(|| "none".to_owned(), |v| v.to_string())
447 }
448
449 fn refusal(name: &str, registry: &PlatformRegistry) -> ResolveError {
450 match resolve_dialect_by_name_with_registry(name, registry) {
451 Ok(dialect) => panic!("'{name}' resolved to {}", dialect.info().name),
452 Err(error) => error,
453 }
454 }
455
456 #[test]
458 fn a_name_with_no_version_is_the_engine_at_its_floor() {
459 let reg = versioned_registry();
460 assert_eq!(version_of("postgres", ®), "15");
461 assert_eq!(version_of("PG", ®), "15");
462 assert_eq!(version_of("snowflake", ®), "none");
463 }
464
465 #[test]
466 fn a_named_version_is_the_dialect_at_that_version() {
467 let reg = versioned_registry();
468 assert_eq!(version_of("postgres@17", ®), "17");
469 assert_eq!(version_of(" PostgreSQL@16 ", ®), "16");
470 assert_eq!(resolved("pg@17", ®), "postgres");
471 }
472
473 #[test]
476 fn a_version_is_kept_in_the_spelling_the_dialect_declares() {
477 let reg = versioned_registry();
478 assert_eq!(version_of("oracle@23", ®), "23ai");
479 assert_eq!(version_of("oracle@23ai", ®), "23ai");
480 }
481
482 #[test]
483 fn a_version_the_engine_does_not_answer_for_is_refused_with_the_list() {
484 let reg = versioned_registry();
485 let error = refusal("postgres@12", ®);
486 assert!(matches!(error, ResolveError::UnsupportedVersion(_)), "{error:?}");
487 let said = error.to_string();
488 assert!(said.contains("15, 16 and 17"), "{said}");
489 assert!(said.contains("`postgres@17`"), "{said}");
490 }
491
492 #[test]
493 fn a_version_on_a_service_that_chooses_its_own_is_refused_by_name() {
494 let reg = versioned_registry();
495 let error = refusal("snowflake@8", ®);
496 assert!(matches!(error, ResolveError::VersionChosenByTheService { .. }), "{error:?}");
497 let said = error.to_string();
498 assert!(said.contains("Snowflake chooses its own version"), "{said}");
499 assert!(said.contains("write `snowflake`"), "{said}");
500 }
501
502 #[test]
503 fn a_dot_before_letters_is_refused_showing_the_accepted_spelling() {
504 let reg = versioned_registry();
505 let error = refusal("oracle@23.ai", ®);
506 assert!(matches!(error, ResolveError::NotAPlatformName { .. }), "{error:?}");
507 assert!(error.to_string().contains("`oracle@23ai`"), "{error}");
508 }
509
510 #[test]
511 fn a_colon_is_refused() {
512 let reg = versioned_registry();
513 let error = refusal("postgres:17", ®);
514 assert!(matches!(error, ResolveError::NotAPlatformName { .. }), "{error:?}");
515 assert!(error.to_string().contains("`postgres@17`"), "{error}");
516 }
517
518 #[test]
519 fn an_unknown_engine_with_a_version_is_refused_as_an_unknown_engine() {
520 let reg = versioned_registry();
521 let error = refusal("postgress@17", ®);
522 let ResolveError::UnknownDialect { suggestions, .. } = &error else { panic!("{error:?}") };
523 assert_eq!(suggestions, &vec!["postgres".to_owned()]);
524 }
525
526 fn resolved(name: &str, registry: &PlatformRegistry) -> String {
527 resolve_dialect_by_name_with_registry(name, registry)
528 .unwrap_or_else(|e| panic!("'{name}' resolves: {e}"))
529 .info()
530 .name
531 .to_owned()
532 }
533
534 #[test]
539 fn every_declared_spelling_resolves_to_the_dialect_that_declared_it() {
540 let reg = test_registry();
541 for info in reg.list() {
542 for spelling in info.spellings() {
543 assert_eq!(resolved(spelling, ®), info.name, "spelling '{spelling}'");
544 }
545 }
546 }
547
548 #[test]
549 fn the_declared_spellings_are_the_accepted_names_and_nothing_else_is() {
550 let reg = test_registry();
551 let accepted = accepted_names(®);
552 for info in reg.list() {
553 for spelling in info.spellings() {
554 assert!(accepted.contains(&spelling.to_owned()), "'{spelling}' is accepted");
555 }
556 }
557 for name in accepted {
558 assert!(
559 reg.list().iter().any(|info| info.spellings().contains(&name.as_str())),
560 "'{name}' is accepted, so some loaded dialect declares it"
561 );
562 }
563 }
564
565 #[test]
566 fn resolution_ignores_case_and_surrounding_whitespace() {
567 let reg = test_registry();
568 assert_eq!(resolved("SNOWFLAKE", ®), "snowflake");
569 assert_eq!(resolved(" DuckDB ", ®), "duckdb");
570 assert_eq!(resolved("PG", ®), "postgres");
571 }
572
573 #[test]
574 fn a_name_the_host_registered_resolves_even_if_the_dialect_never_declared_it() {
575 let mut reg = PlatformRegistry::new();
576 reg.register(
577 "housename".to_string(),
578 Arc::new(stub("mydb", "MyDB", &[], DialectFamily::Custom("mydb"), "MyDB")),
579 );
580 assert_eq!(resolved("housename", ®), "mydb");
581 assert_eq!(resolved("mydb", ®), "mydb");
582 }
583
584 #[test]
589 fn a_dialect_razeri_never_heard_of_resolves_by_its_own_spellings() {
590 let mut reg = test_registry();
591 reg.register(
592 "outsidedb".to_string(),
593 Arc::new(stub(
594 "outsidedb",
595 "Outside DB",
596 &["odb", "outside_db"],
597 DialectFamily::Custom("outsidedb"),
598 "OutsideDB",
599 )),
600 );
601 for spelling in ["outsidedb", "odb", "outside_db", " ODB "] {
602 assert_eq!(resolved(spelling, ®), "outsidedb", "spelling '{spelling}'");
603 }
604 let dialect = resolve_dialect_by_name_with_registry("odb", ®).expect("resolves");
606 assert_eq!(dialect.quote_identifier("orders"), "\"orders\"");
607 assert!(accepted_names(®).contains(&"odb".to_string()));
608 }
609
610 #[test]
611 fn resolve_unknown_returns_error_with_suggestions() {
612 let reg = test_registry();
613 let result = resolve_dialect_by_name_with_registry("postres", ®);
614 let Err(err) = result else { panic!("expected error") };
615 let msg = err.to_string();
616 assert!(msg.contains("unknown dialect"), "error should mention unknown dialect: {msg}");
617 assert!(msg.contains("postgres"), "error should suggest postgres: {msg}");
618 }
619
620 #[test]
621 fn resolve_completely_unknown_returns_registered_list() {
622 let reg = test_registry();
623 let result = resolve_dialect_by_name_with_registry("zzzznotadb", ®);
624 let Err(err) = result else { panic!("expected error") };
625 let msg = err.to_string();
626 assert!(msg.contains("Registered:"), "error should list registered: {msg}");
627 }
628
629 #[test]
633 fn a_spelling_two_dialects_claim_is_refused_and_both_are_named() {
634 let mut reg = test_registry();
635 reg.register(
636 "sparkler".to_string(),
637 Arc::new(stub(
638 "sparkler",
639 "Sparkler",
640 &["spark"],
641 DialectFamily::Custom("sparkler"),
642 "Sparkler",
643 )),
644 );
645 let Err(err) = resolve_dialect_by_name_with_registry("spark", ®) else {
646 panic!("a spelling two dialects claim must not resolve")
647 };
648 let msg = err.to_string();
649 assert!(msg.contains("databricks"), "names the first claimant: {msg}");
650 assert!(msg.contains("sparkler"), "names the second claimant: {msg}");
651 assert!(msg.contains("use one platform's own name"), "says what to do: {msg}");
652 assert_eq!(resolved("databricks", ®), "databricks");
655 assert_eq!(resolved("dbx", ®), "databricks");
656 assert_eq!(resolved("sparkler", ®), "sparkler");
657 }
658
659 #[test]
660 fn a_dialect_claiming_another_dialects_own_name_is_refused_too() {
661 let mut reg = test_registry();
662 reg.register(
663 "impostor".to_string(),
664 Arc::new(stub(
665 "impostor",
666 "Impostor",
667 &["duckdb"],
668 DialectFamily::Custom("impostor"),
669 "Impostor",
670 )),
671 );
672 let Err(err) = resolve_dialect_by_name_with_registry("duckdb", ®) else {
673 panic!("a claimed name must not resolve to whichever came first")
674 };
675 assert!(err.to_string().contains("impostor"), "{err}");
676 }
677
678 #[test]
679 fn levenshtein_distance_basic() {
680 assert_eq!(levenshtein("postgres", "postgres"), 0);
681 assert_eq!(levenshtein("postgre", "postgres"), 1);
682 assert_eq!(levenshtein("postgrse", "postgres"), 2);
683 assert_eq!(levenshtein("", "abc"), 3);
684 }
685}