recce-nightly 0.62.0.20250417__py3-none-any.whl → 1.30.0.20251221__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 (245) 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 +845 -461
  5. recce/adapter/dbt_adapter/dbt_version.py +3 -0
  6. recce/adapter/sqlmesh_adapter.py +24 -35
  7. recce/apis/check_api.py +59 -42
  8. recce/apis/check_events_api.py +353 -0
  9. recce/apis/check_func.py +41 -35
  10. recce/apis/run_api.py +25 -19
  11. recce/apis/run_func.py +64 -25
  12. recce/artifact.py +119 -51
  13. recce/cli.py +1301 -324
  14. recce/config.py +43 -34
  15. recce/connect_to_cloud.py +138 -0
  16. recce/core.py +55 -47
  17. recce/data/404/index.html +2 -0
  18. recce/data/404.html +2 -1
  19. recce/data/__next.@lineage.!KHNsb3Qp.__PAGE__.txt +7 -0
  20. recce/data/__next.@lineage.!KHNsb3Qp.txt +4 -0
  21. recce/data/__next.__PAGE__.txt +6 -0
  22. recce/data/__next._full.txt +32 -0
  23. recce/data/__next._head.txt +8 -0
  24. recce/data/__next._index.txt +14 -0
  25. recce/data/__next._tree.txt +8 -0
  26. recce/data/_next/static/chunks/025a7e3e3f9f40ae.js +1 -0
  27. recce/data/_next/static/chunks/0ce56d67ef5779ca.js +4 -0
  28. recce/data/_next/static/chunks/1a6a78780155dac7.js +48 -0
  29. recce/data/_next/static/chunks/1de8485918b9182a.css +2 -0
  30. recce/data/_next/static/chunks/1e4b1b50d1e34993.js +1 -0
  31. recce/data/_next/static/chunks/206d5d181e4c738e.js +1 -0
  32. recce/data/_next/static/chunks/2c357efc34c5b859.js +25 -0
  33. recce/data/_next/static/chunks/2e9d95d2d48c479c.js +1 -0
  34. recce/data/_next/static/chunks/2f016dc4a3edad2e.js +2 -0
  35. recce/data/_next/static/chunks/313251962d698f7c.js +1 -0
  36. recce/data/_next/static/chunks/3a9f021f38eb5574.css +1 -0
  37. recce/data/_next/static/chunks/40079da8d2b8f651.js +1 -0
  38. recce/data/_next/static/chunks/4599182bffb64661.js +38 -0
  39. recce/data/_next/static/chunks/4e62f6e184173580.js +1 -0
  40. recce/data/_next/static/chunks/5c4dfb0d09eaa401.js +1 -0
  41. recce/data/_next/static/chunks/69e4f06ccfdfc3ac.js +1 -0
  42. recce/data/_next/static/chunks/6b206cb4707d6bee.js +1 -0
  43. recce/data/_next/static/chunks/6d8557f062aa4386.css +1 -0
  44. recce/data/_next/static/chunks/7fbe3650bd83b6b5.js +1 -0
  45. recce/data/_next/static/chunks/83fa823a825674f6.js +1 -0
  46. recce/data/_next/static/chunks/848a6c9b5f55f7ed.js +1 -0
  47. recce/data/_next/static/chunks/859462b0858aef88.css +2 -0
  48. recce/data/_next/static/chunks/923964f18c87d0f1.css +1 -0
  49. recce/data/_next/static/chunks/939390f911895d7c.js +48 -0
  50. recce/data/_next/static/chunks/99a9817237a07f43.js +1 -0
  51. recce/data/_next/static/chunks/9fed8b4b2b924054.js +5 -0
  52. recce/data/_next/static/chunks/b6949f6c5892110c.js +1 -0
  53. recce/data/_next/static/chunks/b851a1d3f8149828.js +1 -0
  54. recce/data/_next/static/chunks/c734f9ad957de0b4.js +1 -0
  55. recce/data/_next/static/chunks/cdde321b0ec75717.js +2 -0
  56. recce/data/_next/static/chunks/d0f91117d77ff844.css +1 -0
  57. recce/data/_next/static/chunks/d6c8667911c2500f.js +1 -0
  58. recce/data/_next/static/chunks/da8dab68c02752cf.js +74 -0
  59. recce/data/_next/static/chunks/dc074049c9d12d97.js +109 -0
  60. recce/data/_next/static/chunks/ee7f1a8227342421.js +1 -0
  61. recce/data/_next/static/chunks/fa2f4e56c2fccc73.js +1 -0
  62. recce/data/_next/static/chunks/turbopack-1fad664f62979b93.js +3 -0
  63. recce/data/_next/static/media/favicon.a8d38d84.ico +0 -0
  64. recce/data/_next/static/media/montserrat-cyrillic-800-normal.d80d830d.woff2 +0 -0
  65. recce/data/_next/static/media/montserrat-cyrillic-800-normal.f9d58125.woff +0 -0
  66. recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.076c2a93.woff2 +0 -0
  67. recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.a4fa76b5.woff +0 -0
  68. recce/data/_next/static/media/montserrat-latin-800-normal.cde454cc.woff2 +0 -0
  69. recce/data/_next/static/media/montserrat-latin-800-normal.d5761935.woff +0 -0
  70. recce/data/_next/static/media/montserrat-latin-ext-800-normal.40ec0659.woff2 +0 -0
  71. recce/data/_next/static/media/montserrat-latin-ext-800-normal.b671449b.woff +0 -0
  72. recce/data/_next/static/media/montserrat-vietnamese-800-normal.9f7b8541.woff +0 -0
  73. recce/data/_next/static/media/montserrat-vietnamese-800-normal.f9eb854e.woff2 +0 -0
  74. recce/data/_next/static/nX-Uz0AH6Tc6hIQUFGqaB/_buildManifest.js +11 -0
  75. recce/data/_next/static/nX-Uz0AH6Tc6hIQUFGqaB/_clientMiddlewareManifest.json +1 -0
  76. recce/data/_not-found/__next._full.txt +24 -0
  77. recce/data/_not-found/__next._head.txt +8 -0
  78. recce/data/_not-found/__next._index.txt +13 -0
  79. recce/data/_not-found/__next._not-found.__PAGE__.txt +5 -0
  80. recce/data/_not-found/__next._not-found.txt +4 -0
  81. recce/data/_not-found/__next._tree.txt +6 -0
  82. recce/data/_not-found/index.html +2 -0
  83. recce/data/_not-found/index.txt +24 -0
  84. recce/data/auth_callback.html +68 -0
  85. recce/data/checks/__next.@lineage.__DEFAULT__.txt +7 -0
  86. recce/data/checks/__next._full.txt +39 -0
  87. recce/data/checks/__next._head.txt +8 -0
  88. recce/data/checks/__next._index.txt +14 -0
  89. recce/data/checks/__next._tree.txt +8 -0
  90. recce/data/checks/__next.checks.__PAGE__.txt +10 -0
  91. recce/data/checks/__next.checks.txt +4 -0
  92. recce/data/checks/index.html +2 -0
  93. recce/data/checks/index.txt +39 -0
  94. recce/data/imgs/reload-image.svg +4 -0
  95. recce/data/index.html +2 -27
  96. recce/data/index.txt +32 -7
  97. recce/data/lineage/__next.@lineage.__DEFAULT__.txt +7 -0
  98. recce/data/lineage/__next._full.txt +39 -0
  99. recce/data/lineage/__next._head.txt +8 -0
  100. recce/data/lineage/__next._index.txt +14 -0
  101. recce/data/lineage/__next._tree.txt +8 -0
  102. recce/data/lineage/__next.lineage.__PAGE__.txt +10 -0
  103. recce/data/lineage/__next.lineage.txt +4 -0
  104. recce/data/lineage/index.html +2 -0
  105. recce/data/lineage/index.txt +39 -0
  106. recce/data/query/__next.@lineage.__DEFAULT__.txt +7 -0
  107. recce/data/query/__next._full.txt +37 -0
  108. recce/data/query/__next._head.txt +8 -0
  109. recce/data/query/__next._index.txt +14 -0
  110. recce/data/query/__next._tree.txt +8 -0
  111. recce/data/query/__next.query.__PAGE__.txt +9 -0
  112. recce/data/query/__next.query.txt +4 -0
  113. recce/data/query/index.html +2 -0
  114. recce/data/query/index.txt +37 -0
  115. recce/diff.py +6 -12
  116. recce/event/CONFIG.bak +1 -0
  117. recce/event/__init__.py +86 -74
  118. recce/event/collector.py +33 -22
  119. recce/event/track.py +49 -27
  120. recce/exceptions.py +1 -1
  121. recce/git.py +7 -7
  122. recce/github.py +57 -53
  123. recce/mcp_server.py +725 -0
  124. recce/models/__init__.py +4 -1
  125. recce/models/check.py +438 -21
  126. recce/models/run.py +1 -0
  127. recce/models/types.py +134 -28
  128. recce/pull_request.py +27 -25
  129. recce/run.py +179 -122
  130. recce/server.py +394 -104
  131. recce/state/__init__.py +31 -0
  132. recce/state/cloud.py +644 -0
  133. recce/state/const.py +26 -0
  134. recce/state/local.py +56 -0
  135. recce/state/state.py +119 -0
  136. recce/state/state_loader.py +174 -0
  137. recce/summary.py +196 -149
  138. recce/tasks/__init__.py +19 -3
  139. recce/tasks/core.py +11 -13
  140. recce/tasks/dataframe.py +82 -18
  141. recce/tasks/histogram.py +69 -34
  142. recce/tasks/lineage.py +2 -2
  143. recce/tasks/profile.py +152 -86
  144. recce/tasks/query.py +180 -89
  145. recce/tasks/rowcount.py +37 -31
  146. recce/tasks/schema.py +18 -15
  147. recce/tasks/top_k.py +35 -35
  148. recce/tasks/utils.py +147 -0
  149. recce/tasks/valuediff.py +247 -155
  150. recce/util/__init__.py +3 -0
  151. recce/util/api_token.py +80 -0
  152. recce/util/breaking.py +105 -100
  153. recce/util/cll.py +274 -219
  154. recce/util/cloud/__init__.py +15 -0
  155. recce/util/cloud/base.py +115 -0
  156. recce/util/cloud/check_events.py +190 -0
  157. recce/util/cloud/checks.py +242 -0
  158. recce/util/io.py +22 -17
  159. recce/util/lineage.py +65 -16
  160. recce/util/logger.py +1 -1
  161. recce/util/onboarding_state.py +45 -0
  162. recce/util/perf_tracking.py +85 -0
  163. recce/util/recce_cloud.py +347 -72
  164. recce/util/singleton.py +4 -4
  165. recce/util/startup_perf.py +121 -0
  166. recce/yaml/__init__.py +7 -10
  167. recce_nightly-1.30.0.20251221.dist-info/METADATA +195 -0
  168. recce_nightly-1.30.0.20251221.dist-info/RECORD +183 -0
  169. {recce_nightly-0.62.0.20250417.dist-info → recce_nightly-1.30.0.20251221.dist-info}/WHEEL +1 -2
  170. recce/data/_next/static/chunks/1f229bf6-d9fe92e56db8d93b.js +0 -1
  171. recce/data/_next/static/chunks/29e3cc0d-8c150e37dff9631b.js +0 -1
  172. recce/data/_next/static/chunks/36e1c10d-bb0210cbd6573a8d.js +0 -1
  173. recce/data/_next/static/chunks/3998a672-eaad84bdd88cc73e.js +0 -1
  174. recce/data/_next/static/chunks/450c323b-1bb5db526e54435a.js +0 -1
  175. recce/data/_next/static/chunks/47d8844f-79a1b53c66a7d7ec.js +0 -1
  176. recce/data/_next/static/chunks/500-e51c92a025a51234.js +0 -65
  177. recce/data/_next/static/chunks/6dc81886-c94b9b91bc2c3caf.js +0 -1
  178. recce/data/_next/static/chunks/700-3b65fc3666820d00.js +0 -2
  179. recce/data/_next/static/chunks/7a8a3e83-d7fa409d97b38b2b.js +0 -1
  180. recce/data/_next/static/chunks/7f27ae6c-413f6b869a04183a.js +0 -1
  181. recce/data/_next/static/chunks/9746af58-d74bef4d03eea6ab.js +0 -1
  182. recce/data/_next/static/chunks/a30376cd-7d806e1602f2dc3a.js +0 -1
  183. recce/data/_next/static/chunks/app/_not-found/page-8a886fa0855c3105.js +0 -1
  184. recce/data/_next/static/chunks/app/layout-9102e22cb73f74d6.js +0 -1
  185. recce/data/_next/static/chunks/app/page-9adc25782272ed2e.js +0 -1
  186. recce/data/_next/static/chunks/b63b1b3f-7395c74e11a14e95.js +0 -1
  187. recce/data/_next/static/chunks/c132bf7d-8102037f9ccf372a.js +0 -1
  188. recce/data/_next/static/chunks/c1ceaa8b-a1e442154d23515e.js +0 -1
  189. recce/data/_next/static/chunks/cd9f8d63-cf0d5a7b0f7a92e8.js +0 -54
  190. recce/data/_next/static/chunks/ce84277d-f42c2c58049cea2d.js +0 -1
  191. recce/data/_next/static/chunks/e24bf851-0f8cbc99656833e7.js +0 -1
  192. recce/data/_next/static/chunks/fee69bc6-f17d36c080742e74.js +0 -1
  193. recce/data/_next/static/chunks/framework-ded83d71b51ce901.js +0 -1
  194. recce/data/_next/static/chunks/main-a0859f1f36d0aa6c.js +0 -1
  195. recce/data/_next/static/chunks/main-app-0225a2255968e566.js +0 -1
  196. recce/data/_next/static/chunks/pages/_app-d5672bf3d8b6371b.js +0 -1
  197. recce/data/_next/static/chunks/pages/_error-ed75be3f25588548.js +0 -1
  198. recce/data/_next/static/chunks/webpack-567d72f0bc0820d5.js +0 -1
  199. recce/data/_next/static/css/c9ecb46a4b21c126.css +0 -14
  200. recce/data/_next/static/media/montserrat-cyrillic-800-normal.22628180.woff2 +0 -0
  201. recce/data/_next/static/media/montserrat-cyrillic-800-normal.31d693bb.woff +0 -0
  202. recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.7e2c1e62.woff +0 -0
  203. recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.94a63aea.woff2 +0 -0
  204. recce/data/_next/static/media/montserrat-latin-800-normal.6f8fa298.woff2 +0 -0
  205. recce/data/_next/static/media/montserrat-latin-800-normal.97e20d5e.woff +0 -0
  206. recce/data/_next/static/media/montserrat-latin-ext-800-normal.013b84f9.woff2 +0 -0
  207. recce/data/_next/static/media/montserrat-latin-ext-800-normal.aff52ab0.woff +0 -0
  208. recce/data/_next/static/media/montserrat-vietnamese-800-normal.5f21869b.woff +0 -0
  209. recce/data/_next/static/media/montserrat-vietnamese-800-normal.c0035377.woff2 +0 -0
  210. recce/data/_next/static/qiyFlux77VkhxiceAJe_F/_buildManifest.js +0 -1
  211. recce/state.py +0 -753
  212. recce_nightly-0.62.0.20250417.dist-info/METADATA +0 -311
  213. recce_nightly-0.62.0.20250417.dist-info/RECORD +0 -139
  214. recce_nightly-0.62.0.20250417.dist-info/top_level.txt +0 -2
  215. tests/__init__.py +0 -0
  216. tests/adapter/__init__.py +0 -0
  217. tests/adapter/dbt_adapter/__init__.py +0 -0
  218. tests/adapter/dbt_adapter/conftest.py +0 -13
  219. tests/adapter/dbt_adapter/dbt_test_helper.py +0 -283
  220. tests/adapter/dbt_adapter/test_dbt_adapter.py +0 -40
  221. tests/adapter/dbt_adapter/test_dbt_cll.py +0 -102
  222. tests/adapter/dbt_adapter/test_selector.py +0 -177
  223. tests/tasks/__init__.py +0 -0
  224. tests/tasks/conftest.py +0 -4
  225. tests/tasks/test_histogram.py +0 -137
  226. tests/tasks/test_lineage.py +0 -42
  227. tests/tasks/test_preset_checks.py +0 -50
  228. tests/tasks/test_profile.py +0 -73
  229. tests/tasks/test_query.py +0 -151
  230. tests/tasks/test_row_count.py +0 -116
  231. tests/tasks/test_schema.py +0 -99
  232. tests/tasks/test_top_k.py +0 -73
  233. tests/tasks/test_valuediff.py +0 -74
  234. tests/test_cli.py +0 -122
  235. tests/test_config.py +0 -45
  236. tests/test_core.py +0 -27
  237. tests/test_dbt.py +0 -36
  238. tests/test_pull_request.py +0 -130
  239. tests/test_server.py +0 -98
  240. tests/test_state.py +0 -123
  241. tests/test_summary.py +0 -57
  242. /recce/data/_next/static/chunks/{polyfills-42372ed130431b0a.js → a6dad97d9634a72d.js} +0 -0
  243. /recce/data/_next/static/{qiyFlux77VkhxiceAJe_F → nX-Uz0AH6Tc6hIQUFGqaB}/_ssgManifest.js +0 -0
  244. {recce_nightly-0.62.0.20250417.dist-info → recce_nightly-1.30.0.20251221.dist-info}/entry_points.txt +0 -0
  245. {recce_nightly-0.62.0.20250417.dist-info → recce_nightly-1.30.0.20251221.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,32 +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
- result = NodeChange(category='non_breaking')
71
+ change_category = "non_breaking"
72
+ changed_columns = {}
76
73
 
77
74
  # check if the upstream scopes is not breaking
78
75
  for source_name, source in new_scope.sources.items():
79
76
  if scope_changes_map.get(source) is not None:
80
- change_category = scope_changes_map[source]
81
- if change_category.category == 'breaking':
82
- return 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"
83
89
 
84
90
  # check if non-select expressions are the same
85
91
  old_select = old_scope.expression # type: exp.Select
86
92
  new_select = new_scope.expression # type: exp.Select
87
93
  for arg_key in old_select.args.keys() | new_select.args.keys():
88
- if arg_key in ['expressions', 'with', 'from']:
94
+ if arg_key in ["expressions", "with", "from", "with_", "from_"]:
89
95
  continue
90
96
 
91
97
  if old_select.args.get(arg_key) != new_select.args.get(arg_key):
92
- return CHANGE_CATEGORY_BREAKING
98
+ change_category = "breaking"
93
99
 
94
100
  def source_column_change_status(ref_column: exp.Column) -> Optional[ChangeStatus]:
95
101
  table_name = ref_column.table
@@ -107,10 +113,10 @@ def _diff_select_scope(
107
113
  # selects
108
114
  old_column_map = {projection.alias_or_name: projection for projection in old_select.selects}
109
115
  new_column_map = {projection.alias_or_name: projection for projection in new_select.selects}
110
- changed_columns = {}
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,133 +130,132 @@ 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
- return CHANGE_CATEGORY_BREAKING
133
+ change_category = "breaking"
134
+ elif _has_udtf(new_column):
135
+ change_category = "breaking"
128
136
 
129
- if _has_udtf(new_column):
130
- return CHANGE_CATEGORY_BREAKING
131
-
132
- changed_columns[column_name] = 'added'
137
+ changed_columns[column_name] = "added"
133
138
  elif new_column is None:
134
139
  if is_distinct:
135
- return CHANGE_CATEGORY_BREAKING
136
-
137
- if _has_udtf(old_column):
138
- return CHANGE_CATEGORY_BREAKING
140
+ change_category = "breaking"
141
+ elif _has_udtf(old_column):
142
+ change_category = "breaking"
139
143
 
140
- changed_columns[column_name] = 'removed'
141
- result.category = 'partial_breaking'
144
+ changed_columns[column_name] = "removed"
145
+ if change_category != "breaking":
146
+ change_category = "partial_breaking"
142
147
  elif old_column != new_column:
143
148
  if is_distinct:
144
- return CHANGE_CATEGORY_BREAKING
145
-
146
- if _has_udtf(old_column) and _has_udtf(new_column):
147
- return CHANGE_CATEGORY_BREAKING
148
-
149
- if _has_aggregate(old_column) != _has_aggregate(new_column):
150
- return CHANGE_CATEGORY_BREAKING
151
-
152
- changed_columns[column_name] = 'modified'
153
- result.category = 'partial_breaking'
149
+ change_category = "breaking"
150
+ elif _has_udtf(old_column) and _has_udtf(new_column):
151
+ change_category = "breaking"
152
+ elif _has_aggregate(old_column) != _has_aggregate(new_column):
153
+ change_category = "breaking"
154
+
155
+ changed_columns[column_name] = "modified"
156
+ if change_category != "breaking":
157
+ change_category = "partial_breaking"
154
158
  else:
155
159
  if _has_star(new_column):
156
160
  for source_name, (_, source) in new_scope.selected_sources.items():
157
161
  change = scope_changes_map.get(source)
158
162
  if change is not None:
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 column_change_status in ['removed', 'modified']:
163
- result.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
- return CHANGE_CATEGORY_BREAKING
171
- if _has_udtf(new_column):
172
- return CHANGE_CATEGORY_BREAKING
173
- result.category = 'partial_breaking'
174
- changed_columns[column_name] = 'modified'
176
+ change_category = "breaking"
177
+ elif _has_udtf(new_column):
178
+ change_category = "breaking"
179
+
180
+ if change_category != "breaking":
181
+ change_category = "partial_breaking"
182
+ changed_columns[column_name] = "modified"
175
183
 
176
184
  def selected_column_change_status(ref_column: exp.Column) -> Optional[ChangeStatus]:
177
185
  column_name = ref_column.name
178
186
  return changed_columns.get(column_name)
179
187
 
180
188
  # joins clause: Reference the source columns
181
- if new_select.args.get('joins'):
182
- joins = new_select.args.get('joins')
189
+ if new_select.args.get("joins"):
190
+ joins = new_select.args.get("joins")
183
191
  for join in joins:
184
192
  if isinstance(join, exp.Join):
185
193
  for ref_column in join.find_all(exp.Column):
186
194
  if source_column_change_status(ref_column) is not None:
187
- return CHANGE_CATEGORY_BREAKING
195
+ change_category = "breaking"
188
196
 
189
197
  # where clauses: Reference the source columns
190
- if new_select.args.get('where'):
191
- where = new_select.args.get('where')
198
+ if new_select.args.get("where"):
199
+ where = new_select.args.get("where")
192
200
  if isinstance(where, exp.Where):
193
201
  for ref_column in where.find_all(exp.Column):
194
202
  if source_column_change_status(ref_column) is not None:
195
- return CHANGE_CATEGORY_BREAKING
203
+ change_category = "breaking"
196
204
 
197
205
  # group by clause: Reference the source columns, column index
198
- if new_select.args.get('group'):
199
- group = new_select.args.get('group')
206
+ if new_select.args.get("group"):
207
+ group = new_select.args.get("group")
200
208
  if isinstance(group, exp.Group):
201
209
  for ref_column in group.find_all(exp.Column):
202
210
  if source_column_change_status(ref_column) is not None:
203
- return CHANGE_CATEGORY_BREAKING
211
+ change_category = "breaking"
204
212
 
205
213
  # having clause: Reference the source columns, selected columns
206
- if new_select.args.get('having'):
207
- having = new_select.args.get('having')
214
+ if new_select.args.get("having"):
215
+ having = new_select.args.get("having")
208
216
  if isinstance(having, exp.Having):
209
217
  for ref_column in having.find_all(exp.Column):
210
218
  if source_column_change_status(ref_column) is not None:
211
- return CHANGE_CATEGORY_BREAKING
212
- if selected_column_change_status(ref_column) is not None:
213
- return CHANGE_CATEGORY_BREAKING
219
+ change_category = "breaking"
220
+ elif selected_column_change_status(ref_column) is not None:
221
+ change_category = "breaking"
214
222
 
215
223
  # order by clause: Reference the source columns, selected columns, column index
216
- if new_select.args.get('order'):
217
- order = new_select.args.get('order')
224
+ if new_select.args.get("order"):
225
+ order = new_select.args.get("order")
218
226
  if isinstance(order, exp.Order):
219
227
  for ref_column in order.find_all(exp.Column):
220
228
  if source_column_change_status(ref_column) is not None:
221
- return CHANGE_CATEGORY_BREAKING
222
- if selected_column_change_status(ref_column) is not None:
223
- return CHANGE_CATEGORY_BREAKING
229
+ change_category = "breaking"
230
+ elif selected_column_change_status(ref_column) is not None:
231
+ change_category = "breaking"
224
232
 
225
- result.columns = changed_columns
226
- return result
233
+ return NodeChange(category=change_category, columns=changed_columns)
227
234
 
228
235
 
229
- def _diff_union_scope(
230
- old_scope: Scope,
231
- new_scope: Scope,
232
- scope_changes_map: dict[Scope, NodeChange]
233
- ) -> NodeChange:
234
- assert old_scope.expression.key == 'union'
235
- 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"
236
239
  assert len(old_scope.union_scopes) == len(new_scope.union_scopes)
237
240
  assert new_scope.union_scopes is not None
238
241
  assert len(new_scope.union_scopes) > 0
239
242
 
240
- result = scope_changes_map.get(new_scope.union_scopes[0])
241
- if result.category in ['breaking', 'unknown']:
242
- return result
243
+ result_left = scope_changes_map.get(new_scope.union_scopes[0])
244
+ change_category = result_left.category
245
+ changed_columns = result_left.columns.copy()
243
246
 
244
247
  for sub_scope in new_scope.union_scopes[1:]:
245
248
  result_right = scope_changes_map.get(sub_scope)
246
- if result_right.category in ['breaking', 'unknown']:
247
- return result_right
248
- if result_right.category == 'partial_breaking':
249
- result.category = 'partial_breaking'
249
+ if change_category == "partial_breaking":
250
+ if result_right.category in ["breaking"]:
251
+ change_category = result_right.category
252
+ elif change_category == "non_breaking":
253
+ if result_right.category in ["breaking", "partial_breaking"]:
254
+ change_category = result_right.category
250
255
  for column_name, column_change_status in result_right.columns.items():
251
- result.columns[column_name] = column_change_status
256
+ changed_columns[column_name] = column_change_status
252
257
 
253
- return result
258
+ return NodeChange(category=change_category, columns=changed_columns)
254
259
 
255
260
 
256
261
  def parse_change_category(
@@ -262,7 +267,7 @@ def parse_change_category(
262
267
  perf_tracking: BreakingPerformanceTracking = None,
263
268
  ) -> NodeChange:
264
269
  if old_sql == new_sql:
265
- return NodeChange(category='non_breaking')
270
+ return NodeChange(category="non_breaking")
266
271
 
267
272
  try:
268
273
  dialect = Dialect.get(dialect)
@@ -291,31 +296,31 @@ def parse_change_category(
291
296
  old_scopes = traverse_scope(old_exp)
292
297
  new_scopes = traverse_scope(new_exp)
293
298
  if len(old_scopes) != len(new_scopes):
294
- return CHANGE_CATEGORY_BREAKING
299
+ return NodeChange(category="breaking", columns={})
295
300
 
296
301
  scope_changes_map = {}
297
302
  for old_scope, new_scope in zip(old_scopes, new_scopes):
298
303
  if old_scope.expression.key != new_scope.expression.key:
299
- scope_changes_map[new_scope] = CHANGE_CATEGORY_BREAKING
304
+ scope_changes_map[new_scope] = NodeChange(category="breaking")
300
305
  continue
301
306
  if old_scope == new_scope:
302
- scope_changes_map[new_scope] = NodeChange(category='non_breaking')
307
+ scope_changes_map[new_scope] = NodeChange(category="non_breaking")
303
308
  continue
304
309
 
305
310
  scope_type = old_scope.expression.key
306
- if scope_type == 'select':
311
+ if scope_type == "select":
307
312
  # CTE, Subquery, Root
308
313
  result = _diff_select_scope(old_scope, new_scope, scope_changes_map)
309
- elif scope_type == 'union':
314
+ elif scope_type == "union":
310
315
  # Union
311
316
  result = _diff_union_scope(old_scope, new_scope, scope_changes_map)
312
317
  else:
313
318
  if old_scope.expression != new_scope.expression:
314
- result = CHANGE_CATEGORY_BREAKING
319
+ result = NodeChange(category="breaking", columns={})
315
320
  else:
316
- result = NodeChange(category='non_breaking', columns={})
321
+ result = NodeChange(category="non_breaking", columns={})
317
322
 
318
- if result.category == 'breaking' or result.category == 'unknown':
323
+ if result.category == "unknown":
319
324
  return result
320
325
 
321
326
  scope_changes_map[new_scope] = result