1use razeri_plugin_contract::DialectFamily;
12use razeri_plugin_contract::{
13 DdlRenderer, DialectCapabilities, DmlRenderer, ExpressionRenderer, FormulaRenderer,
14 ProcedureRenderer, ReplicationRenderer, SqlRenderer, StorageRenderer, TaskRenderer,
15};
16use std::collections::HashMap;
17use std::sync::Arc;
18
19pub use razeri_plugin_contract;
21
22#[derive(Debug, Clone, serde::Serialize)]
29pub struct DialectInfo {
30 pub name: &'static str,
32 pub display_name: &'static str,
34 pub dialect_family: DialectFamily,
36 pub family: &'static str,
38 pub is_cloud_native: bool,
40 pub is_olap: bool,
42 pub is_postgres_compatible: bool,
44}
45
46pub trait PlatformDialect:
55 SqlRenderer
56 + ExpressionRenderer
57 + DdlRenderer
58 + DmlRenderer
59 + ProcedureRenderer
60 + FormulaRenderer
61 + TaskRenderer
62 + DialectCapabilities
63 + ReplicationRenderer
64 + StorageRenderer
65{
66 fn info(&self) -> DialectInfo;
68}
69
70pub struct PlatformRegistry {
75 dialects: HashMap<String, Arc<dyn PlatformDialect>>,
76}
77
78impl PlatformRegistry {
79 #[must_use]
81 pub fn new() -> Self {
82 Self { dialects: HashMap::new() }
83 }
84
85 pub fn register(&mut self, name: String, dialect: Arc<dyn PlatformDialect>) {
87 self.dialects.insert(name, dialect);
88 }
89
90 #[must_use]
92 pub fn get_by_name(&self, name: &str) -> Option<&Arc<dyn PlatformDialect>> {
93 self.dialects.get(name)
94 }
95
96 #[must_use]
98 pub fn resolve(&self, target: &str) -> Option<Arc<dyn PlatformDialect>> {
99 self.dialects.get(target).cloned()
100 }
101
102 #[must_use]
104 pub fn list(&self) -> Vec<DialectInfo> {
105 self.dialects.values().map(|d| d.info()).collect()
106 }
107
108 #[must_use]
114 pub fn registered_names(&self) -> Vec<String> {
115 let mut names: Vec<String> = self.dialects.keys().cloned().collect();
116 names.sort();
117 names
118 }
119
120 #[must_use]
125 pub fn with_auto_discovered() -> Self {
126 use crate::builder::DialectRegistration;
127 let mut registry = Self::new();
128 for reg in inventory::iter::<DialectRegistration> {
129 let dialect = (reg.factory)();
130 let name = dialect.info().name.to_string();
131 registry.register(name, dialect);
132 }
133 registry
134 }
135}
136
137impl Default for PlatformRegistry {
138 fn default() -> Self {
139 Self::new()
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146 use razeri_plugin_contract::DataType;
147
148 struct TestDialect {
150 info: DialectInfo,
151 }
152
153 impl TestDialect {
154 fn postgres() -> Self {
155 Self {
156 info: DialectInfo {
157 name: "postgres",
158 display_name: "PostgreSQL",
159 dialect_family: DialectFamily::PostgreSql,
160 family: "PostgreSQL",
161 is_cloud_native: false,
162 is_olap: false,
163 is_postgres_compatible: true,
164 },
165 }
166 }
167
168 fn redshift() -> Self {
169 Self {
170 info: DialectInfo {
171 name: "redshift",
172 display_name: "Amazon Redshift",
173 dialect_family: DialectFamily::PostgreSql,
174 family: "PostgreSQL",
175 is_cloud_native: true,
176 is_olap: true,
177 is_postgres_compatible: true,
178 },
179 }
180 }
181
182 fn snowflake() -> Self {
183 Self {
184 info: DialectInfo {
185 name: "snowflake",
186 display_name: "Snowflake",
187 dialect_family: DialectFamily::Snowflake,
188 family: "Snowflake",
189 is_cloud_native: true,
190 is_olap: true,
191 is_postgres_compatible: false,
192 },
193 }
194 }
195 }
196
197 impl SqlRenderer for TestDialect {
198 fn render_data_type(&self, _dt: &DataType) -> String {
199 "TEXT".to_string()
200 }
201
202 fn quote_identifier(&self, name: &str) -> String {
203 format!("\"{name}\"")
204 }
205
206 fn qualify_name(&self, schema: Option<&str>, name: &str) -> String {
207 match schema {
208 Some(s) => format!("{}.{}", self.quote_identifier(s), self.quote_identifier(name)),
209 None => self.quote_identifier(name),
210 }
211 }
212
213 fn render_current_timestamp(&self) -> String {
214 "CURRENT_TIMESTAMP".to_string()
215 }
216 }
217
218 impl ExpressionRenderer for TestDialect {
219 fn render_concat(&self, parts: &[String]) -> String {
220 parts.join(" || ")
221 }
222 }
223
224 impl DdlRenderer for TestDialect {}
225 impl DmlRenderer for TestDialect {}
226 impl ProcedureRenderer for TestDialect {}
227 impl FormulaRenderer for TestDialect {}
228 impl TaskRenderer for TestDialect {}
229 impl ReplicationRenderer for TestDialect {}
230 impl StorageRenderer for TestDialect {}
231
232 impl DialectCapabilities for TestDialect {
233 fn supports_identity(&self) -> bool {
234 true
235 }
236 fn supports_sequences(&self) -> bool {
237 true
238 }
239 fn supports_stored_procedures(&self) -> bool {
240 true
241 }
242 }
243
244 impl PlatformDialect for TestDialect {
245 fn info(&self) -> DialectInfo {
246 self.info.clone()
247 }
248 }
249
250 #[test]
251 fn register_dialect_resolves_by_name() {
252 let mut registry = PlatformRegistry::new();
253 registry.register("postgres".to_string(), Arc::new(TestDialect::postgres()));
254 assert!(registry.get_by_name("postgres").is_some());
255 }
256
257 #[test]
258 fn register_extension_resolves_by_name() {
259 let mut registry = PlatformRegistry::new();
260 registry.register("redshift".to_string(), Arc::new(TestDialect::redshift()));
261 assert!(registry.get_by_name("redshift").is_some());
262 }
263
264 #[test]
265 fn resolve_tries_name_lookup() {
266 let mut registry = PlatformRegistry::new();
267 registry.register("postgres".to_string(), Arc::new(TestDialect::postgres()));
268 registry.register("redshift".to_string(), Arc::new(TestDialect::redshift()));
269
270 assert!(registry.resolve("postgres").is_some());
271 assert!(registry.resolve("redshift").is_some());
272 assert!(registry.resolve("nonexistent").is_none());
273 }
274
275 #[test]
276 fn list_returns_all_registered_dialects() {
277 let mut registry = PlatformRegistry::new();
278 registry.register("postgres".to_string(), Arc::new(TestDialect::postgres()));
279 registry.register("snowflake".to_string(), Arc::new(TestDialect::snowflake()));
280 registry.register("redshift".to_string(), Arc::new(TestDialect::redshift()));
281
282 let list = registry.list();
283 assert_eq!(list.len(), 3);
284 }
285
286 #[test]
287 fn duplicate_registration_overwrites() {
288 let mut registry = PlatformRegistry::new();
289 registry.register("postgres".to_string(), Arc::new(TestDialect::postgres()));
290 registry.register("postgres".to_string(), Arc::new(TestDialect::postgres()));
291
292 assert!(registry.get_by_name("postgres").is_some());
293 let list = registry.list();
294 assert_eq!(list.len(), 1);
295 }
296
297 #[test]
298 fn dialect_info_contains_expected_metadata() {
299 let dialect = TestDialect::postgres();
300 let info = dialect.info();
301 assert_eq!(info.name, "postgres");
302 assert_eq!(info.display_name, "PostgreSQL");
303 assert_eq!(info.dialect_family, DialectFamily::PostgreSql);
304 }
305
306 #[test]
307 fn classification_fields_reflect_dialect_characteristics() {
308 let pg = TestDialect::postgres();
309 let pg_info = pg.info();
310 assert_eq!(pg_info.family, "PostgreSQL");
311 assert!(pg_info.is_postgres_compatible);
312 assert!(!pg_info.is_cloud_native);
313 assert!(!pg_info.is_olap);
314
315 let sf = TestDialect::snowflake();
316 let sf_info = sf.info();
317 assert_eq!(sf_info.family, "Snowflake");
318 assert!(sf_info.is_cloud_native);
319 assert!(sf_info.is_olap);
320 assert!(!sf_info.is_postgres_compatible);
321
322 let rd = TestDialect::redshift();
323 let rd_info = rd.info();
324 assert!(rd_info.is_cloud_native);
325 assert!(rd_info.is_olap);
326 assert!(rd_info.is_postgres_compatible);
327 }
328
329 #[test]
330 fn empty_registry_returns_none() {
331 let registry = PlatformRegistry::new();
332 assert!(registry.get_by_name("postgres").is_none());
333 assert!(registry.resolve("postgres").is_none());
334 assert!(registry.list().is_empty());
335 }
336
337 #[test]
340 fn registered_names_are_sorted() {
341 let mut registry = PlatformRegistry::new();
342 registry.register("snowflake".to_string(), Arc::new(TestDialect::snowflake()));
343 registry.register("postgres".to_string(), Arc::new(TestDialect::postgres()));
344 registry.register("redshift".to_string(), Arc::new(TestDialect::redshift()));
345 assert_eq!(registry.registered_names(), vec!["postgres", "redshift", "snowflake"]);
346 }
347
348 #[test]
349 fn dialect_info_serializes_for_discovery() {
350 let info = TestDialect::snowflake().info();
353 let json = serde_json::to_value(&info).expect("DialectInfo serializes");
354 assert_eq!(json["name"], "snowflake");
355 assert_eq!(json["display_name"], "Snowflake");
356 assert_eq!(json["dialect_family"], "snowflake");
357 assert_eq!(json["is_cloud_native"], true);
358 assert_eq!(json["is_postgres_compatible"], false);
359 }
360}