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

  1. Verify parenthesis depth tracking increments on '(' and decrements on ')' starting at 0 for the whole string
  2. Ensure UNION ALL is only a split candidate when depth == 0 and not inside a string or comment
  3. Check that the scanner still scans after the nested subquery (does not stop at the first parenthesis)
  4. 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

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


AI-assisted analysis of dbt-labs/dbt-core@0267ce9170 (2026-09-07). Data as JSON: /api/errors/b87239626d56245f. Report an issue: GitHub.