1use razeri_plugin_contract::DialectFamily;
14use razeri_plugin_contract::{
15 DdlRenderer, DialectCapabilities, DmlRenderer, ExpressionRenderer, FormulaRenderer,
16 ProcedureRenderer, ReplicationRenderer, SqlRenderer, StorageRenderer, TaskRenderer,
17};
18use std::collections::HashMap;
19use std::sync::Arc;
20
21pub use razeri_plugin_contract;
23
24#[derive(Debug, Clone, serde::Serialize)]
31pub struct DialectInfo {
32 pub name: &'static str,
34 pub display_name: &'static str,
36 pub aliases: &'static [&'static str],
48 pub dialect_family: DialectFamily,
50 pub family: &'static str,
52 pub is_cloud_native: bool,
54 pub is_olap: bool,
56 pub is_postgres_compatible: bool,
58}
59
60pub trait PlatformDialect:
69 SqlRenderer
70 + ExpressionRenderer
71 + DdlRenderer
72 + DmlRenderer
73 + ProcedureRenderer
74 + FormulaRenderer
75 + TaskRenderer
76 + DialectCapabilities
77 + ReplicationRenderer
78 + StorageRenderer
79{
80 fn info(&self) -> DialectInfo;
82}
83
84impl DialectInfo {
85 #[must_use]
91 pub fn spellings(&self) -> Vec<&'static str> {
92 let mut out = Vec::with_capacity(self.aliases.len() + 1);
93 out.push(self.name);
94 for alias in self.aliases {
95 if !out.contains(alias) {
96 out.push(alias);
97 }
98 }
99 out
100 }
101
102 #[must_use]
105 pub fn answers_to(&self, spelling: &str) -> bool {
106 let wanted = spelling.trim();
107 self.spellings().iter().any(|known| known.eq_ignore_ascii_case(wanted))
108 }
109}
110
111pub struct PlatformRegistry {
116 dialects: HashMap<String, Arc<dyn PlatformDialect>>,
117}
118
119impl PlatformRegistry {
120 #[must_use]
122 pub fn new() -> Self {
123 Self { dialects: HashMap::new() }
124 }
125
126 pub fn register(&mut self, name: String, dialect: Arc<dyn PlatformDialect>) {
128 self.dialects.insert(name, dialect);
129 }
130
131 #[must_use]
133 pub fn get_by_name(&self, name: &str) -> Option<&Arc<dyn PlatformDialect>> {
134 self.dialects.get(name)
135 }
136
137 #[must_use]
139 pub fn resolve(&self, target: &str) -> Option<Arc<dyn PlatformDialect>> {
140 self.dialects.get(target).cloned()
141 }
142
143 #[must_use]
145 pub fn list(&self) -> Vec<DialectInfo> {
146 self.dialects.values().map(|d| d.info()).collect()
147 }
148
149 #[must_use]
155 pub fn registered_names(&self) -> Vec<String> {
156 let mut names: Vec<String> = self.dialects.keys().cloned().collect();
157 names.sort();
158 names
159 }
160
161 #[must_use]
166 pub fn with_auto_discovered() -> Self {
167 use crate::builder::DialectRegistration;
168 let mut registry = Self::new();
169 for reg in inventory::iter::<DialectRegistration> {
170 let dialect = (reg.factory)();
171 let name = dialect.info().name.to_string();
172 registry.register(name, dialect);
173 }
174 for reg in inventory::iter::<crate::builder::PluginRegistration> {
177 let mut linked = crate::builder::PluginRegistryBuilder::new();
178 (reg.register)(&mut linked);
179 for (name, dialect) in linked.build().dialects.dialects {
180 registry.register(name, dialect);
181 }
182 }
183 registry
184 }
185}
186
187impl Default for PlatformRegistry {
188 fn default() -> Self {
189 Self::new()
190 }
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196 use razeri_plugin_contract::DataType;
197
198 struct TestDialect {
200 info: DialectInfo,
201 }
202
203 impl TestDialect {
204 fn postgres() -> Self {
205 Self {
206 info: DialectInfo {
207 name: "postgres",
208 display_name: "PostgreSQL",
209 aliases: &["postgresql", "pg"],
210 dialect_family: DialectFamily::PostgreSql,
211 family: "PostgreSQL",
212 is_cloud_native: false,
213 is_olap: false,
214 is_postgres_compatible: true,
215 },
216 }
217 }
218
219 fn redshift() -> Self {
220 Self {
221 info: DialectInfo {
222 name: "redshift",
223 display_name: "Amazon Redshift",
224 aliases: &["amazon_redshift", "rs"],
225 dialect_family: DialectFamily::PostgreSql,
226 family: "PostgreSQL",
227 is_cloud_native: true,
228 is_olap: true,
229 is_postgres_compatible: true,
230 },
231 }
232 }
233
234 fn snowflake() -> Self {
235 Self {
236 info: DialectInfo {
237 name: "snowflake",
238 display_name: "Snowflake",
239 aliases: &["sf"],
240 dialect_family: DialectFamily::Snowflake,
241 family: "Snowflake",
242 is_cloud_native: true,
243 is_olap: true,
244 is_postgres_compatible: false,
245 },
246 }
247 }
248 }
249
250 impl SqlRenderer for TestDialect {
251 fn render_data_type(&self, _dt: &DataType) -> String {
252 "TEXT".to_string()
253 }
254
255 fn quote_identifier(&self, name: &str) -> String {
256 format!("\"{name}\"")
257 }
258
259 fn qualify_name(&self, schema: Option<&str>, name: &str) -> String {
260 match schema {
261 Some(s) => format!("{}.{}", self.quote_identifier(s), self.quote_identifier(name)),
262 None => self.quote_identifier(name),
263 }
264 }
265
266 fn render_current_timestamp(&self) -> String {
267 "CURRENT_TIMESTAMP".to_string()
268 }
269 }
270
271 impl ExpressionRenderer for TestDialect {
272 fn render_concat(&self, parts: &[String]) -> String {
273 parts.join(" || ")
274 }
275 }
276
277 impl DdlRenderer for TestDialect {}
278 impl DmlRenderer for TestDialect {}
279 impl ProcedureRenderer for TestDialect {}
280 impl FormulaRenderer for TestDialect {}
281 impl TaskRenderer for TestDialect {}
282 impl ReplicationRenderer for TestDialect {}
283 impl StorageRenderer for TestDialect {}
284
285 impl DialectCapabilities for TestDialect {
286 fn supports_identity(&self) -> bool {
287 true
288 }
289 fn supports_sequences(&self) -> bool {
290 true
291 }
292 fn supports_stored_procedures(&self) -> bool {
293 true
294 }
295 }
296
297 impl PlatformDialect for TestDialect {
298 fn info(&self) -> DialectInfo {
299 self.info.clone()
300 }
301 }
302
303 #[test]
304 fn register_dialect_resolves_by_name() {
305 let mut registry = PlatformRegistry::new();
306 registry.register("postgres".to_string(), Arc::new(TestDialect::postgres()));
307 assert!(registry.get_by_name("postgres").is_some());
308 }
309
310 #[test]
311 fn register_extension_resolves_by_name() {
312 let mut registry = PlatformRegistry::new();
313 registry.register("redshift".to_string(), Arc::new(TestDialect::redshift()));
314 assert!(registry.get_by_name("redshift").is_some());
315 }
316
317 #[test]
318 fn resolve_tries_name_lookup() {
319 let mut registry = PlatformRegistry::new();
320 registry.register("postgres".to_string(), Arc::new(TestDialect::postgres()));
321 registry.register("redshift".to_string(), Arc::new(TestDialect::redshift()));
322
323 assert!(registry.resolve("postgres").is_some());
324 assert!(registry.resolve("redshift").is_some());
325 assert!(registry.resolve("nonexistent").is_none());
326 }
327
328 #[test]
329 fn list_returns_all_registered_dialects() {
330 let mut registry = PlatformRegistry::new();
331 registry.register("postgres".to_string(), Arc::new(TestDialect::postgres()));
332 registry.register("snowflake".to_string(), Arc::new(TestDialect::snowflake()));
333 registry.register("redshift".to_string(), Arc::new(TestDialect::redshift()));
334
335 let list = registry.list();
336 assert_eq!(list.len(), 3);
337 }
338
339 #[test]
340 fn duplicate_registration_overwrites() {
341 let mut registry = PlatformRegistry::new();
342 registry.register("postgres".to_string(), Arc::new(TestDialect::postgres()));
343 registry.register("postgres".to_string(), Arc::new(TestDialect::postgres()));
344
345 assert!(registry.get_by_name("postgres").is_some());
346 let list = registry.list();
347 assert_eq!(list.len(), 1);
348 }
349
350 #[test]
351 fn dialect_info_contains_expected_metadata() {
352 let dialect = TestDialect::postgres();
353 let info = dialect.info();
354 assert_eq!(info.name, "postgres");
355 assert_eq!(info.display_name, "PostgreSQL");
356 assert_eq!(info.dialect_family, DialectFamily::PostgreSql);
357 }
358
359 #[test]
360 fn classification_fields_reflect_dialect_characteristics() {
361 let pg = TestDialect::postgres();
362 let pg_info = pg.info();
363 assert_eq!(pg_info.family, "PostgreSQL");
364 assert!(pg_info.is_postgres_compatible);
365 assert!(!pg_info.is_cloud_native);
366 assert!(!pg_info.is_olap);
367
368 let sf = TestDialect::snowflake();
369 let sf_info = sf.info();
370 assert_eq!(sf_info.family, "Snowflake");
371 assert!(sf_info.is_cloud_native);
372 assert!(sf_info.is_olap);
373 assert!(!sf_info.is_postgres_compatible);
374
375 let rd = TestDialect::redshift();
376 let rd_info = rd.info();
377 assert!(rd_info.is_cloud_native);
378 assert!(rd_info.is_olap);
379 assert!(rd_info.is_postgres_compatible);
380 }
381
382 #[test]
383 fn empty_registry_returns_none() {
384 let registry = PlatformRegistry::new();
385 assert!(registry.get_by_name("postgres").is_none());
386 assert!(registry.resolve("postgres").is_none());
387 assert!(registry.list().is_empty());
388 }
389
390 #[test]
393 fn registered_names_are_sorted() {
394 let mut registry = PlatformRegistry::new();
395 registry.register("snowflake".to_string(), Arc::new(TestDialect::snowflake()));
396 registry.register("postgres".to_string(), Arc::new(TestDialect::postgres()));
397 registry.register("redshift".to_string(), Arc::new(TestDialect::redshift()));
398 assert_eq!(registry.registered_names(), vec!["postgres", "redshift", "snowflake"]);
399 }
400
401 #[test]
402 fn dialect_info_serializes_for_discovery() {
403 let info = TestDialect::snowflake().info();
406 let json = serde_json::to_value(&info).expect("DialectInfo serializes");
407 assert_eq!(json["name"], "snowflake");
408 assert_eq!(json["display_name"], "Snowflake");
409 assert_eq!(json["dialect_family"], "snowflake");
410 assert_eq!(json["is_cloud_native"], true);
411 assert_eq!(json["is_postgres_compatible"], false);
412 assert_eq!(json["aliases"], serde_json::json!(["sf"]));
415 }
416
417 #[test]
418 fn spellings_start_with_the_dialects_own_name() {
419 let info = TestDialect::postgres().info();
420 assert_eq!(info.spellings(), vec!["postgres", "postgresql", "pg"]);
421 }
422
423 #[test]
424 fn a_dialect_answers_to_its_name_and_its_aliases_in_any_case() {
425 let info = TestDialect::postgres().info();
426 assert!(info.answers_to("postgres"));
427 assert!(info.answers_to("pg"));
428 assert!(info.answers_to(" PostgreSQL "));
429 assert!(!info.answers_to("snowflake"));
430 }
431
432 #[test]
433 fn an_alias_repeating_the_name_is_listed_once() {
434 let info = DialectInfo { aliases: &["postgres", "pg"], ..TestDialect::postgres().info() };
435 assert_eq!(info.spellings(), vec!["postgres", "pg"]);
436 }
437}