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