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