@@ -2907,6 +2907,18 @@ def _is_projection(expr: exp.Expr) -> bool:
29072907 return isinstance (parent , exp .Select ) and expr .arg_key == "expressions"
29082908
29092909
2910+ def _has_ordinal_references (query : exp .Select ) -> bool :
2911+ order = query .args .get ("order" )
2912+ if order and any (
2913+ isinstance (ob .this , exp .Literal ) and ob .this .is_number for ob in order .expressions
2914+ ):
2915+ return True
2916+ group = query .args .get ("group" )
2917+ return bool (
2918+ group and any (isinstance (gb , exp .Literal ) and gb .is_number for gb in group .expressions )
2919+ )
2920+
2921+
29102922def _additive_projection_change (
29112923 previous_query : exp .Query ,
29122924 this_query : exp .Query ,
@@ -2923,8 +2935,9 @@ def _additive_projection_change(
29232935 Returns ``False`` (non-breaking) only when the change is provably additive:
29242936 * both queries are simple ``SELECT`` statements,
29252937 * everything other than the projection list is structurally identical,
2926- * no added projection is a (potentially cardinality-changing) ``UDTF``, and
2927- * every previous projection is preserved, in order, within the new projection list.
2938+ * no added projection is a (potentially cardinality-changing) ``UDTF``,
2939+ * every previous projection is preserved, in order, within the new projection list, and
2940+ * no mid-list insert shifts ordinal ``ORDER BY`` / ``GROUP BY`` references.
29282941
29292942 Otherwise returns ``None`` (undetermined), preserving the conservative default.
29302943 """
@@ -2942,7 +2955,8 @@ def _additive_projection_change(
29422955 # Adding a UDTF projection (e.g. EXPLODE / UNNEST) can change row cardinality, so such a
29432956 # change is not safely non-breaking even when it appears as an extra SELECT item.
29442957 for projection in this_projections :
2945- if isinstance (projection , exp .UDTF ) and not projection .find_ancestor (exp .Subquery ):
2958+ bare = projection .this if isinstance (projection , exp .Alias ) else projection
2959+ if isinstance (bare , exp .UDTF ):
29462960 return None
29472961
29482962 # Everything other than the projection list must be structurally identical. Replacing each
@@ -2960,17 +2974,26 @@ def _additive_projection_change(
29602974 # parser built distinct object identities.
29612975 this_projection_sql = [p .sql (dialect = dialect , comments = False ) for p in this_projections ]
29622976 search_start = 0
2977+ matched_at : list [int ] = []
29632978 for projection in previous_projections :
29642979 target_sql = projection .sql (dialect = dialect , comments = False )
29652980 # Continue after the previous match so added columns can appear before, between, or after
29662981 # the original projections, but existing projections cannot be reordered or rewritten.
29672982 for index in range (search_start , len (this_projection_sql )):
29682983 if this_projection_sql [index ] == target_sql :
2984+ matched_at .append (index )
29692985 search_start = index + 1
29702986 break
29712987 else :
29722988 return None
29732989
2990+ # Mid-list inserts shift ordinal references in ORDER BY / GROUP BY clauses.
2991+ if _has_ordinal_references (this_query ):
2992+ matched_set = set (matched_at )
2993+ last_matched = matched_at [- 1 ]
2994+ if any (i < last_matched for i in range (len (this_projections )) if i not in matched_set ):
2995+ return None
2996+
29742997 # At this point the query shape is unchanged and all prior outputs are preserved, so the only
29752998 # remaining difference is one or more additional, non-UDTF projections.
29762999 return False
0 commit comments