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 registry
175 }
176}
177
178impl Default for PlatformRegistry {
179 fn default() -> Self {
180 Self::new()
181 }
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187 use razeri_plugin_contract::DataType;
188
189 struct TestDialect {
191 info: DialectInfo,
192 }
193
194 impl TestDialect {
195 fn postgres() -> Self {
196 Self {
197 info: DialectInfo {
198 name: "postgres",
199 display_name: "PostgreSQL",
200 aliases: &["postgresql", "pg"],
201 dialect_family: DialectFamily::PostgreSql,
202 family: "PostgreSQL",
203 is_cloud_native: false,
204 is_olap: false,
205 is_postgres_compatible: true,
206 },
207 }
208 }
209
210 fn redshift() -> Self {
211 Self {
212 info: DialectInfo {
213 name: "redshift",
214 display_name: "Amazon Redshift",
215 aliases: &["amazon_redshift", "rs"],
216 dialect_family: DialectFamily::PostgreSql,
217 family: "PostgreSQL",
218 is_cloud_native: true,
219 is_olap: true,
220 is_postgres_compatible: true,
221 },
222 }
223 }
224
225 fn snowflake() -> Self {
226 Self {
227 info: DialectInfo {
228 name: "snowflake",
229 display_name: "Snowflake",
230 aliases: &["sf"],
231 dialect_family: DialectFamily::Snowflake,
232 family: "Snowflake",
233 is_cloud_native: true,
234 is_olap: true,
235 is_postgres_compatible: false,
236 },
237 }
238 }
239 }
240
241 impl SqlRenderer for TestDialect {
242 fn render_data_type(&self, _dt: &DataType) -> String {
243 "TEXT".to_string()
244 }
245
246 fn quote_identifier(&self, name: &str) -> String {
247 format!("\"{name}\"")
248 }
249
250 fn qualify_name(&self, schema: Option<&str>, name: &str) -> String {
251 match schema {
252 Some(s) => format!("{}.{}", self.quote_identifier(s), self.quote_identifier(name)),
253 None => self.quote_identifier(name),
254 }
255 }
256
257 fn render_current_timestamp(&self) -> String {
258 "CURRENT_TIMESTAMP".to_string()
259 }
260 }
261
262 impl ExpressionRenderer for TestDialect {
263 fn render_concat(&self, parts: &[String]) -> String {
264 parts.join(" || ")
265 }
266 }
267
268 impl DdlRenderer for TestDialect {}
269 impl DmlRenderer for TestDialect {}
270 impl ProcedureRenderer for TestDialect {}
271 impl FormulaRenderer for TestDialect {}
272 impl TaskRenderer for TestDialect {}
273 impl ReplicationRenderer for TestDialect {}
274 impl StorageRenderer for TestDialect {}
275
276 impl DialectCapabilities for TestDialect {
277 fn supports_identity(&self) -> bool {
278 true
279 }
280 fn supports_sequences(&self) -> bool {
281 true
282 }
283 fn supports_stored_procedures(&self) -> bool {
284 true
285 }
286 }
287
288 impl PlatformDialect for TestDialect {
289 fn info(&self) -> DialectInfo {
290 self.info.clone()
291 }
292 }
293
294 #[test]
295 fn register_dialect_resolves_by_name() {
296 let mut registry = PlatformRegistry::new();
297 registry.register("postgres".to_string(), Arc::new(TestDialect::postgres()));
298 assert!(registry.get_by_name("postgres").is_some());
299 }
300
301 #[test]
302 fn register_extension_resolves_by_name() {
303 let mut registry = PlatformRegistry::new();
304 registry.register("redshift".to_string(), Arc::new(TestDialect::redshift()));
305 assert!(registry.get_by_name("redshift").is_some());
306 }
307
308 #[test]
309 fn resolve_tries_name_lookup() {
310 let mut registry = PlatformRegistry::new();
311 registry.register("postgres".to_string(), Arc::new(TestDialect::postgres()));
312 registry.register("redshift".to_string(), Arc::new(TestDialect::redshift()));
313
314 assert!(registry.resolve("postgres").is_some());
315 assert!(registry.resolve("redshift").is_some());
316 assert!(registry.resolve("nonexistent").is_none());
317 }
318
319 #[test]
320 fn list_returns_all_registered_dialects() {
321 let mut registry = PlatformRegistry::new();
322 registry.register("postgres".to_string(), Arc::new(TestDialect::postgres()));
323 registry.register("snowflake".to_string(), Arc::new(TestDialect::snowflake()));
324 registry.register("redshift".to_string(), Arc::new(TestDialect::redshift()));
325
326 let list = registry.list();
327 assert_eq!(list.len(), 3);
328 }
329
330 #[test]
331 fn duplicate_registration_overwrites() {
332 let mut registry = PlatformRegistry::new();
333 registry.register("postgres".to_string(), Arc::new(TestDialect::postgres()));
334 registry.register("postgres".to_string(), Arc::new(TestDialect::postgres()));
335
336 assert!(registry.get_by_name("postgres").is_some());
337 let list = registry.list();
338 assert_eq!(list.len(), 1);
339 }
340
341 #[test]
342 fn dialect_info_contains_expected_metadata() {
343 let dialect = TestDialect::postgres();
344 let info = dialect.info();
345 assert_eq!(info.name, "postgres");
346 assert_eq!(info.display_name, "PostgreSQL");
347 assert_eq!(info.dialect_family, DialectFamily::PostgreSql);
348 }
349
350 #[test]
351 fn classification_fields_reflect_dialect_characteristics() {
352 let pg = TestDialect::postgres();
353 let pg_info = pg.info();
354 assert_eq!(pg_info.family, "PostgreSQL");
355 assert!(pg_info.is_postgres_compatible);
356 assert!(!pg_info.is_cloud_native);
357 assert!(!pg_info.is_olap);
358
359 let sf = TestDialect::snowflake();
360 let sf_info = sf.info();
361 assert_eq!(sf_info.family, "Snowflake");
362 assert!(sf_info.is_cloud_native);
363 assert!(sf_info.is_olap);
364 assert!(!sf_info.is_postgres_compatible);
365
366 let rd = TestDialect::redshift();
367 let rd_info = rd.info();
368 assert!(rd_info.is_cloud_native);
369 assert!(rd_info.is_olap);
370 assert!(rd_info.is_postgres_compatible);
371 }
372
373 #[test]
374 fn empty_registry_returns_none() {
375 let registry = PlatformRegistry::new();
376 assert!(registry.get_by_name("postgres").is_none());
377 assert!(registry.resolve("postgres").is_none());
378 assert!(registry.list().is_empty());
379 }
380
381 #[test]
384 fn registered_names_are_sorted() {
385 let mut registry = PlatformRegistry::new();
386 registry.register("snowflake".to_string(), Arc::new(TestDialect::snowflake()));
387 registry.register("postgres".to_string(), Arc::new(TestDialect::postgres()));
388 registry.register("redshift".to_string(), Arc::new(TestDialect::redshift()));
389 assert_eq!(registry.registered_names(), vec!["postgres", "redshift", "snowflake"]);
390 }
391
392 #[test]
393 fn dialect_info_serializes_for_discovery() {
394 let info = TestDialect::snowflake().info();
397 let json = serde_json::to_value(&info).expect("DialectInfo serializes");
398 assert_eq!(json["name"], "snowflake");
399 assert_eq!(json["display_name"], "Snowflake");
400 assert_eq!(json["dialect_family"], "snowflake");
401 assert_eq!(json["is_cloud_native"], true);
402 assert_eq!(json["is_postgres_compatible"], false);
403 assert_eq!(json["aliases"], serde_json::json!(["sf"]));
406 }
407
408 #[test]
409 fn spellings_start_with_the_dialects_own_name() {
410 let info = TestDialect::postgres().info();
411 assert_eq!(info.spellings(), vec!["postgres", "postgresql", "pg"]);
412 }
413
414 #[test]
415 fn a_dialect_answers_to_its_name_and_its_aliases_in_any_case() {
416 let info = TestDialect::postgres().info();
417 assert!(info.answers_to("postgres"));
418 assert!(info.answers_to("pg"));
419 assert!(info.answers_to(" PostgreSQL "));
420 assert!(!info.answers_to("snowflake"));
421 }
422
423 #[test]
424 fn an_alias_repeating_the_name_is_listed_once() {
425 let info = DialectInfo { aliases: &["postgres", "pg"], ..TestDialect::postgres().info() };
426 assert_eq!(info.spellings(), vec!["postgres", "pg"]);
427 }
428}