recce-nightly 1.2.0.20250506__py3-none-any.whl → 1.26.0.20251124__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of recce-nightly might be problematic. Click here for more details.

Files changed (213) hide show
  1. recce/VERSION +1 -1
  2. recce/__init__.py +27 -22
  3. recce/adapter/base.py +11 -14
  4. recce/adapter/dbt_adapter/__init__.py +810 -480
  5. recce/adapter/dbt_adapter/dbt_version.py +3 -0
  6. recce/adapter/sqlmesh_adapter.py +24 -35
  7. recce/apis/check_api.py +39 -28
  8. recce/apis/check_func.py +33 -27
  9. recce/apis/run_api.py +25 -19
  10. recce/apis/run_func.py +29 -23
  11. recce/artifact.py +119 -51
  12. recce/cli.py +1299 -323
  13. recce/config.py +42 -33
  14. recce/connect_to_cloud.py +138 -0
  15. recce/core.py +55 -47
  16. recce/data/404.html +1 -1
  17. recce/data/__next.__PAGE__.txt +10 -0
  18. recce/data/__next._full.txt +23 -0
  19. recce/data/__next._head.txt +8 -0
  20. recce/data/__next._index.txt +8 -0
  21. recce/data/__next._tree.txt +5 -0
  22. recce/data/_next/static/52aV_JrNUZU6dMFgvTQEO/_buildManifest.js +11 -0
  23. recce/data/_next/static/52aV_JrNUZU6dMFgvTQEO/_clientMiddlewareManifest.json +1 -0
  24. recce/data/_next/static/chunks/02b996c7f6a29a06.js +4 -0
  25. recce/data/_next/static/chunks/19c10d219a6a21ff.js +1 -0
  26. recce/data/_next/static/chunks/2df9ec28a061971d.js +11 -0
  27. recce/data/_next/static/chunks/3098c987393bda15.js +1 -0
  28. recce/data/_next/static/chunks/393dc43e483f717a.css +2 -0
  29. recce/data/_next/static/chunks/399e8d91a7e45073.js +2 -0
  30. recce/data/_next/static/chunks/4d0186f631230245.js +1 -0
  31. recce/data/_next/static/chunks/5794ba9e10a9c060.js +11 -0
  32. recce/data/_next/static/chunks/715761c929a3f28b.js +110 -0
  33. recce/data/_next/static/chunks/71f88fcc615bf282.js +1 -0
  34. recce/data/_next/static/chunks/80d2a95eaf1201ea.js +1 -0
  35. recce/data/_next/static/chunks/9979c6109bbbee35.js +1 -0
  36. recce/data/_next/static/chunks/99d638224186c118.js +1 -0
  37. recce/data/_next/static/chunks/d003eb36240e92f3.js +1 -0
  38. recce/data/_next/static/chunks/d3167cdfec4fc351.js +1 -0
  39. recce/data/_next/static/chunks/e124bccf574a3361.css +1 -0
  40. recce/data/_next/static/chunks/f40141db1bdb46f0.css +6 -0
  41. recce/data/_next/static/chunks/fcc53a88741a52f9.js +1 -0
  42. recce/data/_next/static/chunks/turbopack-b1920d28cfb1f28d.js +3 -0
  43. recce/data/_next/static/media/favicon.a8d38d84.ico +0 -0
  44. recce/data/_next/static/media/montserrat-cyrillic-800-normal.d80d830d.woff2 +0 -0
  45. recce/data/_next/static/media/montserrat-cyrillic-800-normal.f9d58125.woff +0 -0
  46. recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.076c2a93.woff2 +0 -0
  47. recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.a4fa76b5.woff +0 -0
  48. recce/data/_next/static/media/montserrat-latin-800-normal.cde454cc.woff2 +0 -0
  49. recce/data/_next/static/media/montserrat-latin-800-normal.d5761935.woff +0 -0
  50. recce/data/_next/static/media/montserrat-latin-ext-800-normal.40ec0659.woff2 +0 -0
  51. recce/data/_next/static/media/montserrat-latin-ext-800-normal.b671449b.woff +0 -0
  52. recce/data/_next/static/media/montserrat-vietnamese-800-normal.9f7b8541.woff +0 -0
  53. recce/data/_next/static/media/montserrat-vietnamese-800-normal.f9eb854e.woff2 +0 -0
  54. recce/data/_next/static/media/reload-image.7aa931c7.svg +4 -0
  55. recce/data/_not-found/__next._full.txt +17 -0
  56. recce/data/_not-found/__next._head.txt +8 -0
  57. recce/data/_not-found/__next._index.txt +8 -0
  58. recce/data/_not-found/__next._not-found.__PAGE__.txt +5 -0
  59. recce/data/_not-found/__next._not-found.txt +4 -0
  60. recce/data/_not-found/__next._tree.txt +3 -0
  61. recce/data/_not-found.html +1 -0
  62. recce/data/_not-found.txt +17 -0
  63. recce/data/auth_callback.html +68 -0
  64. recce/data/imgs/reload-image.svg +4 -0
  65. recce/data/index.html +1 -27
  66. recce/data/index.txt +23 -7
  67. recce/diff.py +6 -12
  68. recce/event/__init__.py +86 -74
  69. recce/event/collector.py +33 -22
  70. recce/event/track.py +49 -27
  71. recce/exceptions.py +1 -1
  72. recce/git.py +7 -7
  73. recce/github.py +57 -53
  74. recce/mcp_server.py +716 -0
  75. recce/models/__init__.py +4 -1
  76. recce/models/check.py +6 -7
  77. recce/models/run.py +1 -0
  78. recce/models/types.py +131 -28
  79. recce/pull_request.py +27 -25
  80. recce/run.py +165 -121
  81. recce/server.py +303 -111
  82. recce/state/__init__.py +31 -0
  83. recce/state/cloud.py +632 -0
  84. recce/state/const.py +26 -0
  85. recce/state/local.py +56 -0
  86. recce/state/state.py +119 -0
  87. recce/state/state_loader.py +174 -0
  88. recce/summary.py +188 -143
  89. recce/tasks/__init__.py +19 -3
  90. recce/tasks/core.py +11 -13
  91. recce/tasks/dataframe.py +82 -18
  92. recce/tasks/histogram.py +69 -34
  93. recce/tasks/lineage.py +2 -2
  94. recce/tasks/profile.py +152 -86
  95. recce/tasks/query.py +139 -87
  96. recce/tasks/rowcount.py +37 -31
  97. recce/tasks/schema.py +18 -15
  98. recce/tasks/top_k.py +35 -35
  99. recce/tasks/valuediff.py +216 -152
  100. recce/util/__init__.py +3 -0
  101. recce/util/api_token.py +80 -0
  102. recce/util/breaking.py +87 -85
  103. recce/util/cll.py +274 -219
  104. recce/util/io.py +22 -17
  105. recce/util/lineage.py +65 -16
  106. recce/util/logger.py +1 -1
  107. recce/util/onboarding_state.py +45 -0
  108. recce/util/perf_tracking.py +85 -0
  109. recce/util/recce_cloud.py +322 -72
  110. recce/util/singleton.py +4 -4
  111. recce/yaml/__init__.py +7 -10
  112. recce_cloud/__init__.py +24 -0
  113. recce_cloud/api/__init__.py +17 -0
  114. recce_cloud/api/base.py +111 -0
  115. recce_cloud/api/client.py +150 -0
  116. recce_cloud/api/exceptions.py +26 -0
  117. recce_cloud/api/factory.py +63 -0
  118. recce_cloud/api/github.py +76 -0
  119. recce_cloud/api/gitlab.py +82 -0
  120. recce_cloud/artifact.py +57 -0
  121. recce_cloud/ci_providers/__init__.py +9 -0
  122. recce_cloud/ci_providers/base.py +82 -0
  123. recce_cloud/ci_providers/detector.py +147 -0
  124. recce_cloud/ci_providers/github_actions.py +136 -0
  125. recce_cloud/ci_providers/gitlab_ci.py +130 -0
  126. recce_cloud/cli.py +245 -0
  127. recce_cloud/upload.py +214 -0
  128. {recce_nightly-1.2.0.20250506.dist-info → recce_nightly-1.26.0.20251124.dist-info}/METADATA +68 -37
  129. recce_nightly-1.26.0.20251124.dist-info/RECORD +180 -0
  130. {recce_nightly-1.2.0.20250506.dist-info → recce_nightly-1.26.0.20251124.dist-info}/WHEEL +1 -1
  131. {recce_nightly-1.2.0.20250506.dist-info → recce_nightly-1.26.0.20251124.dist-info}/top_level.txt +1 -0
  132. tests/adapter/dbt_adapter/conftest.py +9 -5
  133. tests/adapter/dbt_adapter/dbt_test_helper.py +37 -22
  134. tests/adapter/dbt_adapter/test_dbt_adapter.py +0 -15
  135. tests/adapter/dbt_adapter/test_dbt_cll.py +656 -41
  136. tests/adapter/dbt_adapter/test_selector.py +22 -21
  137. tests/recce_cloud/__init__.py +0 -0
  138. tests/recce_cloud/test_ci_providers.py +351 -0
  139. tests/recce_cloud/test_cli.py +372 -0
  140. tests/recce_cloud/test_client.py +273 -0
  141. tests/recce_cloud/test_platform_clients.py +333 -0
  142. tests/tasks/conftest.py +1 -1
  143. tests/tasks/test_histogram.py +58 -66
  144. tests/tasks/test_lineage.py +36 -23
  145. tests/tasks/test_preset_checks.py +45 -31
  146. tests/tasks/test_profile.py +339 -15
  147. tests/tasks/test_query.py +46 -46
  148. tests/tasks/test_row_count.py +65 -46
  149. tests/tasks/test_schema.py +65 -42
  150. tests/tasks/test_top_k.py +22 -18
  151. tests/tasks/test_valuediff.py +43 -32
  152. tests/test_cli.py +174 -60
  153. tests/test_cli_mcp_optional.py +45 -0
  154. tests/test_cloud_listing_cli.py +324 -0
  155. tests/test_config.py +7 -9
  156. tests/test_connect_to_cloud.py +82 -0
  157. tests/test_core.py +151 -4
  158. tests/test_dbt.py +7 -7
  159. tests/test_mcp_server.py +332 -0
  160. tests/test_pull_request.py +1 -1
  161. tests/test_server.py +25 -19
  162. tests/test_summary.py +29 -17
  163. recce/data/_next/static/Kcbs3GEIyH2LxgLYat0es/_buildManifest.js +0 -1
  164. recce/data/_next/static/chunks/1f229bf6-d9fe92e56db8d93b.js +0 -1
  165. recce/data/_next/static/chunks/29e3cc0d-8c150e37dff9631b.js +0 -1
  166. recce/data/_next/static/chunks/368-7587b306577df275.js +0 -65
  167. recce/data/_next/static/chunks/36e1c10d-bb0210cbd6573a8d.js +0 -1
  168. recce/data/_next/static/chunks/3998a672-eaad84bdd88cc73e.js +0 -1
  169. recce/data/_next/static/chunks/3a92ee20-3b5d922d4157af5e.js +0 -1
  170. recce/data/_next/static/chunks/450c323b-1bb5db526e54435a.js +0 -1
  171. recce/data/_next/static/chunks/47d8844f-79a1b53c66a7d7ec.js +0 -1
  172. recce/data/_next/static/chunks/6dc81886-c94b9b91bc2c3caf.js +0 -1
  173. recce/data/_next/static/chunks/6ef81909-694dc38134099299.js +0 -1
  174. recce/data/_next/static/chunks/700-3b65fc3666820d00.js +0 -2
  175. recce/data/_next/static/chunks/7a8a3e83-d7fa409d97b38b2b.js +0 -1
  176. recce/data/_next/static/chunks/7f27ae6c-413f6b869a04183a.js +0 -1
  177. recce/data/_next/static/chunks/8d700b6a-f0b1f6b9e0d97ce2.js +0 -1
  178. recce/data/_next/static/chunks/9746af58-d74bef4d03eea6ab.js +0 -1
  179. recce/data/_next/static/chunks/a30376cd-7d806e1602f2dc3a.js +0 -1
  180. recce/data/_next/static/chunks/app/_not-found/page-8a886fa0855c3105.js +0 -1
  181. recce/data/_next/static/chunks/app/layout-9102e22cb73f74d6.js +0 -1
  182. recce/data/_next/static/chunks/app/page-cee661090afbd6aa.js +0 -1
  183. recce/data/_next/static/chunks/b63b1b3f-7395c74e11a14e95.js +0 -1
  184. recce/data/_next/static/chunks/c132bf7d-8102037f9ccf372a.js +0 -1
  185. recce/data/_next/static/chunks/c1ceaa8b-a1e442154d23515e.js +0 -1
  186. recce/data/_next/static/chunks/cd9f8d63-cf0d5a7b0f7a92e8.js +0 -54
  187. recce/data/_next/static/chunks/ce84277d-f42c2c58049cea2d.js +0 -1
  188. recce/data/_next/static/chunks/e24bf851-0f8cbc99656833e7.js +0 -1
  189. recce/data/_next/static/chunks/fee69bc6-f17d36c080742e74.js +0 -1
  190. recce/data/_next/static/chunks/framework-ded83d71b51ce901.js +0 -1
  191. recce/data/_next/static/chunks/main-a0859f1f36d0aa6c.js +0 -1
  192. recce/data/_next/static/chunks/main-app-0225a2255968e566.js +0 -1
  193. recce/data/_next/static/chunks/pages/_app-d5672bf3d8b6371b.js +0 -1
  194. recce/data/_next/static/chunks/pages/_error-ed75be3f25588548.js +0 -1
  195. recce/data/_next/static/chunks/webpack-567d72f0bc0820d5.js +0 -1
  196. recce/data/_next/static/css/c9ecb46a4b21c126.css +0 -14
  197. recce/data/_next/static/media/montserrat-cyrillic-800-normal.22628180.woff2 +0 -0
  198. recce/data/_next/static/media/montserrat-cyrillic-800-normal.31d693bb.woff +0 -0
  199. recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.7e2c1e62.woff +0 -0
  200. recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.94a63aea.woff2 +0 -0
  201. recce/data/_next/static/media/montserrat-latin-800-normal.6f8fa298.woff2 +0 -0
  202. recce/data/_next/static/media/montserrat-latin-800-normal.97e20d5e.woff +0 -0
  203. recce/data/_next/static/media/montserrat-latin-ext-800-normal.013b84f9.woff2 +0 -0
  204. recce/data/_next/static/media/montserrat-latin-ext-800-normal.aff52ab0.woff +0 -0
  205. recce/data/_next/static/media/montserrat-vietnamese-800-normal.5f21869b.woff +0 -0
  206. recce/data/_next/static/media/montserrat-vietnamese-800-normal.c0035377.woff2 +0 -0
  207. recce/state.py +0 -753
  208. recce_nightly-1.2.0.20250506.dist-info/RECORD +0 -142
  209. tests/test_state.py +0 -123
  210. /recce/data/_next/static/{Kcbs3GEIyH2LxgLYat0es → 52aV_JrNUZU6dMFgvTQEO}/_ssgManifest.js +0 -0
  211. /recce/data/_next/static/chunks/{polyfills-42372ed130431b0a.js → a6dad97d9634a72d.js} +0 -0
  212. {recce_nightly-1.2.0.20250506.dist-info → recce_nightly-1.26.0.20251124.dist-info}/entry_points.txt +0 -0
  213. {recce_nightly-1.2.0.20250506.dist-info → recce_nightly-1.26.0.20251124.dist-info}/licenses/LICENSE +0 -0
recce/util/breaking.py CHANGED
@@ -3,15 +3,15 @@ from dataclasses import dataclass
3
3
  from typing import Optional
4
4
 
5
5
  import sqlglot.expressions as exp
6
- from sqlglot import parse_one, Dialect
6
+ from sqlglot import Dialect, parse_one
7
7
  from sqlglot.errors import SqlglotError
8
- from sqlglot.optimizer import traverse_scope, Scope
8
+ from sqlglot.optimizer import Scope, traverse_scope
9
9
  from sqlglot.optimizer.qualify import qualify
10
10
 
11
- from recce.models.types import NodeChange, ChangeStatus
11
+ from recce.models.types import ChangeStatus, NodeChange
12
12
 
13
- CHANGE_CATEGORY_UNKNOWN = NodeChange(category='unknown')
14
- CHANGE_CATEGORY_BREAKING = NodeChange(category='breaking')
13
+ CHANGE_CATEGORY_UNKNOWN = NodeChange(category="unknown")
14
+ CHANGE_CATEGORY_BREAKING = NodeChange(category="breaking")
15
15
 
16
16
 
17
17
  @dataclass
@@ -48,11 +48,11 @@ class BreakingPerformanceTracking:
48
48
 
49
49
  def to_dict(self):
50
50
  return {
51
- 'lineage_diff_elapsed_ms': self.lineage_diff_elapsed,
52
- 'modified_nodes': self.modified_nodes,
53
- 'sqlglot_error_nodes': self.sqlglot_error_nodes,
54
- 'other_error_nodes': self.other_error_nodes,
55
- 'checkpoints': self.checkpoints,
51
+ "lineage_diff_elapsed_ms": self.lineage_diff_elapsed,
52
+ "modified_nodes": self.modified_nodes,
53
+ "sqlglot_error_nodes": self.sqlglot_error_nodes,
54
+ "other_error_nodes": self.other_error_nodes,
55
+ "checkpoints": self.checkpoints,
56
56
  }
57
57
 
58
58
  def reset(self):
@@ -64,33 +64,38 @@ class BreakingPerformanceTracking:
64
64
  self.checkpoints = {}
65
65
 
66
66
 
67
- def _diff_select_scope(
68
- old_scope: Scope,
69
- new_scope: Scope,
70
- scope_changes_map: dict[Scope, NodeChange]
71
- ) -> NodeChange:
72
- assert old_scope.expression.key == 'select'
73
- assert new_scope.expression.key == 'select'
67
+ def _diff_select_scope(old_scope: Scope, new_scope: Scope, scope_changes_map: dict[Scope, NodeChange]) -> NodeChange:
68
+ assert old_scope.expression.key == "select"
69
+ assert new_scope.expression.key == "select"
74
70
 
75
- change_category = 'non_breaking'
71
+ change_category = "non_breaking"
76
72
  changed_columns = {}
77
73
 
78
74
  # check if the upstream scopes is not breaking
79
75
  for source_name, source in new_scope.sources.items():
80
76
  if scope_changes_map.get(source) is not None:
81
- chanage = scope_changes_map[source]
82
- if chanage.category == 'breaking':
83
- change_category = 'breaking'
77
+ change = scope_changes_map[source]
78
+ if change.category == "breaking":
79
+ change_category = "breaking"
80
+
81
+ # check if the upstream scopes sources table are the same
82
+ if len(old_scope.sources) != len(new_scope.sources):
83
+ change_category = "breaking"
84
+ else:
85
+ old_source_tables = [s.name for s in old_scope.sources.values() if isinstance(s, exp.Table)]
86
+ new_source_tables = [s.name for s in new_scope.sources.values() if isinstance(s, exp.Table)]
87
+ if sorted(old_source_tables) != sorted(new_source_tables):
88
+ change_category = "breaking"
84
89
 
85
90
  # check if non-select expressions are the same
86
91
  old_select = old_scope.expression # type: exp.Select
87
92
  new_select = new_scope.expression # type: exp.Select
88
93
  for arg_key in old_select.args.keys() | new_select.args.keys():
89
- if arg_key in ['expressions', 'with', 'from']:
94
+ if arg_key in ["expressions", "with", "from", "with_", "from_"]:
90
95
  continue
91
96
 
92
97
  if old_select.args.get(arg_key) != new_select.args.get(arg_key):
93
- change_category = 'breaking'
98
+ change_category = "breaking"
94
99
 
95
100
  def source_column_change_status(ref_column: exp.Column) -> Optional[ChangeStatus]:
96
101
  table_name = ref_column.table
@@ -108,9 +113,10 @@ def _diff_select_scope(
108
113
  # selects
109
114
  old_column_map = {projection.alias_or_name: projection for projection in old_select.selects}
110
115
  new_column_map = {projection.alias_or_name: projection for projection in new_select.selects}
111
- is_distinct = new_select.args.get('distinct') is not None
116
+ is_distinct = new_select.args.get("distinct") is not None
117
+
118
+ for column_name in old_column_map.keys() | new_column_map.keys():
112
119
 
113
- for column_name in (old_column_map.keys() | new_column_map.keys()):
114
120
  def _has_udtf(expr: exp.Expression) -> bool:
115
121
  return expr.find(exp.UDTF) is not None
116
122
 
@@ -124,116 +130,112 @@ def _diff_select_scope(
124
130
  new_column = new_column_map.get(column_name)
125
131
  if old_column is None:
126
132
  if is_distinct:
127
- change_category = 'breaking'
133
+ change_category = "breaking"
128
134
  elif _has_udtf(new_column):
129
- change_category = 'breaking'
135
+ change_category = "breaking"
130
136
 
131
- changed_columns[column_name] = 'added'
137
+ changed_columns[column_name] = "added"
132
138
  elif new_column is None:
133
139
  if is_distinct:
134
- change_category = 'breaking'
140
+ change_category = "breaking"
135
141
  elif _has_udtf(old_column):
136
- change_category = 'breaking'
142
+ change_category = "breaking"
137
143
 
138
- changed_columns[column_name] = 'removed'
139
- if change_category != 'breaking':
140
- change_category = 'partial_breaking'
144
+ changed_columns[column_name] = "removed"
145
+ if change_category != "breaking":
146
+ change_category = "partial_breaking"
141
147
  elif old_column != new_column:
142
148
  if is_distinct:
143
- change_category = 'breaking'
149
+ change_category = "breaking"
144
150
  elif _has_udtf(old_column) and _has_udtf(new_column):
145
- change_category = 'breaking'
151
+ change_category = "breaking"
146
152
  elif _has_aggregate(old_column) != _has_aggregate(new_column):
147
- change_category = 'breaking'
153
+ change_category = "breaking"
148
154
 
149
- changed_columns[column_name] = 'modified'
150
- if change_category != 'breaking':
151
- change_category = 'partial_breaking'
155
+ changed_columns[column_name] = "modified"
156
+ if change_category != "breaking":
157
+ change_category = "partial_breaking"
152
158
  else:
153
159
  if _has_star(new_column):
154
160
  for source_name, (_, source) in new_scope.selected_sources.items():
155
161
  change = scope_changes_map.get(source)
156
162
  if change is not None:
157
- if change.category == 'breaking':
158
- change_category = 'breaking'
163
+ if change.category == "breaking":
164
+ change_category = "breaking"
159
165
  for sub_column_name in change.columns.keys():
160
166
  column_change_status = change.columns[sub_column_name]
161
167
  changed_columns[sub_column_name] = column_change_status
162
- if change_category != 'breaking' and column_change_status in ['removed', 'modified']:
163
- change_category = 'partial_breaking'
168
+ if change_category != "breaking" and column_change_status in ["removed", "modified"]:
169
+ change_category = "partial_breaking"
164
170
  continue
165
171
 
166
172
  ref_columns = new_column.find_all(exp.Column)
167
173
  for ref_column in ref_columns:
168
174
  if source_column_change_status(ref_column) is not None:
169
175
  if is_distinct:
170
- change_category = 'breaking'
176
+ change_category = "breaking"
171
177
  elif _has_udtf(new_column):
172
- change_category = 'breaking'
178
+ change_category = "breaking"
173
179
 
174
- if change_category != 'breaking':
175
- change_category = 'partial_breaking'
176
- changed_columns[column_name] = 'modified'
180
+ if change_category != "breaking":
181
+ change_category = "partial_breaking"
182
+ changed_columns[column_name] = "modified"
177
183
 
178
184
  def selected_column_change_status(ref_column: exp.Column) -> Optional[ChangeStatus]:
179
185
  column_name = ref_column.name
180
186
  return changed_columns.get(column_name)
181
187
 
182
188
  # joins clause: Reference the source columns
183
- if new_select.args.get('joins'):
184
- joins = new_select.args.get('joins')
189
+ if new_select.args.get("joins"):
190
+ joins = new_select.args.get("joins")
185
191
  for join in joins:
186
192
  if isinstance(join, exp.Join):
187
193
  for ref_column in join.find_all(exp.Column):
188
194
  if source_column_change_status(ref_column) is not None:
189
- change_category = 'breaking'
195
+ change_category = "breaking"
190
196
 
191
197
  # where clauses: Reference the source columns
192
- if new_select.args.get('where'):
193
- where = new_select.args.get('where')
198
+ if new_select.args.get("where"):
199
+ where = new_select.args.get("where")
194
200
  if isinstance(where, exp.Where):
195
201
  for ref_column in where.find_all(exp.Column):
196
202
  if source_column_change_status(ref_column) is not None:
197
- change_category = 'breaking'
203
+ change_category = "breaking"
198
204
 
199
205
  # group by clause: Reference the source columns, column index
200
- if new_select.args.get('group'):
201
- group = new_select.args.get('group')
206
+ if new_select.args.get("group"):
207
+ group = new_select.args.get("group")
202
208
  if isinstance(group, exp.Group):
203
209
  for ref_column in group.find_all(exp.Column):
204
210
  if source_column_change_status(ref_column) is not None:
205
- change_category = 'breaking'
211
+ change_category = "breaking"
206
212
 
207
213
  # having clause: Reference the source columns, selected columns
208
- if new_select.args.get('having'):
209
- having = new_select.args.get('having')
214
+ if new_select.args.get("having"):
215
+ having = new_select.args.get("having")
210
216
  if isinstance(having, exp.Having):
211
217
  for ref_column in having.find_all(exp.Column):
212
218
  if source_column_change_status(ref_column) is not None:
213
- change_category = 'breaking'
219
+ change_category = "breaking"
214
220
  elif selected_column_change_status(ref_column) is not None:
215
- change_category = 'breaking'
221
+ change_category = "breaking"
216
222
 
217
223
  # order by clause: Reference the source columns, selected columns, column index
218
- if new_select.args.get('order'):
219
- order = new_select.args.get('order')
224
+ if new_select.args.get("order"):
225
+ order = new_select.args.get("order")
220
226
  if isinstance(order, exp.Order):
221
227
  for ref_column in order.find_all(exp.Column):
222
228
  if source_column_change_status(ref_column) is not None:
223
- change_category = 'breaking'
229
+ change_category = "breaking"
224
230
  elif selected_column_change_status(ref_column) is not None:
225
- change_category = 'breaking'
231
+ change_category = "breaking"
226
232
 
227
233
  return NodeChange(category=change_category, columns=changed_columns)
228
234
 
229
235
 
230
- def _diff_union_scope(
231
- old_scope: Scope,
232
- new_scope: Scope,
233
- scope_changes_map: dict[Scope, NodeChange]
234
- ) -> NodeChange:
235
- assert old_scope.expression.key == 'union'
236
- assert new_scope.expression.key == 'union'
236
+ def _diff_union_scope(old_scope: Scope, new_scope: Scope, scope_changes_map: dict[Scope, NodeChange]) -> NodeChange:
237
+ assert old_scope.expression.key == "union"
238
+ assert new_scope.expression.key == "union"
237
239
  assert len(old_scope.union_scopes) == len(new_scope.union_scopes)
238
240
  assert new_scope.union_scopes is not None
239
241
  assert len(new_scope.union_scopes) > 0
@@ -244,11 +246,11 @@ def _diff_union_scope(
244
246
 
245
247
  for sub_scope in new_scope.union_scopes[1:]:
246
248
  result_right = scope_changes_map.get(sub_scope)
247
- if change_category == 'partial_breaking':
248
- if result_right.category in ['breaking']:
249
+ if change_category == "partial_breaking":
250
+ if result_right.category in ["breaking"]:
249
251
  change_category = result_right.category
250
- elif change_category == 'non_breaking':
251
- if result_right.category in ['breaking', 'partial_breaking']:
252
+ elif change_category == "non_breaking":
253
+ if result_right.category in ["breaking", "partial_breaking"]:
252
254
  change_category = result_right.category
253
255
  for column_name, column_change_status in result_right.columns.items():
254
256
  changed_columns[column_name] = column_change_status
@@ -265,7 +267,7 @@ def parse_change_category(
265
267
  perf_tracking: BreakingPerformanceTracking = None,
266
268
  ) -> NodeChange:
267
269
  if old_sql == new_sql:
268
- return NodeChange(category='non_breaking')
270
+ return NodeChange(category="non_breaking")
269
271
 
270
272
  try:
271
273
  dialect = Dialect.get(dialect)
@@ -294,31 +296,31 @@ def parse_change_category(
294
296
  old_scopes = traverse_scope(old_exp)
295
297
  new_scopes = traverse_scope(new_exp)
296
298
  if len(old_scopes) != len(new_scopes):
297
- return NodeChange(category='breaking', columns={})
299
+ return NodeChange(category="breaking", columns={})
298
300
 
299
301
  scope_changes_map = {}
300
302
  for old_scope, new_scope in zip(old_scopes, new_scopes):
301
303
  if old_scope.expression.key != new_scope.expression.key:
302
- scope_changes_map[new_scope] = NodeChange(category='breaking')
304
+ scope_changes_map[new_scope] = NodeChange(category="breaking")
303
305
  continue
304
306
  if old_scope == new_scope:
305
- scope_changes_map[new_scope] = NodeChange(category='non_breaking')
307
+ scope_changes_map[new_scope] = NodeChange(category="non_breaking")
306
308
  continue
307
309
 
308
310
  scope_type = old_scope.expression.key
309
- if scope_type == 'select':
311
+ if scope_type == "select":
310
312
  # CTE, Subquery, Root
311
313
  result = _diff_select_scope(old_scope, new_scope, scope_changes_map)
312
- elif scope_type == 'union':
314
+ elif scope_type == "union":
313
315
  # Union
314
316
  result = _diff_union_scope(old_scope, new_scope, scope_changes_map)
315
317
  else:
316
318
  if old_scope.expression != new_scope.expression:
317
- result = NodeChange(category='breaking', columns={})
319
+ result = NodeChange(category="breaking", columns={})
318
320
  else:
319
- result = NodeChange(category='non_breaking', columns={})
321
+ result = NodeChange(category="non_breaking", columns={})
320
322
 
321
- if result.category == 'unknown':
323
+ if result.category == "unknown":
322
324
  return result
323
325
 
324
326
  scope_changes_map[new_scope] = result