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