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