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