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