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