From aa17eaaea5aa6f47160e99ebc09696820f06a11c Mon Sep 17 00:00:00 2001 From: "Wei-Chun, Chang" Date: Wed, 23 Sep 2026 14:58:24 +0800 Subject: [PATCH] fix(cll): support the sqlglot 30.19 set operation scope name sqlglot 30.19 renamed Scope.union_scopes to Scope.set_operation_scopes. With that version, column-level lineage and breaking change classification returned unknown for queries with set operations such as UNION. Both now read the scope list by either name. Co-Authored-By: Claude Opus 5.5 (1M context) Signed-off-by: Wei-Chun, Chang --- recce/util/change_classifier.py | 13 ++++++++----- recce/util/cll.py | 9 ++++++++- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/recce/util/change_classifier.py b/recce/util/change_classifier.py index b11b15cce..1cc82fc94 100644 --- a/recce/util/change_classifier.py +++ b/recce/util/change_classifier.py @@ -16,6 +16,7 @@ normalize_change_category, to_v2_change_category, ) +from recce.util.cll import set_operation_scopes CHANGE_CATEGORY_UNKNOWN = NodeChange(category="unknown") CHANGE_CATEGORY_BREAKING = NodeChange(category="breaking") @@ -345,15 +346,17 @@ def selected_column_change_status(ref_column: exp.Column) -> Optional[ChangeStat def _diff_union_scope(old_scope: Scope, new_scope: Scope, scope_changes_map: dict[Scope, NodeChange]) -> NodeChange: assert old_scope.expression.key == "union" assert new_scope.expression.key == "union" - assert len(old_scope.union_scopes) == len(new_scope.union_scopes) - assert new_scope.union_scopes is not None - assert len(new_scope.union_scopes) > 0 + old_sub_scopes = set_operation_scopes(old_scope) + new_sub_scopes = set_operation_scopes(new_scope) + assert len(old_sub_scopes) == len(new_sub_scopes) + assert new_sub_scopes is not None + assert len(new_sub_scopes) > 0 - result_left = scope_changes_map.get(new_scope.union_scopes[0]) + result_left = scope_changes_map.get(new_sub_scopes[0]) change_category = result_left.category changed_columns = result_left.columns.copy() - for sub_scope in new_scope.union_scopes[1:]: + for sub_scope in new_sub_scopes[1:]: result_right = scope_changes_map.get(sub_scope) if change_category == "partial_breaking": if result_right.category in ["breaking"]: diff --git a/recce/util/cll.py b/recce/util/cll.py index 926c2a7f4..376e69d8d 100644 --- a/recce/util/cll.py +++ b/recce/util/cll.py @@ -297,13 +297,20 @@ def _dedeup_depends_on(depends_on: List[CllColumnDep]) -> List[CllColumnDep]: return dedup_list +def set_operation_scopes(scope: Scope) -> List[Scope]: + # sqlglot 30.19 renamed Scope.union_scopes to Scope.set_operation_scopes. + if hasattr(scope, "set_operation_scopes"): + return scope.set_operation_scopes + return scope.union_scopes + + def _cll_set_scope(scope: Scope, scope_cll_map: dict[Scope, CllResult]) -> CllResult: # model-to-column m2c: List[CllColumnDep] = [] # column-to-column c2c_map: Dict[str, CllColumn] = {} - for union_scope in scope.union_scopes: + for union_scope in set_operation_scopes(scope): sub_scope_result = scope_cll_map.get(union_scope) if sub_scope_result is None: raise RecceException(f"Scope {union_scope} not found in scope_cll_map")