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