recce-nightly 1.9.0.20250623__py3-none-any.whl → 1.25.0.20251112a2066__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.
Files changed (169) hide show
  1. recce/VERSION +1 -1
  2. recce/__init__.py +5 -0
  3. recce/adapter/dbt_adapter/__init__.py +318 -240
  4. recce/artifact.py +76 -3
  5. recce/cli.py +703 -71
  6. recce/config.py +3 -3
  7. recce/connect_to_cloud.py +138 -0
  8. recce/core.py +3 -3
  9. recce/data/404.html +1 -22
  10. recce/data/__next.__PAGE__.txt +10 -0
  11. recce/data/__next._full.txt +23 -0
  12. recce/data/__next._index.txt +8 -0
  13. recce/data/__next._tree.txt +12 -0
  14. recce/data/_next/static/6LypcDXgyuSaiSCrsmUub/_buildManifest.js +11 -0
  15. recce/data/_next/static/6LypcDXgyuSaiSCrsmUub/_clientMiddlewareManifest.json +1 -0
  16. recce/data/_next/static/chunks/0a2b2dd4b57049c2.js +1 -0
  17. recce/data/_next/static/chunks/19c10d219a6a21ff.js +1 -0
  18. recce/data/_next/static/chunks/24fd885c7180a612.js +1 -0
  19. recce/data/_next/static/chunks/27e66b2eab4adc32.js +19 -0
  20. recce/data/_next/static/chunks/71f88fcc615bf282.js +1 -0
  21. recce/data/_next/static/chunks/917619ab62a32388.js +1 -0
  22. recce/data/_next/static/chunks/93ba5a62932b704f.js +4 -0
  23. recce/data/_next/static/chunks/a43a2a5e06d5a92b.js +1 -0
  24. recce/data/_next/static/chunks/a6c78b24bd8b84fc.js +1 -0
  25. recce/data/_next/static/chunks/b2610ba997ff8c4f.js +110 -0
  26. recce/data/_next/static/chunks/ba2d87265a68599d.css +2 -0
  27. recce/data/_next/static/chunks/c117fd1c1382dd83.js +11 -0
  28. recce/data/_next/static/chunks/c9425ca46eebdde9.js +1 -0
  29. recce/data/_next/static/chunks/cc8a9eadba012be0.css +6 -0
  30. recce/data/_next/static/chunks/e124bccf574a3361.css +1 -0
  31. recce/data/_next/static/chunks/e392ad92847c3e17.js +1 -0
  32. recce/data/_next/static/chunks/e4ce95efe88dae79.js +11 -0
  33. recce/data/_next/static/chunks/e69c777814fea6ed.js +2 -0
  34. recce/data/_next/static/chunks/turbopack-21cfd73037ff57ab.js +3 -0
  35. recce/data/_next/static/media/favicon.a8d38d84.ico +0 -0
  36. recce/data/_next/static/media/montserrat-cyrillic-800-normal.d80d830d.woff2 +0 -0
  37. recce/data/_next/static/media/{montserrat-cyrillic-800-normal.bd5c9f50.woff → montserrat-cyrillic-800-normal.f9d58125.woff} +0 -0
  38. recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.076c2a93.woff2 +0 -0
  39. recce/data/_next/static/media/montserrat-latin-800-normal.cde454cc.woff2 +0 -0
  40. recce/data/_next/static/media/{montserrat-latin-800-normal.fc315020.woff → montserrat-latin-800-normal.d5761935.woff} +0 -0
  41. recce/data/_next/static/media/montserrat-latin-ext-800-normal.40ec0659.woff2 +0 -0
  42. recce/data/_next/static/media/{montserrat-latin-ext-800-normal.2e5381b2.woff → montserrat-latin-ext-800-normal.b671449b.woff} +0 -0
  43. recce/data/_next/static/media/{montserrat-vietnamese-800-normal.20c545e6.woff → montserrat-vietnamese-800-normal.9f7b8541.woff} +0 -0
  44. recce/data/_next/static/media/montserrat-vietnamese-800-normal.f9eb854e.woff2 +0 -0
  45. recce/data/_not-found/__next._full.txt +17 -0
  46. recce/data/_not-found/__next._index.txt +8 -0
  47. recce/data/_not-found/__next._not-found.__PAGE__.txt +5 -0
  48. recce/data/_not-found/__next._not-found.txt +4 -0
  49. recce/data/_not-found/__next._tree.txt +10 -0
  50. recce/data/_not-found.html +1 -0
  51. recce/data/_not-found.txt +17 -0
  52. recce/data/auth_callback.html +68 -0
  53. recce/data/index.html +1 -27
  54. recce/data/index.txt +23 -8
  55. recce/event/__init__.py +9 -8
  56. recce/event/collector.py +6 -2
  57. recce/event/track.py +10 -0
  58. recce/github.py +1 -1
  59. recce/mcp_server.py +632 -0
  60. recce/models/types.py +23 -2
  61. recce/pull_request.py +1 -1
  62. recce/run.py +23 -16
  63. recce/server.py +194 -19
  64. recce/state/__init__.py +31 -0
  65. recce/state/cloud.py +632 -0
  66. recce/state/const.py +26 -0
  67. recce/state/local.py +56 -0
  68. recce/state/state.py +119 -0
  69. recce/state/state_loader.py +174 -0
  70. recce/summary.py +2 -1
  71. recce/tasks/dataframe.py +59 -2
  72. recce/tasks/rowcount.py +4 -1
  73. recce/tasks/schema.py +4 -1
  74. recce/tasks/valuediff.py +1 -1
  75. recce/util/api_token.py +11 -2
  76. recce/util/breaking.py +9 -0
  77. recce/util/cll.py +1 -2
  78. recce/util/io.py +2 -2
  79. recce/util/lineage.py +19 -18
  80. recce/util/perf_tracking.py +85 -0
  81. recce/util/recce_cloud.py +229 -5
  82. recce/yaml/__init__.py +2 -2
  83. recce_cloud/__init__.py +15 -0
  84. recce_cloud/api/__init__.py +17 -0
  85. recce_cloud/api/base.py +104 -0
  86. recce_cloud/api/client.py +150 -0
  87. recce_cloud/api/exceptions.py +26 -0
  88. recce_cloud/api/factory.py +63 -0
  89. recce_cloud/api/github.py +72 -0
  90. recce_cloud/api/gitlab.py +78 -0
  91. recce_cloud/artifact.py +57 -0
  92. recce_cloud/ci_providers/__init__.py +9 -0
  93. recce_cloud/ci_providers/base.py +82 -0
  94. recce_cloud/ci_providers/detector.py +147 -0
  95. recce_cloud/ci_providers/github_actions.py +136 -0
  96. recce_cloud/ci_providers/gitlab_ci.py +130 -0
  97. recce_cloud/cli.py +303 -0
  98. recce_cloud/upload.py +213 -0
  99. {recce_nightly-1.9.0.20250623.dist-info → recce_nightly-1.25.0.20251112a2066.dist-info}/METADATA +31 -27
  100. recce_nightly-1.25.0.20251112a2066.dist-info/RECORD +178 -0
  101. {recce_nightly-1.9.0.20250623.dist-info → recce_nightly-1.25.0.20251112a2066.dist-info}/top_level.txt +1 -0
  102. tests/adapter/dbt_adapter/test_dbt_cll.py +412 -79
  103. tests/recce_cloud/__init__.py +0 -0
  104. tests/recce_cloud/test_ci_providers.py +351 -0
  105. tests/recce_cloud/test_cli.py +372 -0
  106. tests/recce_cloud/test_client.py +273 -0
  107. tests/recce_cloud/test_platform_clients.py +279 -0
  108. tests/test_cli.py +106 -3
  109. tests/test_cli_mcp_optional.py +45 -0
  110. tests/test_cloud_listing_cli.py +324 -0
  111. tests/test_connect_to_cloud.py +82 -0
  112. tests/test_core.py +148 -3
  113. tests/test_mcp_server.py +332 -0
  114. tests/test_server.py +6 -6
  115. tests/test_summary.py +14 -6
  116. recce/data/_next/static/WrRUb3nV8BhAZG_R8kVma/_buildManifest.js +0 -1
  117. recce/data/_next/static/chunks/181-acc61ddada3bc0ca.js +0 -43
  118. recce/data/_next/static/chunks/1bff33f1-1ef85cf5e658a751.js +0 -1
  119. recce/data/_next/static/chunks/217-879a84d70f7a907c.js +0 -2
  120. recce/data/_next/static/chunks/29e3cc0d-60045b2e47aa3916.js +0 -1
  121. recce/data/_next/static/chunks/36e1c10d-8e7be4a6c1f6ab2d.js +0 -1
  122. recce/data/_next/static/chunks/3998a672-03adacad07b346ac.js +0 -1
  123. recce/data/_next/static/chunks/3a92ee20-1081c360214f9602.js +0 -1
  124. recce/data/_next/static/chunks/42-cd3c06533f5fd47c.js +0 -9
  125. recce/data/_next/static/chunks/450c323b-fd94e7ffaa4a5efa.js +0 -1
  126. recce/data/_next/static/chunks/47d8844f-929aed9b1c73a905.js +0 -1
  127. recce/data/_next/static/chunks/608-3b079b544e5d5f5e.js +0 -15
  128. recce/data/_next/static/chunks/6dc81886-adbfa45836061d79.js +0 -1
  129. recce/data/_next/static/chunks/7a8a3e83-edf6dc64b5d5f0a5.js +0 -1
  130. recce/data/_next/static/chunks/7f27ae6c-d5f0438edd5c2a5b.js +0 -1
  131. recce/data/_next/static/chunks/86730205-cfb14e3f051bab35.js +0 -1
  132. recce/data/_next/static/chunks/8d700b6a.8bb140898499c512.js +0 -1
  133. recce/data/_next/static/chunks/92-7ab55ae02606193c.js +0 -1
  134. recce/data/_next/static/chunks/9746af58-a42b7d169cacadf0.js +0 -1
  135. recce/data/_next/static/chunks/a30376cd-de84559016d7e133.js +0 -1
  136. recce/data/_next/static/chunks/app/_not-found/page-01ed58b7f971d311.js +0 -1
  137. recce/data/_next/static/chunks/app/layout-177a410a97e0d018.js +0 -1
  138. recce/data/_next/static/chunks/app/page-59241c42b7dd4fcf.js +0 -1
  139. recce/data/_next/static/chunks/b63b1b3f-4282bdcf459e075c.js +0 -1
  140. recce/data/_next/static/chunks/bbda5537-9ec25eb1dd62348a.js +0 -1
  141. recce/data/_next/static/chunks/c132bf7d-08cb668a789d6afd.js +0 -1
  142. recce/data/_next/static/chunks/ce84277d-2e5d1d46910cf052.js +0 -1
  143. recce/data/_next/static/chunks/febdd86e-c6b525341634b860.js +0 -54
  144. recce/data/_next/static/chunks/fee69bc6-2dbccaf9b90474e6.js +0 -1
  145. recce/data/_next/static/chunks/framework-ded83d71b51ce901.js +0 -1
  146. recce/data/_next/static/chunks/main-app-39061b0166c47f55.js +0 -1
  147. recce/data/_next/static/chunks/main-b5b3ae20a1405261.js +0 -1
  148. recce/data/_next/static/chunks/pages/_app-437c455677d62394.js +0 -1
  149. recce/data/_next/static/chunks/pages/_error-e7650df18ca04bde.js +0 -1
  150. recce/data/_next/static/chunks/webpack-7b49d5ba7e3a434d.js +0 -1
  151. recce/data/_next/static/css/17a96168e3a9db13.css +0 -1
  152. recce/data/_next/static/css/1b121dc4d36aeb4d.css +0 -3
  153. recce/data/_next/static/css/35c6679a098e1e34.css +0 -1
  154. recce/data/_next/static/css/951e2e0eea2d4a5b.css +0 -14
  155. recce/data/_next/static/media/montserrat-cyrillic-800-normal.22628180.woff2 +0 -0
  156. recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.94a63aea.woff2 +0 -0
  157. recce/data/_next/static/media/montserrat-latin-800-normal.6f8fa298.woff2 +0 -0
  158. recce/data/_next/static/media/montserrat-latin-ext-800-normal.013b84f9.woff2 +0 -0
  159. recce/data/_next/static/media/montserrat-vietnamese-800-normal.c0035377.woff2 +0 -0
  160. recce/state.py +0 -785
  161. recce_nightly-1.9.0.20250623.dist-info/RECORD +0 -151
  162. tests/test_state.py +0 -134
  163. /recce/data/_next/static/{WrRUb3nV8BhAZG_R8kVma → 6LypcDXgyuSaiSCrsmUub}/_ssgManifest.js +0 -0
  164. /recce/data/_next/static/chunks/{polyfills-42372ed130431b0a.js → a6dad97d9634a72d.js} +0 -0
  165. /recce/data/_next/static/media/{montserrat-cyrillic-ext-800-normal.e6e0d8d0.woff → montserrat-cyrillic-ext-800-normal.a4fa76b5.woff} +0 -0
  166. /recce/data/_next/static/media/{reload-image.79aabb7d.svg → reload-image.7aa931c7.svg} +0 -0
  167. {recce_nightly-1.9.0.20250623.dist-info → recce_nightly-1.25.0.20251112a2066.dist-info}/WHEEL +0 -0
  168. {recce_nightly-1.9.0.20250623.dist-info → recce_nightly-1.25.0.20251112a2066.dist-info}/entry_points.txt +0 -0
  169. {recce_nightly-1.9.0.20250623.dist-info → recce_nightly-1.25.0.20251112a2066.dist-info}/licenses/LICENSE +0 -0
@@ -1,34 +1,80 @@
1
1
  from recce.adapter.dbt_adapter import DbtAdapter
2
2
  from recce.models.types import CllData
3
-
4
-
5
- def assert_column(result: CllData, node_name, column_name, transformation_type, depends_on):
6
- column_id = f"{node_name}_{column_name}"
3
+ from recce.util.lineage import build_column_key
4
+
5
+
6
+ def assert_parent_map(result: CllData, node_or_column_id, parents):
7
+ a_parents = result.parent_map.get(node_or_column_id) or set()
8
+
9
+ assert len(a_parents) == len(parents), "parents length mismatch"
10
+ for parent in parents:
11
+ if isinstance(parent, str):
12
+ node_id = parent
13
+ assert node_id in a_parents, f"Node {node_id} not found in parent list"
14
+ elif len(parent) == 1:
15
+ (node_id,) = parent
16
+ assert node_id in a_parents, f"Column {parent} not found in parent list"
17
+ elif len(parent) == 2:
18
+ node, column = parent
19
+ column_id = build_column_key(node, column)
20
+ assert column_id in a_parents, f"Column {column_id} not found in parent list for {node_or_column_id}"
21
+ else:
22
+ raise ValueError(f"Invalid parent format: {parent}. Expected node_id or (node_id, column_name).")
23
+
24
+
25
+ def assert_column(
26
+ result: CllData,
27
+ node_id,
28
+ column_name,
29
+ transformation_type=None,
30
+ change_status=None,
31
+ parents=None,
32
+ ):
33
+ column_id = build_column_key(node_id, column_name)
7
34
  entry = result.columns.get(column_id)
8
35
  assert entry is not None, f"Column {column_id} not found in result"
9
36
  assert (
10
37
  entry.transformation_type == transformation_type
11
38
  ), f"Column {column_name} type mismatch: expected {transformation_type}, got {entry.transformation_type}"
12
- parents = result.parent_map.get(column_id)
39
+ assert_parent_map(result, column_id, parents)
40
+ assert (
41
+ entry.change_status == change_status
42
+ ), f"Column {column_name} change status mismatch: expected {change_status}, got {entry.change_status}"
13
43
 
14
- assert len(parents) == len(depends_on), "depends_on length mismatch"
15
- for i in range(len(depends_on)):
16
- node, column = depends_on[i]
17
- parent_column_id = f"{node}_{column}"
18
44
 
19
- assert parent_column_id in parents, f"Column {parent_column_id} not found in {column_id}'s parent list"
45
+ def assert_model(
46
+ result: CllData,
47
+ node_id,
48
+ change_category=None,
49
+ impacted=None,
50
+ parents=None,
51
+ ):
52
+ entry = result.nodes.get(node_id)
53
+ assert entry is not None, f"Node {node_id} not found in result"
54
+ assert_parent_map(result, node_id, parents)
20
55
 
56
+ assert (
57
+ entry.change_category == change_category
58
+ ), f"Node {node_id} change category mismatch: expected {change_category}, got {entry.change_category}"
21
59
 
22
- def assert_model(result: CllData, node_name, depends_on):
23
- assert result.nodes.get(node_name) is not None, f"Node {node_name} not found in result"
24
- parent_map = result.parent_map.get(node_name)
25
- assert parent_map is not None, f"Parent map {node_name} not found in result"
26
- # assert len(parent_map) == len(depends_on), "depends_on length mismatch"
27
- for i in range(len(depends_on)):
28
- node, column = depends_on[i]
29
- column_id = f"{node}_{column}"
60
+ assert (
61
+ entry.impacted == impacted
62
+ ), f"Node {node_id} impacted status mismatch: expected {impacted}, got {entry.impacted}"
30
63
 
31
- assert column_id in parent_map, f"Parent map {node_name} does not contain {column_id}"
64
+
65
+ def assert_cll_contain_nodes(cll_data: CllData, nodes):
66
+ assert len(nodes) == len(cll_data.nodes), "Model count mismatch"
67
+ for node in nodes:
68
+ assert node in cll_data.nodes, f"Model {node} not found in lineage"
69
+
70
+
71
+ def assert_cll_contain_columns(cll_data: CllData, columns):
72
+ assert len(columns) == len(cll_data.columns), "Column count mismatch"
73
+ for column in columns:
74
+ column_key = f"{column[0]}_{column[1]}"
75
+ assert column_key in cll_data.columns, f"Column {column} not found in lineage"
76
+ assert column[0] == cll_data.columns[column_key].table_id, f"Column {column[0]} node mismatch"
77
+ assert column[1] == cll_data.columns[column_key].name, f"Column {column[1]} name mismatch"
32
78
 
33
79
 
34
80
  def test_cll_basic(dbt_test_helper):
@@ -51,13 +97,12 @@ def test_cll_basic(dbt_test_helper):
51
97
  )
52
98
  adapter: DbtAdapter = dbt_test_helper.context.adapter
53
99
 
54
- result = adapter.get_cll("model.model2", no_filter=True)
55
- assert_model(result, "model.model2", [("model.model1", "c")])
56
- assert_column(result, "model.model2", "c", "passthrough", [("model.model1", "c")])
100
+ result = adapter.get_cll("model.model1", "c")
101
+ assert_model(result, "model.model2", parents=[("model.model1", "c")])
102
+ assert_column(result, "model.model2", "c", transformation_type="passthrough", parents=[("model.model1", "c")])
57
103
 
58
- result = adapter.get_cll("model.model3", no_filter=True)
59
- assert_model(result, "model.model3", [("model.model1", "c")])
60
- assert_column(result, "model.model3", "c", "passthrough", [("model.model1", "c")])
104
+ assert_model(result, "model.model3", parents=[("model.model1", "c")])
105
+ assert_column(result, "model.model3", "c", transformation_type="passthrough", parents=[("model.model1", "c")])
61
106
 
62
107
 
63
108
  def test_cll_table_alisa(dbt_test_helper):
@@ -75,8 +120,8 @@ def test_cll_table_alisa(dbt_test_helper):
75
120
  depends_on=["model.model1"],
76
121
  )
77
122
  adapter: DbtAdapter = dbt_test_helper.context.adapter
78
- result = adapter.get_cll("model.model1", no_filter=True)
79
- assert_column(result, "model.model2", "c", "passthrough", [("model.model1", "c")])
123
+ result = adapter.get_cll("model.model1", "c")
124
+ assert_column(result, "model.model2", "c", transformation_type="passthrough", parents=[("model.model1", "c")])
80
125
 
81
126
 
82
127
  def test_seed(dbt_test_helper):
@@ -103,11 +148,17 @@ def test_seed(dbt_test_helper):
103
148
  )
104
149
  adapter: DbtAdapter = dbt_test_helper.context.adapter
105
150
 
106
- result = adapter.get_cll("model.model1", no_filter=True)
107
- assert_model(result, "seed.seed1", [])
108
- assert_column(result, "seed.seed1", "customer_id", "source", [])
109
- assert_model(result, "model.model1", [("seed.seed1", "age")])
110
- assert_column(result, "model.model1", "customer_id", "passthrough", [("seed.seed1", "customer_id")])
151
+ result = adapter.get_cll("model.model1")
152
+ assert_model(result, "seed.seed1", parents=[])
153
+ assert_column(result, "seed.seed1", "customer_id", transformation_type="source", parents=[])
154
+ assert_model(result, "model.model1", parents=["seed.seed1", ("seed.seed1", "age")])
155
+ assert_column(
156
+ result,
157
+ "model.model1",
158
+ "customer_id",
159
+ transformation_type="passthrough",
160
+ parents=[("seed.seed1", "customer_id")],
161
+ )
111
162
 
112
163
 
113
164
  def test_python_model(dbt_test_helper):
@@ -134,8 +185,9 @@ def test_python_model(dbt_test_helper):
134
185
  assert not adapter.is_python_model("model1")
135
186
  assert adapter.is_python_model("model2")
136
187
 
137
- result = adapter.get_cll("model1", no_filter=True)
138
- assert_column(result, "model2", "customer_id", "unknown", [])
188
+ result = adapter.get_cll("model2")
189
+ assert_model(result, "model2", parents=["model1"])
190
+ assert_column(result, "model2", "customer_id", transformation_type="unknown", parents=[])
139
191
 
140
192
 
141
193
  def test_source(dbt_test_helper):
@@ -161,43 +213,33 @@ def test_source(dbt_test_helper):
161
213
  depends_on=["source.source1.table1"],
162
214
  )
163
215
  adapter: DbtAdapter = dbt_test_helper.context.adapter
164
- result = adapter.get_cll("source.source1.table1", no_filter=True)
165
- assert_column(result, "source.source1.table1", "customer_id", "source", [])
166
- result = adapter.get_cll("model.model1", no_filter=True)
167
- assert_column(result, "model.model1", "customer_id", "passthrough", [("source.source1.table1", "customer_id")])
216
+ result = adapter.get_cll("model.model1")
217
+ assert_column(result, "source.source1.table1", "customer_id", transformation_type="source", parents=[])
218
+ assert_column(
219
+ result,
220
+ "model.model1",
221
+ "customer_id",
222
+ transformation_type="passthrough",
223
+ parents=[("source.source1.table1", "customer_id")],
224
+ )
168
225
 
169
226
 
170
227
  def test_parse_error(dbt_test_helper):
171
228
  dbt_test_helper.create_model("model1", curr_sql="select 1 as c", curr_columns={"c": "int"})
172
229
  dbt_test_helper.create_model("model2", curr_sql="this is not a valid sql", curr_columns={"c": "int"})
173
230
  adapter: DbtAdapter = dbt_test_helper.context.adapter
174
- result = adapter.get_cll("model2", no_filter=True)
175
- assert_column(result, "model2", "c", "unknown", [])
231
+ result = adapter.get_cll("model2")
232
+ assert_column(result, "model2", "c", transformation_type="unknown", parents=[])
176
233
 
177
234
 
178
235
  def test_model_without_catalog(dbt_test_helper):
179
236
  dbt_test_helper.create_model("model1", curr_sql="select 1 as c")
180
237
  adapter: DbtAdapter = dbt_test_helper.context.adapter
181
- result = adapter.get_cll("model1", no_filter=True)
238
+ result = adapter.get_cll("model1")
182
239
  assert not result.nodes["model1"].columns
183
240
 
184
241
 
185
- def assert_lineage_model(cll_data: CllData, nodes):
186
- assert len(nodes) == len(cll_data.nodes), "Model count mismatch"
187
- for node in nodes:
188
- assert node in cll_data.nodes, f"Model {node} not found in lineage"
189
-
190
-
191
- def assert_lineage_column(cll_data: CllData, columns):
192
- assert len(columns) == len(cll_data.columns), "Column count mismatch"
193
- for column in columns:
194
- column_key = f"{column[0]}_{column[1]}"
195
- assert column_key in cll_data.columns, f"Column {column} not found in lineage"
196
- assert column[0] == cll_data.columns[column_key].table_id, f"Column {column[0]} node mismatch"
197
- assert column[1] == cll_data.columns[column_key].name, f"Column {column[1]} name mismatch"
198
-
199
-
200
- def test_cll_column_filter(dbt_test_helper):
242
+ def test_column_level_lineage(dbt_test_helper):
201
243
  dbt_test_helper.create_model(
202
244
  "model1", unique_id="model.model1", curr_sql="select 1 as c", curr_columns={"c": "int"}
203
245
  )
@@ -226,19 +268,300 @@ def test_cll_column_filter(dbt_test_helper):
226
268
  adapter: DbtAdapter = dbt_test_helper.context.adapter
227
269
 
228
270
  result = adapter.get_cll("model.model2", "c")
229
- assert_lineage_model(result, [])
230
- assert_lineage_column(result, [("model.model1", "c"), ("model.model2", "c"), ("model.model3", "c")])
271
+ assert_cll_contain_nodes(result, [])
272
+ assert_cll_contain_columns(result, [("model.model1", "c"), ("model.model2", "c"), ("model.model3", "c")])
273
+ assert_column(result, "model.model2", "c", transformation_type="passthrough", parents=[("model.model1", "c")])
231
274
 
232
275
  result = adapter.get_cll("model.model2", "y")
233
- assert_lineage_model(result, ["model.model3"])
234
- assert_lineage_column(result, [("model.model2", "y"), ("model.model4", "y")])
276
+ assert_cll_contain_nodes(result, ["model.model3"])
277
+ assert_cll_contain_columns(result, [("model.model2", "y"), ("model.model4", "y")])
278
+ assert_column(result, "model.model2", "y", transformation_type="source", parents=[])
235
279
 
236
280
  result = adapter.get_cll("model.model3", "c")
237
- assert_lineage_model(result, [])
238
- assert_lineage_column(result, [("model.model1", "c"), ("model.model2", "c"), ("model.model3", "c")])
281
+ assert_cll_contain_nodes(result, [])
282
+ assert_cll_contain_columns(result, [("model.model1", "c"), ("model.model2", "c"), ("model.model3", "c")])
283
+ assert_column(result, "model.model2", "c", transformation_type="passthrough", parents=[("model.model1", "c")])
284
+
285
+ result = adapter.get_cll("model.model2", "c", no_upstream=True, no_downstream=True)
286
+ assert_cll_contain_nodes(result, [])
287
+ assert_cll_contain_columns(result, [("model.model2", "c")])
288
+ assert_column(result, "model.model2", "c", transformation_type="passthrough", parents=[])
289
+
290
+
291
+ def test_impact_radius_no_change_analysis_no_cll(dbt_test_helper):
292
+ dbt_test_helper.create_model(
293
+ "model1",
294
+ unique_id="model.model1",
295
+ curr_sql="select 1 as c",
296
+ base_sql="select 1 as c --- non-breaking",
297
+ curr_columns={"c": "int"},
298
+ base_columns={"c": "int"},
299
+ )
300
+ dbt_test_helper.create_model(
301
+ "model2",
302
+ unique_id="model.model2",
303
+ curr_sql='select c, 2025 as y from {{ ref("model1") }}',
304
+ base_sql='select c, 2025 as y from {{ ref("model1") }} where c > 0 --- breaking',
305
+ curr_columns={"c": "int", "y": "int"},
306
+ base_columns={"c": "int", "y": "int"},
307
+ depends_on=["model.model1"],
308
+ )
309
+ dbt_test_helper.create_model(
310
+ "model3",
311
+ unique_id="model.model3",
312
+ curr_sql='select c from {{ ref("model2") }} where y < 2025',
313
+ base_sql='select c from {{ ref("model2") }} where y < 2025',
314
+ curr_columns={"c": "int"},
315
+ base_columns={"c": "int"},
316
+ depends_on=["model.model2"],
317
+ )
318
+ dbt_test_helper.create_model(
319
+ "model4",
320
+ unique_id="model.model4",
321
+ curr_sql='select y + 1 as year from {{ ref("model2") }} --- partial breaking',
322
+ base_sql='select y as year from {{ ref("model2") }}',
323
+ curr_columns={"year": "int"},
324
+ base_columns={"year": "int"},
325
+ depends_on=["model.model2"],
326
+ )
327
+ dbt_test_helper.create_model(
328
+ "model5",
329
+ unique_id="model.model5",
330
+ curr_sql='select c, 2025 as y from {{ ref("model1") }}',
331
+ base_sql='select c, 2025 as y from {{ ref("model1") }}',
332
+ curr_columns={"c": "int", "y": "int"},
333
+ base_columns={"c": "int", "y": "int"},
334
+ depends_on=["model.model1"],
335
+ )
336
+
337
+ adapter: DbtAdapter = dbt_test_helper.context.adapter
338
+
339
+ result = adapter.get_cll(no_cll=True)
340
+ assert_cll_contain_nodes(result, ["model.model1", "model.model2", "model.model3", "model.model4", "model.model5"])
341
+ assert_model(result, "model.model1", parents=[])
342
+ assert_model(result, "model.model2", parents=["model.model1"])
343
+ assert_model(result, "model.model3", parents=["model.model2"])
344
+ assert_model(result, "model.model4", parents=["model.model2"])
345
+ assert_model(result, "model.model5", parents=["model.model1"])
346
+
347
+
348
+ def test_impact_radius_with_change_analysis_no_cll(dbt_test_helper):
349
+ dbt_test_helper.create_model(
350
+ "model1",
351
+ unique_id="model.model1",
352
+ curr_sql="select 1 as c",
353
+ base_sql="select 1 as c --- non-breaking",
354
+ curr_columns={"c": "int"},
355
+ base_columns={"c": "int"},
356
+ )
357
+ dbt_test_helper.create_model(
358
+ "model2",
359
+ unique_id="model.model2",
360
+ curr_sql='select c, 2025 as y from {{ ref("model1") }}',
361
+ base_sql='select c, 2025 as y from {{ ref("model1") }} where c > 0 --- breaking',
362
+ curr_columns={"c": "int", "y": "int"},
363
+ base_columns={"c": "int", "y": "int"},
364
+ depends_on=["model.model1"],
365
+ )
366
+ dbt_test_helper.create_model(
367
+ "model3",
368
+ unique_id="model.model3",
369
+ curr_sql='select c from {{ ref("model2") }} where y < 2025',
370
+ base_sql='select c from {{ ref("model2") }} where y < 2025',
371
+ curr_columns={"c": "int"},
372
+ base_columns={"c": "int"},
373
+ depends_on=["model.model2"],
374
+ )
375
+ dbt_test_helper.create_model(
376
+ "model4",
377
+ unique_id="model.model4",
378
+ curr_sql='select y + 1 as year from {{ ref("model2") }} --- partial breaking',
379
+ base_sql='select y as year from {{ ref("model2") }}',
380
+ curr_columns={"year": "int"},
381
+ base_columns={"year": "int"},
382
+ depends_on=["model.model2"],
383
+ )
384
+ dbt_test_helper.create_model(
385
+ "model5",
386
+ unique_id="model.model5",
387
+ curr_sql='select c, 2025 as y from {{ ref("model1") }}',
388
+ base_sql='select c, 2025 as y from {{ ref("model1") }}',
389
+ curr_columns={"c": "int", "y": "int"},
390
+ base_columns={"c": "int", "y": "int"},
391
+ depends_on=["model.model1"],
392
+ )
393
+
394
+ adapter: DbtAdapter = dbt_test_helper.context.adapter
395
+
396
+ # breaking
397
+ result = adapter.get_cll(change_analysis=True, no_cll=True, no_upstream=True)
398
+ assert_cll_contain_nodes(result, ["model.model1", "model.model2", "model.model3", "model.model4"])
399
+ assert_model(result, "model.model1", parents=[], change_category="non_breaking", impacted=False)
400
+ assert_model(result, "model.model2", parents=[], change_category="breaking", impacted=True)
401
+ assert_model(result, "model.model3", parents=["model.model2"], change_category=None, impacted=True)
402
+ assert_model(result, "model.model4", parents=["model.model2"], change_category="partial_breaking", impacted=True)
403
+
404
+
405
+ def test_impact_radius_with_change_analysis_no_cll_2(dbt_test_helper):
406
+ # partial breaking
407
+ dbt_test_helper.create_model(
408
+ "model1",
409
+ unique_id="model.model1",
410
+ curr_sql="select 1 as c",
411
+ base_sql="select 2 as c",
412
+ curr_columns={"c": "int"},
413
+ base_columns={"c": "int"},
414
+ )
415
+ # breaking
416
+ dbt_test_helper.create_model(
417
+ "model2",
418
+ unique_id="model.model2",
419
+ curr_sql='select c, 2025 as y from {{ ref("model1") }}',
420
+ base_sql='select c, 2025 as y from {{ ref("model1") }} where c > 0 --- breaking',
421
+ curr_columns={"c": "int", "y": "int"},
422
+ base_columns={"c": "int", "y": "int"},
423
+ depends_on=["model.model1"],
424
+ )
425
+ # no change
426
+ dbt_test_helper.create_model(
427
+ "model3",
428
+ unique_id="model.model3",
429
+ curr_sql='select c from {{ ref("model2") }} where y < 2025',
430
+ base_sql='select c from {{ ref("model2") }} where y < 2025',
431
+ curr_columns={"c": "int"},
432
+ base_columns={"c": "int"},
433
+ depends_on=["model.model2"],
434
+ )
435
+ # partial breaking
436
+ dbt_test_helper.create_model(
437
+ "model4",
438
+ unique_id="model.model4",
439
+ curr_sql='select y + 1 as year from {{ ref("model2") }} --- partial breaking',
440
+ base_sql='select y as year from {{ ref("model2") }}',
441
+ curr_columns={"year": "int"},
442
+ base_columns={"year": "int"},
443
+ depends_on=["model.model2"],
444
+ )
445
+ # no change
446
+ dbt_test_helper.create_model(
447
+ "model5",
448
+ unique_id="model.model5",
449
+ curr_sql='select c, 2025 as y from {{ ref("model1") }}',
450
+ base_sql='select c, 2025 as y from {{ ref("model1") }}',
451
+ curr_columns={"c": "int", "y": "int"},
452
+ base_columns={"c": "int", "y": "int"},
453
+ depends_on=["model.model1"],
454
+ )
455
+
456
+ adapter: DbtAdapter = dbt_test_helper.context.adapter
457
+
458
+ # breaking
459
+ result = adapter.get_cll(change_analysis=True, no_cll=True, no_upstream=True)
460
+ assert_cll_contain_nodes(result, ["model.model1", "model.model2", "model.model3", "model.model4", "model.model5"])
461
+ assert_model(result, "model.model1", parents=[], change_category="partial_breaking", impacted=True)
462
+ assert_model(result, "model.model2", parents=["model.model1"], change_category="breaking", impacted=True)
463
+ assert_model(result, "model.model3", parents=["model.model2"], change_category=None, impacted=True)
464
+ assert_model(result, "model.model4", parents=["model.model2"], change_category="partial_breaking", impacted=True)
465
+ assert_model(result, "model.model5", parents=["model.model1"], impacted=True)
466
+
467
+
468
+ def test_impact_radius_with_change_analysis_with_cll(dbt_test_helper):
469
+ dbt_test_helper.create_model(
470
+ "model1",
471
+ unique_id="model.model1",
472
+ curr_sql="select 1 as c",
473
+ base_sql="select 1 as c --- non-breaking",
474
+ curr_columns={"c": "int"},
475
+ base_columns={"c": "int"},
476
+ )
477
+ dbt_test_helper.create_model(
478
+ "model2",
479
+ unique_id="model.model2",
480
+ curr_sql='select c, 2025 as y from {{ ref("model1") }}',
481
+ base_sql='select c, 2025 as y from {{ ref("model1") }} where c > 0 --- breaking',
482
+ curr_columns={"c": "int", "y": "int"},
483
+ base_columns={"c": "int", "y": "int"},
484
+ depends_on=["model.model1"],
485
+ )
486
+ dbt_test_helper.create_model(
487
+ "model3",
488
+ unique_id="model.model3",
489
+ curr_sql='select c from {{ ref("model2") }} where y < 2025',
490
+ base_sql='select c from {{ ref("model2") }} where y < 2025',
491
+ curr_columns={"c": "int"},
492
+ base_columns={"c": "int"},
493
+ depends_on=["model.model2"],
494
+ )
495
+ dbt_test_helper.create_model(
496
+ "model4",
497
+ unique_id="model.model4",
498
+ curr_sql='select y + 1 as year from {{ ref("model2") }} --- partial breaking',
499
+ base_sql='select y as year from {{ ref("model2") }}',
500
+ curr_columns={"year": "int"},
501
+ base_columns={"year": "int"},
502
+ depends_on=["model.model2"],
503
+ )
504
+ dbt_test_helper.create_model(
505
+ "model5",
506
+ unique_id="model.model5",
507
+ curr_sql='select c, 2025 as y from {{ ref("model1") }}',
508
+ base_sql='select c, 2025 as y from {{ ref("model1") }}',
509
+ curr_columns={"c": "int", "y": "int"},
510
+ base_columns={"c": "int", "y": "int"},
511
+ depends_on=["model.model1"],
512
+ )
513
+
514
+ adapter: DbtAdapter = dbt_test_helper.context.adapter
515
+
516
+ result = adapter.get_cll(change_analysis=True, no_upstream=True)
517
+ assert_cll_contain_nodes(result, ["model.model1", "model.model2", "model.model3", "model.model4"])
518
+ assert_cll_contain_columns(result, [("model.model4", "year")])
519
+ assert_model(result, "model.model1", parents=[], change_category="non_breaking", impacted=False)
520
+ assert_model(result, "model.model2", parents=[], change_category="breaking", impacted=True)
521
+ assert_model(result, "model.model3", parents=["model.model2"], change_category=None, impacted=True)
522
+ assert_model(result, "model.model4", parents=["model.model2"], change_category="partial_breaking", impacted=True)
523
+
524
+
525
+ def test_impact_radius_with_change_analysis_with_cll_added_removed(dbt_test_helper):
526
+ # rename model
527
+ dbt_test_helper.create_model(
528
+ "model1",
529
+ unique_id="model.model1",
530
+ base_sql="select 1 as c",
531
+ base_columns={"c": "int"},
532
+ )
533
+ dbt_test_helper.create_model(
534
+ "model1_v2",
535
+ unique_id="model.model1_v2",
536
+ curr_sql="select 1 as c",
537
+ curr_columns={"c": "int"},
538
+ )
539
+ # change upstream
540
+ dbt_test_helper.create_model(
541
+ "model2",
542
+ unique_id="model.model2",
543
+ base_sql='select c from {{ ref("model1") }}',
544
+ base_columns={"c": "int"},
545
+ depends_on=["model.model1"],
546
+ )
547
+ dbt_test_helper.create_model(
548
+ "model2",
549
+ unique_id="model.model2",
550
+ curr_sql='select c from {{ ref("model1_v2") }}',
551
+ curr_columns={"c": "int"},
552
+ depends_on=["model.model1_v2"],
553
+ )
554
+
555
+ adapter: DbtAdapter = dbt_test_helper.context.adapter
556
+ result = adapter.get_cll(change_analysis=True, no_upstream=True)
557
+ assert_model(result, "model.model1_v2", parents=[], impacted=True)
558
+ assert_column(result, "model.model1_v2", "c", transformation_type="source", change_status="added", parents=[])
559
+ assert_model(result, "model.model2", parents=["model.model1_v2"], change_category="breaking", impacted=True)
560
+ assert_cll_contain_nodes(result, ["model.model1_v2", "model.model2"])
561
+ assert_cll_contain_columns(result, [("model.model1_v2", "c"), ("model.model2", "c")])
239
562
 
240
563
 
241
- def test_impact_radius_nodes(dbt_test_helper):
564
+ def test_impact_radius_by_node_no_cll(dbt_test_helper):
242
565
  # non-breaking
243
566
  dbt_test_helper.create_model(
244
567
  "model1",
@@ -280,15 +603,15 @@ def test_impact_radius_nodes(dbt_test_helper):
280
603
  adapter: DbtAdapter = dbt_test_helper.context.adapter
281
604
 
282
605
  # breaking
283
- result = adapter.get_impacted_nodes("model.model2")
284
- assert_lineage_model(result, ["model.model2", "model.model3", "model.model4"])
606
+ result = adapter.get_cll(node_id="model.model2", change_analysis=True, no_cll=True, no_upstream=True)
607
+ assert_cll_contain_nodes(result, ["model.model2", "model.model3", "model.model4"])
285
608
 
286
609
  # non-breaking
287
- result = adapter.get_impacted_nodes("model.model1")
288
- assert_lineage_model(result, [])
610
+ result = adapter.get_cll(node_id="model.model1", change_analysis=True, no_cll=True, no_upstream=True)
611
+ assert_cll_contain_nodes(result, ["model.model1"])
289
612
 
290
613
 
291
- def test_impact_radius_columns(dbt_test_helper):
614
+ def test_impact_radius_by_node_with_cll(dbt_test_helper):
292
615
  # added column
293
616
  dbt_test_helper.create_model(
294
617
  "model1",
@@ -329,16 +652,21 @@ def test_impact_radius_columns(dbt_test_helper):
329
652
 
330
653
  adapter: DbtAdapter = dbt_test_helper.context.adapter
331
654
 
332
- result = adapter.get_impacted_cll("model.model2")
333
- assert_lineage_model(result, ["model.model3"])
334
- assert_lineage_column(result, [("model.model2", "y"), ("model.model4", "y")])
655
+ result = adapter.get_cll(node_id="model.model2", change_analysis=True, no_upstream=True)
656
+ assert_model(result, "model.model2", parents=[], change_category="partial_breaking", impacted=False)
657
+ assert_model(result, "model.model3", parents=[("model.model2", "y")], impacted=True)
658
+ assert_column(result, "model.model2", "y", transformation_type="source", parents=[], change_status="modified")
659
+ assert_cll_contain_nodes(result, ["model.model2", "model.model3"])
660
+ assert_cll_contain_columns(result, [("model.model2", "y"), ("model.model4", "y")])
335
661
 
336
- result = adapter.get_impacted_cll("model.model1")
337
- assert_lineage_model(result, [])
338
- assert_lineage_column(result, [("model.model1", "d")])
662
+ result = adapter.get_cll(node_id="model.model1", change_analysis=True, no_upstream=True)
663
+ assert_cll_contain_nodes(result, ["model.model1"])
664
+ assert_cll_contain_columns(result, [("model.model1", "d")])
665
+ assert_model(result, "model.model1", parents=[], change_category="non_breaking", impacted=False)
666
+ assert_column(result, "model.model1", "d", transformation_type="source", parents=[], change_status="added")
339
667
 
340
668
 
341
- def test_impact_radius(dbt_test_helper):
669
+ def test_impact_radius_by_node_with_cll_2(dbt_test_helper):
342
670
  # added column
343
671
  dbt_test_helper.create_model(
344
672
  "model1",
@@ -379,6 +707,11 @@ def test_impact_radius(dbt_test_helper):
379
707
 
380
708
  adapter: DbtAdapter = dbt_test_helper.context.adapter
381
709
 
382
- result = adapter.get_impact_radius("model.model2")
383
- assert_lineage_model(result, ["model.model2", "model.model3", "model.model4"])
384
- assert_lineage_column(result, [("model.model2", "d"), ("model.model2", "y"), ("model.model4", "y")])
710
+ result = adapter.get_cll(node_id="model.model2", change_analysis=True, no_upstream=True)
711
+ assert_model(result, "model.model2", parents=[], change_category="breaking", impacted=True)
712
+ assert_column(result, "model.model2", "y", transformation_type="source", parents=[], change_status="modified")
713
+ assert_model(result, "model.model3", parents=["model.model2", ("model.model2", "y")], impacted=True)
714
+ assert_model(result, "model.model4", parents=["model.model2"], impacted=True)
715
+ assert_column(result, "model.model4", "y", transformation_type="passthrough", parents=[("model.model2", "y")])
716
+ assert_cll_contain_nodes(result, ["model.model2", "model.model3", "model.model4"])
717
+ assert_cll_contain_columns(result, [("model.model2", "d"), ("model.model2", "y"), ("model.model4", "y")])
File without changes