dbt-labs/dbt-core · error
should split on the top-level UNION ALL
Error message
should split on the top-level UNION ALL
What it means
Test panic from Option::expect at crates/dbt-adapter/src/sql/diff.rs:3785: split_union_all_top_level(sql) returned None for input whose only top-level UNION ALL sits between two top-level SELECTs while a nested UNION ALL lives inside parentheses. The splitter must split on the outer operator only, returning ["select 1 as a", "select (select 2 as b union all select 3 as c)"]; returning None (or splitting at the wrong depth) means paren-depth tracking failed.
Source
Thrown at crates/dbt-adapter/src/sql/diff.rs:3785
let sql = "select 1 as a /* “unicode” */ UNION select 2 as b";
let parts = split_union_top_level(sql).expect("should split on top-level UNION");
assert_eq!(parts, vec!["select 1 as a", "select 2 as b"]);
}
#[test]
fn test_split_union_all_top_level_splits_and_handles_unicode() {
// Regression test: previously this could panic if the scan index landed in the middle
// of a multi-byte UTF-8 char (e.g. “).
let sql = "select 1 as a /* “unicode” */ UNION ALL select 2 as b";
let parts = split_union_all_top_level(sql).expect("should split on top-level UNION ALL");
assert_eq!(parts, vec!["select 1 as a", "select 2 as b"]);
}
#[test]
fn test_split_union_all_top_level_does_not_split_inside_parentheses() {
let sql = "select 1 as a union all select (select 2 as b union all select 3 as c)";
let parts =
split_union_all_top_level(sql).expect("should split on the top-level UNION ALL");
assert_eq!(
parts,
vec![
"select 1 as a",
"select (select 2 as b union all select 3 as c)"
]
);
}
#[test]
fn test_empty_sql_comparison() {
let result1 = compare_sql("", "", AdapterType::Snowflake);
assert!(result1.is_ok(), "Empty SQL should match empty SQL");
let result2 = compare_sql("SELECT 1", "", AdapterType::Snowflake);
assert!(result2.is_err(), "Non-empty SQL should not match empty SQL");
let result3 = compare_sql("", "SELECT 1", AdapterType::Snowflake);View on GitHub (pinned to 0267ce9170)
Solutions
- Verify parenthesis depth tracking increments on '(' and decrements on ')' starting at 0 for the whole string
- Ensure UNION ALL is only a split candidate when depth == 0 and not inside a string or comment
- Check that the scanner still scans after the nested subquery (does not stop at the first parenthesis)
- Add nested-subquery + smart-quote combined cases to the splitter unit tests
Example fix
// before: splits or bails at any UNION ALL
depth = 0; ... if kw == "UNION ALL" { split_here(); }
// after: depth-gated
if kw == "UNION ALL" && depth == 0 && !in_string && !in_comment { split_here(); } Defensive patterns
Strategy: validation
Validate before calling
// depth-aware pre-scan: verify exactly one top-level UNION ALL
let depth = std::cell::Cell::new(0i32);
let top_level = count_keyword_at_depth(sql, "UNION ALL", &depth); // depth==0 only
if top_level != 1 { eprintln!("expected exactly one top-level UNION ALL, got {top_level}"); } Try / catch
match split_union_all_top_level(sql) {
Some(parts) => assert_eq!(parts.len(), 2),
None => eprintln!("splitter failed to find top-level UNION ALL"),
} Prevention
- Track parenthesis depth across the whole string; only split at depth 0
- Don't stop scanning at the first '(' — nested subqueries must be consumed
- Unit-test scalar subqueries containing their own UNION ALL
- Combine nested-paren and unicode-comment cases in splitter tests
When it happens
Trigger: cargo test -p dbt-adapter with test_split_union_all_top_level_does_not_split_inside_parentheses: input "select 1 as a union all select (select 2 as b union all select 3 as c)" and split_union_all_top_level returns None, panicking "should split on the top-level UNION ALL".
Common situations: A depth-tracking fix (e.g. switching to char_indices) accidentally started depth at the wrong value or never decremented, so even the top-level UNION ALL was treated as nested; scalar subqueries in the second branch of a UNION ALL are common in generated SQL and replay comparisons.
Related errors
- should split on top-level UNION ALL
- should split on top-level UNION
- bare vs wrapped CREATE VIEW body should compare as equal (or
- STRUCT field order drift should be ignored
- projection order drift should be ignored
AI-assisted analysis of dbt-labs/dbt-core@0267ce9170 (2026-09-07).
Data as JSON: /api/errors/b87239626d56245f.
Report an issue: GitHub.