1pub mod ast_json;
16#[cfg(any(feature = "ast-tools", feature = "generate", feature = "semantic"))]
17pub mod ast_transforms;
18#[cfg(feature = "builder")]
19pub mod builder;
20pub mod dialects;
21#[cfg(feature = "diff")]
22pub mod diff;
23pub mod error;
24pub mod expressions;
25#[cfg(feature = "semantic")]
26pub mod function_catalog;
27mod function_registry;
28#[cfg(feature = "generate")]
29pub mod generator;
30pub mod guard;
31#[cfg(feature = "semantic")]
32pub mod helper;
33#[cfg(feature = "semantic")]
34pub mod lineage;
35#[cfg(feature = "openlineage")]
36pub mod openlineage;
37#[cfg(feature = "semantic")]
38pub mod optimizer;
39pub mod parser;
40#[cfg(feature = "planner")]
41pub mod planner;
42#[cfg(all(feature = "semantic", feature = "generate"))]
43pub mod query_analysis;
44#[cfg(feature = "semantic")]
45pub mod resolver;
46#[cfg(feature = "semantic")]
47pub mod schema;
48#[cfg(feature = "semantic")]
49pub mod scope;
50#[cfg(feature = "time")]
51pub mod time;
52pub mod tokens;
53#[cfg(feature = "transpile")]
54pub mod transforms;
55#[cfg(any(feature = "ast-tools", feature = "generate", feature = "semantic"))]
56pub mod traversal;
57#[cfg(not(any(feature = "ast-tools", feature = "generate", feature = "semantic")))]
58mod traversal;
59#[cfg(any(feature = "semantic", feature = "time"))]
60pub mod trie;
61#[cfg(feature = "semantic")]
62pub mod validation;
63
64#[cfg(any(feature = "generate", feature = "semantic"))]
65use serde::{Deserialize, Serialize};
66
67#[cfg(feature = "ast-tools")]
68pub use ast_transforms::{
69 add_select_columns, add_where, get_aggregate_functions, get_column_names, get_functions,
70 get_identifiers, get_literals, get_output_column_names, get_subqueries, get_table_names,
71 get_window_functions, node_count, qualify_columns, remove_limit_offset, remove_nodes,
72 remove_select_columns, remove_where, rename_columns, rename_tables, rename_tables_with_options,
73 replace_by_type, replace_nodes, set_distinct, set_limit, set_limit_expr, set_offset,
74 set_offset_expr, set_order_by, RenameTablesOptions,
75};
76pub use dialects::{unregister_custom_dialect, CustomDialectBuilder, Dialect, DialectType};
77#[cfg(feature = "transpile")]
78pub use dialects::{TranspileOptions, TranspileTarget};
79pub use error::{Error, Result};
80#[cfg(feature = "semantic")]
81pub use error::{ValidationError, ValidationResult, ValidationSeverity};
82pub use expressions::{DataType, Expression};
83#[cfg(feature = "semantic")]
84pub use function_catalog::{
85 FunctionCatalog, FunctionNameCase, FunctionSignature, HashMapFunctionCatalog,
86};
87#[cfg(feature = "generate")]
88pub use generator::{Generator, UnsupportedLevel};
89pub use guard::ComplexityGuardOptions;
90#[cfg(feature = "semantic")]
91pub use helper::{
92 csv, find_new_name, is_date_unit, is_float, is_int, is_iso_date, is_iso_datetime, merge_ranges,
93 name_sequence, seq_get, split_num_words, tsort, while_changing, DATE_UNITS,
94};
95#[cfg(feature = "semantic")]
96pub use optimizer::{
97 annotate_types, qualify_tables, QualifyTablesOptions, TypeAnnotator, TypeCoercionClass,
98};
99pub use parser::Parser;
100#[cfg(all(feature = "semantic", feature = "generate"))]
101pub use query_analysis::{
102 analyze_query, AnalyzeQueryOptions, ColumnReferenceFact, CteFact, ProjectionFact,
103 ProjectionNullability, QueryAnalysis, QueryShape, ReferenceConfidence, RelationFact,
104 SetOperationBranchFact, SetOperationFact, StarProjectionFact, TransformFunctionFact,
105 TransformKind,
106};
107#[cfg(feature = "semantic")]
108pub use resolver::{is_column_ambiguous, resolve_column, Resolver, ResolverError, ResolverResult};
109#[cfg(feature = "semantic")]
110pub use schema::{
111 ensure_schema, from_simple_map, normalize_name, MappingSchema, Schema, SchemaError,
112};
113#[cfg(feature = "semantic")]
114pub use scope::{
115 build_scope, find_all_in_scope, find_in_scope, traverse_scope, walk_in_scope, ColumnRef, Scope,
116 ScopeType, SourceInfo,
117};
118#[cfg(feature = "time")]
119pub use time::{format_time, is_valid_timezone, subsecond_precision, TIMEZONES};
120pub use tokens::{Token, TokenType, Tokenizer};
121#[cfg(feature = "ast-tools")]
122pub use traversal::{
123 contains_aggregate,
124 contains_subquery,
125 contains_window_function,
126 find_ancestor,
127 find_parent,
128 get_all_tables,
129 get_columns,
130 get_merge_source,
131 get_merge_target,
132 get_tables,
133 is_add,
134 is_aggregate,
135 is_alias,
136 is_alter_table,
137 is_and,
138 is_arithmetic,
139 is_avg,
140 is_between,
141 is_boolean,
142 is_case,
143 is_cast,
144 is_coalesce,
145 is_column,
146 is_comparison,
147 is_concat,
148 is_count,
149 is_create_index,
150 is_create_table,
151 is_create_view,
152 is_cte,
153 is_ddl,
154 is_delete,
155 is_div,
156 is_drop_index,
157 is_drop_table,
158 is_drop_view,
159 is_eq,
160 is_except,
161 is_exists,
162 is_from,
163 is_function,
164 is_group_by,
165 is_gt,
166 is_gte,
167 is_having,
168 is_identifier,
169 is_ilike,
170 is_in,
171 is_insert,
173 is_intersect,
174 is_is_null,
175 is_join,
176 is_like,
177 is_limit,
178 is_literal,
179 is_logical,
180 is_lt,
181 is_lte,
182 is_max_func,
183 is_merge,
184 is_min_func,
185 is_mod,
186 is_mul,
187 is_neq,
188 is_not,
189 is_null_if,
190 is_null_literal,
191 is_offset,
192 is_or,
193 is_order_by,
194 is_ordered,
195 is_paren,
196 is_query,
198 is_safe_cast,
199 is_select,
200 is_set_operation,
201 is_star,
202 is_sub,
203 is_subquery,
204 is_sum,
205 is_table,
206 is_try_cast,
207 is_union,
208 is_update,
209 is_where,
210 is_window_function,
211 is_with,
212 transform,
213 transform_map,
214 BfsIter,
215 DfsIter,
216 ExpressionWalk,
217 ParentInfo,
218 TreeContext,
219};
220#[cfg(any(feature = "semantic", feature = "time"))]
221pub use trie::{new_trie, new_trie_from_keys, Trie, TrieResult};
222#[cfg(feature = "semantic")]
223pub use validation::{
224 mapping_schema_from_validation_schema, validate_with_schema, SchemaColumn,
225 SchemaColumnReference, SchemaForeignKey, SchemaTable, SchemaTableReference,
226 SchemaValidationOptions, ValidationSchema,
227};
228
229#[cfg(feature = "generate")]
230const DEFAULT_FORMAT_MAX_INPUT_BYTES: usize = 16 * 1024 * 1024; #[cfg(feature = "generate")]
232const DEFAULT_FORMAT_MAX_TOKENS: usize = 1_000_000;
233#[cfg(feature = "generate")]
234const DEFAULT_FORMAT_MAX_AST_NODES: usize = 1_000_000;
235#[cfg(feature = "generate")]
236const DEFAULT_FORMAT_MAX_SET_OP_CHAIN: usize = 256;
237
238#[cfg(feature = "generate")]
239fn default_format_max_input_bytes() -> Option<usize> {
240 Some(DEFAULT_FORMAT_MAX_INPUT_BYTES)
241}
242
243#[cfg(feature = "generate")]
244fn default_format_max_tokens() -> Option<usize> {
245 Some(DEFAULT_FORMAT_MAX_TOKENS)
246}
247
248#[cfg(feature = "generate")]
249fn default_format_max_ast_nodes() -> Option<usize> {
250 Some(DEFAULT_FORMAT_MAX_AST_NODES)
251}
252
253#[cfg(feature = "generate")]
254fn default_format_max_set_op_chain() -> Option<usize> {
255 Some(DEFAULT_FORMAT_MAX_SET_OP_CHAIN)
256}
257
258#[cfg(feature = "generate")]
263#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
264#[serde(rename_all = "camelCase")]
265pub struct FormatGuardOptions {
266 #[serde(default = "default_format_max_input_bytes")]
269 pub max_input_bytes: Option<usize>,
270 #[serde(default = "default_format_max_tokens")]
273 pub max_tokens: Option<usize>,
274 #[serde(default = "default_format_max_ast_nodes")]
277 pub max_ast_nodes: Option<usize>,
278 #[serde(default = "default_format_max_set_op_chain")]
283 pub max_set_op_chain: Option<usize>,
284}
285
286#[cfg(feature = "generate")]
287impl Default for FormatGuardOptions {
288 fn default() -> Self {
289 Self {
290 max_input_bytes: default_format_max_input_bytes(),
291 max_tokens: default_format_max_tokens(),
292 max_ast_nodes: default_format_max_ast_nodes(),
293 max_set_op_chain: default_format_max_set_op_chain(),
294 }
295 }
296}
297
298#[cfg(feature = "generate")]
299fn format_guard_error(code: &str, actual: usize, limit: usize) -> Error {
300 Error::generate(format!(
301 "{code}: value {actual} exceeds configured limit {limit}"
302 ))
303}
304
305#[cfg(feature = "generate")]
306fn enforce_input_guard(sql: &str, options: &FormatGuardOptions) -> Result<()> {
307 if let Some(max) = options.max_input_bytes {
308 let input_bytes = sql.len();
309 if input_bytes > max {
310 return Err(format_guard_error(
311 "E_GUARD_INPUT_TOO_LARGE",
312 input_bytes,
313 max,
314 ));
315 }
316 }
317 Ok(())
318}
319
320#[cfg(feature = "generate")]
321fn parse_with_token_guard(
322 sql: &str,
323 dialect: &Dialect,
324 options: &FormatGuardOptions,
325) -> Result<Vec<Expression>> {
326 let tokens = dialect.tokenize(sql)?;
327 if let Some(max) = options.max_tokens {
328 let token_count = tokens.len();
329 if token_count > max {
330 return Err(format_guard_error(
331 "E_GUARD_TOKEN_BUDGET_EXCEEDED",
332 token_count,
333 max,
334 ));
335 }
336 }
337 enforce_set_op_chain_guard(&tokens, options)?;
338
339 let complexity_guard = ComplexityGuardOptions {
340 max_input_bytes: options.max_input_bytes,
341 max_tokens: options.max_tokens,
342 max_ast_nodes: options.max_ast_nodes,
343 ..Default::default()
344 };
345 let config = crate::parser::ParserConfig {
346 dialect: Some(dialect.dialect_type()),
347 complexity_guard,
348 ..Default::default()
349 };
350 let mut parser = Parser::with_source(tokens, config, sql.to_string());
351 parser.parse()
352}
353
354#[cfg(feature = "generate")]
355fn is_trivia_token(token_type: TokenType) -> bool {
356 matches!(
357 token_type,
358 TokenType::Space | TokenType::Break | TokenType::LineComment | TokenType::BlockComment
359 )
360}
361
362#[cfg(feature = "generate")]
363fn next_significant_token(tokens: &[Token], start: usize) -> Option<&Token> {
364 tokens
365 .iter()
366 .skip(start)
367 .find(|token| !is_trivia_token(token.token_type))
368}
369
370#[cfg(feature = "generate")]
371fn is_set_operation_token(tokens: &[Token], idx: usize) -> bool {
372 let token = &tokens[idx];
373 match token.token_type {
374 TokenType::Union | TokenType::Intersect => true,
375 TokenType::Except => {
376 if token.text.eq_ignore_ascii_case("minus")
379 && matches!(
380 next_significant_token(tokens, idx + 1).map(|t| t.token_type),
381 Some(TokenType::LParen)
382 )
383 {
384 return false;
385 }
386 true
387 }
388 _ => false,
389 }
390}
391
392#[cfg(feature = "generate")]
393fn enforce_set_op_chain_guard(tokens: &[Token], options: &FormatGuardOptions) -> Result<()> {
394 let Some(max) = options.max_set_op_chain else {
395 return Ok(());
396 };
397
398 let mut set_op_count = 0usize;
399 for (idx, token) in tokens.iter().enumerate() {
400 if token.token_type == TokenType::Semicolon {
401 set_op_count = 0;
402 continue;
403 }
404
405 if is_set_operation_token(tokens, idx) {
406 set_op_count += 1;
407 if set_op_count > max {
408 return Err(format_guard_error(
409 "E_GUARD_SET_OP_CHAIN_EXCEEDED",
410 set_op_count,
411 max,
412 ));
413 }
414 }
415 }
416
417 Ok(())
418}
419
420#[cfg(feature = "generate")]
421fn enforce_ast_guard(expressions: &[Expression], options: &FormatGuardOptions) -> Result<()> {
422 if let Some(max) = options.max_ast_nodes {
423 let ast_nodes: usize = expressions
424 .iter()
425 .map(crate::ast_transforms::node_count)
426 .sum();
427 if ast_nodes > max {
428 return Err(format_guard_error(
429 "E_GUARD_AST_BUDGET_EXCEEDED",
430 ast_nodes,
431 max,
432 ));
433 }
434 }
435 Ok(())
436}
437
438#[cfg(feature = "generate")]
439fn format_with_dialect(
440 sql: &str,
441 dialect: &Dialect,
442 options: &FormatGuardOptions,
443) -> Result<Vec<String>> {
444 enforce_input_guard(sql, options)?;
445 let expressions = parse_with_token_guard(sql, dialect, options)?;
446 enforce_ast_guard(&expressions, options)?;
447
448 expressions
449 .iter()
450 .map(|expr| dialect.generate_pretty(expr))
451 .collect()
452}
453
454#[cfg(feature = "transpile")]
475pub fn transpile(sql: &str, read: DialectType, write: DialectType) -> Result<Vec<String>> {
476 Dialect::get(read).transpile(sql, write)
481}
482
483pub fn parse(sql: &str, dialect: DialectType) -> Result<Vec<Expression>> {
492 let d = Dialect::get(dialect);
493 d.parse(sql)
494}
495
496pub fn parse_one(sql: &str, dialect: DialectType) -> Result<Expression> {
505 let mut expressions = parse(sql, dialect)?;
506
507 if expressions.len() != 1 {
508 return Err(Error::parse(
509 format!("Expected 1 statement, found {}", expressions.len()),
510 0,
511 0,
512 0,
513 0,
514 ));
515 }
516
517 Ok(expressions.remove(0))
518}
519
520pub fn parse_data_type(sql: &str, dialect: DialectType) -> Result<DataType> {
529 Dialect::get(dialect).parse_data_type(sql)
530}
531
532#[cfg(feature = "generate")]
541pub fn generate_data_type(data_type: &DataType, dialect: DialectType) -> Result<String> {
542 Dialect::get(dialect).generate(&Expression::DataType(data_type.clone()))
543}
544
545#[cfg(feature = "generate")]
554pub fn generate(expression: &Expression, dialect: DialectType) -> Result<String> {
555 let d = Dialect::get(dialect);
556 d.generate(expression)
557}
558
559#[cfg(feature = "generate")]
563pub fn format(sql: &str, dialect: DialectType) -> Result<Vec<String>> {
564 format_with_options(sql, dialect, &FormatGuardOptions::default())
565}
566
567#[cfg(feature = "generate")]
569pub fn format_with_options(
570 sql: &str,
571 dialect: DialectType,
572 options: &FormatGuardOptions,
573) -> Result<Vec<String>> {
574 let d = Dialect::get(dialect);
575 format_with_dialect(sql, &d, options)
576}
577
578#[cfg(feature = "semantic")]
587pub fn validate(sql: &str, dialect: DialectType) -> ValidationResult {
588 validate_with_options(sql, dialect, &ValidationOptions::default())
589}
590
591#[cfg(feature = "semantic")]
593#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
594#[serde(rename_all = "camelCase")]
595pub struct ValidationOptions {
596 #[serde(default)]
599 pub strict_syntax: bool,
600}
601
602#[cfg(feature = "semantic")]
604pub fn validate_with_options(
605 sql: &str,
606 dialect: DialectType,
607 options: &ValidationOptions,
608) -> ValidationResult {
609 let d = Dialect::get(dialect);
610 match d.parse(sql) {
611 Ok(expressions) => {
612 for expr in &expressions {
616 if !expr.is_statement() {
617 let msg = format!("Invalid expression / Unexpected token");
618 return ValidationResult::with_errors(vec![ValidationError::error(
619 msg, "E004",
620 )]);
621 }
622 }
623 if options.strict_syntax {
624 if let Some(error) = strict_syntax_error(sql, &d) {
625 return ValidationResult::with_errors(vec![error]);
626 }
627 }
628 ValidationResult::success()
629 }
630 Err(e) => {
631 let error = match &e {
632 Error::Syntax {
633 message,
634 line,
635 column,
636 start,
637 end,
638 } => ValidationError::error(message.clone(), "E001")
639 .with_location(*line, *column)
640 .with_span(Some(*start), Some(*end)),
641 Error::Tokenize {
642 message,
643 line,
644 column,
645 start,
646 end,
647 } => ValidationError::error(message.clone(), "E002")
648 .with_location(*line, *column)
649 .with_span(Some(*start), Some(*end)),
650 Error::Parse {
651 message,
652 line,
653 column,
654 start,
655 end,
656 } => ValidationError::error(message.clone(), "E003")
657 .with_location(*line, *column)
658 .with_span(Some(*start), Some(*end)),
659 _ => ValidationError::error(e.to_string(), "E000"),
660 };
661 ValidationResult::with_errors(vec![error])
662 }
663 }
664}
665
666#[cfg(feature = "semantic")]
667fn strict_syntax_error(sql: &str, dialect: &Dialect) -> Option<ValidationError> {
668 let tokens = dialect.tokenize(sql).ok()?;
669
670 for (idx, token) in tokens.iter().enumerate() {
671 if token.token_type != TokenType::Comma {
672 continue;
673 }
674
675 let next = tokens.get(idx + 1);
676 let (is_boundary, boundary_name) = match next.map(|t| t.token_type) {
677 Some(TokenType::From) => (true, "FROM"),
678 Some(TokenType::Where) => (true, "WHERE"),
679 Some(TokenType::GroupBy) => (true, "GROUP BY"),
680 Some(TokenType::Having) => (true, "HAVING"),
681 Some(TokenType::Order) | Some(TokenType::OrderBy) => (true, "ORDER BY"),
682 Some(TokenType::Limit) => (true, "LIMIT"),
683 Some(TokenType::Offset) => (true, "OFFSET"),
684 Some(TokenType::Union) => (true, "UNION"),
685 Some(TokenType::Intersect) => (true, "INTERSECT"),
686 Some(TokenType::Except) => (true, "EXCEPT"),
687 Some(TokenType::Qualify) => (true, "QUALIFY"),
688 Some(TokenType::Window) => (true, "WINDOW"),
689 Some(TokenType::Semicolon) | None => (true, "end of statement"),
690 _ => (false, ""),
691 };
692
693 if is_boundary {
694 let message = format!(
695 "Trailing comma before {} is not allowed in strict syntax mode",
696 boundary_name
697 );
698 return Some(
699 ValidationError::error(message, "E005")
700 .with_location(token.span.line, token.span.column),
701 );
702 }
703 }
704
705 None
706}
707
708#[cfg(feature = "transpile")]
721pub fn transpile_by_name(sql: &str, read: &str, write: &str) -> Result<Vec<String>> {
722 transpile_with_by_name(sql, read, write, &TranspileOptions::default())
723}
724
725#[cfg(feature = "transpile")]
729pub fn transpile_with_by_name(
730 sql: &str,
731 read: &str,
732 write: &str,
733 opts: &TranspileOptions,
734) -> Result<Vec<String>> {
735 let read_dialect = Dialect::get_by_name(read)
736 .ok_or_else(|| Error::parse(format!("Unknown dialect: {}", read), 0, 0, 0, 0))?;
737 let write_dialect = Dialect::get_by_name(write)
738 .ok_or_else(|| Error::parse(format!("Unknown dialect: {}", write), 0, 0, 0, 0))?;
739 read_dialect.transpile_with(sql, &write_dialect, opts.clone())
740}
741
742pub fn parse_by_name(sql: &str, dialect: &str) -> Result<Vec<Expression>> {
746 let d = Dialect::get_by_name(dialect)
747 .ok_or_else(|| Error::parse(format!("Unknown dialect: {}", dialect), 0, 0, 0, 0))?;
748 d.parse(sql)
749}
750
751#[cfg(feature = "generate")]
755pub fn generate_by_name(expression: &Expression, dialect: &str) -> Result<String> {
756 let d = Dialect::get_by_name(dialect)
757 .ok_or_else(|| Error::parse(format!("Unknown dialect: {}", dialect), 0, 0, 0, 0))?;
758 d.generate(expression)
759}
760
761#[cfg(feature = "generate")]
765pub fn format_by_name(sql: &str, dialect: &str) -> Result<Vec<String>> {
766 format_with_options_by_name(sql, dialect, &FormatGuardOptions::default())
767}
768
769#[cfg(feature = "generate")]
771pub fn format_with_options_by_name(
772 sql: &str,
773 dialect: &str,
774 options: &FormatGuardOptions,
775) -> Result<Vec<String>> {
776 let d = Dialect::get_by_name(dialect)
777 .ok_or_else(|| Error::parse(format!("Unknown dialect: {}", dialect), 0, 0, 0, 0))?;
778 format_with_dialect(sql, &d, options)
779}
780
781#[cfg(all(test, feature = "semantic"))]
782mod validation_tests {
783 use super::*;
784
785 #[test]
786 fn validate_is_permissive_by_default_for_trailing_commas() {
787 let result = validate("SELECT name, FROM employees", DialectType::Generic);
788 assert!(result.valid, "Result: {:?}", result.errors);
789 }
790
791 #[test]
792 fn validate_with_options_rejects_trailing_comma_before_from() {
793 let options = ValidationOptions {
794 strict_syntax: true,
795 };
796 let result = validate_with_options(
797 "SELECT name, FROM employees",
798 DialectType::Generic,
799 &options,
800 );
801 assert!(!result.valid, "Result should be invalid");
802 assert!(
803 result.errors.iter().any(|e| e.code == "E005"),
804 "Expected E005, got: {:?}",
805 result.errors
806 );
807 }
808
809 #[test]
810 fn validate_with_options_rejects_trailing_comma_before_where() {
811 let options = ValidationOptions {
812 strict_syntax: true,
813 };
814 let result = validate_with_options(
815 "SELECT name FROM employees, WHERE salary > 10",
816 DialectType::Generic,
817 &options,
818 );
819 assert!(!result.valid, "Result should be invalid");
820 assert!(
821 result.errors.iter().any(|e| e.code == "E005"),
822 "Expected E005, got: {:?}",
823 result.errors
824 );
825 }
826}
827
828#[cfg(all(test, feature = "generate"))]
829mod format_tests {
830 use super::*;
831
832 #[test]
833 fn format_basic_query() {
834 let result = format("SELECT a,b FROM t", DialectType::Generic).expect("format failed");
835 assert_eq!(result.len(), 1);
836 assert!(result[0].contains('\n'));
837 }
838
839 #[test]
840 fn format_guard_rejects_large_input() {
841 let options = FormatGuardOptions {
842 max_input_bytes: Some(7),
843 max_tokens: None,
844 max_ast_nodes: None,
845 max_set_op_chain: None,
846 };
847 let err = format_with_options("SELECT 1", DialectType::Generic, &options)
848 .expect_err("expected guard error");
849 assert!(err.to_string().contains("E_GUARD_INPUT_TOO_LARGE"));
850 }
851
852 #[test]
853 fn format_guard_rejects_token_budget() {
854 let options = FormatGuardOptions {
855 max_input_bytes: None,
856 max_tokens: Some(1),
857 max_ast_nodes: None,
858 max_set_op_chain: None,
859 };
860 let err = format_with_options("SELECT 1", DialectType::Generic, &options)
861 .expect_err("expected guard error");
862 assert!(err.to_string().contains("E_GUARD_TOKEN_BUDGET_EXCEEDED"));
863 }
864
865 #[test]
866 fn format_guard_rejects_ast_budget() {
867 let options = FormatGuardOptions {
868 max_input_bytes: None,
869 max_tokens: None,
870 max_ast_nodes: Some(1),
871 max_set_op_chain: None,
872 };
873 let err = format_with_options("SELECT 1", DialectType::Generic, &options)
874 .expect_err("expected guard error");
875 assert!(err.to_string().contains("E_GUARD_AST_BUDGET_EXCEEDED"));
876 }
877
878 #[test]
879 fn format_guard_rejects_set_op_chain_budget() {
880 let options = FormatGuardOptions {
881 max_input_bytes: None,
882 max_tokens: None,
883 max_ast_nodes: None,
884 max_set_op_chain: Some(1),
885 };
886 let err = format_with_options(
887 "SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3",
888 DialectType::Generic,
889 &options,
890 )
891 .expect_err("expected guard error");
892 assert!(err.to_string().contains("E_GUARD_SET_OP_CHAIN_EXCEEDED"));
893 }
894
895 #[test]
896 fn format_guard_does_not_treat_clickhouse_minus_function_as_set_op() {
897 let options = FormatGuardOptions {
898 max_input_bytes: None,
899 max_tokens: None,
900 max_ast_nodes: None,
901 max_set_op_chain: Some(0),
902 };
903 let result = format_with_options("SELECT minus(3, 2)", DialectType::ClickHouse, &options);
904 assert!(result.is_ok(), "Result: {:?}", result);
905 }
906
907 #[test]
908 fn issue57_invalid_ternary_returns_error() {
909 let sql = "SELECT x > 0 ? 1 : 0 FROM t";
912
913 let parse_result = parse(sql, DialectType::PostgreSQL);
914 assert!(
915 parse_result.is_err(),
916 "Expected parse error for invalid ternary SQL, got: {:?}",
917 parse_result
918 );
919
920 let format_result = format(sql, DialectType::PostgreSQL);
921 assert!(
922 format_result.is_err(),
923 "Expected format error for invalid ternary SQL, got: {:?}",
924 format_result
925 );
926
927 let transpile_result = transpile(sql, DialectType::PostgreSQL, DialectType::PostgreSQL);
928 assert!(
929 transpile_result.is_err(),
930 "Expected transpile error for invalid ternary SQL, got: {:?}",
931 transpile_result
932 );
933 }
934
935 #[test]
940 fn transpile_applies_cross_dialect_rewrites() {
941 let out = transpile(
943 "SELECT to_timestamp(col) FROM t",
944 DialectType::DuckDB,
945 DialectType::Trino,
946 )
947 .expect("transpile failed");
948 assert_eq!(out[0], "SELECT FROM_UNIXTIME(col) FROM t");
949
950 let out = transpile(
952 "SELECT CAST(col AS JSON) FROM t",
953 DialectType::DuckDB,
954 DialectType::Trino,
955 )
956 .expect("transpile failed");
957 assert_eq!(out[0], "SELECT JSON_PARSE(col) FROM t");
958 }
959
960 #[test]
964 fn transpile_matches_dialect_method() {
965 let cases: &[(DialectType, DialectType, &str, &str, &str)] = &[
966 (
967 DialectType::DuckDB,
968 DialectType::Trino,
969 "duckdb",
970 "trino",
971 "SELECT to_timestamp(col) FROM t",
972 ),
973 (
974 DialectType::DuckDB,
975 DialectType::Trino,
976 "duckdb",
977 "trino",
978 "SELECT CAST(col AS JSON) FROM t",
979 ),
980 (
981 DialectType::DuckDB,
982 DialectType::Trino,
983 "duckdb",
984 "trino",
985 "SELECT json_valid(col) FROM t",
986 ),
987 (
988 DialectType::Snowflake,
989 DialectType::DuckDB,
990 "snowflake",
991 "duckdb",
992 "SELECT DATEDIFF(day, a, b) FROM t",
993 ),
994 (
995 DialectType::BigQuery,
996 DialectType::DuckDB,
997 "bigquery",
998 "duckdb",
999 "SELECT DATE_DIFF(a, b, DAY) FROM t",
1000 ),
1001 (
1002 DialectType::Generic,
1003 DialectType::Generic,
1004 "generic",
1005 "generic",
1006 "SELECT 1",
1007 ),
1008 ];
1009 for (read, write, read_name, write_name, sql) in cases {
1010 let via_lib = transpile(sql, *read, *write).expect("lib::transpile failed");
1011 let via_name = transpile_by_name(sql, read_name, write_name)
1012 .expect("lib::transpile_by_name failed");
1013 let via_dialect = Dialect::get(*read)
1014 .transpile(sql, *write)
1015 .expect("Dialect::transpile failed");
1016 assert_eq!(
1017 via_lib, via_dialect,
1018 "lib::transpile / Dialect::transpile diverged for {:?} -> {:?}: {sql}",
1019 read, write
1020 );
1021 assert_eq!(
1022 via_name, via_dialect,
1023 "lib::transpile_by_name / Dialect::transpile diverged for {read_name} -> {write_name}: {sql}"
1024 );
1025 }
1026 }
1027
1028 #[test]
1029 fn format_default_guard_rejects_deep_union_chain_before_parse() {
1030 let base = "SELECT col0, col1 FROM t";
1031 let mut sql = base.to_string();
1032 for _ in 0..1100 {
1033 sql.push_str(" UNION ALL ");
1034 sql.push_str(base);
1035 }
1036
1037 let err = format(&sql, DialectType::Athena).expect_err("expected guard error");
1038 assert!(err.to_string().contains("E_GUARD_SET_OP_CHAIN_EXCEEDED"));
1039 }
1040}