1use crate::PlatformDialect;
17use crate::dialect::PlatformRegistry;
18use std::sync::Arc;
19use thiserror::Error;
20
21#[derive(Debug, Error)]
23pub enum ResolveError {
24 #[error(
26 "unknown dialect: '{name}'. {suggestions_text}Registered: {registered}",
27 suggestions_text = if suggestions.is_empty() {
28 String::new()
29 } else {
30 format!("Did you mean: {}? ", suggestions.join(", "))
31 },
32 registered = registered.join(", ")
33 )]
34 UnknownDialect {
35 name: String,
37 suggestions: Vec<String>,
39 registered: Vec<String>,
41 },
42
43 #[error(
49 "the platform name '{name}' belongs to {claimed_by_text} at once — use one platform's \
50 own name, or load only one of them",
51 claimed_by_text = claimed_by.join(" and ")
52 )]
53 AmbiguousName {
54 name: String,
56 claimed_by: Vec<String>,
58 },
59}
60
61pub fn resolve_dialect_by_name(name: &str) -> Result<Arc<dyn PlatformDialect>, ResolveError> {
86 let registry = PlatformRegistry::with_auto_discovered();
87 resolve_dialect_by_name_with_registry(name, ®istry)
88}
89
90pub fn resolve_dialect_by_name_with_registry(
97 name: &str,
98 registry: &PlatformRegistry,
99) -> Result<Arc<dyn PlatformDialect>, ResolveError> {
100 let wanted = name.trim();
101 let mut claimants: Vec<String> = registry
102 .registered_names()
103 .into_iter()
104 .filter(|key| answers_to(registry, key, wanted))
105 .collect();
106 claimants.sort();
107 claimants.dedup();
108
109 match claimants.as_slice() {
110 [] => {
111 let all_names = registered_names(registry);
112 let suggestions = near_misses(&wanted.to_lowercase(), &all_names);
113 Err(ResolveError::UnknownDialect {
114 name: name.to_string(),
115 suggestions,
116 registered: all_names,
117 })
118 }
119 [only] => registry.get_by_name(only).map(Arc::clone).ok_or_else(|| {
120 let all_names = registered_names(registry);
121 ResolveError::UnknownDialect {
122 name: name.to_string(),
123 suggestions: Vec::new(),
124 registered: all_names,
125 }
126 }),
127 _ => Err(ResolveError::AmbiguousName { name: wanted.to_string(), claimed_by: claimants }),
128 }
129}
130
131#[must_use]
136pub fn accepted_names(registry: &PlatformRegistry) -> Vec<String> {
137 let mut names: Vec<String> = registry
138 .list()
139 .iter()
140 .flat_map(|info| info.spellings().into_iter().map(str::to_owned))
141 .collect();
142 names.extend(registry.registered_names());
145 names.sort();
146 names.dedup();
147 names
148}
149
150#[must_use]
152pub fn all_accepted_names() -> Vec<String> {
153 accepted_names(&PlatformRegistry::with_auto_discovered())
154}
155
156fn answers_to(registry: &PlatformRegistry, key: &str, wanted: &str) -> bool {
164 if key.trim().eq_ignore_ascii_case(wanted) {
165 return true;
166 }
167 registry.get_by_name(key).is_some_and(|dialect| dialect.info().answers_to(wanted))
168}
169
170fn registered_names(registry: &PlatformRegistry) -> Vec<String> {
172 let mut names: Vec<String> = registry.list().iter().map(|info| info.name.to_string()).collect();
173 names.sort();
174 names
175}
176
177fn near_misses(input: &str, candidates: &[String]) -> Vec<String> {
179 let mut scored: Vec<(&str, usize)> = candidates
180 .iter()
181 .filter_map(|c| {
182 let dist = levenshtein(input, c);
183 if dist <= 3 { Some((c.as_str(), dist)) } else { None }
184 })
185 .collect();
186 scored.sort_by_key(|(_, d)| *d);
187 scored.into_iter().map(|(name, _)| name.to_string()).collect()
188}
189
190fn levenshtein(a: &str, b: &str) -> usize {
192 let a_bytes = a.as_bytes();
193 let b_bytes = b.as_bytes();
194 let m = a_bytes.len();
195 let n = b_bytes.len();
196
197 let mut prev: Vec<usize> = (0..=n).collect();
198 let mut curr = vec![0; n + 1];
199
200 for i in 1..=m {
201 curr[0] = i;
202 for j in 1..=n {
203 let cost = usize::from(a_bytes[i - 1] != b_bytes[j - 1]);
204 curr[j] = (prev[j] + 1).min(curr[j - 1] + 1).min(prev[j - 1] + cost);
205 }
206 std::mem::swap(&mut prev, &mut curr);
207 }
208 prev[n]
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214 use crate::dialect::{DialectInfo, PlatformDialect, PlatformRegistry};
215 use razeri_plugin_contract::DataType;
216 use razeri_plugin_contract::DialectFamily;
217 use razeri_plugin_contract::{
218 DdlRenderer, DialectCapabilities, DmlRenderer, ExpressionRenderer, FormulaRenderer,
219 ProcedureRenderer, ReplicationRenderer, SqlRenderer, StorageRenderer, TaskRenderer,
220 };
221
222 struct StubDialect {
224 info: DialectInfo,
225 }
226
227 impl SqlRenderer for StubDialect {
228 fn render_data_type(&self, _dt: &DataType) -> String {
229 String::new()
230 }
231 fn quote_identifier(&self, name: &str) -> String {
232 format!("\"{name}\"")
233 }
234 fn qualify_name(&self, _schema: Option<&str>, name: &str) -> String {
235 self.quote_identifier(name)
236 }
237 fn render_current_timestamp(&self) -> String {
238 String::new()
239 }
240 }
241 impl ExpressionRenderer for StubDialect {}
242 impl DdlRenderer for StubDialect {}
243 impl DmlRenderer for StubDialect {}
244 impl ProcedureRenderer for StubDialect {}
245 impl FormulaRenderer for StubDialect {}
246 impl TaskRenderer for StubDialect {}
247 impl DialectCapabilities for StubDialect {}
248 impl ReplicationRenderer for StubDialect {}
249 impl StorageRenderer for StubDialect {}
250 impl PlatformDialect for StubDialect {
251 fn info(&self) -> DialectInfo {
252 self.info.clone()
253 }
254 }
255
256 fn test_registry() -> PlatformRegistry {
259 let mut reg = PlatformRegistry::new();
260 let entries: &[(&str, &str, &[&str], DialectFamily, &str)] = &[
261 (
262 "postgres",
263 "PostgreSQL",
264 &["postgresql", "pg"],
265 DialectFamily::PostgreSql,
266 "PostgreSQL",
267 ),
268 ("snowflake", "Snowflake", &["sf"], DialectFamily::Snowflake, "Snowflake"),
269 (
270 "sqlserver",
271 "SQL Server",
272 &["tsql", "mssql", "sql_server"],
273 DialectFamily::TSql,
274 "T-SQL",
275 ),
276 ("databricks", "Databricks", &["dbx", "spark"], DialectFamily::SparkSql, "Spark SQL"),
277 ("clickhouse", "ClickHouse", &["ch"], DialectFamily::ClickHouse, "ClickHouse SQL"),
278 ("duckdb", "DuckDB", &["duck"], DialectFamily::PostgreSql, "PostgreSQL"),
279 ("citus", "Citus", &["citusdb"], DialectFamily::PostgreSql, "PostgreSQL"),
280 ("mariadb", "MariaDB", &["mariadb_columnstore"], DialectFamily::MariaDB, "MariaDB"),
281 ("mysql", "MySQL", &[], DialectFamily::MariaDB, "MariaDB"),
282 ("agnostic", "Agnostic", &[], DialectFamily::Razeri, "ANSI"),
283 ("sqlite", "SQLite", &[], DialectFamily::MariaDB, "SQLite"),
284 ];
285 for &(name, display, aliases, family, fam_str) in entries {
286 reg.register(name.to_string(), Arc::new(stub(name, display, aliases, family, fam_str)));
287 }
288 reg
289 }
290
291 fn stub(
292 name: &'static str,
293 display: &'static str,
294 aliases: &'static [&'static str],
295 family: DialectFamily,
296 fam_str: &'static str,
297 ) -> StubDialect {
298 StubDialect {
299 info: DialectInfo {
300 name,
301 display_name: display,
302 aliases,
303 dialect_family: family,
304 family: fam_str,
305 is_cloud_native: false,
306 is_olap: false,
307 is_postgres_compatible: false,
308 },
309 }
310 }
311
312 fn resolved(name: &str, registry: &PlatformRegistry) -> String {
313 resolve_dialect_by_name_with_registry(name, registry)
314 .unwrap_or_else(|e| panic!("'{name}' resolves: {e}"))
315 .info()
316 .name
317 .to_owned()
318 }
319
320 #[test]
325 fn every_declared_spelling_resolves_to_the_dialect_that_declared_it() {
326 let reg = test_registry();
327 for info in reg.list() {
328 for spelling in info.spellings() {
329 assert_eq!(resolved(spelling, ®), info.name, "spelling '{spelling}'");
330 }
331 }
332 }
333
334 #[test]
335 fn the_declared_spellings_are_the_accepted_names_and_nothing_else_is() {
336 let reg = test_registry();
337 let accepted = accepted_names(®);
338 for info in reg.list() {
339 for spelling in info.spellings() {
340 assert!(accepted.contains(&spelling.to_owned()), "'{spelling}' is accepted");
341 }
342 }
343 for name in accepted {
344 assert!(
345 reg.list().iter().any(|info| info.spellings().contains(&name.as_str())),
346 "'{name}' is accepted, so some loaded dialect declares it"
347 );
348 }
349 }
350
351 #[test]
352 fn resolution_ignores_case_and_surrounding_whitespace() {
353 let reg = test_registry();
354 assert_eq!(resolved("SNOWFLAKE", ®), "snowflake");
355 assert_eq!(resolved(" DuckDB ", ®), "duckdb");
356 assert_eq!(resolved("PG", ®), "postgres");
357 }
358
359 #[test]
360 fn a_name_the_host_registered_resolves_even_if_the_dialect_never_declared_it() {
361 let mut reg = PlatformRegistry::new();
362 reg.register(
363 "housename".to_string(),
364 Arc::new(stub("mydb", "MyDB", &[], DialectFamily::Custom("mydb"), "MyDB")),
365 );
366 assert_eq!(resolved("housename", ®), "mydb");
367 assert_eq!(resolved("mydb", ®), "mydb");
368 }
369
370 #[test]
375 fn a_dialect_razeri_never_heard_of_resolves_by_its_own_spellings() {
376 let mut reg = test_registry();
377 reg.register(
378 "outsidedb".to_string(),
379 Arc::new(stub(
380 "outsidedb",
381 "Outside DB",
382 &["odb", "outside_db"],
383 DialectFamily::Custom("outsidedb"),
384 "OutsideDB",
385 )),
386 );
387 for spelling in ["outsidedb", "odb", "outside_db", " ODB "] {
388 assert_eq!(resolved(spelling, ®), "outsidedb", "spelling '{spelling}'");
389 }
390 let dialect = resolve_dialect_by_name_with_registry("odb", ®).expect("resolves");
392 assert_eq!(dialect.quote_identifier("orders"), "\"orders\"");
393 assert!(accepted_names(®).contains(&"odb".to_string()));
394 }
395
396 #[test]
397 fn resolve_unknown_returns_error_with_suggestions() {
398 let reg = test_registry();
399 let result = resolve_dialect_by_name_with_registry("postres", ®);
400 let Err(err) = result else { panic!("expected error") };
401 let msg = err.to_string();
402 assert!(msg.contains("unknown dialect"), "error should mention unknown dialect: {msg}");
403 assert!(msg.contains("postgres"), "error should suggest postgres: {msg}");
404 }
405
406 #[test]
407 fn resolve_completely_unknown_returns_registered_list() {
408 let reg = test_registry();
409 let result = resolve_dialect_by_name_with_registry("zzzznotadb", ®);
410 let Err(err) = result else { panic!("expected error") };
411 let msg = err.to_string();
412 assert!(msg.contains("Registered:"), "error should list registered: {msg}");
413 }
414
415 #[test]
419 fn a_spelling_two_dialects_claim_is_refused_and_both_are_named() {
420 let mut reg = test_registry();
421 reg.register(
422 "sparkler".to_string(),
423 Arc::new(stub(
424 "sparkler",
425 "Sparkler",
426 &["spark"],
427 DialectFamily::Custom("sparkler"),
428 "Sparkler",
429 )),
430 );
431 let Err(err) = resolve_dialect_by_name_with_registry("spark", ®) else {
432 panic!("a spelling two dialects claim must not resolve")
433 };
434 let msg = err.to_string();
435 assert!(msg.contains("databricks"), "names the first claimant: {msg}");
436 assert!(msg.contains("sparkler"), "names the second claimant: {msg}");
437 assert!(msg.contains("use one platform's own name"), "says what to do: {msg}");
438 assert_eq!(resolved("databricks", ®), "databricks");
441 assert_eq!(resolved("dbx", ®), "databricks");
442 assert_eq!(resolved("sparkler", ®), "sparkler");
443 }
444
445 #[test]
446 fn a_dialect_claiming_another_dialects_own_name_is_refused_too() {
447 let mut reg = test_registry();
448 reg.register(
449 "impostor".to_string(),
450 Arc::new(stub(
451 "impostor",
452 "Impostor",
453 &["duckdb"],
454 DialectFamily::Custom("impostor"),
455 "Impostor",
456 )),
457 );
458 let Err(err) = resolve_dialect_by_name_with_registry("duckdb", ®) else {
459 panic!("a claimed name must not resolve to whichever came first")
460 };
461 assert!(err.to_string().contains("impostor"), "{err}");
462 }
463
464 #[test]
465 fn levenshtein_distance_basic() {
466 assert_eq!(levenshtein("postgres", "postgres"), 0);
467 assert_eq!(levenshtein("postgre", "postgres"), 1);
468 assert_eq!(levenshtein("postgrse", "postgres"), 2);
469 assert_eq!(levenshtein("", "abc"), 3);
470 }
471}