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::VariancePop { .. } => "VAR_POP",
331        TypedFunction::Stddev { .. } => "STDDEV",
332        TypedFunction::StddevPop { .. } => "STDDEV_POP",
333        TypedFunction::GroupConcat { .. } => "GROUP_CONCAT",
334        TypedFunction::ArrayConcat { .. } => "ARRAY_CONCAT",
335        TypedFunction::ArrayContains { .. } => "ARRAY_CONTAINS",
336        TypedFunction::ArraySize { .. } => "ARRAY_SIZE",
337        TypedFunction::Explode { .. } => "EXPLODE",
338        TypedFunction::GenerateSeries { .. } => "GENERATE_SERIES",
339        TypedFunction::Flatten { .. } => "FLATTEN",
340        TypedFunction::JSONExtract { .. } => "JSON_EXTRACT",
341        TypedFunction::JSONExtractScalar { .. } => "JSON_EXTRACT_SCALAR",
342        TypedFunction::ParseJSON { .. } => "PARSE_JSON",
343        TypedFunction::JSONFormat { .. } => "JSON_FORMAT",
344        TypedFunction::RowNumber => "ROW_NUMBER",
345        TypedFunction::Rank => "RANK",
346        TypedFunction::DenseRank => "DENSE_RANK",
347        TypedFunction::NTile { .. } => "NTILE",
348        TypedFunction::Lead { .. } => "LEAD",
349        TypedFunction::Lag { .. } => "LAG",
350        TypedFunction::FirstValue { .. } => "FIRST_VALUE",
351        TypedFunction::LastValue { .. } => "LAST_VALUE",
352        TypedFunction::Abs { .. } => "ABS",
353        TypedFunction::Ceil { .. } => "CEIL",
354        TypedFunction::Floor { .. } => "FLOOR",
355        TypedFunction::Round { .. } => "ROUND",
356        TypedFunction::Log { .. } => "LOG",
357        TypedFunction::Ln { .. } => "LN",
358        TypedFunction::Pow { .. } => "POW",
359        TypedFunction::Sqrt { .. } => "SQRT",
360        TypedFunction::Greatest { .. } => "GREATEST",
361        TypedFunction::Least { .. } => "LEAST",
362        TypedFunction::Mod { .. } => "MOD",
363        TypedFunction::Hex { .. } => "HEX",
364        TypedFunction::Unhex { .. } => "UNHEX",
365        TypedFunction::Md5 { .. } => "MD5",
366        TypedFunction::Sha { .. } => "SHA",
367        TypedFunction::Sha2 { .. } => "SHA2",
368    }
369}
370
371/// Extract the argument expressions from a TypedFunction (in positional order).
372fn typed_function_args(func: &TypedFunction) -> Vec<Expr> {
373    match func {
374        TypedFunction::CurrentDate
375        | TypedFunction::CurrentTime
376        | TypedFunction::CurrentTimestamp => vec![],
377        TypedFunction::RowNumber | TypedFunction::Rank | TypedFunction::DenseRank => vec![],
378        TypedFunction::Length { expr }
379        | TypedFunction::Upper { expr }
380        | TypedFunction::Lower { expr }
381        | TypedFunction::Initcap { expr }
382        | TypedFunction::Reverse { expr }
383        | TypedFunction::Abs { expr }
384        | TypedFunction::Ceil { expr }
385        | TypedFunction::Floor { expr }
386        | TypedFunction::Ln { expr }
387        | TypedFunction::Sqrt { expr }
388        | TypedFunction::Explode { expr }
389        | TypedFunction::Flatten { expr }
390        | TypedFunction::ArraySize { expr }
391        | TypedFunction::ParseJSON { expr }
392        | TypedFunction::JSONFormat { expr }
393        | TypedFunction::Hex { expr }
394        | TypedFunction::Unhex { expr }
395        | TypedFunction::Md5 { expr }
396        | TypedFunction::Sha { expr }
397        | TypedFunction::TsOrDsToDate { expr }
398        | TypedFunction::Year { expr }
399        | TypedFunction::Month { expr }
400        | TypedFunction::Day { expr }
401        | TypedFunction::ApproxDistinct { expr }
402        | TypedFunction::Variance { expr }
403        | TypedFunction::VariancePop { expr }
404        | TypedFunction::Stddev { expr }
405        | TypedFunction::StddevPop { expr }
406        | TypedFunction::FirstValue { expr }
407        | TypedFunction::LastValue { expr } => vec![*expr.clone()],
408        TypedFunction::DateTrunc { unit, expr } => {
409            vec![Expr::StringLiteral(format!("{unit:?}")), *expr.clone()]
410        }
411        TypedFunction::DateAdd { expr, interval, .. }
412        | TypedFunction::DateSub { expr, interval, .. } => {
413            vec![*expr.clone(), *interval.clone()]
414        }
415        TypedFunction::DateDiff { start, end, .. } => vec![*start.clone(), *end.clone()],
416        TypedFunction::StrToTime { expr, format } | TypedFunction::TimeToStr { expr, format } => {
417            vec![*expr.clone(), *format.clone()]
418        }
419        TypedFunction::Trim { expr, .. } => vec![*expr.clone()],
420        TypedFunction::Substring {
421            expr,
422            start,
423            length,
424        } => {
425            let mut args = vec![*expr.clone(), *start.clone()];
426            if let Some(len) = length {
427                args.push(*len.clone());
428            }
429            args
430        }
431        TypedFunction::RegexpLike {
432            expr,
433            pattern,
434            flags,
435        } => {
436            let mut args = vec![*expr.clone(), *pattern.clone()];
437            if let Some(f) = flags {
438                args.push(*f.clone());
439            }
440            args
441        }
442        TypedFunction::RegexpExtract {
443            expr,
444            pattern,
445            group_index,
446        } => {
447            let mut args = vec![*expr.clone(), *pattern.clone()];
448            if let Some(g) = group_index {
449                args.push(*g.clone());
450            }
451            args
452        }
453        TypedFunction::RegexpReplace {
454            expr,
455            pattern,
456            replacement,
457            flags,
458        } => {
459            let mut args = vec![*expr.clone(), *pattern.clone(), *replacement.clone()];
460            if let Some(f) = flags {
461                args.push(*f.clone());
462            }
463            args
464        }
465        TypedFunction::ConcatWs { separator, exprs } => {
466            let mut args = vec![*separator.clone()];
467            args.extend(exprs.iter().cloned());
468            args
469        }
470        TypedFunction::Split { expr, delimiter } => vec![*expr.clone(), *delimiter.clone()],
471        TypedFunction::Replace { expr, from, to } => {
472            vec![*expr.clone(), *from.clone(), *to.clone()]
473        }
474        TypedFunction::Left { expr, n } | TypedFunction::Right { expr, n } => {
475            vec![*expr.clone(), *n.clone()]
476        }
477        TypedFunction::Lpad { expr, length, pad } | TypedFunction::Rpad { expr, length, pad } => {
478            let mut args = vec![*expr.clone(), *length.clone()];
479            if let Some(p) = pad {
480                args.push(*p.clone());
481            }
482            args
483        }
484        TypedFunction::Count { expr, .. }
485        | TypedFunction::Sum { expr, .. }
486        | TypedFunction::Avg { expr, .. }
487        | TypedFunction::Min { expr }
488        | TypedFunction::Max { expr }
489        | TypedFunction::ArrayAgg { expr, .. } => vec![*expr.clone()],
490        TypedFunction::ArrayConcat { arrays } => arrays.clone(),
491        TypedFunction::ArrayContains { array, element } => {
492            vec![*array.clone(), *element.clone()]
493        }
494        TypedFunction::GenerateSeries { start, stop, step } => {
495            let mut args = vec![*start.clone(), *stop.clone()];
496            if let Some(s) = step {
497                args.push(*s.clone());
498            }
499            args
500        }
501        TypedFunction::JSONExtract { expr, path }
502        | TypedFunction::JSONExtractScalar { expr, path } => {
503            vec![*expr.clone(), *path.clone()]
504        }
505        TypedFunction::NTile { n } => vec![*n.clone()],
506        TypedFunction::Lead {
507            expr,
508            offset,
509            default,
510        }
511        | TypedFunction::Lag {
512            expr,
513            offset,
514            default,
515        } => {
516            let mut args = vec![*expr.clone()];
517            if let Some(o) = offset {
518                args.push(*o.clone());
519            }
520            if let Some(d) = default {
521                args.push(*d.clone());
522            }
523            args
524        }
525        TypedFunction::Round { expr, decimals } => {
526            let mut args = vec![*expr.clone()];
527            if let Some(d) = decimals {
528                args.push(*d.clone());
529            }
530            args
531        }
532        TypedFunction::Log { expr, base } => {
533            let mut args = vec![*expr.clone()];
534            if let Some(b) = base {
535                args.push(*b.clone());
536            }
537            args
538        }
539        TypedFunction::Pow { base, exponent } => vec![*base.clone(), *exponent.clone()],
540        TypedFunction::Greatest { exprs } | TypedFunction::Least { exprs } => exprs.clone(),
541        TypedFunction::Mod { left, right } => vec![*left.clone(), *right.clone()],
542        TypedFunction::Sha2 { expr, bit_length } => vec![*expr.clone(), *bit_length.clone()],
543        TypedFunction::GroupConcat {
544            exprs, separator, ..
545        } => {
546            let mut args = exprs.clone();
547            if let Some(s) = separator {
548                args.push(*s.clone());
549            }
550            args
551        }
552    }
553}
554
555/// Transform a statement from one dialect to another, supporting custom
556/// dialect plugins.
557///
558/// For built-in → built-in transforms this delegates to the existing
559/// [`super::transform`]. When either side is a custom dialect the plugin's
560/// transform hooks are applied.
561#[must_use]
562pub fn transform(statement: &Statement, from: &DialectRef, to: &DialectRef) -> Statement {
563    // Fast path: both built-in → use existing logic
564    if let (DialectRef::BuiltIn(f), DialectRef::BuiltIn(t)) = (from, to) {
565        return super::transform(statement, *f, *t);
566    }
567
568    // If the target is a custom dialect with a full statement transform, try that first.
569    if let Some(plugin) = to.as_plugin()
570        && let Some(transformed) = plugin.transform_statement(statement)
571    {
572        return transformed;
573    }
574
575    // Otherwise apply expression-level transforms via DialectRef helpers.
576    let mut stmt = statement.clone();
577    transform_statement_plugin(&mut stmt, to);
578    stmt
579}
580
581/// Recursively transform a statement using plugin-aware rules.
582fn transform_statement_plugin(statement: &mut Statement, target: &DialectRef) {
583    match statement {
584        Statement::Select(sel) => {
585            for item in &mut sel.columns {
586                if let crate::ast::SelectItem::Expr { expr, .. } = item {
587                    *expr = transform_expr_plugin(expr.clone(), target);
588                }
589            }
590            if let Some(wh) = &mut sel.where_clause {
591                *wh = transform_expr_plugin(wh.clone(), target);
592            }
593            for gb in &mut sel.group_by {
594                *gb = transform_expr_plugin(gb.clone(), target);
595            }
596            if let Some(having) = &mut sel.having {
597                *having = transform_expr_plugin(having.clone(), target);
598            }
599        }
600        Statement::CreateTable(ct) => {
601            for col in &mut ct.columns {
602                col.data_type = target.map_data_type(&col.data_type);
603                if let Some(default) = &mut col.default {
604                    *default = transform_expr_plugin(default.clone(), target);
605                }
606            }
607        }
608        _ => {}
609    }
610}
611
612/// Transform an expression using plugin-aware rules.
613fn transform_expr_plugin(expr: Expr, target: &DialectRef) -> Expr {
614    // Let the plugin have first shot at transforming the whole expression
615    if let Some(plugin) = target.as_plugin()
616        && let Some(transformed) = plugin.transform_expr(&expr)
617    {
618        return transformed;
619    }
620
621    match expr {
622        // For TypedFunction, check if the plugin wants to rename the function.
623        // If so, convert it to a plain Expr::Function with the new name.
624        Expr::TypedFunction { func, filter, over } => {
625            if let DialectRef::Custom(_) = target {
626                let canonical = typed_function_canonical_name(&func);
627                let new_name = target.map_function_name(canonical);
628                if new_name != canonical {
629                    // Plugin wants to rename this function — convert to Expr::Function
630                    let args = typed_function_args(&func)
631                        .into_iter()
632                        .map(|a| transform_expr_plugin(a, target))
633                        .collect();
634                    return Expr::Function {
635                        name: new_name,
636                        args,
637                        distinct: false,
638                        filter: filter.map(|f| Box::new(transform_expr_plugin(*f, target))),
639                        over,
640                        order_by: vec![],
641                        within_group: false,
642                    };
643                }
644            }
645            // No rename — recurse into children
646            Expr::TypedFunction {
647                func: func.transform_children(&|e| transform_expr_plugin(e, target)),
648                filter: filter.map(|f| Box::new(transform_expr_plugin(*f, target))),
649                over,
650            }
651        }
652        Expr::Function {
653            name,
654            args,
655            distinct,
656            filter,
657            over,
658            order_by,
659            within_group,
660        } => {
661            let new_name = target.map_function_name(&name);
662            let new_args: Vec<Expr> = args
663                .into_iter()
664                .map(|a| transform_expr_plugin(a, target))
665                .collect();
666            Expr::Function {
667                name: new_name,
668                args: new_args,
669                distinct,
670                filter: filter.map(|f| Box::new(transform_expr_plugin(*f, target))),
671                over,
672                order_by,
673                within_group,
674            }
675        }
676        Expr::Cast { expr, data_type } => Expr::Cast {
677            expr: Box::new(transform_expr_plugin(*expr, target)),
678            data_type: target.map_data_type(&data_type),
679        },
680        Expr::ILike {
681            expr,
682            pattern,
683            negated,
684            escape,
685        } if !target.supports_ilike() => Expr::Like {
686            expr: Box::new(Expr::TypedFunction {
687                func: crate::ast::TypedFunction::Lower {
688                    expr: Box::new(transform_expr_plugin(*expr, target)),
689                },
690                filter: None,
691                over: None,
692            }),
693            pattern: Box::new(Expr::TypedFunction {
694                func: crate::ast::TypedFunction::Lower {
695                    expr: Box::new(transform_expr_plugin(*pattern, target)),
696                },
697                filter: None,
698                over: None,
699            }),
700            negated,
701            escape,
702        },
703        Expr::BinaryOp { left, op, right } => Expr::BinaryOp {
704            left: Box::new(transform_expr_plugin(*left, target)),
705            op,
706            right: Box::new(transform_expr_plugin(*right, target)),
707        },
708        Expr::UnaryOp { op, expr } => Expr::UnaryOp {
709            op,
710            expr: Box::new(transform_expr_plugin(*expr, target)),
711        },
712        Expr::Nested(inner) => Expr::Nested(Box::new(transform_expr_plugin(*inner, target))),
713        Expr::Column {
714            table,
715            name,
716            quote_style,
717            table_quote_style,
718        } => {
719            let new_qs = if quote_style.is_quoted() {
720                target.quote_style()
721            } else {
722                QuoteStyle::None
723            };
724            let new_tqs = if table_quote_style.is_quoted() {
725                target.quote_style()
726            } else {
727                QuoteStyle::None
728            };
729            Expr::Column {
730                table,
731                name,
732                quote_style: new_qs,
733                table_quote_style: new_tqs,
734            }
735        }
736        other => other,
737    }
738}
739
740// ═══════════════════════════════════════════════════════════════════════════
741// Plugin-aware top-level API
742// ═══════════════════════════════════════════════════════════════════════════
743
744use crate::errors;
745
746/// Transpile a SQL string using [`DialectRef`], supporting custom plugins.
747///
748/// # Example
749///
750/// ```rust
751/// use sqlglot_rust::dialects::plugin::{DialectRef, transpile_ext};
752/// use sqlglot_rust::Dialect;
753///
754/// let result = transpile_ext(
755///     "SELECT NOW()",
756///     &DialectRef::from(Dialect::Postgres),
757///     &DialectRef::from(Dialect::Mysql),
758/// ).unwrap();
759/// ```
760///
761/// # Errors
762///
763/// Returns a [`SqlglotError`](crate::errors::SqlglotError) if parsing fails.
764pub fn transpile_ext(
765    sql: &str,
766    read_dialect: &DialectRef,
767    write_dialect: &DialectRef,
768) -> errors::Result<String> {
769    // Parse using the read dialect (fall back to Ansi for custom dialects)
770    let parse_dialect = read_dialect.as_builtin().unwrap_or(Dialect::Ansi);
771    let ast = crate::parser::parse(sql, parse_dialect)?;
772    let transformed = transform(&ast, read_dialect, write_dialect);
773    let gen_dialect = write_dialect.as_builtin().unwrap_or(Dialect::Ansi);
774    Ok(crate::generator::generate(&transformed, gen_dialect))
775}
776
777/// Transpile multiple statements using [`DialectRef`], supporting custom plugins.
778///
779/// # Errors
780///
781/// Returns a [`SqlglotError`](crate::errors::SqlglotError) if parsing fails.
782pub fn transpile_statements_ext(
783    sql: &str,
784    read_dialect: &DialectRef,
785    write_dialect: &DialectRef,
786) -> errors::Result<Vec<String>> {
787    let parse_dialect = read_dialect.as_builtin().unwrap_or(Dialect::Ansi);
788    let stmts = crate::parser::parse_statements(sql, parse_dialect)?;
789    let gen_dialect = write_dialect.as_builtin().unwrap_or(Dialect::Ansi);
790    let mut results = Vec::with_capacity(stmts.len());
791    for stmt in &stmts {
792        let transformed = transform(stmt, read_dialect, write_dialect);
793        results.push(crate::generator::generate(&transformed, gen_dialect));
794    }
795    Ok(results)
796}
797
798// ═══════════════════════════════════════════════════════════════════════════
799// Convenience registration functions
800// ═══════════════════════════════════════════════════════════════════════════
801
802/// Register a custom dialect plugin in the global registry.
803///
804/// This is a convenience wrapper around [`DialectRegistry::global().register()`].
805pub fn register_dialect<P: DialectPlugin + 'static>(plugin: P) {
806    DialectRegistry::global().register(plugin);
807}
808
809/// Look up a dialect by name, returning either a built-in or custom [`DialectRef`].
810///
811/// Checks built-in dialects first, then the custom plugin registry.
812#[must_use]
813pub fn resolve_dialect(name: &str) -> Option<DialectRef> {
814    // Try built-in first
815    if let Some(d) = Dialect::from_str(name) {
816        return Some(DialectRef::BuiltIn(d));
817    }
818    // Then try plugin registry
819    if DialectRegistry::global().get(name).is_some() {
820        return Some(DialectRef::Custom(name.to_lowercase()));
821    }
822    None
823}