Skip to main content

sqlglot_rust/dialects/
plugin.rs

1use std::collections::HashMap;
2use std::sync::{Arc, Mutex, OnceLock};
3
4use crate::ast::{DataType, Expr, QuoteStyle, Statement};
5
6/// Trait that external code can implement to define a custom SQL dialect.
7///
8/// All methods have default implementations that return `None`, meaning
9/// "no custom behaviour — fall through to the built-in logic". Implementors
10/// only need to override the methods they care about.
11///
12/// # Thread Safety
13///
14/// Implementations must be `Send + Sync` because the global registry is
15/// shared across threads.
16///
17/// # Example
18///
19/// ```rust
20/// use sqlglot_rust::dialects::plugin::{DialectPlugin, DialectRegistry};
21/// use sqlglot_rust::ast::{DataType, Expr, QuoteStyle, Statement};
22///
23/// struct MyDialect;
24///
25/// impl DialectPlugin for MyDialect {
26///     fn name(&self) -> &str { "mydialect" }
27///
28///     fn map_function_name(&self, name: &str) -> Option<String> {
29///         match name.to_uppercase().as_str() {
30///             "MY_FUNC" => Some("BUILTIN_FUNC".to_string()),
31///             _ => None,
32///         }
33///     }
34///
35///     fn quote_style(&self) -> Option<QuoteStyle> {
36///         Some(QuoteStyle::Backtick)
37///     }
38/// }
39///
40/// // Register once, then use via DialectRef::Custom("mydialect")
41/// DialectRegistry::global().register(MyDialect);
42/// ```
43pub trait DialectPlugin: Send + Sync {
44    /// Canonical lower-case name for this dialect (e.g. `"mydialect"`).
45    fn name(&self) -> &str;
46
47    // ── Tokenizer rules ──────────────────────────────────────────────
48
49    /// Preferred quoting style for identifiers.
50    fn quote_style(&self) -> Option<QuoteStyle> {
51        None
52    }
53
54    // ── Parser rules ─────────────────────────────────────────────────
55
56    /// Whether this dialect natively supports `ILIKE`.
57    fn supports_ilike(&self) -> Option<bool> {
58        None
59    }
60
61    // ── Generator / transform rules ──────────────────────────────────
62
63    /// Map a function name for this dialect.
64    ///
65    /// Return `Some(new_name)` to override, or `None` to keep the original.
66    fn map_function_name(&self, name: &str) -> Option<String> {
67        let _ = name;
68        None
69    }
70
71    /// Map a data type for this dialect.
72    ///
73    /// Return `Some(new_type)` to override, or `None` to keep the original.
74    fn map_data_type(&self, data_type: &DataType) -> Option<DataType> {
75        let _ = data_type;
76        None
77    }
78
79    /// Transform an entire expression for this dialect.
80    ///
81    /// Return `Some(new_expr)` to replace the expression, or `None` to
82    /// fall through to the default transformation logic.
83    fn transform_expr(&self, expr: &Expr) -> Option<Expr> {
84        let _ = expr;
85        None
86    }
87
88    /// Transform a complete statement for this dialect.
89    ///
90    /// Return `Some(new_stmt)` to replace the statement, or `None` to
91    /// fall through to the default transformation logic.
92    fn transform_statement(&self, statement: &Statement) -> Option<Statement> {
93        let _ = statement;
94        None
95    }
96}
97
98// ═══════════════════════════════════════════════════════════════════════════
99// Global registry
100// ═══════════════════════════════════════════════════════════════════════════
101
102/// Thread-safe registry for custom dialect plugins.
103///
104/// Access the singleton with [`DialectRegistry::global()`].
105pub struct DialectRegistry {
106    dialects: Mutex<HashMap<String, Arc<dyn DialectPlugin>>>,
107}
108
109impl DialectRegistry {
110    /// Returns the global registry singleton.
111    pub fn global() -> &'static DialectRegistry {
112        static INSTANCE: OnceLock<DialectRegistry> = OnceLock::new();
113        INSTANCE.get_or_init(|| DialectRegistry {
114            dialects: Mutex::new(HashMap::new()),
115        })
116    }
117
118    /// Register a custom dialect plugin.
119    ///
120    /// If a plugin with the same name already exists it is replaced.
121    pub fn register<P: DialectPlugin + 'static>(&self, plugin: P) {
122        let name = plugin.name().to_lowercase();
123        let mut map = self
124            .dialects
125            .lock()
126            .expect("dialect registry lock poisoned");
127        map.insert(name, Arc::new(plugin));
128    }
129
130    /// Look up a custom dialect by name (case-insensitive).
131    #[must_use]
132    pub fn get(&self, name: &str) -> Option<Arc<dyn DialectPlugin>> {
133        let map = self
134            .dialects
135            .lock()
136            .expect("dialect registry lock poisoned");
137        map.get(&name.to_lowercase()).cloned()
138    }
139
140    /// Remove a custom dialect plugin by name.
141    ///
142    /// Returns `true` if the dialect was found and removed.
143    pub fn unregister(&self, name: &str) -> bool {
144        let mut map = self
145            .dialects
146            .lock()
147            .expect("dialect registry lock poisoned");
148        map.remove(&name.to_lowercase()).is_some()
149    }
150
151    /// Returns the names of all registered custom dialects.
152    #[must_use]
153    pub fn registered_names(&self) -> Vec<String> {
154        let map = self
155            .dialects
156            .lock()
157            .expect("dialect registry lock poisoned");
158        map.keys().cloned().collect()
159    }
160}
161
162// ═══════════════════════════════════════════════════════════════════════════
163// DialectRef — unified built-in + custom dialect handle
164// ═══════════════════════════════════════════════════════════════════════════
165
166use crate::dialects::Dialect;
167
168/// A reference to either a built-in [`Dialect`] or a custom dialect
169/// registered via the plugin system.
170///
171/// This is the primary handle that plugin-aware API functions accept.
172///
173/// # Example
174///
175/// ```rust
176/// use sqlglot_rust::dialects::plugin::DialectRef;
177/// use sqlglot_rust::Dialect;
178///
179/// let builtin = DialectRef::from(Dialect::Postgres);
180/// let custom  = DialectRef::custom("mydialect");
181/// ```
182#[derive(Debug, Clone, PartialEq, Eq, Hash)]
183pub enum DialectRef {
184    /// A built-in dialect variant.
185    BuiltIn(Dialect),
186    /// A custom dialect identified by its registered name.
187    Custom(String),
188}
189
190impl DialectRef {
191    /// Create a `DialectRef` for a custom dialect by name.
192    #[must_use]
193    pub fn custom(name: &str) -> Self {
194        DialectRef::Custom(name.to_lowercase())
195    }
196
197    /// Try to resolve this reference to a built-in dialect.
198    #[must_use]
199    pub fn as_builtin(&self) -> Option<Dialect> {
200        match self {
201            DialectRef::BuiltIn(d) => Some(*d),
202            DialectRef::Custom(_) => None,
203        }
204    }
205
206    /// Try to resolve this reference to a custom plugin.
207    #[must_use]
208    pub fn as_plugin(&self) -> Option<Arc<dyn DialectPlugin>> {
209        match self {
210            DialectRef::Custom(name) => DialectRegistry::global().get(name),
211            DialectRef::BuiltIn(_) => None,
212        }
213    }
214
215    /// Get the quote style for this dialect reference.
216    #[must_use]
217    pub fn quote_style(&self) -> QuoteStyle {
218        match self {
219            DialectRef::BuiltIn(d) => QuoteStyle::for_dialect(*d),
220            DialectRef::Custom(name) => DialectRegistry::global()
221                .get(name)
222                .and_then(|p| p.quote_style())
223                .unwrap_or(QuoteStyle::DoubleQuote),
224        }
225    }
226
227    /// Check if this dialect supports ILIKE natively.
228    #[must_use]
229    pub fn supports_ilike(&self) -> bool {
230        match self {
231            DialectRef::BuiltIn(d) => super::supports_ilike_builtin(*d),
232            DialectRef::Custom(name) => DialectRegistry::global()
233                .get(name)
234                .and_then(|p| p.supports_ilike())
235                .unwrap_or(false),
236        }
237    }
238
239    /// Map a function name using this dialect's rules.
240    #[must_use]
241    pub fn map_function_name(&self, name: &str) -> String {
242        match self {
243            DialectRef::BuiltIn(d) => super::map_function_name(name, *d),
244            DialectRef::Custom(cname) => DialectRegistry::global()
245                .get(cname)
246                .and_then(|p| p.map_function_name(name))
247                .unwrap_or_else(|| name.to_string()),
248        }
249    }
250
251    /// Map a data type using this dialect's rules.
252    #[must_use]
253    pub fn map_data_type(&self, dt: &DataType) -> DataType {
254        match self {
255            DialectRef::BuiltIn(d) => super::map_data_type(dt.clone(), *d),
256            DialectRef::Custom(name) => DialectRegistry::global()
257                .get(name)
258                .and_then(|p| p.map_data_type(dt))
259                .unwrap_or_else(|| dt.clone()),
260        }
261    }
262}
263
264impl From<Dialect> for DialectRef {
265    fn from(d: Dialect) -> Self {
266        DialectRef::BuiltIn(d)
267    }
268}
269
270impl std::fmt::Display for DialectRef {
271    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
272        match self {
273            DialectRef::BuiltIn(d) => write!(f, "{d}"),
274            DialectRef::Custom(name) => write!(f, "Custom({name})"),
275        }
276    }
277}
278
279// ═══════════════════════════════════════════════════════════════════════════
280// Plugin-aware transform
281// ═══════════════════════════════════════════════════════════════════════════
282
283// ═══════════════════════════════════════════════════════════════════════════
284// Plugin-aware transform
285// ═══════════════════════════════════════════════════════════════════════════
286
287use crate::ast::TypedFunction;
288
289/// Return the canonical SQL function name for a TypedFunction variant.
290fn typed_function_canonical_name(func: &TypedFunction) -> &'static str {
291    match func {
292        TypedFunction::DateAdd { .. } => "DATE_ADD",
293        TypedFunction::DateDiff { .. } => "DATE_DIFF",
294        TypedFunction::DateTrunc { .. } => "DATE_TRUNC",
295        TypedFunction::DateSub { .. } => "DATE_SUB",
296        TypedFunction::CurrentDate => "CURRENT_DATE",
297        TypedFunction::CurrentTime => "CURRENT_TIME",
298        TypedFunction::CurrentTimestamp => "NOW",
299        TypedFunction::StrToTime { .. } => "STR_TO_TIME",
300        TypedFunction::TimeToStr { .. } => "TIME_TO_STR",
301        TypedFunction::TsOrDsToDate { .. } => "TS_OR_DS_TO_DATE",
302        TypedFunction::Year { .. } => "YEAR",
303        TypedFunction::Month { .. } => "MONTH",
304        TypedFunction::Day { .. } => "DAY",
305        TypedFunction::Trim { .. } => "TRIM",
306        TypedFunction::Substring { .. } => "SUBSTRING",
307        TypedFunction::Upper { .. } => "UPPER",
308        TypedFunction::Lower { .. } => "LOWER",
309        TypedFunction::RegexpLike { .. } => "REGEXP_LIKE",
310        TypedFunction::RegexpExtract { .. } => "REGEXP_EXTRACT",
311        TypedFunction::RegexpReplace { .. } => "REGEXP_REPLACE",
312        TypedFunction::ConcatWs { .. } => "CONCAT_WS",
313        TypedFunction::Split { .. } => "SPLIT",
314        TypedFunction::Initcap { .. } => "INITCAP",
315        TypedFunction::Length { .. } => "LENGTH",
316        TypedFunction::Replace { .. } => "REPLACE",
317        TypedFunction::Reverse { .. } => "REVERSE",
318        TypedFunction::Left { .. } => "LEFT",
319        TypedFunction::Right { .. } => "RIGHT",
320        TypedFunction::Lpad { .. } => "LPAD",
321        TypedFunction::Rpad { .. } => "RPAD",
322        TypedFunction::Count { .. } => "COUNT",
323        TypedFunction::Sum { .. } => "SUM",
324        TypedFunction::Avg { .. } => "AVG",
325        TypedFunction::Min { .. } => "MIN",
326        TypedFunction::Max { .. } => "MAX",
327        TypedFunction::ArrayAgg { .. } => "ARRAY_AGG",
328        TypedFunction::ApproxDistinct { .. } => "APPROX_DISTINCT",
329        TypedFunction::Variance { .. } => "VARIANCE",
330        TypedFunction::Stddev { .. } => "STDDEV",
331        TypedFunction::GroupConcat { .. } => "GROUP_CONCAT",
332        TypedFunction::ArrayConcat { .. } => "ARRAY_CONCAT",
333        TypedFunction::ArrayContains { .. } => "ARRAY_CONTAINS",
334        TypedFunction::ArraySize { .. } => "ARRAY_SIZE",
335        TypedFunction::Explode { .. } => "EXPLODE",
336        TypedFunction::GenerateSeries { .. } => "GENERATE_SERIES",
337        TypedFunction::Flatten { .. } => "FLATTEN",
338        TypedFunction::JSONExtract { .. } => "JSON_EXTRACT",
339        TypedFunction::JSONExtractScalar { .. } => "JSON_EXTRACT_SCALAR",
340        TypedFunction::ParseJSON { .. } => "PARSE_JSON",
341        TypedFunction::JSONFormat { .. } => "JSON_FORMAT",
342        TypedFunction::RowNumber => "ROW_NUMBER",
343        TypedFunction::Rank => "RANK",
344        TypedFunction::DenseRank => "DENSE_RANK",
345        TypedFunction::NTile { .. } => "NTILE",
346        TypedFunction::Lead { .. } => "LEAD",
347        TypedFunction::Lag { .. } => "LAG",
348        TypedFunction::FirstValue { .. } => "FIRST_VALUE",
349        TypedFunction::LastValue { .. } => "LAST_VALUE",
350        TypedFunction::Abs { .. } => "ABS",
351        TypedFunction::Ceil { .. } => "CEIL",
352        TypedFunction::Floor { .. } => "FLOOR",
353        TypedFunction::Round { .. } => "ROUND",
354        TypedFunction::Log { .. } => "LOG",
355        TypedFunction::Ln { .. } => "LN",
356        TypedFunction::Pow { .. } => "POW",
357        TypedFunction::Sqrt { .. } => "SQRT",
358        TypedFunction::Greatest { .. } => "GREATEST",
359        TypedFunction::Least { .. } => "LEAST",
360        TypedFunction::Mod { .. } => "MOD",
361        TypedFunction::Hex { .. } => "HEX",
362        TypedFunction::Unhex { .. } => "UNHEX",
363        TypedFunction::Md5 { .. } => "MD5",
364        TypedFunction::Sha { .. } => "SHA",
365        TypedFunction::Sha2 { .. } => "SHA2",
366    }
367}
368
369/// Extract the argument expressions from a TypedFunction (in positional order).
370fn typed_function_args(func: &TypedFunction) -> Vec<Expr> {
371    match func {
372        TypedFunction::CurrentDate
373        | TypedFunction::CurrentTime
374        | TypedFunction::CurrentTimestamp => vec![],
375        TypedFunction::RowNumber | TypedFunction::Rank | TypedFunction::DenseRank => vec![],
376        TypedFunction::Length { expr }
377        | TypedFunction::Upper { expr }
378        | TypedFunction::Lower { expr }
379        | TypedFunction::Initcap { expr }
380        | TypedFunction::Reverse { expr }
381        | TypedFunction::Abs { expr }
382        | TypedFunction::Ceil { expr }
383        | TypedFunction::Floor { expr }
384        | TypedFunction::Ln { expr }
385        | TypedFunction::Sqrt { expr }
386        | TypedFunction::Explode { expr }
387        | TypedFunction::Flatten { expr }
388        | TypedFunction::ArraySize { expr }
389        | TypedFunction::ParseJSON { expr }
390        | TypedFunction::JSONFormat { expr }
391        | TypedFunction::Hex { expr }
392        | TypedFunction::Unhex { expr }
393        | TypedFunction::Md5 { expr }
394        | TypedFunction::Sha { expr }
395        | TypedFunction::TsOrDsToDate { expr }
396        | TypedFunction::Year { expr }
397        | TypedFunction::Month { expr }
398        | TypedFunction::Day { expr }
399        | TypedFunction::ApproxDistinct { expr }
400        | TypedFunction::Variance { expr }
401        | TypedFunction::Stddev { expr }
402        | TypedFunction::FirstValue { expr }
403        | TypedFunction::LastValue { expr } => vec![*expr.clone()],
404        TypedFunction::DateTrunc { unit, expr } => {
405            vec![Expr::StringLiteral(format!("{unit:?}")), *expr.clone()]
406        }
407        TypedFunction::DateAdd { expr, interval, .. }
408        | TypedFunction::DateSub { expr, interval, .. } => {
409            vec![*expr.clone(), *interval.clone()]
410        }
411        TypedFunction::DateDiff { start, end, .. } => vec![*start.clone(), *end.clone()],
412        TypedFunction::StrToTime { expr, format } | TypedFunction::TimeToStr { expr, format } => {
413            vec![*expr.clone(), *format.clone()]
414        }
415        TypedFunction::Trim { expr, .. } => vec![*expr.clone()],
416        TypedFunction::Substring {
417            expr,
418            start,
419            length,
420        } => {
421            let mut args = vec![*expr.clone(), *start.clone()];
422            if let Some(len) = length {
423                args.push(*len.clone());
424            }
425            args
426        }
427        TypedFunction::RegexpLike {
428            expr,
429            pattern,
430            flags,
431        } => {
432            let mut args = vec![*expr.clone(), *pattern.clone()];
433            if let Some(f) = flags {
434                args.push(*f.clone());
435            }
436            args
437        }
438        TypedFunction::RegexpExtract {
439            expr,
440            pattern,
441            group_index,
442        } => {
443            let mut args = vec![*expr.clone(), *pattern.clone()];
444            if let Some(g) = group_index {
445                args.push(*g.clone());
446            }
447            args
448        }
449        TypedFunction::RegexpReplace {
450            expr,
451            pattern,
452            replacement,
453            flags,
454        } => {
455            let mut args = vec![*expr.clone(), *pattern.clone(), *replacement.clone()];
456            if let Some(f) = flags {
457                args.push(*f.clone());
458            }
459            args
460        }
461        TypedFunction::ConcatWs { separator, exprs } => {
462            let mut args = vec![*separator.clone()];
463            args.extend(exprs.iter().cloned());
464            args
465        }
466        TypedFunction::Split { expr, delimiter } => vec![*expr.clone(), *delimiter.clone()],
467        TypedFunction::Replace { expr, from, to } => {
468            vec![*expr.clone(), *from.clone(), *to.clone()]
469        }
470        TypedFunction::Left { expr, n } | TypedFunction::Right { expr, n } => {
471            vec![*expr.clone(), *n.clone()]
472        }
473        TypedFunction::Lpad { expr, length, pad } | TypedFunction::Rpad { expr, length, pad } => {
474            let mut args = vec![*expr.clone(), *length.clone()];
475            if let Some(p) = pad {
476                args.push(*p.clone());
477            }
478            args
479        }
480        TypedFunction::Count { expr, .. }
481        | TypedFunction::Sum { expr, .. }
482        | TypedFunction::Avg { expr, .. }
483        | TypedFunction::Min { expr }
484        | TypedFunction::Max { expr }
485        | TypedFunction::ArrayAgg { expr, .. } => vec![*expr.clone()],
486        TypedFunction::ArrayConcat { arrays } => arrays.clone(),
487        TypedFunction::ArrayContains { array, element } => {
488            vec![*array.clone(), *element.clone()]
489        }
490        TypedFunction::GenerateSeries { start, stop, step } => {
491            let mut args = vec![*start.clone(), *stop.clone()];
492            if let Some(s) = step {
493                args.push(*s.clone());
494            }
495            args
496        }
497        TypedFunction::JSONExtract { expr, path }
498        | TypedFunction::JSONExtractScalar { expr, path } => {
499            vec![*expr.clone(), *path.clone()]
500        }
501        TypedFunction::NTile { n } => vec![*n.clone()],
502        TypedFunction::Lead {
503            expr,
504            offset,
505            default,
506        }
507        | TypedFunction::Lag {
508            expr,
509            offset,
510            default,
511        } => {
512            let mut args = vec![*expr.clone()];
513            if let Some(o) = offset {
514                args.push(*o.clone());
515            }
516            if let Some(d) = default {
517                args.push(*d.clone());
518            }
519            args
520        }
521        TypedFunction::Round { expr, decimals } => {
522            let mut args = vec![*expr.clone()];
523            if let Some(d) = decimals {
524                args.push(*d.clone());
525            }
526            args
527        }
528        TypedFunction::Log { expr, base } => {
529            let mut args = vec![*expr.clone()];
530            if let Some(b) = base {
531                args.push(*b.clone());
532            }
533            args
534        }
535        TypedFunction::Pow { base, exponent } => vec![*base.clone(), *exponent.clone()],
536        TypedFunction::Greatest { exprs } | TypedFunction::Least { exprs } => exprs.clone(),
537        TypedFunction::Mod { left, right } => vec![*left.clone(), *right.clone()],
538        TypedFunction::Sha2 { expr, bit_length } => vec![*expr.clone(), *bit_length.clone()],
539        TypedFunction::GroupConcat {
540            exprs, separator, ..
541        } => {
542            let mut args = exprs.clone();
543            if let Some(s) = separator {
544                args.push(*s.clone());
545            }
546            args
547        }
548    }
549}
550
551/// Transform a statement from one dialect to another, supporting custom
552/// dialect plugins.
553///
554/// For built-in → built-in transforms this delegates to the existing
555/// [`super::transform`]. When either side is a custom dialect the plugin's
556/// transform hooks are applied.
557#[must_use]
558pub fn transform(statement: &Statement, from: &DialectRef, to: &DialectRef) -> Statement {
559    // Fast path: both built-in → use existing logic
560    if let (DialectRef::BuiltIn(f), DialectRef::BuiltIn(t)) = (from, to) {
561        return super::transform(statement, *f, *t);
562    }
563
564    // If the target is a custom dialect with a full statement transform, try that first.
565    if let Some(plugin) = to.as_plugin()
566        && let Some(transformed) = plugin.transform_statement(statement)
567    {
568        return transformed;
569    }
570
571    // Otherwise apply expression-level transforms via DialectRef helpers.
572    let mut stmt = statement.clone();
573    transform_statement_plugin(&mut stmt, to);
574    stmt
575}
576
577/// Recursively transform a statement using plugin-aware rules.
578fn transform_statement_plugin(statement: &mut Statement, target: &DialectRef) {
579    match statement {
580        Statement::Select(sel) => {
581            for item in &mut sel.columns {
582                if let crate::ast::SelectItem::Expr { expr, .. } = item {
583                    *expr = transform_expr_plugin(expr.clone(), target);
584                }
585            }
586            if let Some(wh) = &mut sel.where_clause {
587                *wh = transform_expr_plugin(wh.clone(), target);
588            }
589            for gb in &mut sel.group_by {
590                *gb = transform_expr_plugin(gb.clone(), target);
591            }
592            if let Some(having) = &mut sel.having {
593                *having = transform_expr_plugin(having.clone(), target);
594            }
595        }
596        Statement::CreateTable(ct) => {
597            for col in &mut ct.columns {
598                col.data_type = target.map_data_type(&col.data_type);
599                if let Some(default) = &mut col.default {
600                    *default = transform_expr_plugin(default.clone(), target);
601                }
602            }
603        }
604        _ => {}
605    }
606}
607
608/// Transform an expression using plugin-aware rules.
609fn transform_expr_plugin(expr: Expr, target: &DialectRef) -> Expr {
610    // Let the plugin have first shot at transforming the whole expression
611    if let Some(plugin) = target.as_plugin()
612        && let Some(transformed) = plugin.transform_expr(&expr)
613    {
614        return transformed;
615    }
616
617    match expr {
618        // For TypedFunction, check if the plugin wants to rename the function.
619        // If so, convert it to a plain Expr::Function with the new name.
620        Expr::TypedFunction { func, filter, over } => {
621            if let DialectRef::Custom(_) = target {
622                let canonical = typed_function_canonical_name(&func);
623                let new_name = target.map_function_name(canonical);
624                if new_name != canonical {
625                    // Plugin wants to rename this function — convert to Expr::Function
626                    let args = typed_function_args(&func)
627                        .into_iter()
628                        .map(|a| transform_expr_plugin(a, target))
629                        .collect();
630                    return Expr::Function {
631                        name: new_name,
632                        args,
633                        distinct: false,
634                        filter: filter.map(|f| Box::new(transform_expr_plugin(*f, target))),
635                        over,
636                    };
637                }
638            }
639            // No rename — recurse into children
640            Expr::TypedFunction {
641                func: func.transform_children(&|e| transform_expr_plugin(e, target)),
642                filter: filter.map(|f| Box::new(transform_expr_plugin(*f, target))),
643                over,
644            }
645        }
646        Expr::Function {
647            name,
648            args,
649            distinct,
650            filter,
651            over,
652        } => {
653            let new_name = target.map_function_name(&name);
654            let new_args: Vec<Expr> = args
655                .into_iter()
656                .map(|a| transform_expr_plugin(a, target))
657                .collect();
658            Expr::Function {
659                name: new_name,
660                args: new_args,
661                distinct,
662                filter: filter.map(|f| Box::new(transform_expr_plugin(*f, target))),
663                over,
664            }
665        }
666        Expr::Cast { expr, data_type } => Expr::Cast {
667            expr: Box::new(transform_expr_plugin(*expr, target)),
668            data_type: target.map_data_type(&data_type),
669        },
670        Expr::ILike {
671            expr,
672            pattern,
673            negated,
674            escape,
675        } if !target.supports_ilike() => Expr::Like {
676            expr: Box::new(Expr::TypedFunction {
677                func: crate::ast::TypedFunction::Lower {
678                    expr: Box::new(transform_expr_plugin(*expr, target)),
679                },
680                filter: None,
681                over: None,
682            }),
683            pattern: Box::new(Expr::TypedFunction {
684                func: crate::ast::TypedFunction::Lower {
685                    expr: Box::new(transform_expr_plugin(*pattern, target)),
686                },
687                filter: None,
688                over: None,
689            }),
690            negated,
691            escape,
692        },
693        Expr::BinaryOp { left, op, right } => Expr::BinaryOp {
694            left: Box::new(transform_expr_plugin(*left, target)),
695            op,
696            right: Box::new(transform_expr_plugin(*right, target)),
697        },
698        Expr::UnaryOp { op, expr } => Expr::UnaryOp {
699            op,
700            expr: Box::new(transform_expr_plugin(*expr, target)),
701        },
702        Expr::Nested(inner) => Expr::Nested(Box::new(transform_expr_plugin(*inner, target))),
703        Expr::Column {
704            table,
705            name,
706            quote_style,
707            table_quote_style,
708        } => {
709            let new_qs = if quote_style.is_quoted() {
710                target.quote_style()
711            } else {
712                QuoteStyle::None
713            };
714            let new_tqs = if table_quote_style.is_quoted() {
715                target.quote_style()
716            } else {
717                QuoteStyle::None
718            };
719            Expr::Column {
720                table,
721                name,
722                quote_style: new_qs,
723                table_quote_style: new_tqs,
724            }
725        }
726        other => other,
727    }
728}
729
730// ═══════════════════════════════════════════════════════════════════════════
731// Plugin-aware top-level API
732// ═══════════════════════════════════════════════════════════════════════════
733
734use crate::errors;
735
736/// Transpile a SQL string using [`DialectRef`], supporting custom plugins.
737///
738/// # Example
739///
740/// ```rust
741/// use sqlglot_rust::dialects::plugin::{DialectRef, transpile_ext};
742/// use sqlglot_rust::Dialect;
743///
744/// let result = transpile_ext(
745///     "SELECT NOW()",
746///     &DialectRef::from(Dialect::Postgres),
747///     &DialectRef::from(Dialect::Mysql),
748/// ).unwrap();
749/// ```
750///
751/// # Errors
752///
753/// Returns a [`SqlglotError`](crate::errors::SqlglotError) if parsing fails.
754pub fn transpile_ext(
755    sql: &str,
756    read_dialect: &DialectRef,
757    write_dialect: &DialectRef,
758) -> errors::Result<String> {
759    // Parse using the read dialect (fall back to Ansi for custom dialects)
760    let parse_dialect = read_dialect.as_builtin().unwrap_or(Dialect::Ansi);
761    let ast = crate::parser::parse(sql, parse_dialect)?;
762    let transformed = transform(&ast, read_dialect, write_dialect);
763    let gen_dialect = write_dialect.as_builtin().unwrap_or(Dialect::Ansi);
764    Ok(crate::generator::generate(&transformed, gen_dialect))
765}
766
767/// Transpile multiple statements using [`DialectRef`], supporting custom plugins.
768///
769/// # Errors
770///
771/// Returns a [`SqlglotError`](crate::errors::SqlglotError) if parsing fails.
772pub fn transpile_statements_ext(
773    sql: &str,
774    read_dialect: &DialectRef,
775    write_dialect: &DialectRef,
776) -> errors::Result<Vec<String>> {
777    let parse_dialect = read_dialect.as_builtin().unwrap_or(Dialect::Ansi);
778    let stmts = crate::parser::parse_statements(sql, parse_dialect)?;
779    let gen_dialect = write_dialect.as_builtin().unwrap_or(Dialect::Ansi);
780    let mut results = Vec::with_capacity(stmts.len());
781    for stmt in &stmts {
782        let transformed = transform(stmt, read_dialect, write_dialect);
783        results.push(crate::generator::generate(&transformed, gen_dialect));
784    }
785    Ok(results)
786}
787
788// ═══════════════════════════════════════════════════════════════════════════
789// Convenience registration functions
790// ═══════════════════════════════════════════════════════════════════════════
791
792/// Register a custom dialect plugin in the global registry.
793///
794/// This is a convenience wrapper around [`DialectRegistry::global().register()`].
795pub fn register_dialect<P: DialectPlugin + 'static>(plugin: P) {
796    DialectRegistry::global().register(plugin);
797}
798
799/// Look up a dialect by name, returning either a built-in or custom [`DialectRef`].
800///
801/// Checks built-in dialects first, then the custom plugin registry.
802#[must_use]
803pub fn resolve_dialect(name: &str) -> Option<DialectRef> {
804    // Try built-in first
805    if let Some(d) = Dialect::from_str(name) {
806        return Some(DialectRef::BuiltIn(d));
807    }
808    // Then try plugin registry
809    if DialectRegistry::global().get(name).is_some() {
810        return Some(DialectRef::Custom(name.to_lowercase()));
811    }
812    None
813}