Skip to main content

polyglot_sql/
lib.rs

1//! Polyglot Core - SQL parsing and dialect translation library
2//!
3//! This library provides the core functionality for parsing SQL statements,
4//! building an abstract syntax tree (AST), and generating SQL in different dialects.
5//!
6//! # Architecture
7//!
8//! The library follows a pipeline architecture:
9//! 1. **Tokenizer** - Converts SQL string to token stream
10//! 2. **Parser** - Builds AST from tokens
11//! 3. **Generator** - Converts AST back to SQL string
12//!
13//! Each stage can be customized per dialect.
14
15pub mod ast_json;
16#[cfg(any(feature = "ast-tools", feature = "generate", feature = "semantic"))]
17pub mod ast_transforms;
18#[cfg(feature = "builder")]
19pub mod builder;
20pub mod dialects;
21#[cfg(feature = "diff")]
22pub mod diff;
23pub mod error;
24pub mod expressions;
25#[cfg(any(test, feature = "dialect-tsql"))]
26mod format_tokens;
27#[cfg(feature = "semantic")]
28pub mod function_catalog;
29mod function_registry;
30#[cfg(feature = "generate")]
31pub mod generator;
32pub mod guard;
33#[cfg(feature = "semantic")]
34pub mod helper;
35#[cfg(feature = "semantic")]
36pub mod lineage;
37#[cfg(feature = "openlineage")]
38pub mod openlineage;
39#[cfg(feature = "semantic")]
40pub mod optimizer;
41pub mod parser;
42#[cfg(feature = "planner")]
43pub mod planner;
44#[cfg(all(feature = "semantic", feature = "generate"))]
45pub mod query_analysis;
46#[cfg(feature = "semantic")]
47pub mod resolver;
48#[cfg(feature = "semantic")]
49pub mod schema;
50#[cfg(feature = "semantic")]
51pub mod scope;
52#[cfg(feature = "time")]
53pub mod time;
54pub mod tokens;
55#[cfg(feature = "transpile")]
56pub mod transforms;
57#[cfg(any(feature = "ast-tools", feature = "generate", feature = "semantic"))]
58pub mod traversal;
59#[cfg(not(any(feature = "ast-tools", feature = "generate", feature = "semantic")))]
60mod traversal;
61#[cfg(any(feature = "semantic", feature = "time"))]
62pub mod trie;
63#[cfg(feature = "semantic")]
64pub mod validation;
65
66#[cfg(any(feature = "generate", feature = "semantic"))]
67use serde::{Deserialize, Serialize};
68
69#[cfg(feature = "ast-tools")]
70pub use ast_transforms::{
71    add_select_columns, add_where, get_aggregate_functions, get_column_names, get_functions,
72    get_identifiers, get_literals, get_output_column_names, get_subqueries, get_table_names,
73    get_window_functions, node_count, qualify_columns, remove_limit_offset, remove_nodes,
74    remove_select_columns, remove_where, rename_columns, rename_tables, rename_tables_with_options,
75    replace_by_type, replace_nodes, set_distinct, set_limit, set_limit_expr, set_offset,
76    set_offset_expr, set_order_by, RenameTablesOptions,
77};
78pub use dialects::{unregister_custom_dialect, CustomDialectBuilder, Dialect, DialectType};
79#[cfg(feature = "transpile")]
80pub use dialects::{TranspileOptions, TranspileTarget};
81pub use error::{Error, Result};
82#[cfg(feature = "semantic")]
83pub use error::{ValidationError, ValidationResult, ValidationSeverity};
84pub use expressions::{DataType, Expression};
85#[cfg(feature = "semantic")]
86pub use function_catalog::{
87    FunctionCatalog, FunctionNameCase, FunctionSignature, HashMapFunctionCatalog,
88};
89#[cfg(feature = "generate")]
90pub use generator::{Generator, UnsupportedLevel};
91pub use guard::ComplexityGuardOptions;
92#[cfg(feature = "semantic")]
93pub use helper::{
94    csv, find_new_name, is_date_unit, is_float, is_int, is_iso_date, is_iso_datetime, merge_ranges,
95    name_sequence, seq_get, split_num_words, tsort, while_changing, DATE_UNITS,
96};
97#[cfg(feature = "semantic")]
98pub use optimizer::{
99    annotate_types, qualify_tables, QualifyTablesOptions, TypeAnnotator, TypeCoercionClass,
100};
101pub use parser::Parser;
102#[cfg(all(feature = "semantic", feature = "generate"))]
103pub use query_analysis::{
104    analyze_query, AnalyzeQueryOptions, ColumnReferenceFact, CteFact, ProjectionFact,
105    ProjectionNullability, QueryAnalysis, QueryShape, ReferenceConfidence, RelationFact,
106    SetOperationBranchFact, SetOperationFact, StarProjectionFact, TransformFunctionFact,
107    TransformKind,
108};
109#[cfg(feature = "semantic")]
110pub use resolver::{is_column_ambiguous, resolve_column, Resolver, ResolverError, ResolverResult};
111#[cfg(feature = "semantic")]
112pub use schema::{
113    ensure_schema, from_simple_map, normalize_name, MappingSchema, Schema, SchemaError,
114};
115#[cfg(feature = "semantic")]
116pub use scope::{
117    build_scope, find_all_in_scope, find_in_scope, traverse_scope, walk_in_scope, ColumnRef, Scope,
118    ScopeType, SourceInfo,
119};
120#[cfg(feature = "time")]
121pub use time::{format_time, is_valid_timezone, subsecond_precision, TIMEZONES};
122pub use tokens::{Token, TokenType, Tokenizer};
123#[cfg(feature = "ast-tools")]
124pub use traversal::{
125    contains_aggregate,
126    contains_subquery,
127    contains_window_function,
128    find_ancestor,
129    find_parent,
130    get_all_tables,
131    get_columns,
132    get_merge_source,
133    get_merge_target,
134    get_tables,
135    is_add,
136    is_aggregate,
137    is_alias,
138    is_alter_table,
139    is_and,
140    is_arithmetic,
141    is_avg,
142    is_between,
143    is_boolean,
144    is_case,
145    is_cast,
146    is_coalesce,
147    is_column,
148    is_comparison,
149    is_concat,
150    is_count,
151    is_create_index,
152    is_create_table,
153    is_create_view,
154    is_cte,
155    is_ddl,
156    is_delete,
157    is_div,
158    is_drop_index,
159    is_drop_table,
160    is_drop_view,
161    is_eq,
162    is_except,
163    is_exists,
164    is_from,
165    is_function,
166    is_group_by,
167    is_gt,
168    is_gte,
169    is_having,
170    is_identifier,
171    is_ilike,
172    is_in,
173    // Extended type predicates
174    is_insert,
175    is_intersect,
176    is_is_null,
177    is_join,
178    is_like,
179    is_limit,
180    is_literal,
181    is_logical,
182    is_lt,
183    is_lte,
184    is_max_func,
185    is_merge,
186    is_min_func,
187    is_mod,
188    is_mul,
189    is_neq,
190    is_not,
191    is_null_if,
192    is_null_literal,
193    is_offset,
194    is_or,
195    is_order_by,
196    is_ordered,
197    is_paren,
198    // Composite predicates
199    is_query,
200    is_safe_cast,
201    is_select,
202    is_set_operation,
203    is_star,
204    is_sub,
205    is_subquery,
206    is_sum,
207    is_table,
208    is_try_cast,
209    is_union,
210    is_update,
211    is_where,
212    is_window_function,
213    is_with,
214    transform,
215    transform_map,
216    BfsIter,
217    DfsIter,
218    ExpressionWalk,
219    ParentInfo,
220    TreeContext,
221};
222#[cfg(any(feature = "semantic", feature = "time"))]
223pub use trie::{new_trie, new_trie_from_keys, Trie, TrieResult};
224#[cfg(feature = "semantic")]
225pub use validation::{
226    mapping_schema_from_validation_schema, validate_with_schema, SchemaColumn,
227    SchemaColumnReference, SchemaForeignKey, SchemaTable, SchemaTableReference,
228    SchemaValidationOptions, ValidationSchema,
229};
230
231#[cfg(feature = "generate")]
232const DEFAULT_FORMAT_MAX_INPUT_BYTES: usize = 16 * 1024 * 1024; // 16 MiB
233#[cfg(feature = "generate")]
234const DEFAULT_FORMAT_MAX_TOKENS: usize = 1_000_000;
235#[cfg(feature = "generate")]
236const DEFAULT_FORMAT_MAX_AST_NODES: usize = 1_000_000;
237#[cfg(feature = "generate")]
238const DEFAULT_FORMAT_MAX_SET_OP_CHAIN: usize = 256;
239
240#[cfg(feature = "generate")]
241fn default_format_max_input_bytes() -> Option<usize> {
242    Some(DEFAULT_FORMAT_MAX_INPUT_BYTES)
243}
244
245#[cfg(feature = "generate")]
246fn default_format_max_tokens() -> Option<usize> {
247    Some(DEFAULT_FORMAT_MAX_TOKENS)
248}
249
250#[cfg(feature = "generate")]
251fn default_format_max_ast_nodes() -> Option<usize> {
252    Some(DEFAULT_FORMAT_MAX_AST_NODES)
253}
254
255#[cfg(feature = "generate")]
256fn default_format_max_set_op_chain() -> Option<usize> {
257    Some(DEFAULT_FORMAT_MAX_SET_OP_CHAIN)
258}
259
260/// Guard options for SQL pretty-formatting.
261///
262/// These limits protect against extremely large/complex queries that can cause
263/// high memory pressure in constrained runtimes (for example browser WASM).
264#[cfg(feature = "generate")]
265#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
266#[serde(rename_all = "camelCase")]
267pub struct FormatGuardOptions {
268    /// Maximum allowed SQL input size in bytes.
269    /// `None` disables this check.
270    #[serde(default = "default_format_max_input_bytes")]
271    pub max_input_bytes: Option<usize>,
272    /// Maximum allowed number of tokens after tokenization.
273    /// `None` disables this check.
274    #[serde(default = "default_format_max_tokens")]
275    pub max_tokens: Option<usize>,
276    /// Maximum allowed AST node count after parsing.
277    /// `None` disables this check.
278    #[serde(default = "default_format_max_ast_nodes")]
279    pub max_ast_nodes: Option<usize>,
280    /// Maximum allowed count of set-operation operators (`UNION`/`INTERSECT`/`EXCEPT`)
281    /// observed in a statement before parsing.
282    ///
283    /// `None` disables this check.
284    #[serde(default = "default_format_max_set_op_chain")]
285    pub max_set_op_chain: Option<usize>,
286}
287
288#[cfg(feature = "generate")]
289impl Default for FormatGuardOptions {
290    fn default() -> Self {
291        Self {
292            max_input_bytes: default_format_max_input_bytes(),
293            max_tokens: default_format_max_tokens(),
294            max_ast_nodes: default_format_max_ast_nodes(),
295            max_set_op_chain: default_format_max_set_op_chain(),
296        }
297    }
298}
299
300#[cfg(feature = "generate")]
301fn format_guard_error(code: &str, actual: usize, limit: usize) -> Error {
302    Error::generate(format!(
303        "{code}: value {actual} exceeds configured limit {limit}"
304    ))
305}
306
307#[cfg(feature = "generate")]
308fn enforce_input_guard(sql: &str, options: &FormatGuardOptions) -> Result<()> {
309    if let Some(max) = options.max_input_bytes {
310        let input_bytes = sql.len();
311        if input_bytes > max {
312            return Err(format_guard_error(
313                "E_GUARD_INPUT_TOO_LARGE",
314                input_bytes,
315                max,
316            ));
317        }
318    }
319    Ok(())
320}
321
322#[cfg(feature = "generate")]
323fn parse_with_token_guard(
324    sql: &str,
325    dialect: &Dialect,
326    options: &FormatGuardOptions,
327) -> Result<Vec<Expression>> {
328    let tokens = dialect.tokenize(sql)?;
329    if let Some(max) = options.max_tokens {
330        let token_count = tokens.len();
331        if token_count > max {
332            return Err(format_guard_error(
333                "E_GUARD_TOKEN_BUDGET_EXCEEDED",
334                token_count,
335                max,
336            ));
337        }
338    }
339    enforce_set_op_chain_guard(&tokens, options)?;
340
341    let complexity_guard = ComplexityGuardOptions {
342        max_input_bytes: options.max_input_bytes,
343        max_tokens: options.max_tokens,
344        max_ast_nodes: options.max_ast_nodes,
345        ..Default::default()
346    };
347    let config = crate::parser::ParserConfig {
348        dialect: Some(dialect.dialect_type()),
349        complexity_guard,
350        ..Default::default()
351    };
352    let mut parser = Parser::with_source(tokens, config, sql.to_string());
353    parser.parse()
354}
355
356#[cfg(feature = "generate")]
357fn is_trivia_token(token_type: TokenType) -> bool {
358    matches!(
359        token_type,
360        TokenType::Space | TokenType::Break | TokenType::LineComment | TokenType::BlockComment
361    )
362}
363
364#[cfg(feature = "generate")]
365fn next_significant_token(tokens: &[Token], start: usize) -> Option<&Token> {
366    tokens
367        .iter()
368        .skip(start)
369        .find(|token| !is_trivia_token(token.token_type))
370}
371
372#[cfg(feature = "generate")]
373fn is_set_operation_token(tokens: &[Token], idx: usize) -> bool {
374    let token = &tokens[idx];
375    match token.token_type {
376        TokenType::Union | TokenType::Intersect => true,
377        TokenType::Except => {
378            // MINUS is aliased to EXCEPT in the tokenizer, but in ClickHouse minus(...)
379            // is a function call rather than a set operation.
380            if token.text.eq_ignore_ascii_case("minus")
381                && matches!(
382                    next_significant_token(tokens, idx + 1).map(|t| t.token_type),
383                    Some(TokenType::LParen)
384                )
385            {
386                return false;
387            }
388            true
389        }
390        _ => false,
391    }
392}
393
394#[cfg(feature = "generate")]
395fn enforce_set_op_chain_guard(tokens: &[Token], options: &FormatGuardOptions) -> Result<()> {
396    let Some(max) = options.max_set_op_chain else {
397        return Ok(());
398    };
399
400    let mut set_op_count = 0usize;
401    for (idx, token) in tokens.iter().enumerate() {
402        if token.token_type == TokenType::Semicolon {
403            set_op_count = 0;
404            continue;
405        }
406
407        if is_set_operation_token(tokens, idx) {
408            set_op_count += 1;
409            if set_op_count > max {
410                return Err(format_guard_error(
411                    "E_GUARD_SET_OP_CHAIN_EXCEEDED",
412                    set_op_count,
413                    max,
414                ));
415            }
416        }
417    }
418
419    Ok(())
420}
421
422#[cfg(feature = "generate")]
423fn enforce_ast_guard(expressions: &[Expression], options: &FormatGuardOptions) -> Result<()> {
424    if let Some(max) = options.max_ast_nodes {
425        let ast_nodes: usize = expressions
426            .iter()
427            .map(crate::ast_transforms::node_count)
428            .sum();
429        if ast_nodes > max {
430            return Err(format_guard_error(
431                "E_GUARD_AST_BUDGET_EXCEEDED",
432                ast_nodes,
433                max,
434            ));
435        }
436    }
437    Ok(())
438}
439
440#[cfg(feature = "generate")]
441fn format_with_dialect(
442    sql: &str,
443    dialect: &Dialect,
444    options: &FormatGuardOptions,
445) -> Result<Vec<String>> {
446    enforce_input_guard(sql, options)?;
447    let expressions = parse_with_token_guard(sql, dialect, options)?;
448    enforce_ast_guard(&expressions, options)?;
449
450    expressions
451        .iter()
452        .map(|expr| dialect.generate_pretty(expr))
453        .collect()
454}
455
456/// Transpile SQL from one dialect to another.
457///
458/// # Arguments
459/// * `sql` - The SQL string to transpile
460/// * `read` - The source dialect to parse with
461/// * `write` - The target dialect to generate
462///
463/// # Returns
464/// A vector of transpiled SQL statements
465///
466/// # Example
467/// ```
468/// use polyglot_sql::{transpile, DialectType};
469///
470/// let result = transpile(
471///     "SELECT EPOCH_MS(1618088028295)",
472///     DialectType::DuckDB,
473///     DialectType::Hive
474/// );
475/// ```
476#[cfg(feature = "transpile")]
477pub fn transpile(sql: &str, read: DialectType, write: DialectType) -> Result<Vec<String>> {
478    // Delegate to Dialect::transpile so that the full cross-dialect rewrite
479    // pipeline (source+target-aware normalization in `cross_dialect_normalize`)
480    // runs here as well. This keeps Rust crate users on the same code path as
481    // the WASM/FFI/Python bindings and the playground.
482    Dialect::get(read).transpile(sql, write)
483}
484
485/// Parse SQL into an AST.
486///
487/// # Arguments
488/// * `sql` - The SQL string to parse
489/// * `dialect` - The dialect to use for parsing
490///
491/// # Returns
492/// A vector of parsed expressions
493pub fn parse(sql: &str, dialect: DialectType) -> Result<Vec<Expression>> {
494    let d = Dialect::get(dialect);
495    d.parse(sql)
496}
497
498/// Parse a single SQL statement.
499///
500/// # Arguments
501/// * `sql` - The SQL string containing a single statement
502/// * `dialect` - The dialect to use for parsing
503///
504/// # Returns
505/// The parsed expression, or an error if multiple statements found
506pub fn parse_one(sql: &str, dialect: DialectType) -> Result<Expression> {
507    let mut expressions = parse(sql, dialect)?;
508
509    if expressions.len() != 1 {
510        return Err(Error::parse(
511            format!("Expected 1 statement, found {}", expressions.len()),
512            0,
513            0,
514            0,
515            0,
516        ));
517    }
518
519    Ok(expressions.remove(0))
520}
521
522/// Parse a standalone SQL data type.
523///
524/// # Arguments
525/// * `sql` - The data type string to parse, e.g. `DECIMAL(10, 2)`
526/// * `dialect` - The dialect to use for parsing
527///
528/// # Returns
529/// The parsed data type
530pub fn parse_data_type(sql: &str, dialect: DialectType) -> Result<DataType> {
531    Dialect::get(dialect).parse_data_type(sql)
532}
533
534/// Generate SQL from a standalone data type.
535///
536/// # Arguments
537/// * `data_type` - The data type to render
538/// * `dialect` - The target dialect
539///
540/// # Returns
541/// The generated type SQL string
542#[cfg(feature = "generate")]
543pub fn generate_data_type(data_type: &DataType, dialect: DialectType) -> Result<String> {
544    Dialect::get(dialect).generate(&Expression::DataType(data_type.clone()))
545}
546
547/// Generate SQL from an AST.
548///
549/// # Arguments
550/// * `expression` - The expression to generate SQL from
551/// * `dialect` - The target dialect
552///
553/// # Returns
554/// The generated SQL string
555#[cfg(feature = "generate")]
556pub fn generate(expression: &Expression, dialect: DialectType) -> Result<String> {
557    let d = Dialect::get(dialect);
558    d.generate(expression)
559}
560
561/// Format/pretty-print SQL statements.
562///
563/// Uses [`FormatGuardOptions::default`] guards.
564#[cfg(feature = "generate")]
565pub fn format(sql: &str, dialect: DialectType) -> Result<Vec<String>> {
566    format_with_options(sql, dialect, &FormatGuardOptions::default())
567}
568
569/// Format/pretty-print SQL statements with configurable guard limits.
570#[cfg(feature = "generate")]
571pub fn format_with_options(
572    sql: &str,
573    dialect: DialectType,
574    options: &FormatGuardOptions,
575) -> Result<Vec<String>> {
576    let d = Dialect::get(dialect);
577    format_with_dialect(sql, &d, options)
578}
579
580/// Validate SQL syntax.
581///
582/// # Arguments
583/// * `sql` - The SQL string to validate
584/// * `dialect` - The dialect to use for validation
585///
586/// # Returns
587/// A validation result with any errors found
588#[cfg(feature = "semantic")]
589pub fn validate(sql: &str, dialect: DialectType) -> ValidationResult {
590    validate_with_options(sql, dialect, &ValidationOptions::default())
591}
592
593/// Options for syntax validation behavior.
594#[cfg(feature = "semantic")]
595#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
596#[serde(rename_all = "camelCase")]
597pub struct ValidationOptions {
598    /// When enabled, validation rejects non-canonical trailing commas that the parser
599    /// would otherwise accept for compatibility (e.g. `SELECT a, FROM t`).
600    #[serde(default)]
601    pub strict_syntax: bool,
602}
603
604/// Validate SQL syntax with additional validation options.
605#[cfg(feature = "semantic")]
606pub fn validate_with_options(
607    sql: &str,
608    dialect: DialectType,
609    options: &ValidationOptions,
610) -> ValidationResult {
611    let d = Dialect::get(dialect);
612    match d.parse(sql) {
613        Ok(expressions) => {
614            // Reject bare expressions that aren't valid SQL statements.
615            // The parser accepts any expression at the top level, but bare identifiers,
616            // literals, function calls, etc. are not valid statements.
617            for expr in &expressions {
618                if !expr.is_statement() {
619                    let msg = format!("Invalid expression / Unexpected token");
620                    return ValidationResult::with_errors(vec![ValidationError::error(
621                        msg, "E004",
622                    )]);
623                }
624            }
625            if options.strict_syntax {
626                if let Some(error) = strict_syntax_error(sql, &d) {
627                    return ValidationResult::with_errors(vec![error]);
628                }
629            }
630            ValidationResult::success()
631        }
632        Err(e) => {
633            let error = match &e {
634                Error::Syntax {
635                    message,
636                    line,
637                    column,
638                    start,
639                    end,
640                } => ValidationError::error(message.clone(), "E001")
641                    .with_location(*line, *column)
642                    .with_span(Some(*start), Some(*end)),
643                Error::Tokenize {
644                    message,
645                    line,
646                    column,
647                    start,
648                    end,
649                } => ValidationError::error(message.clone(), "E002")
650                    .with_location(*line, *column)
651                    .with_span(Some(*start), Some(*end)),
652                Error::Parse {
653                    message,
654                    line,
655                    column,
656                    start,
657                    end,
658                } => ValidationError::error(message.clone(), "E003")
659                    .with_location(*line, *column)
660                    .with_span(Some(*start), Some(*end)),
661                _ => ValidationError::error(e.to_string(), "E000"),
662            };
663            ValidationResult::with_errors(vec![error])
664        }
665    }
666}
667
668#[cfg(feature = "semantic")]
669fn strict_syntax_error(sql: &str, dialect: &Dialect) -> Option<ValidationError> {
670    let tokens = dialect.tokenize(sql).ok()?;
671
672    for (idx, token) in tokens.iter().enumerate() {
673        if token.token_type != TokenType::Comma {
674            continue;
675        }
676
677        let next = tokens.get(idx + 1);
678        let (is_boundary, boundary_name) = match next.map(|t| t.token_type) {
679            Some(TokenType::From) => (true, "FROM"),
680            Some(TokenType::Where) => (true, "WHERE"),
681            Some(TokenType::GroupBy) => (true, "GROUP BY"),
682            Some(TokenType::Having) => (true, "HAVING"),
683            Some(TokenType::Order) | Some(TokenType::OrderBy) => (true, "ORDER BY"),
684            Some(TokenType::Limit) => (true, "LIMIT"),
685            Some(TokenType::Offset) => (true, "OFFSET"),
686            Some(TokenType::Union) => (true, "UNION"),
687            Some(TokenType::Intersect) => (true, "INTERSECT"),
688            Some(TokenType::Except) => (true, "EXCEPT"),
689            Some(TokenType::Qualify) => (true, "QUALIFY"),
690            Some(TokenType::Window) => (true, "WINDOW"),
691            Some(TokenType::Semicolon) | None => (true, "end of statement"),
692            _ => (false, ""),
693        };
694
695        if is_boundary {
696            let message = format!(
697                "Trailing comma before {} is not allowed in strict syntax mode",
698                boundary_name
699            );
700            return Some(
701                ValidationError::error(message, "E005")
702                    .with_location(token.span.line, token.span.column),
703            );
704        }
705    }
706
707    None
708}
709
710/// Transpile SQL from one dialect to another, using string dialect names.
711///
712/// This supports both built-in dialect names (e.g., "postgresql", "mysql") and
713/// custom dialects registered via [`CustomDialectBuilder`].
714///
715/// # Arguments
716/// * `sql` - The SQL string to transpile
717/// * `read` - The source dialect name
718/// * `write` - The target dialect name
719///
720/// # Returns
721/// A vector of transpiled SQL statements, or an error if a dialect name is unknown.
722#[cfg(feature = "transpile")]
723pub fn transpile_by_name(sql: &str, read: &str, write: &str) -> Result<Vec<String>> {
724    transpile_with_by_name(sql, read, write, &TranspileOptions::default())
725}
726
727/// Transpile SQL with configurable [`TranspileOptions`], using string dialect names.
728///
729/// Same as [`transpile_by_name`] but accepts options (e.g., pretty-printing).
730#[cfg(feature = "transpile")]
731pub fn transpile_with_by_name(
732    sql: &str,
733    read: &str,
734    write: &str,
735    opts: &TranspileOptions,
736) -> Result<Vec<String>> {
737    let read_dialect = Dialect::get_by_name(read)
738        .ok_or_else(|| Error::parse(format!("Unknown dialect: {}", read), 0, 0, 0, 0))?;
739    let write_dialect = Dialect::get_by_name(write)
740        .ok_or_else(|| Error::parse(format!("Unknown dialect: {}", write), 0, 0, 0, 0))?;
741    read_dialect.transpile_with(sql, &write_dialect, opts.clone())
742}
743
744/// Parse SQL into an AST using a string dialect name.
745///
746/// Supports both built-in and custom dialect names.
747pub fn parse_by_name(sql: &str, dialect: &str) -> Result<Vec<Expression>> {
748    let d = Dialect::get_by_name(dialect)
749        .ok_or_else(|| Error::parse(format!("Unknown dialect: {}", dialect), 0, 0, 0, 0))?;
750    d.parse(sql)
751}
752
753/// Generate SQL from an AST using a string dialect name.
754///
755/// Supports both built-in and custom dialect names.
756#[cfg(feature = "generate")]
757pub fn generate_by_name(expression: &Expression, dialect: &str) -> Result<String> {
758    let d = Dialect::get_by_name(dialect)
759        .ok_or_else(|| Error::parse(format!("Unknown dialect: {}", dialect), 0, 0, 0, 0))?;
760    d.generate(expression)
761}
762
763/// Format SQL using a string dialect name.
764///
765/// Uses [`FormatGuardOptions::default`] guards.
766#[cfg(feature = "generate")]
767pub fn format_by_name(sql: &str, dialect: &str) -> Result<Vec<String>> {
768    format_with_options_by_name(sql, dialect, &FormatGuardOptions::default())
769}
770
771/// Format SQL using a string dialect name with configurable guard limits.
772#[cfg(feature = "generate")]
773pub fn format_with_options_by_name(
774    sql: &str,
775    dialect: &str,
776    options: &FormatGuardOptions,
777) -> Result<Vec<String>> {
778    let d = Dialect::get_by_name(dialect)
779        .ok_or_else(|| Error::parse(format!("Unknown dialect: {}", dialect), 0, 0, 0, 0))?;
780    format_with_dialect(sql, &d, options)
781}
782
783#[cfg(all(test, feature = "semantic"))]
784mod validation_tests {
785    use super::*;
786
787    #[test]
788    fn validate_is_permissive_by_default_for_trailing_commas() {
789        let result = validate("SELECT name, FROM employees", DialectType::Generic);
790        assert!(result.valid, "Result: {:?}", result.errors);
791    }
792
793    #[test]
794    fn validate_with_options_rejects_trailing_comma_before_from() {
795        let options = ValidationOptions {
796            strict_syntax: true,
797        };
798        let result = validate_with_options(
799            "SELECT name, FROM employees",
800            DialectType::Generic,
801            &options,
802        );
803        assert!(!result.valid, "Result should be invalid");
804        assert!(
805            result.errors.iter().any(|e| e.code == "E005"),
806            "Expected E005, got: {:?}",
807            result.errors
808        );
809    }
810
811    #[test]
812    fn validate_with_options_rejects_trailing_comma_before_where() {
813        let options = ValidationOptions {
814            strict_syntax: true,
815        };
816        let result = validate_with_options(
817            "SELECT name FROM employees, WHERE salary > 10",
818            DialectType::Generic,
819            &options,
820        );
821        assert!(!result.valid, "Result should be invalid");
822        assert!(
823            result.errors.iter().any(|e| e.code == "E005"),
824            "Expected E005, got: {:?}",
825            result.errors
826        );
827    }
828}
829
830#[cfg(all(test, feature = "generate"))]
831mod format_tests {
832    use super::*;
833
834    #[test]
835    fn format_basic_query() {
836        let result = format("SELECT a,b FROM t", DialectType::Generic).expect("format failed");
837        assert_eq!(result.len(), 1);
838        assert!(result[0].contains('\n'));
839    }
840
841    #[test]
842    fn format_guard_rejects_large_input() {
843        let options = FormatGuardOptions {
844            max_input_bytes: Some(7),
845            max_tokens: None,
846            max_ast_nodes: None,
847            max_set_op_chain: None,
848        };
849        let err = format_with_options("SELECT 1", DialectType::Generic, &options)
850            .expect_err("expected guard error");
851        assert!(err.to_string().contains("E_GUARD_INPUT_TOO_LARGE"));
852    }
853
854    #[test]
855    fn format_guard_rejects_token_budget() {
856        let options = FormatGuardOptions {
857            max_input_bytes: None,
858            max_tokens: Some(1),
859            max_ast_nodes: None,
860            max_set_op_chain: None,
861        };
862        let err = format_with_options("SELECT 1", DialectType::Generic, &options)
863            .expect_err("expected guard error");
864        assert!(err.to_string().contains("E_GUARD_TOKEN_BUDGET_EXCEEDED"));
865    }
866
867    #[test]
868    fn format_guard_rejects_ast_budget() {
869        let options = FormatGuardOptions {
870            max_input_bytes: None,
871            max_tokens: None,
872            max_ast_nodes: Some(1),
873            max_set_op_chain: None,
874        };
875        let err = format_with_options("SELECT 1", DialectType::Generic, &options)
876            .expect_err("expected guard error");
877        assert!(err.to_string().contains("E_GUARD_AST_BUDGET_EXCEEDED"));
878    }
879
880    #[test]
881    fn format_guard_rejects_set_op_chain_budget() {
882        let options = FormatGuardOptions {
883            max_input_bytes: None,
884            max_tokens: None,
885            max_ast_nodes: None,
886            max_set_op_chain: Some(1),
887        };
888        let err = format_with_options(
889            "SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3",
890            DialectType::Generic,
891            &options,
892        )
893        .expect_err("expected guard error");
894        assert!(err.to_string().contains("E_GUARD_SET_OP_CHAIN_EXCEEDED"));
895    }
896
897    #[test]
898    fn format_guard_does_not_treat_clickhouse_minus_function_as_set_op() {
899        let options = FormatGuardOptions {
900            max_input_bytes: None,
901            max_tokens: None,
902            max_ast_nodes: None,
903            max_set_op_chain: Some(0),
904        };
905        let result = format_with_options("SELECT minus(3, 2)", DialectType::ClickHouse, &options);
906        assert!(result.is_ok(), "Result: {:?}", result);
907    }
908
909    #[test]
910    fn issue57_invalid_ternary_returns_error() {
911        // https://github.com/tobilg/polyglot/issues/57
912        // Invalid SQL with ternary operator should return an error, not garbled output.
913        let sql = "SELECT x > 0 ? 1 : 0 FROM t";
914
915        let parse_result = parse(sql, DialectType::PostgreSQL);
916        assert!(
917            parse_result.is_err(),
918            "Expected parse error for invalid ternary SQL, got: {:?}",
919            parse_result
920        );
921
922        let format_result = format(sql, DialectType::PostgreSQL);
923        assert!(
924            format_result.is_err(),
925            "Expected format error for invalid ternary SQL, got: {:?}",
926            format_result
927        );
928
929        let transpile_result = transpile(sql, DialectType::PostgreSQL, DialectType::PostgreSQL);
930        assert!(
931            transpile_result.is_err(),
932            "Expected transpile error for invalid ternary SQL, got: {:?}",
933            transpile_result
934        );
935    }
936
937    /// Regression guard: `lib::transpile()` must apply the full cross-dialect
938    /// rewrite pipeline (same as `Dialect::transpile()`). If these two paths
939    /// diverge again, Rust crate users silently get under-transformed SQL that
940    /// differs from what WASM/FFI/Python bindings produce.
941    #[test]
942    fn transpile_applies_cross_dialect_rewrites() {
943        // DuckDB to_timestamp → Trino FROM_UNIXTIME (different input semantics).
944        let out = transpile(
945            "SELECT to_timestamp(col) FROM t",
946            DialectType::DuckDB,
947            DialectType::Trino,
948        )
949        .expect("transpile failed");
950        assert_eq!(out[0], "SELECT FROM_UNIXTIME(col) FROM t");
951
952        // DuckDB CAST(x AS JSON) → Trino JSON_PARSE(x) (different CAST semantics).
953        let out = transpile(
954            "SELECT CAST(col AS JSON) FROM t",
955            DialectType::DuckDB,
956            DialectType::Trino,
957        )
958        .expect("transpile failed");
959        assert_eq!(out[0], "SELECT JSON_PARSE(col) FROM t");
960    }
961
962    /// Regression guard: all three transpile entry points (lib::transpile,
963    /// lib::transpile_by_name, Dialect::transpile) must produce identical
964    /// output. transpile_by_name is the one used by Python and C FFI bindings.
965    #[test]
966    fn transpile_matches_dialect_method() {
967        let cases: &[(DialectType, DialectType, &str, &str, &str)] = &[
968            (
969                DialectType::DuckDB,
970                DialectType::Trino,
971                "duckdb",
972                "trino",
973                "SELECT to_timestamp(col) FROM t",
974            ),
975            (
976                DialectType::DuckDB,
977                DialectType::Trino,
978                "duckdb",
979                "trino",
980                "SELECT CAST(col AS JSON) FROM t",
981            ),
982            (
983                DialectType::DuckDB,
984                DialectType::Trino,
985                "duckdb",
986                "trino",
987                "SELECT json_valid(col) FROM t",
988            ),
989            (
990                DialectType::Snowflake,
991                DialectType::DuckDB,
992                "snowflake",
993                "duckdb",
994                "SELECT DATEDIFF(day, a, b) FROM t",
995            ),
996            (
997                DialectType::BigQuery,
998                DialectType::DuckDB,
999                "bigquery",
1000                "duckdb",
1001                "SELECT DATE_DIFF(a, b, DAY) FROM t",
1002            ),
1003            (
1004                DialectType::Generic,
1005                DialectType::Generic,
1006                "generic",
1007                "generic",
1008                "SELECT 1",
1009            ),
1010        ];
1011        for (read, write, read_name, write_name, sql) in cases {
1012            let via_lib = transpile(sql, *read, *write).expect("lib::transpile failed");
1013            let via_name = transpile_by_name(sql, read_name, write_name)
1014                .expect("lib::transpile_by_name failed");
1015            let via_dialect = Dialect::get(*read)
1016                .transpile(sql, *write)
1017                .expect("Dialect::transpile failed");
1018            assert_eq!(
1019                via_lib, via_dialect,
1020                "lib::transpile / Dialect::transpile diverged for {:?} -> {:?}: {sql}",
1021                read, write
1022            );
1023            assert_eq!(
1024                via_name, via_dialect,
1025                "lib::transpile_by_name / Dialect::transpile diverged for {read_name} -> {write_name}: {sql}"
1026            );
1027        }
1028    }
1029
1030    #[test]
1031    fn format_default_guard_rejects_deep_union_chain_before_parse() {
1032        let base = "SELECT col0, col1 FROM t";
1033        let mut sql = base.to_string();
1034        for _ in 0..1100 {
1035            sql.push_str(" UNION ALL ");
1036            sql.push_str(base);
1037        }
1038
1039        let err = format(&sql, DialectType::Athena).expect_err("expected guard error");
1040        assert!(err.to_string().contains("E_GUARD_SET_OP_CHAIN_EXCEEDED"));
1041    }
1042}