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
15mod ast_children;
16pub mod ast_json;
17#[cfg(any(feature = "ast-tools", feature = "generate", feature = "semantic"))]
18mod ast_mutation;
19#[cfg(any(feature = "ast-tools", feature = "generate", feature = "semantic"))]
20pub mod ast_transforms;
21#[cfg(feature = "semantic")]
22mod binding;
23#[cfg(feature = "builder")]
24pub mod builder;
25pub mod dialects;
26#[cfg(feature = "diff")]
27pub mod diff;
28pub mod error;
29pub mod expressions;
30#[cfg(any(
31    test,
32    feature = "generate",
33    feature = "dialect-tsql",
34    feature = "dialect-fabric"
35))]
36mod format_tokens;
37#[cfg(feature = "semantic")]
38pub mod function_catalog;
39mod function_registry;
40#[cfg(feature = "generate")]
41pub mod generator;
42pub mod guard;
43#[cfg(any(feature = "semantic", feature = "transpile"))]
44pub mod helper;
45#[cfg(feature = "semantic")]
46pub mod lineage;
47#[cfg(feature = "openlineage")]
48pub mod openlineage;
49#[cfg(feature = "semantic")]
50pub mod optimizer;
51pub mod parser;
52#[cfg(feature = "planner")]
53pub mod planner;
54#[cfg(all(feature = "semantic", feature = "generate"))]
55pub mod query_analysis;
56#[cfg(feature = "semantic")]
57pub mod resolver;
58#[cfg(feature = "semantic")]
59pub mod schema;
60#[cfg(feature = "semantic")]
61pub mod scope;
62#[cfg(any(feature = "ast-tools", feature = "generate", feature = "semantic"))]
63mod set_operation;
64#[cfg(feature = "time")]
65pub mod time;
66pub mod tokens;
67#[cfg(feature = "transpile")]
68pub mod transforms;
69#[cfg(any(feature = "ast-tools", feature = "generate", feature = "semantic"))]
70pub mod traversal;
71#[cfg(not(any(feature = "ast-tools", feature = "generate", feature = "semantic")))]
72mod traversal;
73#[cfg(any(feature = "semantic", feature = "time"))]
74pub mod trie;
75#[cfg(feature = "semantic")]
76pub mod validation;
77
78#[cfg(any(feature = "generate", feature = "semantic"))]
79use serde::{Deserialize, Serialize};
80
81#[cfg(feature = "ast-tools")]
82pub use ast_transforms::{
83    add_select_columns, add_where, get_aggregate_functions, get_column_names, get_functions,
84    get_identifiers, get_literals, get_output_column_names, get_subqueries, get_table_names,
85    get_window_functions, node_count, qualify_columns, remove_limit_offset, remove_nodes,
86    remove_select_columns, remove_where, rename_columns, rename_tables, rename_tables_with_options,
87    replace_by_type, replace_nodes, set_distinct, set_limit, set_limit_expr, set_offset,
88    set_offset_expr, set_order_by, RenameTablesOptions,
89};
90pub use dialects::{unregister_custom_dialect, CustomDialectBuilder, Dialect, DialectType};
91#[cfg(feature = "transpile")]
92pub use dialects::{TranspileOptions, TranspileTarget};
93pub use error::{ColumnResolutionReason, ColumnResolutionTarget, Error, Result};
94#[cfg(feature = "semantic")]
95pub use error::{ValidationError, ValidationResult, ValidationSeverity};
96pub use expressions::{DataType, Expression};
97#[cfg(feature = "semantic")]
98pub use function_catalog::{
99    FunctionCatalog, FunctionNameCase, FunctionSignature, HashMapFunctionCatalog,
100};
101#[cfg(feature = "generate")]
102pub use generator::{Generator, UnsupportedLevel};
103pub use guard::{ComplexityGuardOptions, ParseOptions};
104#[cfg(feature = "semantic")]
105pub use helper::{
106    csv, find_new_name, is_date_unit, is_float, is_int, is_iso_date, is_iso_datetime, merge_ranges,
107    name_sequence, seq_get, split_num_words, tsort, while_changing, DATE_UNITS,
108};
109#[cfg(feature = "semantic")]
110pub use lineage::{
111    lineage, lineage_at, lineage_at_with_schema, lineage_with_schema, output_columns,
112    output_columns_with_schema, LineageNode, OutputColumn, QueryOutput, SetBranch, SetOperator,
113};
114#[cfg(feature = "semantic")]
115pub use optimizer::{
116    annotate_types, qualify_tables, QualifyTablesOptions, TypeAnnotator, TypeCoercionClass,
117};
118pub use parser::Parser;
119#[cfg(all(feature = "semantic", feature = "generate"))]
120pub use query_analysis::{
121    analyze_query, AnalyzeQueryOptions, ColumnReferenceFact, ColumnUseContext, ColumnUseFact,
122    ColumnUseReferenceFact, CteFact, ProjectionFact, ProjectionNullability, QueryAnalysis,
123    QueryShape, QuerySourceSpan, ReferenceConfidence, RelationFact, SetOperationBranchFact,
124    SetOperationBranchRole, SetOperationFact, StarProjectionFact, TransformFunctionFact,
125    TransformKind,
126};
127#[cfg(feature = "semantic")]
128pub use resolver::{is_column_ambiguous, resolve_column, Resolver, ResolverError, ResolverResult};
129#[cfg(feature = "semantic")]
130pub use schema::{
131    ensure_schema, from_simple_map, normalize_name, MappingSchema, Schema, SchemaError,
132};
133#[cfg(feature = "semantic")]
134pub use scope::{
135    build_scope, find_all_in_scope, find_in_scope, traverse_scope, walk_in_scope, ColumnRef, Scope,
136    ScopeType, SourceInfo,
137};
138#[cfg(feature = "time")]
139pub use time::{format_time, is_valid_timezone, subsecond_precision, TIMEZONES};
140pub use tokens::{Token, TokenType, Tokenizer};
141#[cfg(feature = "ast-tools")]
142pub use traversal::{
143    contains_aggregate,
144    contains_subquery,
145    contains_window_function,
146    find_ancestor,
147    find_parent,
148    get_all_tables,
149    get_columns,
150    get_merge_source,
151    get_merge_target,
152    get_tables,
153    is_add,
154    is_aggregate,
155    is_alias,
156    is_alter_table,
157    is_and,
158    is_arithmetic,
159    is_avg,
160    is_between,
161    is_boolean,
162    is_case,
163    is_cast,
164    is_coalesce,
165    is_column,
166    is_comparison,
167    is_concat,
168    is_count,
169    is_create_index,
170    is_create_table,
171    is_create_view,
172    is_cte,
173    is_ddl,
174    is_delete,
175    is_div,
176    is_drop_index,
177    is_drop_table,
178    is_drop_view,
179    is_eq,
180    is_except,
181    is_exists,
182    is_from,
183    is_function,
184    is_group_by,
185    is_gt,
186    is_gte,
187    is_having,
188    is_identifier,
189    is_ilike,
190    is_in,
191    // Extended type predicates
192    is_insert,
193    is_intersect,
194    is_is_null,
195    is_join,
196    is_like,
197    is_limit,
198    is_literal,
199    is_logical,
200    is_lt,
201    is_lte,
202    is_max_func,
203    is_merge,
204    is_min_func,
205    is_mod,
206    is_mul,
207    is_neq,
208    is_not,
209    is_null_if,
210    is_null_literal,
211    is_offset,
212    is_or,
213    is_order_by,
214    is_ordered,
215    is_paren,
216    // Composite predicates
217    is_query,
218    is_safe_cast,
219    is_select,
220    is_set_operation,
221    is_star,
222    is_sub,
223    is_subquery,
224    is_sum,
225    is_table,
226    is_try_cast,
227    is_union,
228    is_update,
229    is_where,
230    is_window_function,
231    is_with,
232    transform,
233    transform_all,
234    transform_map,
235    BfsIter,
236    DfsIter,
237    ExpressionWalk,
238    ParentInfo,
239    TreeContext,
240};
241#[cfg(any(feature = "semantic", feature = "time"))]
242pub use trie::{new_trie, new_trie_from_keys, Trie, TrieResult};
243#[cfg(feature = "semantic")]
244pub use validation::{
245    mapping_schema_from_validation_schema, mapping_schema_from_validation_schema_with_dialect,
246    validate_with_schema, SchemaColumn, SchemaColumnReference, SchemaForeignKey, SchemaTable,
247    SchemaTableReference, SchemaValidationOptions, ValidationSchema,
248};
249
250#[cfg(feature = "generate")]
251const DEFAULT_FORMAT_MAX_INPUT_BYTES: usize = 16 * 1024 * 1024; // 16 MiB
252#[cfg(feature = "generate")]
253const DEFAULT_FORMAT_MAX_TOKENS: usize = 1_000_000;
254#[cfg(feature = "generate")]
255const DEFAULT_FORMAT_MAX_AST_NODES: usize = 1_000_000;
256#[cfg(feature = "generate")]
257const DEFAULT_FORMAT_MAX_SET_OP_CHAIN: usize = 256;
258
259#[cfg(feature = "generate")]
260fn default_format_max_input_bytes() -> Option<usize> {
261    Some(DEFAULT_FORMAT_MAX_INPUT_BYTES)
262}
263
264#[cfg(feature = "generate")]
265fn default_format_max_tokens() -> Option<usize> {
266    Some(DEFAULT_FORMAT_MAX_TOKENS)
267}
268
269#[cfg(feature = "generate")]
270fn default_format_max_ast_nodes() -> Option<usize> {
271    Some(DEFAULT_FORMAT_MAX_AST_NODES)
272}
273
274#[cfg(feature = "generate")]
275fn default_format_max_set_op_chain() -> Option<usize> {
276    Some(DEFAULT_FORMAT_MAX_SET_OP_CHAIN)
277}
278
279/// Guard options for SQL pretty-formatting.
280///
281/// These limits protect against extremely large/complex queries that can cause
282/// high memory pressure in constrained runtimes (for example browser WASM).
283#[cfg(feature = "generate")]
284#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
285#[serde(rename_all = "camelCase")]
286pub struct FormatGuardOptions {
287    /// Maximum allowed SQL input size in bytes.
288    /// `None` disables this check.
289    #[serde(default = "default_format_max_input_bytes")]
290    pub max_input_bytes: Option<usize>,
291    /// Maximum allowed number of tokens after tokenization.
292    /// `None` disables this check.
293    #[serde(default = "default_format_max_tokens")]
294    pub max_tokens: Option<usize>,
295    /// Maximum allowed AST node count after parsing.
296    /// `None` disables this check.
297    #[serde(default = "default_format_max_ast_nodes")]
298    pub max_ast_nodes: Option<usize>,
299    /// Maximum allowed count of set-operation operators (`UNION`/`INTERSECT`/`EXCEPT`)
300    /// observed in a statement before parsing.
301    ///
302    /// `None` disables this check.
303    #[serde(default = "default_format_max_set_op_chain")]
304    pub max_set_op_chain: Option<usize>,
305}
306
307#[cfg(feature = "generate")]
308impl Default for FormatGuardOptions {
309    fn default() -> Self {
310        Self {
311            max_input_bytes: default_format_max_input_bytes(),
312            max_tokens: default_format_max_tokens(),
313            max_ast_nodes: default_format_max_ast_nodes(),
314            max_set_op_chain: default_format_max_set_op_chain(),
315        }
316    }
317}
318
319#[cfg(feature = "generate")]
320fn format_guard_error(code: &str, actual: usize, limit: usize) -> Error {
321    Error::generate(format!(
322        "{code}: value {actual} exceeds configured limit {limit}"
323    ))
324}
325
326#[cfg(feature = "generate")]
327fn enforce_input_guard(sql: &str, options: &FormatGuardOptions) -> Result<()> {
328    if let Some(max) = options.max_input_bytes {
329        let input_bytes = sql.len();
330        if input_bytes > max {
331            return Err(format_guard_error(
332                "E_GUARD_INPUT_TOO_LARGE",
333                input_bytes,
334                max,
335            ));
336        }
337    }
338    Ok(())
339}
340
341#[cfg(feature = "generate")]
342fn parse_with_token_guard(
343    sql: &str,
344    dialect: &Dialect,
345    options: &FormatGuardOptions,
346) -> Result<Vec<Expression>> {
347    let tokens = dialect.tokenize(sql)?;
348    if let Some(max) = options.max_tokens {
349        let token_count = tokens.len();
350        if token_count > max {
351            return Err(format_guard_error(
352                "E_GUARD_TOKEN_BUDGET_EXCEEDED",
353                token_count,
354                max,
355            ));
356        }
357    }
358    enforce_set_op_chain_guard(&tokens, options)?;
359
360    let complexity_guard = ComplexityGuardOptions {
361        max_input_bytes: options.max_input_bytes,
362        max_tokens: options.max_tokens,
363        max_ast_nodes: options.max_ast_nodes,
364        ..Default::default()
365    };
366    let config = crate::parser::ParserConfig {
367        dialect: Some(dialect.dialect_type()),
368        complexity_guard,
369        ..Default::default()
370    };
371    let mut parser = Parser::with_source(tokens, config, sql.to_string());
372    parser.parse()
373}
374
375#[cfg(feature = "generate")]
376fn is_trivia_token(token_type: TokenType) -> bool {
377    matches!(
378        token_type,
379        TokenType::Space | TokenType::Break | TokenType::LineComment | TokenType::BlockComment
380    )
381}
382
383#[cfg(feature = "generate")]
384fn next_significant_token(tokens: &[Token], start: usize) -> Option<&Token> {
385    tokens
386        .iter()
387        .skip(start)
388        .find(|token| !is_trivia_token(token.token_type))
389}
390
391#[cfg(feature = "generate")]
392fn is_set_operation_token(tokens: &[Token], idx: usize) -> bool {
393    let token = &tokens[idx];
394    match token.token_type {
395        TokenType::Union | TokenType::Intersect => true,
396        TokenType::Except => {
397            // MINUS is aliased to EXCEPT in the tokenizer, but in ClickHouse minus(...)
398            // is a function call rather than a set operation.
399            if token.text.eq_ignore_ascii_case("minus")
400                && matches!(
401                    next_significant_token(tokens, idx + 1).map(|t| t.token_type),
402                    Some(TokenType::LParen)
403                )
404            {
405                return false;
406            }
407            true
408        }
409        _ => false,
410    }
411}
412
413#[cfg(feature = "generate")]
414fn enforce_set_op_chain_guard(tokens: &[Token], options: &FormatGuardOptions) -> Result<()> {
415    let Some(max) = options.max_set_op_chain else {
416        return Ok(());
417    };
418
419    let mut set_op_count = 0usize;
420    for (idx, token) in tokens.iter().enumerate() {
421        if token.token_type == TokenType::Semicolon {
422            set_op_count = 0;
423            continue;
424        }
425
426        if is_set_operation_token(tokens, idx) {
427            set_op_count += 1;
428            if set_op_count > max {
429                return Err(format_guard_error(
430                    "E_GUARD_SET_OP_CHAIN_EXCEEDED",
431                    set_op_count,
432                    max,
433                ));
434            }
435        }
436    }
437
438    Ok(())
439}
440
441#[cfg(feature = "generate")]
442fn enforce_ast_guard(expressions: &[Expression], options: &FormatGuardOptions) -> Result<()> {
443    if let Some(max) = options.max_ast_nodes {
444        let ast_nodes: usize = expressions
445            .iter()
446            .map(crate::ast_transforms::node_count)
447            .sum();
448        if ast_nodes > max {
449            return Err(format_guard_error(
450                "E_GUARD_AST_BUDGET_EXCEEDED",
451                ast_nodes,
452                max,
453            ));
454        }
455    }
456    Ok(())
457}
458
459#[cfg(feature = "generate")]
460fn format_with_dialect(
461    sql: &str,
462    dialect: &Dialect,
463    options: &FormatGuardOptions,
464) -> Result<Vec<String>> {
465    enforce_input_guard(sql, options)?;
466    let expressions = parse_with_token_guard(sql, dialect, options)?;
467    enforce_ast_guard(&expressions, options)?;
468
469    expressions
470        .iter()
471        .map(|expr| dialect.generate_pretty(expr))
472        .collect()
473}
474
475/// Transpile SQL from one dialect to another.
476///
477/// # Arguments
478/// * `sql` - The SQL string to transpile
479/// * `read` - The source dialect to parse with
480/// * `write` - The target dialect to generate
481///
482/// # Returns
483/// A vector of transpiled SQL statements
484///
485/// # Example
486/// ```
487/// use polyglot_sql::{transpile, DialectType};
488///
489/// let result = transpile(
490///     "SELECT EPOCH_MS(1618088028295)",
491///     DialectType::DuckDB,
492///     DialectType::Hive
493/// );
494/// ```
495#[cfg(feature = "transpile")]
496pub fn transpile(sql: &str, read: DialectType, write: DialectType) -> Result<Vec<String>> {
497    // Delegate to Dialect::transpile so that the full cross-dialect rewrite
498    // pipeline (source+target-aware normalization in `cross_dialect_normalize`)
499    // runs here as well. This keeps Rust crate users on the same code path as
500    // the WASM/FFI/Python bindings and the playground.
501    Dialect::get(read).transpile(sql, write)
502}
503
504/// Parse SQL into an AST.
505///
506/// # Arguments
507/// * `sql` - The SQL string to parse
508/// * `dialect` - The dialect to use for parsing
509///
510/// # Returns
511/// A vector of parsed expressions
512pub fn parse(sql: &str, dialect: DialectType) -> Result<Vec<Expression>> {
513    let d = Dialect::get(dialect);
514    d.parse(sql)
515}
516
517/// Parse SQL with per-call complexity limits.
518pub fn parse_with_options(
519    sql: &str,
520    dialect: DialectType,
521    options: &ParseOptions,
522) -> Result<Vec<Expression>> {
523    Dialect::get(dialect).parse_with_options(sql, options)
524}
525
526/// Parse a single SQL statement.
527///
528/// # Arguments
529/// * `sql` - The SQL string containing a single statement
530/// * `dialect` - The dialect to use for parsing
531///
532/// # Returns
533/// The parsed expression, or an error if multiple statements found
534pub fn parse_one(sql: &str, dialect: DialectType) -> Result<Expression> {
535    parse_one_with_options(sql, dialect, &ParseOptions::default())
536}
537
538/// Parse exactly one SQL statement with per-call complexity limits.
539pub fn parse_one_with_options(
540    sql: &str,
541    dialect: DialectType,
542    options: &ParseOptions,
543) -> Result<Expression> {
544    let mut expressions = parse_with_options(sql, dialect, options)?;
545
546    if expressions.len() != 1 {
547        return Err(Error::parse(
548            format!("Expected 1 statement, found {}", expressions.len()),
549            0,
550            0,
551            0,
552            0,
553        ));
554    }
555
556    Ok(expressions.remove(0))
557}
558
559/// Parse a standalone SQL data type.
560///
561/// # Arguments
562/// * `sql` - The data type string to parse, e.g. `DECIMAL(10, 2)`
563/// * `dialect` - The dialect to use for parsing
564///
565/// # Returns
566/// The parsed data type
567pub fn parse_data_type(sql: &str, dialect: DialectType) -> Result<DataType> {
568    Dialect::get(dialect).parse_data_type(sql)
569}
570
571/// Parse a standalone SQL data type with per-call complexity limits.
572pub fn parse_data_type_with_options(
573    sql: &str,
574    dialect: DialectType,
575    options: &ParseOptions,
576) -> Result<DataType> {
577    Dialect::get(dialect).parse_data_type_with_options(sql, options)
578}
579
580/// Generate SQL from a standalone data type.
581///
582/// # Arguments
583/// * `data_type` - The data type to render
584/// * `dialect` - The target dialect
585///
586/// # Returns
587/// The generated type SQL string
588#[cfg(feature = "generate")]
589pub fn generate_data_type(data_type: &DataType, dialect: DialectType) -> Result<String> {
590    Dialect::get(dialect).generate(&Expression::DataType(data_type.clone()))
591}
592
593/// Generate SQL from an AST.
594///
595/// # Arguments
596/// * `expression` - The expression to generate SQL from
597/// * `dialect` - The target dialect
598///
599/// # Returns
600/// The generated SQL string
601#[cfg(feature = "generate")]
602pub fn generate(expression: &Expression, dialect: DialectType) -> Result<String> {
603    let d = Dialect::get(dialect);
604    d.generate(expression)
605}
606
607/// Format/pretty-print SQL statements.
608///
609/// Uses [`FormatGuardOptions::default`] guards.
610#[cfg(feature = "generate")]
611pub fn format(sql: &str, dialect: DialectType) -> Result<Vec<String>> {
612    format_with_options(sql, dialect, &FormatGuardOptions::default())
613}
614
615/// Format/pretty-print SQL statements with configurable guard limits.
616#[cfg(feature = "generate")]
617pub fn format_with_options(
618    sql: &str,
619    dialect: DialectType,
620    options: &FormatGuardOptions,
621) -> Result<Vec<String>> {
622    let d = Dialect::get(dialect);
623    format_with_dialect(sql, &d, options)
624}
625
626/// Validate SQL syntax.
627///
628/// # Arguments
629/// * `sql` - The SQL string to validate
630/// * `dialect` - The dialect to use for validation
631///
632/// # Returns
633/// A validation result with any errors found
634#[cfg(feature = "semantic")]
635pub fn validate(sql: &str, dialect: DialectType) -> ValidationResult {
636    validate_with_options(sql, dialect, &ValidationOptions::default())
637}
638
639/// Options for syntax validation behavior.
640#[cfg(feature = "semantic")]
641#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
642#[serde(rename_all = "camelCase", deny_unknown_fields)]
643pub struct ValidationOptions {
644    /// Optional per-call parser limits; absence preserves dialect defaults.
645    #[serde(default)]
646    pub complexity_guard: Option<ComplexityGuardOptions>,
647    /// When enabled, validation rejects non-canonical trailing commas that the parser
648    /// would otherwise accept for compatibility (e.g. `SELECT a, FROM t`).
649    #[serde(default)]
650    pub strict_syntax: bool,
651    /// Enable semantic correctness errors and query-quality warnings.
652    #[serde(default)]
653    pub semantic: bool,
654}
655
656/// Validate SQL syntax and optional semantic correctness/quality checks.
657#[cfg(feature = "semantic")]
658pub fn validate_with_options(
659    sql: &str,
660    dialect: DialectType,
661    options: &ValidationOptions,
662) -> ValidationResult {
663    let d = Dialect::get(dialect);
664    validate_with_dialect(sql, &d, options)
665}
666
667/// Validate SQL using an already-resolved dialect.
668///
669/// This is useful for wrappers and custom dialect consumers that should retain
670/// the exact tokenizer and parser configuration of a [`Dialect`] handle.
671#[cfg(feature = "semantic")]
672pub fn validate_with_dialect(
673    sql: &str,
674    dialect: &Dialect,
675    options: &ValidationOptions,
676) -> ValidationResult {
677    match parse_for_validation(sql, dialect, options) {
678        Ok(expressions) => ValidationResult::with_errors(if options.semantic {
679            expressions
680                .iter()
681                .flat_map(|expression| {
682                    validation::check_semantics(expression, dialect.dialect_type(), None)
683                })
684                .collect()
685        } else {
686            Vec::new()
687        }),
688        Err(result) => result,
689    }
690}
691
692#[cfg(feature = "semantic")]
693pub(crate) fn parse_for_validation(
694    sql: &str,
695    dialect: &Dialect,
696    options: &ValidationOptions,
697) -> std::result::Result<Vec<Expression>, ValidationResult> {
698    match dialect.parse_with_options(
699        sql,
700        &ParseOptions {
701            complexity_guard: options.complexity_guard,
702        },
703    ) {
704        Ok(expressions) => {
705            // Reject bare expressions that aren't valid SQL statements.
706            // The parser accepts any expression at the top level, but bare identifiers,
707            // literals, function calls, etc. are not valid statements.
708            for expr in &expressions {
709                if !expr.is_statement() {
710                    let msg = format!("Invalid expression / Unexpected token");
711                    return Err(ValidationResult::with_errors(vec![ValidationError::error(
712                        msg, "E004",
713                    )]));
714                }
715            }
716            if options.strict_syntax {
717                if let Some(error) = strict_syntax_error(sql, dialect) {
718                    return Err(ValidationResult::with_errors(vec![error]));
719                }
720            }
721            Ok(expressions)
722        }
723        Err(e) => {
724            let error = match &e {
725                Error::Syntax {
726                    message,
727                    line,
728                    column,
729                    start,
730                    end,
731                } => ValidationError::error(message.clone(), "E001")
732                    .with_location(*line, *column)
733                    .with_span(Some(*start), Some(*end)),
734                Error::Tokenize {
735                    message,
736                    line,
737                    column,
738                    start,
739                    end,
740                } => ValidationError::error(message.clone(), "E002")
741                    .with_location(*line, *column)
742                    .with_span(Some(*start), Some(*end)),
743                Error::Parse {
744                    message,
745                    line,
746                    column,
747                    start,
748                    end,
749                } => ValidationError::error(message.clone(), "E003")
750                    .with_location(*line, *column)
751                    .with_span(Some(*start), Some(*end)),
752                _ => ValidationError::error(e.to_string(), "E000"),
753            };
754            Err(ValidationResult::with_errors(vec![error]))
755        }
756    }
757}
758
759#[cfg(feature = "semantic")]
760fn strict_syntax_error(sql: &str, dialect: &Dialect) -> Option<ValidationError> {
761    let tokens = dialect.tokenize(sql).ok()?;
762
763    for (idx, token) in tokens.iter().enumerate() {
764        if token.token_type != TokenType::Comma {
765            continue;
766        }
767
768        let next = tokens.get(idx + 1);
769        let (is_boundary, boundary_name) = match next.map(|t| t.token_type) {
770            Some(TokenType::From) => (true, "FROM"),
771            Some(TokenType::Where) => (true, "WHERE"),
772            Some(TokenType::GroupBy) => (true, "GROUP BY"),
773            Some(TokenType::Having) => (true, "HAVING"),
774            Some(TokenType::Order) | Some(TokenType::OrderBy) => (true, "ORDER BY"),
775            Some(TokenType::Limit) => (true, "LIMIT"),
776            Some(TokenType::Offset) => (true, "OFFSET"),
777            Some(TokenType::Union) => (true, "UNION"),
778            Some(TokenType::Intersect) => (true, "INTERSECT"),
779            Some(TokenType::Except) => (true, "EXCEPT"),
780            Some(TokenType::Qualify) => (true, "QUALIFY"),
781            Some(TokenType::Window) => (true, "WINDOW"),
782            Some(TokenType::Semicolon) | None => (true, "end of statement"),
783            _ => (false, ""),
784        };
785
786        if is_boundary {
787            let message = format!(
788                "Trailing comma before {} is not allowed in strict syntax mode",
789                boundary_name
790            );
791            return Some(
792                ValidationError::error(message, "E005")
793                    .with_location(token.span.line, token.span.column),
794            );
795        }
796    }
797
798    None
799}
800
801/// Transpile SQL from one dialect to another, using string dialect names.
802///
803/// This supports both built-in dialect names (e.g., "postgresql", "mysql") and
804/// custom dialects registered via [`CustomDialectBuilder`].
805///
806/// # Arguments
807/// * `sql` - The SQL string to transpile
808/// * `read` - The source dialect name
809/// * `write` - The target dialect name
810///
811/// # Returns
812/// A vector of transpiled SQL statements, or an error if a dialect name is unknown.
813#[cfg(feature = "transpile")]
814pub fn transpile_by_name(sql: &str, read: &str, write: &str) -> Result<Vec<String>> {
815    transpile_with_by_name(sql, read, write, &TranspileOptions::default())
816}
817
818/// Transpile SQL with configurable [`TranspileOptions`], using string dialect names.
819///
820/// Same as [`transpile_by_name`] but accepts options (e.g., pretty-printing).
821#[cfg(feature = "transpile")]
822pub fn transpile_with_by_name(
823    sql: &str,
824    read: &str,
825    write: &str,
826    opts: &TranspileOptions,
827) -> Result<Vec<String>> {
828    let read_dialect = Dialect::get_by_name(read)
829        .ok_or_else(|| Error::parse(format!("Unknown dialect: {}", read), 0, 0, 0, 0))?;
830    let write_dialect = Dialect::get_by_name(write)
831        .ok_or_else(|| Error::parse(format!("Unknown dialect: {}", write), 0, 0, 0, 0))?;
832    read_dialect.transpile_with(sql, &write_dialect, opts.clone())
833}
834
835/// Parse SQL into an AST using a string dialect name.
836///
837/// Supports both built-in and custom dialect names.
838pub fn parse_by_name(sql: &str, dialect: &str) -> Result<Vec<Expression>> {
839    let d = Dialect::get_by_name(dialect)
840        .ok_or_else(|| Error::parse(format!("Unknown dialect: {}", dialect), 0, 0, 0, 0))?;
841    d.parse(sql)
842}
843
844/// Generate SQL from an AST using a string dialect name.
845///
846/// Supports both built-in and custom dialect names.
847#[cfg(feature = "generate")]
848pub fn generate_by_name(expression: &Expression, dialect: &str) -> Result<String> {
849    let d = Dialect::get_by_name(dialect)
850        .ok_or_else(|| Error::parse(format!("Unknown dialect: {}", dialect), 0, 0, 0, 0))?;
851    d.generate(expression)
852}
853
854/// Format SQL using a string dialect name.
855///
856/// Uses [`FormatGuardOptions::default`] guards.
857#[cfg(feature = "generate")]
858pub fn format_by_name(sql: &str, dialect: &str) -> Result<Vec<String>> {
859    format_with_options_by_name(sql, dialect, &FormatGuardOptions::default())
860}
861
862/// Format SQL using a string dialect name with configurable guard limits.
863#[cfg(feature = "generate")]
864pub fn format_with_options_by_name(
865    sql: &str,
866    dialect: &str,
867    options: &FormatGuardOptions,
868) -> Result<Vec<String>> {
869    let d = Dialect::get_by_name(dialect)
870        .ok_or_else(|| Error::parse(format!("Unknown dialect: {}", dialect), 0, 0, 0, 0))?;
871    format_with_dialect(sql, &d, options)
872}
873
874#[cfg(test)]
875mod api_contract_tests {
876    use super::*;
877    use serde_json::Value;
878    use std::collections::{BTreeMap, BTreeSet};
879
880    macro_rules! exported_symbols {
881        ($($capability:literal => { $($name:literal => $symbol:expr),+ $(,)? }),+ $(,)?) => {{
882            let mut capabilities = BTreeMap::new();
883            $(
884                $(let _ = $symbol;)+
885                capabilities.insert(
886                    $capability,
887                    BTreeSet::from([$($name),+]),
888                );
889            )+
890            capabilities
891        }};
892    }
893
894    #[test]
895    fn public_api_matches_capability_contract() {
896        let Ok(path) = std::env::var("POLYGLOT_API_CONTRACT") else {
897            return;
898        };
899        let contract: Value = serde_json::from_str(
900            &std::fs::read_to_string(path).expect("read API capability contract"),
901        )
902        .expect("parse API capability contract");
903
904        let actual = exported_symbols! {
905            "dialects" => { "Dialect.get" => Dialect::get },
906            "transpile" => { "transpile" => transpile },
907            "parse" => { "parse" => parse, "parse_one" => parse_one, "parse_with_options" => parse_with_options, "parse_one_with_options" => parse_one_with_options },
908            "data_types" => { "parse_data_type" => parse_data_type, "parse_data_type_with_options" => parse_data_type_with_options, "generate_data_type" => generate_data_type },
909            "generate" => { "generate" => generate },
910            "format" => { "format" => format, "format_with_options" => format_with_options },
911            "validate" => {
912                "validate" => validate,
913                "validate_with_options" => validate_with_options,
914                "validate_with_dialect" => validate_with_dialect
915            },
916            "validate_schema" => { "validation.validate_with_schema" => validation::validate_with_schema },
917            "optimize" => { "optimizer.optimize" => optimizer::optimize },
918            "tokenize" => { "Tokenizer.tokenize" => Tokenizer::tokenize },
919            "annotate_types" => { "annotate_types" => annotate_types },
920            "diff" => { "diff.diff" => diff::diff },
921            "ast_transforms" => {
922                "rename_tables" => rename_tables,
923                "set_limit" => set_limit,
924                "set_offset" => set_offset,
925                "set_order_by" => set_order_by
926            },
927            "lineage" => {
928                "lineage.lineage" => lineage::lineage,
929                "lineage.lineage_at" => lineage::lineage_at,
930                "lineage.lineage_at_with_schema" => lineage::lineage_at_with_schema,
931                "lineage.lineage_with_schema" => lineage::lineage_with_schema,
932                "lineage.output_columns" => lineage::output_columns,
933                "lineage.output_columns_with_schema" => lineage::output_columns_with_schema,
934                "lineage.get_source_tables" => lineage::get_source_tables
935            },
936            "openlineage" => {
937                "openlineage.openlineage_column_lineage" => openlineage::openlineage_column_lineage,
938                "openlineage.openlineage_job_event" => openlineage::openlineage_job_event,
939                "openlineage.openlineage_run_event" => openlineage::openlineage_run_event
940            },
941            "analyze_query" => { "analyze_query" => analyze_query },
942            "planner" => { "planner.Plan.from_expression" => planner::Plan::from_expression },
943            "builders" => { "builder.col" => builder::col },
944            "visitors" => {
945                "traversal.transform" => traversal::transform::<fn(Expression) -> Result<Option<Expression>>>,
946                "traversal.get_columns" => traversal::get_columns
947            },
948        };
949
950        assert_layer_contract(&contract, "rust", &actual);
951    }
952
953    fn assert_layer_contract(
954        contract: &Value,
955        layer: &str,
956        actual: &BTreeMap<&str, BTreeSet<&str>>,
957    ) {
958        let capabilities = contract["capabilities"]
959            .as_array()
960            .expect("capabilities must be an array");
961        let mut declared_available = BTreeSet::new();
962
963        for capability in capabilities {
964            let id = capability["id"].as_str().expect("capability id");
965            let entry = &capability["layers"][layer];
966            let status = entry["status"].as_str().expect("capability status");
967            assert!(matches!(status, "supported" | "partial" | "unavailable"));
968            if status != "supported" {
969                assert!(entry["notes"].as_str().is_some_and(|note| !note.is_empty()));
970            }
971            if status == "unavailable" {
972                assert!(!actual.contains_key(id), "{id} is declared unavailable");
973                continue;
974            }
975
976            declared_available.insert(id);
977            let expected = entry["symbols"]
978                .as_array()
979                .expect("symbols must be an array")
980                .iter()
981                .map(|symbol| symbol.as_str().expect("symbol must be a string"))
982                .collect::<BTreeSet<_>>();
983            assert_eq!(actual.get(id), Some(&expected), "capability {id}");
984        }
985
986        assert_eq!(
987            actual.keys().copied().collect::<BTreeSet<_>>(),
988            declared_available
989        );
990    }
991}
992
993#[cfg(all(test, feature = "semantic"))]
994mod validation_tests {
995    use super::*;
996
997    #[test]
998    fn validate_is_permissive_by_default_for_trailing_commas() {
999        let result = validate("SELECT name, FROM employees", DialectType::Generic);
1000        assert!(result.valid, "Result: {:?}", result.errors);
1001    }
1002
1003    #[test]
1004    fn validate_with_options_rejects_trailing_comma_before_from() {
1005        let options = ValidationOptions {
1006            strict_syntax: true,
1007            ..Default::default()
1008        };
1009        let result = validate_with_options(
1010            "SELECT name, FROM employees",
1011            DialectType::Generic,
1012            &options,
1013        );
1014        assert!(!result.valid, "Result should be invalid");
1015        assert!(
1016            result.errors.iter().any(|e| e.code == "E005"),
1017            "Expected E005, got: {:?}",
1018            result.errors
1019        );
1020    }
1021
1022    #[test]
1023    fn validate_with_options_rejects_trailing_comma_before_where() {
1024        let options = ValidationOptions {
1025            strict_syntax: true,
1026            ..Default::default()
1027        };
1028        let result = validate_with_options(
1029            "SELECT name FROM employees, WHERE salary > 10",
1030            DialectType::Generic,
1031            &options,
1032        );
1033        assert!(!result.valid, "Result should be invalid");
1034        assert!(
1035            result.errors.iter().any(|e| e.code == "E005"),
1036            "Expected E005, got: {:?}",
1037            result.errors
1038        );
1039    }
1040
1041    #[test]
1042    fn validate_with_options_reports_semantic_warnings() {
1043        let options = ValidationOptions {
1044            semantic: true,
1045            ..Default::default()
1046        };
1047        let result = validate_with_options(
1048            "SELECT *, category, COUNT(*) FROM products LIMIT 10",
1049            DialectType::Generic,
1050            &options,
1051        );
1052
1053        assert!(!result.valid, "Semantic correctness errors invalidate SQL");
1054        assert!(result.errors.iter().any(|error| error.code == "W001"));
1055        assert!(result.errors.iter().any(|error| error.code == "E230"));
1056        assert!(result.errors.iter().any(|error| error.code == "W004"));
1057
1058        let median_result = validate_with_options(
1059            "SELECT category, MEDIAN(price) FROM products",
1060            DialectType::DuckDB,
1061            &options,
1062        );
1063
1064        assert!(
1065            !median_result.valid,
1066            "Semantic correctness errors invalidate SQL"
1067        );
1068        assert!(median_result
1069            .errors
1070            .iter()
1071            .any(|error| error.code == "E230"));
1072    }
1073
1074    #[test]
1075    fn validate_with_dialect_combines_strict_and_semantic_options() {
1076        let options = ValidationOptions {
1077            strict_syntax: true,
1078            semantic: true,
1079            ..Default::default()
1080        };
1081        let dialect = Dialect::get_by_name("generic").expect("generic dialect");
1082        let result = validate_with_dialect("SELECT *, FROM products", &dialect, &options);
1083
1084        assert!(!result.valid);
1085        assert_eq!(result.errors.len(), 1);
1086        assert_eq!(result.errors[0].code, "E005");
1087    }
1088}
1089
1090#[cfg(all(test, feature = "generate"))]
1091mod format_tests {
1092    use super::*;
1093
1094    #[test]
1095    fn format_basic_query() {
1096        let result = format("SELECT a,b FROM t", DialectType::Generic).expect("format failed");
1097        assert_eq!(result.len(), 1);
1098        assert!(result[0].contains('\n'));
1099    }
1100
1101    #[test]
1102    fn format_preserves_explicit_null_ordering() {
1103        // Formatting must not drop clauses merely because they match a dialect
1104        // default, or add clauses when the input relies on the server settings.
1105        for name in [
1106            "generic",
1107            "snowflake",
1108            "duckdb",
1109            "postgres",
1110            "dremio",
1111            "bigquery",
1112        ] {
1113            let Some(dialect) = Dialect::get_by_name(name) else {
1114                continue; // Dialect may be disabled in feature-gate test builds.
1115            };
1116            for ordering in [
1117                "x",
1118                "x DESC",
1119                "x ASC",
1120                "x NULLS FIRST",
1121                "x NULLS LAST",
1122                "x ASC NULLS FIRST",
1123                "x ASC NULLS LAST",
1124                "x DESC NULLS FIRST",
1125                "x DESC NULLS LAST",
1126            ] {
1127                for sql in [
1128                    std::format!("SELECT x FROM t ORDER BY {ordering}"),
1129                    std::format!("SELECT ROW_NUMBER() OVER (ORDER BY {ordering}) FROM t"),
1130                    std::format!("SELECT ARRAY_AGG(x) WITHIN GROUP (ORDER BY {ordering}) FROM t"),
1131                ] {
1132                    let formatted =
1133                        format_with_dialect(&sql, &dialect, &Default::default()).unwrap();
1134                    assert_eq!(
1135                        formatted[0]
1136                            .split_whitespace()
1137                            .collect::<Vec<_>>()
1138                            .join(" "),
1139                        sql,
1140                        "{name}: {sql}"
1141                    );
1142                    assert_eq!(
1143                        format_with_dialect(&formatted[0], &dialect, &Default::default()).unwrap(),
1144                        formatted,
1145                        "formatting must be idempotent: {name}: {sql}"
1146                    );
1147                }
1148            }
1149        }
1150    }
1151
1152    #[test]
1153    fn format_guard_rejects_large_input() {
1154        let options = FormatGuardOptions {
1155            max_input_bytes: Some(7),
1156            max_tokens: None,
1157            max_ast_nodes: None,
1158            max_set_op_chain: None,
1159        };
1160        let err = format_with_options("SELECT 1", DialectType::Generic, &options)
1161            .expect_err("expected guard error");
1162        assert!(err.to_string().contains("E_GUARD_INPUT_TOO_LARGE"));
1163    }
1164
1165    #[test]
1166    fn format_guard_rejects_token_budget() {
1167        let options = FormatGuardOptions {
1168            max_input_bytes: None,
1169            max_tokens: Some(1),
1170            max_ast_nodes: None,
1171            max_set_op_chain: None,
1172        };
1173        let err = format_with_options("SELECT 1", DialectType::Generic, &options)
1174            .expect_err("expected guard error");
1175        assert!(err.to_string().contains("E_GUARD_TOKEN_BUDGET_EXCEEDED"));
1176    }
1177
1178    #[test]
1179    fn format_guard_rejects_ast_budget() {
1180        let options = FormatGuardOptions {
1181            max_input_bytes: None,
1182            max_tokens: None,
1183            max_ast_nodes: Some(1),
1184            max_set_op_chain: None,
1185        };
1186        let err = format_with_options("SELECT 1", DialectType::Generic, &options)
1187            .expect_err("expected guard error");
1188        assert!(err.to_string().contains("E_GUARD_AST_BUDGET_EXCEEDED"));
1189    }
1190
1191    #[test]
1192    fn format_guard_rejects_set_op_chain_budget() {
1193        let options = FormatGuardOptions {
1194            max_input_bytes: None,
1195            max_tokens: None,
1196            max_ast_nodes: None,
1197            max_set_op_chain: Some(1),
1198        };
1199        let err = format_with_options(
1200            "SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3",
1201            DialectType::Generic,
1202            &options,
1203        )
1204        .expect_err("expected guard error");
1205        assert!(err.to_string().contains("E_GUARD_SET_OP_CHAIN_EXCEEDED"));
1206    }
1207
1208    #[test]
1209    fn format_guard_does_not_treat_clickhouse_minus_function_as_set_op() {
1210        let options = FormatGuardOptions {
1211            max_input_bytes: None,
1212            max_tokens: None,
1213            max_ast_nodes: None,
1214            max_set_op_chain: Some(0),
1215        };
1216        let result = format_with_options("SELECT minus(3, 2)", DialectType::ClickHouse, &options);
1217        assert!(result.is_ok(), "Result: {:?}", result);
1218    }
1219
1220    #[test]
1221    fn issue57_invalid_ternary_returns_error() {
1222        // https://github.com/tobilg/polyglot/issues/57
1223        // Invalid SQL with ternary operator should return an error, not garbled output.
1224        let sql = "SELECT x > 0 ? 1 : 0 FROM t";
1225
1226        let parse_result = parse(sql, DialectType::PostgreSQL);
1227        assert!(
1228            parse_result.is_err(),
1229            "Expected parse error for invalid ternary SQL, got: {:?}",
1230            parse_result
1231        );
1232
1233        let format_result = format(sql, DialectType::PostgreSQL);
1234        assert!(
1235            format_result.is_err(),
1236            "Expected format error for invalid ternary SQL, got: {:?}",
1237            format_result
1238        );
1239
1240        let transpile_result = transpile(sql, DialectType::PostgreSQL, DialectType::PostgreSQL);
1241        assert!(
1242            transpile_result.is_err(),
1243            "Expected transpile error for invalid ternary SQL, got: {:?}",
1244            transpile_result
1245        );
1246    }
1247
1248    /// Regression guard: `lib::transpile()` must apply the full cross-dialect
1249    /// rewrite pipeline (same as `Dialect::transpile()`). If these two paths
1250    /// diverge again, Rust crate users silently get under-transformed SQL that
1251    /// differs from what WASM/FFI/Python bindings produce.
1252    #[test]
1253    fn transpile_applies_cross_dialect_rewrites() {
1254        // DuckDB to_timestamp → Trino FROM_UNIXTIME (different input semantics).
1255        let out = transpile(
1256            "SELECT to_timestamp(col) FROM t",
1257            DialectType::DuckDB,
1258            DialectType::Trino,
1259        )
1260        .expect("transpile failed");
1261        assert_eq!(out[0], "SELECT FROM_UNIXTIME(col) FROM t");
1262
1263        // DuckDB CAST(x AS JSON) → Trino JSON_PARSE(x) (different CAST semantics).
1264        let out = transpile(
1265            "SELECT CAST(col AS JSON) FROM t",
1266            DialectType::DuckDB,
1267            DialectType::Trino,
1268        )
1269        .expect("transpile failed");
1270        assert_eq!(out[0], "SELECT JSON_PARSE(col) FROM t");
1271    }
1272
1273    /// Regression guard: all three transpile entry points (lib::transpile,
1274    /// lib::transpile_by_name, Dialect::transpile) must produce identical
1275    /// output. transpile_by_name is the one used by Python and C FFI bindings.
1276    #[test]
1277    fn transpile_matches_dialect_method() {
1278        let cases: &[(DialectType, DialectType, &str, &str, &str)] = &[
1279            (
1280                DialectType::DuckDB,
1281                DialectType::Trino,
1282                "duckdb",
1283                "trino",
1284                "SELECT to_timestamp(col) FROM t",
1285            ),
1286            (
1287                DialectType::DuckDB,
1288                DialectType::Trino,
1289                "duckdb",
1290                "trino",
1291                "SELECT CAST(col AS JSON) FROM t",
1292            ),
1293            (
1294                DialectType::DuckDB,
1295                DialectType::Trino,
1296                "duckdb",
1297                "trino",
1298                "SELECT json_valid(col) FROM t",
1299            ),
1300            (
1301                DialectType::Snowflake,
1302                DialectType::DuckDB,
1303                "snowflake",
1304                "duckdb",
1305                "SELECT DATEDIFF(day, a, b) FROM t",
1306            ),
1307            (
1308                DialectType::BigQuery,
1309                DialectType::DuckDB,
1310                "bigquery",
1311                "duckdb",
1312                "SELECT DATE_DIFF(a, b, DAY) FROM t",
1313            ),
1314            (
1315                DialectType::Generic,
1316                DialectType::Generic,
1317                "generic",
1318                "generic",
1319                "SELECT 1",
1320            ),
1321        ];
1322        for (read, write, read_name, write_name, sql) in cases {
1323            let via_lib = transpile(sql, *read, *write).expect("lib::transpile failed");
1324            let via_name = transpile_by_name(sql, read_name, write_name)
1325                .expect("lib::transpile_by_name failed");
1326            let via_dialect = Dialect::get(*read)
1327                .transpile(sql, *write)
1328                .expect("Dialect::transpile failed");
1329            assert_eq!(
1330                via_lib, via_dialect,
1331                "lib::transpile / Dialect::transpile diverged for {:?} -> {:?}: {sql}",
1332                read, write
1333            );
1334            assert_eq!(
1335                via_name, via_dialect,
1336                "lib::transpile_by_name / Dialect::transpile diverged for {read_name} -> {write_name}: {sql}"
1337            );
1338        }
1339    }
1340
1341    #[test]
1342    fn format_default_guard_rejects_deep_union_chain_before_parse() {
1343        let base = "SELECT col0, col1 FROM t";
1344        let mut sql = base.to_string();
1345        for _ in 0..1100 {
1346            sql.push_str(" UNION ALL ");
1347            sql.push_str(base);
1348        }
1349
1350        let err = format(&sql, DialectType::Athena).expect_err("expected guard error");
1351        assert!(err.to_string().contains("E_GUARD_SET_OP_CHAIN_EXCEEDED"));
1352    }
1353}