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