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