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