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