use crate::err::TranslationErrors;
use crate::tree::ast::expression::Expression;
use crate::tree::ast::query::Query;
use crate::tree::ast::ParseWithErrors;
use indoc::indoc;
use pretty_assertions::assert_eq;
use rstest::rstest;
#[rstest]
#[case::int_zero("0")]
#[case::int_positive("42")]
#[case::int_negative("-1")]
#[case::int_from_hex("4660")]
#[case::decimal("1.5m")]
#[case::decimal_without_integer_digits(".5m")]
#[case::decimal_with_zero_integer_digit("0.5m")]
#[case::decimal_with_leading_zero_integer_digits("00.5m")]
#[case::decimal_without_integer_digits_and_leading_fractional_zero(".05m")]
#[case::decimal_with_leading_fractional_zero("0.05m")]
#[case::double("1.5")]
#[case::scientific("1.5e10")]
#[case::double_cast("3.14 AS double")]
#[case::string_single_quoted("'hi'")]
#[case::string_empty("''")]
#[case::string_with_quote("'it\\'s'")]
#[case::string_with_backslash("'a\\\\b'")]
#[case::binary("x'00ff'")]
#[case::binary_empty("x''")]
#[case::rows_literal("5r")]
#[case::bool_true("true")]
#[case::bool_false("false")]
#[case::null("null")]
#[case::field_reference("a")]
#[case::unbound_range("..")]
#[case::interval_milliseconds("500ms")]
#[case::interval_seconds("60s")]
#[case::interval_minutes("30min")]
#[case::interval_hours("12h")]
#[case::interval_days("7d")]
#[case::interval_weeks("4w")]
#[case::interval_months("6mon")]
#[case::interval_quarters("2q")]
#[case::interval_years("1y")]
#[case::backquoted_identifier("`a b`")]
#[case::backquoted_dotted_identifier("`a.b`")]
#[case::backquoted_identifier_with_escaped_backtick("`a``b`")]
#[case::backquoted_reserved_identifier("`SELECT`")]
#[case::field_lookup_backquoted("a.`b c`")]
#[case::template_parameter("${v}")]
#[case::backquoted_template_parameter("${`value name`}")]
#[case::template_parameters_in_sum("${a} + ${b}")]
#[case::template_parameters_in_range("${a} .. ${b}")]
#[case::templated_trunc("ts@${v}")]
#[case::product_before_sum("1 + 2 * 3")]
#[case::product_before_sum_reversed("1 * 2 + 3")]
#[case::sum_chain("1 + 2 + 3")]
#[case::difference_chain("1 - 2 - 3")]
#[case::product_quotient_chain("a * b / c")]
#[case::sum_under_product("(1 + 2) * 3")]
#[case::sum_under_modulo("10 % (3 + 1)")]
#[case::sum_on_right_of_product("a * (b + c)")]
#[case::sums_on_both_sides_of_product("(a + b) * (c + d)")]
#[case::index_access_inside_grouped_sum("(a[0] + b) * c")]
#[case::difference_grouped_right("10 - (4 - 1)")]
#[case::quotient_grouped_right("100 / (10 / 2)")]
#[case::sum_on_right_of_difference("a - (b + c)")]
#[case::difference_on_right_of_sum("a + (b - c)")]
#[case::sum_on_right_of_sum("a + (b + c)")]
#[case::product_on_right_of_quotient("a / (b * c)")]
#[case::quotient_on_right_of_product("a * (b / c)")]
#[case::product_on_right_of_product("a * (b * c)")]
#[case::conjunction_on_right_of_conjunction("a AND (b AND c)")]
#[case::disjunction_on_right_of_disjunction("a OR (b OR c)")]
#[case::negation_of_field("-a")]
#[case::unary_plus("+a")]
#[case::negation_binds_tighter_than_product("-a * b")]
#[case::double_negation("--a")]
#[case::sum_under_negation("-(1 + 2)")]
#[case::product_under_negation("-(2 * 3)")]
#[case::not_field("NOT a")]
#[case::double_not("NOT NOT a")]
#[case::not_binds_tighter_than_conjunction("NOT a AND b")]
#[case::conjunction_under_not("NOT (a AND b)")]
#[case::not_under_comparison("(NOT a) == b")]
#[case::equal("a == 1")]
#[case::not_equal("a != 1")]
#[case::less_than("a < 1")]
#[case::less_than_or_equal("a <= 1")]
#[case::greater_than("a > 1")]
#[case::greater_than_or_equal("a >= 1")]
#[case::is_null("a IS null")]
#[case::is_not_null("a IS NOT null")]
#[case::in_array("a IN [1, 2]")]
#[case::in_tuple("a IN (1, 2)")]
#[case::not_in_array("a NOT IN [1, 2]")]
#[case::comparison_binds_tighter_than_conjunction("a == 1 AND b == 2")]
#[case::conjunction_binds_tighter_than_disjunction("a AND b OR c")]
#[case::disjunction_under_conjunction("(a OR b) AND c")]
#[case::conjunction_under_comparison("(a AND b) == c")]
#[case::disjunction_under_membership("(a OR b) IN [1]")]
#[case::cast_to_int("a AS int")]
#[case::cast_to_string("a AS string")]
#[case::cast_to_boolean("a AS boolean")]
#[case::cast_to_timestamp("a AS timestamp")]
#[case::cast_to_interval("a AS interval")]
#[case::cast_to_calendar_interval("a AS calendar_interval")]
#[case::cast_to_binary("a AS binary")]
#[case::cast_to_rows("a AS rows")]
#[case::cast_to_variant("a AS variant")]
#[case::cast_to_parameterized_type("a AS array(int)")]
#[case::cast_to_map_type("a AS map(string, int)")]
#[case::cast_to_range_type("a AS range(timestamp)")]
#[case::cast_to_inclusive_range_type("a AS range_inclusive(timestamp)")]
#[case::cast_to_nested_type("a AS array(map(string, int))")]
#[case::cast_to_struct_type("a AS {x: int}")]
#[case::cast_to_multi_field_struct_type("a AS {x: int, y: array(string)}")]
#[case::cast_to_type_with_arguments("a AS decimal(10, 2)")]
#[case::cast_chain("a AS int AS string")]
#[case::cast_binds_looser_than_sum("1 + 2 AS string")]
#[case::cast_under_product("(a AS int) * 1000")]
#[case::two_casts_under_sum("(a AS int) + (b AS int)")]
#[case::comparison_under_cast("(a == b) AS int")]
#[case::cast_under_field_lookup("(a AS int).b")]
#[case::cast_to_singleton_tuple_type("a AS (int)")]
#[case::cast_to_tuple_type("a AS (int, string)")]
#[case::field_lookup("a.b")]
#[case::field_lookup_chain("a.b.c")]
#[case::index_access("a[0]")]
#[case::field_lookup_then_index("a.b[0]")]
#[case::index_then_field_lookup("a[0].b")]
#[case::index_into_array_literal("[1, 2][0]")]
#[case::field_lookup_on_struct_literal("{a: 1}.a")]
#[case::sum_under_field_lookup("(a + b).c")]
#[case::sum_under_index_access("(a + b)[0]")]
#[case::trunc_hours("ts@2h")]
#[case::trunc_seconds("ts@1s")]
#[case::trunc_minutes("ts@1min")]
#[case::trunc_days("ts@1d")]
#[case::trunc_weeks("ts@1w")]
#[case::trunc_months("ts@1mon")]
#[case::trunc_quarters("ts@1q")]
#[case::trunc_years("ts@1y")]
#[case::sum_under_trunc("(a + b)@2h")]
#[case::range_prefix("..5")]
#[case::range_prefix_inclusive("..=5")]
#[case::range_postfix("5..")]
#[case::range_binary("1 .. 5")]
#[case::range_binary_inclusive("1 ..= 5")]
#[case::call_no_args("count()")]
#[case::call_one_arg("sum(a)")]
#[case::call_two_args("coalesce(a, b)")]
#[case::call_three_args("if(a, b, c)")]
#[case::call_named_arg("f(a=1)")]
#[case::call_multiple_named_args("f(a=1, b=2)")]
#[case::call_positional_and_named_args("f(1, b=2)")]
#[case::call_lambda_arg("f(x -> x + 1, y)")]
#[case::call_with_grouped_arg("f((1 + 2) * 3)")]
#[case::array("[1, 2, 3]")]
#[case::array_empty("[]")]
#[case::tuple("(1, 2)")]
#[case::tuple_singleton("(1,)")]
#[case::struct_literal("{a: 1, b: 2}")]
#[case::pair("a: 1")]
#[case::pair_under_disjunction("(a: b) OR c")]
#[case::pair_grouped_on_right("a: (b: c)")]
#[case::array_with_grouped_element("[(1 + 2) * 3]")]
#[case::tuple_with_grouped_element("((1 + 2) * 3, 4)")]
#[case::struct_with_grouped_value("{a: (1 + 2) * 3}")]
#[case::grouped_template_parameters_under_product("(${a} + ${b}) * 2")]
#[case::lambda("x -> x + 1")]
#[case::lambda_multiple_params("(x, y) -> x + y")]
#[case::lambda_backquoted_param("`x y` -> `x y`")]
#[case::lambda_in_call("map(a, x -> x + 1)")]
#[case::lambda_with_grouped_body("x -> (a OR b) AND c")]
fn canonical_expression_is_a_fixed_point(#[case] canonical: &str) -> Result<(), TranslationErrors> {
let parsed = Expression::parse_result(canonical)?;
let printed = parsed.to_string();
let Ok(reparsed) = Expression::parse_result(printed.clone()) else {
panic!("`{canonical}` printed as `{printed}`, which no longer parses")
};
assert_eq!(
parsed, reparsed,
"`{canonical}` printed as `{printed}`, which parses as a different expression"
);
assert_eq!(
printed, canonical,
"printed text is not a fixed point (meaning survived, spelling did not)"
);
Ok(())
}
#[rstest]
#[case::from_only("FROM test:logs")]
#[case::from_aliased("FROM t = test:logs")]
#[case::from_multiple("FROM a:b, c:d")]
#[case::from_dotted_path("FROM a.b.c")]
#[case::from_backquoted_path("FROM `my space`:`my table`")]
#[case::from_templated_path("FROM ${tenant}_events")]
#[case::from_templated_path_with_backquoted_suffix("FROM ${tenant}` event logs`")]
#[case::from_backquoted_template_parameter("FROM ${`tenant id`}_events")]
#[case::from_templated_space("FROM ${space}:logs")]
#[case::from_multiple_template_holes("FROM ${space}:${tenant}_${kind}")]
#[case::bare_expression_query("SELECT result = 1 + 2")]
#[case::where_command(indoc! {"
FROM test:logs
| WHERE a == 1"})]
#[case::set_single(indoc! {"
FROM test:logs
| SET x = 1"})]
#[case::set_multiple(indoc! {"
FROM test:logs
| SET x = 1, y = 2"})]
#[case::select_command(indoc! {"
FROM test:logs
| SELECT a = a, b = b"})]
#[case::drop_command(indoc! {"
FROM test:logs
| DROP a, b"})]
#[case::drop_dotted_identifier(indoc! {"
FROM test:logs
| DROP a.b"})]
#[case::select_expression_without_alias_normalized(indoc! {"
FROM test:logs
| SELECT a = a"})]
#[case::agg_without_grouping(indoc! {"
FROM test:logs
| AGG count = count()"})]
#[case::agg_command(indoc! {"
FROM test:logs
| AGG count = count() BY a = a"})]
#[case::agg_with_trunc(indoc! {"
FROM test:logs
| AGG total = sum(x) BY ts = timestamp@2h"})]
#[case::agg_with_sort(indoc! {"
FROM test:logs
| AGG c = count() BY a = a SORT a ASC"})]
#[case::agg_multiple_clauses(indoc! {"
FROM test:logs
| AGG lo = min(x), hi = max(x)
BY a = a, b = b
SORT lo DESC, hi"})]
#[case::sort_command(indoc! {"
FROM test:logs
| SORT a ASC, b DESC"})]
#[case::sort_without_direction(indoc! {"
FROM test:logs
| SORT a"})]
#[case::limit_command(indoc! {"
FROM test:logs
| LIMIT 10"})]
#[case::distinct_command(indoc! {"
FROM test:logs
| DISTINCT a = a"})]
#[case::distinct_multiple(indoc! {"
FROM test:logs
| DISTINCT a = a, b = b"})]
#[case::join_command(indoc! {"
FROM test:logs
| JOIN test:other ON a == b"})]
#[case::lookup_command(indoc! {"
FROM test:logs
| LOOKUP test:other ON a == b"})]
#[case::join_aliased_table(indoc! {"
FROM test:logs
| JOIN other = test:other ON a == b"})]
#[case::union_command(indoc! {"
FROM test:logs
| UNION test:other"})]
#[case::union_multiple(indoc! {"
FROM test:logs
| UNION a:b, c:d"})]
#[case::append_command(indoc! {"
FROM test:logs
| APPEND test:other"})]
#[case::append_distinct(indoc! {"
FROM test:logs
| APPEND test:other DISTINCT a"})]
#[case::append_distinct_multiple(indoc! {"
FROM test:logs
| APPEND test:other DISTINCT a, b"})]
#[case::unnest_command(indoc! {"
FROM test:logs
| UNNEST a"})]
#[case::rows_command(indoc! {"
FROM test:logs
| ROWS a"})]
#[case::nest_command(indoc! {"
FROM test:logs
| NEST a"})]
#[case::explode_command(indoc! {"
FROM test:logs
| EXPLODE a = b"})]
#[case::trimstrings_command(indoc! {"
FROM test:logs
| TRIMSTRINGS 100"})]
#[case::within_command(indoc! {"
FROM test:logs
| WITHIN 5min"})]
#[case::suppress_command(indoc! {"
FROM test:logs
| SUPPRESS a"})]
#[case::suppress_with_by(indoc! {"
FROM test:logs
| SUPPRESS a BY b = b"})]
#[case::window_command(indoc! {"
FROM test:logs
| WINDOW r = count() BY a = a SORT b ASC"})]
#[case::window_with_within(indoc! {"
FROM test:logs
| WINDOW r = count() BY a = a WITHIN 5min"})]
#[case::window_all_subclauses(indoc! {"
FROM test:logs
| WINDOW r = count() BY a = a SORT b DESC WITHIN 5min"})]
#[case::parse_command(indoc! {"
FROM test:logs
| PARSE 'pat' AS a"})]
#[case::parse_with_source_and_targets(indoc! {"
FROM test:logs
| PARSE msg 'pat' AS a, b"})]
#[case::parse_nodrop(indoc! {"
FROM test:logs
| PARSE 'pat' AS a NODROP"})]
#[case::parse_dotted_target(indoc! {"
FROM test:logs
| PARSE 'pat' AS a.b"})]
#[case::match_single_pattern(indoc! {"
FROM test:logs
| MATCH a:b"})]
#[case::match_quantified_exactly(indoc! {"
FROM test:logs
| MATCH a:b{2}"})]
#[case::match_quantified_optional(indoc! {"
FROM test:logs
| MATCH a:b?"})]
#[case::match_quantified_any_number(indoc! {"
FROM test:logs
| MATCH a:b*"})]
#[case::match_aliased_pattern(indoc! {"
FROM test:logs
| MATCH event = a:b+"})]
#[case::match_with_agg_and_sort(indoc! {"
FROM test:logs
| MATCH a:b AGG c = count() SORT c DESC"})]
#[case::match_with_by_and_within(indoc! {"
FROM test:logs
| MATCH a:b BY x = x WITHIN 5min"})]
#[case::match_multiple_patterns(indoc! {"
FROM test:logs
| MATCH a:b c:d"})]
#[case::match_multiple_quantified_patterns(indoc! {"
FROM test:logs
| MATCH a:b* c:d+"})]
#[case::match_nested_pattern(indoc! {"
FROM test:logs
| MATCH (a:b c:d)*"})]
#[case::multi_command_pipeline(indoc! {"
FROM test:logs
| WHERE a == 1
| SET b = 2
| LIMIT 5"})]
#[case::def_expression_then_main(indoc! {"
DEF x = 1;
FROM test:logs"})]
#[case::def_pipeline_then_main(indoc! {"
DEF x = FROM test:logs;
FROM x"})]
#[case::multiple_defs(indoc! {"
DEF x = FROM a:b;
DEF y = FROM c:d;
FROM x"})]
#[case::backquoted_def_name(indoc! {"
DEF `saved query` = FROM a:b;
FROM `saved query`"})]
#[case::grouping_in_where(indoc! {"
FROM test:logs
| WHERE (a OR b) AND c"})]
#[case::grouping_in_where_under_not(indoc! {"
FROM test:logs
| WHERE NOT (a AND b)"})]
#[case::grouping_in_where_arithmetic(indoc! {"
FROM test:logs
| WHERE a - (b + c) == 0"})]
#[case::grouping_in_set(indoc! {"
FROM test:logs
| SET x = (1 + 2) * 3"})]
#[case::grouping_in_select(indoc! {"
FROM test:logs
| SELECT x = (a + b) * c"})]
#[case::grouping_in_sort(indoc! {"
FROM test:logs
| SORT (a + b) * c ASC"})]
#[case::grouping_in_limit(indoc! {"
FROM test:logs
| LIMIT (1 + 2) * 3"})]
#[case::grouping_in_trimstrings(indoc! {"
FROM test:logs
| TRIMSTRINGS (a + b) * c"})]
#[case::grouping_in_within(indoc! {"
FROM test:logs
| WITHIN (a + b) * c"})]
#[case::grouping_in_parse_source(indoc! {"
FROM test:logs
| PARSE (a + b) * c 'pat' AS x"})]
#[case::grouping_in_agg_clauses(indoc! {"
FROM test:logs
| AGG x = (a + b) * c
BY y = (d + e) * f
SORT (g + h) * i"})]
#[case::grouping_in_distinct(indoc! {"
FROM test:logs
| DISTINCT x = (a + b) * c"})]
#[case::grouping_in_suppress(indoc! {"
FROM test:logs
| SUPPRESS (a + b) * c
BY x = (d + e) * f"})]
#[case::grouping_in_window_clauses(indoc! {"
FROM test:logs
| WINDOW x = (a + b) * c
BY y = (d + e) * f
SORT (g + h) * i
WITHIN (j + k) * l"})]
#[case::grouping_in_join_condition(indoc! {"
FROM test:logs
| JOIN test:other ON (a OR b) AND c"})]
#[case::grouping_in_lookup_condition(indoc! {"
FROM test:logs
| LOOKUP test:other ON (a OR b) AND c"})]
#[case::grouping_in_explode(indoc! {"
FROM test:logs
| EXPLODE x = (a + b) * c"})]
#[case::grouping_in_unnest(indoc! {"
FROM test:logs
| UNNEST (a + b) * c"})]
#[case::grouping_in_rows(indoc! {"
FROM test:logs
| ROWS (a + b) * c"})]
#[case::grouping_in_match_subclauses(indoc! {"
FROM test:logs
| MATCH a:b
AGG x = (a + b) * c
BY y = (d + e) * f
SORT (g + h) * i
WITHIN (j + k) * l"})]
#[case::grouping_in_def_expression(indoc! {"
DEF x = (a + b) * c;
FROM test:logs"})]
#[case::sim_5904_dashboard_panel(indoc! {"
FROM test:logs
| SET ver_num = if(
mj IS NOT null,
(mj AS int) * 1000000 + (mn AS int) * 1000 + (pt AS int)
)"})]
fn canonical_query_is_a_fixed_point(#[case] canonical: &str) -> Result<(), TranslationErrors> {
let parsed = Query::parse_result(canonical)?;
let printed = parsed.to_string();
let Ok(reparsed) = Query::parse_result(printed.clone()) else {
panic!("query printed as:\n{printed}\nwhich no longer parses")
};
assert_eq!(
parsed, reparsed,
"query printed as:\n{printed}\nwhich parses as a different query"
);
assert_eq!(
printed, canonical,
"printed text is not a fixed point (meaning survived, spelling did not)"
);
Ok(())
}
#[rstest]
#[case::redundant_parens_around_literal("(42)", "42")]
#[case::redundant_parens_around_sum("(1 + 2)", "1 + 2")]
#[case::redundant_nested_parens("((1 + 2)) * 3", "(1 + 2) * 3")]
#[case::redundant_parens_implied_by_precedence("(1 * 2) + 3", "1 * 2 + 3")]
#[case::redundant_parens_around_cast_operand("(1 + 2) AS string", "1 + 2 AS string")]
#[case::redundant_parens_on_left_of_cast_chain("(a AS int) AS string", "a AS int AS string")]
#[case::redundant_parens_around_negation("-(-a)", "--a")]
#[case::redundant_parens_around_left_associative_product(
"((a + b) * (c + d)) / e",
"(a + b) * (c + d) / e"
)]
#[case::redundant_parens_under_prefix_range("..(a + b)", "..a + b")]
#[case::redundant_parens_under_postfix_range("(a + b)..", "a + b..")]
#[case::redundant_parens_on_left_of_binary_range("(1 + 2) .. 5", "1 + 2 .. 5")]
#[case::redundant_parens_on_right_of_binary_range("1 .. (2 + 3)", "1 .. 2 + 3")]
#[case::redundant_parens_around_pair("(a: 1)", "a: 1")]
#[case::redundant_parens_around_lambda("(x -> x + 1)", "x -> x + 1")]
#[case::outer_parens_redundant_inner_parens_not(
"((a AS int) * 1000000) + ((b AS int) * 1000)",
"(a AS int) * 1000000 + (b AS int) * 1000"
)]
#[case::double_quotes_become_single(r#""hi""#, "'hi'")]
#[case::double_quotes_become_single_and_apostrophe_is_escaped(r#""it's""#, "'it\\'s'")]
#[case::unnecessarily_backquoted_identifier("`abc`", "abc")]
#[case::unicode_single_quotes_become_basic("U&'hi'", "'hi'")]
#[case::unicode_double_quotes_become_basic(r#"U&"hi""#, "'hi'")]
#[case::unicode_single_quoted_uescape_clause_removed("U&'hi' UESCAPE '#'", "'hi'")]
#[case::unicode_double_quoted_uescape_clause_removed(r##"U&"hi" uescape "#""##, "'hi'")]
#[case::unicode_default_escape_decoded(r"U&'snowman: \2603'", "'snowman: ☃'")]
#[case::unicode_custom_escape_decoded("U&'snowman: #2603' UESCAPE '#'", "'snowman: ☃'")]
#[case::unicode_six_digit_escape_decoded(r"U&'face: \+01F600'", "'face: 😀'")]
#[case::unicode_escape_character_escaped("U&'a##b' UESCAPE '#'", "'a#b'")]
#[case::null_keyword_lowercased("NULL", "null")]
#[case::true_keyword_lowercased("TRUE", "true")]
#[case::false_keyword_lowercased("FALSE", "false")]
#[case::hex_literal_becomes_decimal("0x1234", "4660")]
#[case::octal_literal_becomes_decimal("0o10", "8")]
#[case::binary_integer_becomes_decimal("0b1010", "10")]
#[case::leading_zeroes_are_removed_from_integer("00042", "42")]
#[case::numeric_separators_are_removed("1_000", "1000")]
#[case::numeric_separators_are_removed_from_decimal("1_0.0_5m", "10.05m")]
#[case::numeric_separators_are_removed_from_double("1_0.5", "10.5")]
#[case::numeric_separators_are_removed_from_interval("5_000ms", "5000ms")]
#[case::numeric_separators_are_removed_from_rows("5_000rows", "5000r")]
#[case::leading_zero_added_to_double(".5", "0.5")]
#[case::leading_zero_scientific_normalized(".5e2", "5e1")]
#[case::scientific_notation_normalized("1.0e+2", "1e2")]
#[case::binary_literal_hex_lowercased("x'A0FF'", "x'a0ff'")]
#[case::rows_abbreviated("5rows", "5r")]
#[case::singular_row_abbreviated("1row", "1r")]
#[case::plural_one_row_abbreviated("1rows", "1r")]
#[case::interval_hours_abbreviated("12hr", "12h")]
#[case::interval_seconds_abbreviated("60sec", "60s")]
#[case::interval_minutes_abbreviated("30minutes", "30min")]
#[case::interval_days_abbreviated("7days", "7d")]
#[case::interval_weeks_abbreviated("4weeks", "4w")]
#[case::interval_months_abbreviated("6months", "6mon")]
#[case::interval_quarters_abbreviated("2quarters", "2q")]
#[case::interval_years_abbreviated("1year", "1y")]
#[case::trunc_multiplier_made_explicit("ts@d", "ts@1d")]
#[case::trunc_seconds_abbreviated("ts@seconds", "ts@1s")]
#[case::trunc_minutes_abbreviated("ts@minutes", "ts@1min")]
#[case::trunc_hours_abbreviated("ts@hours", "ts@1h")]
#[case::trunc_days_abbreviated("ts@days", "ts@1d")]
#[case::trunc_weeks_abbreviated("ts@weeks", "ts@1w")]
#[case::trunc_months_abbreviated("ts@months", "ts@1mon")]
#[case::trunc_quarters_abbreviated("ts@quarters", "ts@1q")]
#[case::trunc_years_abbreviated("ts@years", "ts@1y")]
#[case::trunc_numeric_separators_and_long_unit_removed("ts@2_4hours", "ts@24h")]
#[case::range_operator_spaced("1..5", "1 .. 5")]
#[case::named_argument_spacing_removed("f(a = 1)", "f(a=1)")]
#[case::lowercase_not_and_or_uppercased("not true or false", "NOT true OR false")]
#[case::lowercase_is_uppercased("a is null", "a IS null")]
#[case::lowercase_in_uppercased("a in [1]", "a IN [1]")]
#[case::lowercase_is_not_uppercased("a is not null", "a IS NOT null")]
#[case::lowercase_not_in_uppercased("a not in [1]", "a NOT IN [1]")]
#[case::lowercase_as_uppercased_and_type_lowercased("a as INT", "a AS int")]
#[case::parenthesized_single_lambda_parameter_removed("(x) -> x", "x -> x")]
#[case::parenthesized_single_lambda_parameter_with_comma_removed("(x,) -> x", "x -> x")]
#[case::array_trailing_comma_removed("[1, 2,]", "[1, 2]")]
#[case::tuple_trailing_comma_removed("(1, 2,)", "(1, 2)")]
#[case::struct_trailing_comma_removed("{a: 1,}", "{a: 1}")]
#[case::positional_argument_trailing_comma_removed("f(1,)", "f(1)")]
#[case::named_argument_trailing_comma_removed("f(a=1,)", "f(a=1)")]
#[case::lambda_parameter_trailing_comma_removed("(x, y,) -> x + y", "(x, y) -> x + y")]
#[case::type_argument_trailing_comma_removed("a AS array(int,)", "a AS array(int)")]
#[case::tuple_type_trailing_comma_removed("a AS (int, string,)", "a AS (int, string)")]
#[case::struct_type_trailing_comma_removed("a AS {x: int,}", "a AS {x: int}")]
#[case::type_name_lowercased("a AS ARRAY(INT)", "a AS array(int)")]
#[case::reserved_rows_type_lowercased("a AS ROWS", "a AS rows")]
#[case::decimal_type_numeric_separators_removed("a AS decimal(1_0, 0_2)", "a AS decimal(10, 2)")]
#[case::bare_decimal_type_gets_default_arguments("a AS decimal", "a AS decimal(38, 17)")]
#[case::array_wrapped(
"[1, 2, 3, 4, 5]",
indoc! {"
[1,
2,
3,
4,
5]"}
)]
#[case::tuple_wrapped(
"(1, 2, 3, 4, 5)",
indoc! {"
(1,
2,
3,
4,
5)"}
)]
#[case::struct_wrapped(
"{a: 1, b: 2, c: 3, d: 4, e: 5}",
indoc! {"
{
a: 1,
b: 2,
c: 3,
d: 4,
e: 5
}"}
)]
#[case::function_call_wrapped(
"f(1, 2, 3, 4, 5)",
indoc! {"
f(
1,
2,
3,
4,
5
)"}
)]
fn non_canonical_input_normalizes(
#[case] input: &str,
#[case] canonical: &str,
) -> Result<(), TranslationErrors> {
let parsed = Expression::parse_result(input)?;
assert_eq!(
Expression::parse_result(canonical)?,
parsed,
"test case is wrong: `{input}` and `{canonical}` are different expressions"
);
assert_eq!(
parsed.to_string(),
canonical,
"`{input}` normalized wrongly"
);
Ok(())
}
#[rstest]
#[case::implicit_select_aliases(
"FROM test:logs | SELECT a, b",
indoc! {"
FROM test:logs
| SELECT a = a, b = b"}
)]
#[case::implicit_agg_aliases(
"FROM test:logs | AGG count() BY a",
indoc! {"
FROM test:logs
| AGG count = count() BY a = a"}
)]
#[case::implicit_explode_alias(
"FROM test:logs | EXPLODE a",
indoc! {"
FROM test:logs
| EXPLODE a = a"}
)]
#[case::bare_expression_wrapped_in_select("1 + 2", "SELECT result = 1 + 2")]
#[case::keywords_uppercased(
"from test:logs | where a and b | sort a desc",
indoc! {"
FROM test:logs
| WHERE a AND b
| SORT a DESC"}
)]
#[case::optional_query_semicolon_removed("FROM test:logs;", "FROM test:logs")]
#[case::optional_parse_as_inserted(
"FROM test:logs | PARSE 'pat' a",
indoc! {"
FROM test:logs
| PARSE 'pat' AS a"}
)]
#[case::set_trailing_comma_removed(
"FROM test:logs | SET x = 1,",
indoc! {"
FROM test:logs
| SET x = 1"}
)]
#[case::select_trailing_comma_removed(
"FROM test:logs | SELECT x = 1,",
indoc! {"
FROM test:logs
| SELECT x = 1"}
)]
#[case::from_trailing_comma_removed("FROM test:logs,", "FROM test:logs")]
#[case::unnecessary_backquotes_removed_from_table_path("FROM `test`:`logs`", "FROM test:logs")]
#[case::drop_trailing_comma_removed(
"FROM test:logs | DROP a,",
indoc! {"
FROM test:logs
| DROP a"}
)]
#[case::union_trailing_comma_removed(
"FROM test:logs | UNION test:other,",
indoc! {"
FROM test:logs
| UNION test:other"}
)]
#[case::sort_trailing_comma_removed(
"FROM test:logs | SORT a DESC,",
indoc! {"
FROM test:logs
| SORT a DESC"}
)]
#[case::parse_trailing_comma_removed(
"FROM test:logs | PARSE 'pat' AS a,",
indoc! {"
FROM test:logs
| PARSE 'pat' AS a"}
)]
#[case::parse_trailing_comma_before_nodrop_removed(
"FROM test:logs | PARSE 'pat' AS a, NODROP",
indoc! {"
FROM test:logs
| PARSE 'pat' AS a NODROP"}
)]
#[case::agg_trailing_commas_removed(
"FROM test:logs | AGG c = count(), BY a, SORT b DESC,",
indoc! {"
FROM test:logs
| AGG c = count() BY a = a SORT b DESC"}
)]
#[case::distinct_trailing_comma_removed(
"FROM test:logs | DISTINCT a,",
indoc! {"
FROM test:logs
| DISTINCT a = a"}
)]
#[case::suppress_trailing_comma_removed(
"FROM test:logs | SUPPRESS 5min BY a,",
indoc! {"
FROM test:logs
| SUPPRESS 5min BY a = a"}
)]
#[case::window_trailing_commas_removed(
"FROM test:logs | WINDOW count(), BY a, SORT b DESC, WITHIN 5min",
indoc! {"
FROM test:logs
| WINDOW count = count() BY a = a SORT b DESC WITHIN 5min"}
)]
#[case::append_trailing_comma_removed(
"FROM test:logs | APPEND test:other DISTINCT a,",
indoc! {"
FROM test:logs
| APPEND test:other DISTINCT a"}
)]
#[case::exact_quantifier_separators_removed(
"FROM test:logs | MATCH a:b{1_000}",
indoc! {"
FROM test:logs
| MATCH a:b{1000}"}
)]
#[case::match_trailing_comma_removed(
"FROM test:logs | MATCH a:b,",
indoc! {"
FROM test:logs
| MATCH a:b"}
)]
#[case::match_subclause_trailing_commas_removed(
"FROM test:logs | MATCH a:b AGG count(), BY x, SORT y, WITHIN 5min,",
indoc! {"
FROM test:logs
| MATCH a:b
AGG count = count()
BY x = x
SORT y
WITHIN 5min"}
)]
#[case::comments_and_whitespace_removed(
"/* before */ FROM test:logs // source\n | WHERE a == 1 /* after */",
indoc! {"
FROM test:logs
| WHERE a == 1"}
)]
#[case::pipeline_split_across_lines(
"FROM test:logs | WHERE a == 1 | LIMIT 5",
indoc! {"
FROM test:logs
| WHERE a == 1
| LIMIT 5"}
)]
fn non_canonical_query_normalizes(
#[case] input: &str,
#[case] canonical: &str,
) -> Result<(), TranslationErrors> {
let parsed = Query::parse_result(input)?;
assert_eq!(
Query::parse_result(canonical)?,
parsed,
"test case is wrong: `{input}` and `{canonical}` are different queries"
);
assert_eq!(
parsed.to_string(),
canonical,
"`{input}` normalized wrongly"
);
Ok(())
}