Skip to main content

polyglot_sql/
lib.rs

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