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
@@ -1,27 +1,127 @@
1
1
  from recce.adapter.dbt_adapter import DbtAdapter
2
+ from recce.models.types import CllData
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)
34
+ entry = result.columns.get(column_id)
35
+ assert entry is not None, f"Column {column_id} not found in result"
36
+ assert (
37
+ entry.transformation_type == transformation_type
38
+ ), f"Column {column_name} type mismatch: expected {transformation_type}, got {entry.transformation_type}"
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}"
43
+
44
+
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)
55
+
56
+ assert (
57
+ entry.change_category == change_category
58
+ ), f"Node {node_id} change category mismatch: expected {change_category}, got {entry.change_category}"
59
+
60
+ assert (
61
+ entry.impacted == impacted
62
+ ), f"Node {node_id} impacted status mismatch: expected {impacted}, got {entry.impacted}"
63
+
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"
2
78
 
3
79
 
4
80
  def test_cll_basic(dbt_test_helper):
5
- dbt_test_helper.create_model("model1", curr_sql="select 1 as c", curr_columns={"c": "int"})
6
- dbt_test_helper.create_model("model2", curr_sql='select c from {{ ref("model1") }}', curr_columns={"c": "int"},
7
- depends_on=["model1"])
81
+ dbt_test_helper.create_model(
82
+ "model1", unique_id="model.model1", curr_sql="select 1 as c", curr_columns={"c": "int"}
83
+ )
84
+ dbt_test_helper.create_model(
85
+ "model2",
86
+ unique_id="model.model2",
87
+ curr_sql='select c from {{ ref("model1") }} where c > 0',
88
+ curr_columns={"c": "int"},
89
+ depends_on=["model.model1"],
90
+ )
91
+ dbt_test_helper.create_model(
92
+ "model3",
93
+ unique_id="model.model3",
94
+ curr_sql='select c from {{ ref("recce_test", "model1") }} where c > 0',
95
+ curr_columns={"c": "int"},
96
+ depends_on=["model.model1"],
97
+ )
8
98
  adapter: DbtAdapter = dbt_test_helper.context.adapter
9
- result = adapter.get_cll_by_node_id("model1")
10
- assert result['nodes']['model2']['columns']['c']['depends_on'][0].column == 'c'
11
- assert result['nodes']['model2']['columns']['c']['depends_on'][0].node == 'model1'
99
+
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")])
103
+
104
+ assert_model(result, "model.model3", parents=[("model.model1", "c")])
105
+ assert_column(result, "model.model3", "c", transformation_type="passthrough", parents=[("model.model1", "c")])
12
106
 
13
107
 
14
108
  def test_cll_table_alisa(dbt_test_helper):
15
109
  def patch_node(node):
16
- node['alias'] = 'model1_alias'
110
+ node["alias"] = "model1_alias"
17
111
 
18
- dbt_test_helper.create_model("model1", curr_sql="select 1 as c", curr_columns={"c": "int"}, patch_func=patch_node)
19
- dbt_test_helper.create_model("model2", curr_sql='select c from {{ ref("model1") }}', curr_columns={"c": "int"},
20
- depends_on=["model1"])
112
+ dbt_test_helper.create_model(
113
+ "model1", unique_id="model.model1", curr_sql="select 1 as c", curr_columns={"c": "int"}, patch_func=patch_node
114
+ )
115
+ dbt_test_helper.create_model(
116
+ "model2",
117
+ unique_id="model.model2",
118
+ curr_sql='select c from {{ ref("model1") }}',
119
+ curr_columns={"c": "int"},
120
+ depends_on=["model.model1"],
121
+ )
21
122
  adapter: DbtAdapter = dbt_test_helper.context.adapter
22
- result = adapter.get_cll_by_node_id("model1")
23
- assert result['nodes']['model2']['columns']['c']['depends_on'][0].column == 'c'
24
- assert result['nodes']['model2']['columns']['c']['depends_on'][0].node == 'model1'
123
+ result = adapter.get_cll("model.model1", "c")
124
+ assert_column(result, "model.model2", "c", transformation_type="passthrough", parents=[("model.model1", "c")])
25
125
 
26
126
 
27
127
  def test_seed(dbt_test_helper):
@@ -32,20 +132,38 @@ def test_seed(dbt_test_helper):
32
132
  3,Charlie,35
33
133
  """
34
134
 
35
- dbt_test_helper.create_model("seed1",
36
- curr_csv=csv_data_curr,
37
- curr_columns={"customer_id": "varchar", "name": "varchar", "age": "int"},
38
- resource_type="seed")
135
+ dbt_test_helper.create_model(
136
+ "seed1",
137
+ unique_id="seed.seed1",
138
+ curr_csv=csv_data_curr,
139
+ curr_columns={"customer_id": "varchar", "name": "varchar", "age": "int"},
140
+ resource_type="seed",
141
+ )
142
+ dbt_test_helper.create_model(
143
+ "model1",
144
+ unique_id="model.model1",
145
+ curr_sql='select customer_id from {{ ref("seed1") }} where age > 0',
146
+ curr_columns={"customer_id": "varchar"},
147
+ depends_on=["seed.seed1"],
148
+ )
39
149
  adapter: DbtAdapter = dbt_test_helper.context.adapter
40
- result = adapter.get_cll_by_node_id("seed1")
41
150
 
42
- assert result['nodes']['seed1']['columns']['customer_id']['transformation_type'] == 'source'
43
- assert len(result['nodes']['seed1']['columns']['customer_id']['depends_on']) == 0
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
+ )
44
162
 
45
163
 
46
164
  def test_python_model(dbt_test_helper):
47
165
  def python_node(node):
48
- node['language'] = 'python'
166
+ node["language"] = "python"
49
167
 
50
168
  csv_data_curr = """
51
169
  customer_id,name,age
@@ -53,20 +171,23 @@ def test_python_model(dbt_test_helper):
53
171
  2,Bob,25
54
172
  3,Charlie,35
55
173
  """
56
- dbt_test_helper.create_model("model1",
57
- curr_csv=csv_data_curr,
58
- curr_columns={"customer_id": "varchar", "name": "varchar", "age": "int"})
59
- dbt_test_helper.create_model("model2",
60
- curr_csv=csv_data_curr,
61
- curr_columns={"customer_id": "varchar", "name": "varchar", "age": "int"},
62
- depends_on=["model1"],
63
- patch_func=python_node)
174
+ dbt_test_helper.create_model(
175
+ "model1", curr_csv=csv_data_curr, curr_columns={"customer_id": "varchar", "name": "varchar", "age": "int"}
176
+ )
177
+ dbt_test_helper.create_model(
178
+ "model2",
179
+ curr_csv=csv_data_curr,
180
+ curr_columns={"customer_id": "varchar", "name": "varchar", "age": "int"},
181
+ depends_on=["model1"],
182
+ patch_func=python_node,
183
+ )
64
184
  adapter: DbtAdapter = dbt_test_helper.context.adapter
65
- assert not adapter.is_python_model('model1')
66
- assert adapter.is_python_model('model2')
185
+ assert not adapter.is_python_model("model1")
186
+ assert adapter.is_python_model("model2")
67
187
 
68
- result = adapter.get_cll_by_node_id("model1")
69
- assert result['nodes']['model2']['columns']['customer_id']['transformation_type'] == '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=[])
70
191
 
71
192
 
72
193
  def test_source(dbt_test_helper):
@@ -80,23 +201,517 @@ def test_source(dbt_test_helper):
80
201
  dbt_test_helper.create_source(
81
202
  "source1",
82
203
  "table1",
204
+ unique_id="source.source1.table1",
83
205
  curr_csv=csv_data_curr,
84
- curr_columns={"customer_id": "varchar", "name": "varchar", "age": "int"})
206
+ curr_columns={"customer_id": "varchar", "name": "varchar", "age": "int"},
207
+ )
208
+ dbt_test_helper.create_model(
209
+ "model1",
210
+ unique_id="model.model1",
211
+ curr_sql='select customer_id from {{ source("source1", "table1") }}',
212
+ curr_columns={"customer_id": "int"},
213
+ depends_on=["source.source1.table1"],
214
+ )
85
215
  adapter: DbtAdapter = dbt_test_helper.context.adapter
86
- result = adapter.get_cll_by_node_id("source1.table1")
87
- assert result['nodes']['source1.table1']['columns']['customer_id']['transformation_type'] == 'source'
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
+ )
88
225
 
89
226
 
90
227
  def test_parse_error(dbt_test_helper):
91
228
  dbt_test_helper.create_model("model1", curr_sql="select 1 as c", curr_columns={"c": "int"})
92
- dbt_test_helper.create_model("model2", curr_sql='this is not a valid sql', curr_columns={"c": "int"})
229
+ dbt_test_helper.create_model("model2", curr_sql="this is not a valid sql", curr_columns={"c": "int"})
93
230
  adapter: DbtAdapter = dbt_test_helper.context.adapter
94
- result = adapter.get_cll_by_node_id("model2")
95
- assert result['nodes']['model2']['columns']['c']['transformation_type'] == 'unknown'
231
+ result = adapter.get_cll("model2")
232
+ assert_column(result, "model2", "c", transformation_type="unknown", parents=[])
96
233
 
97
234
 
98
235
  def test_model_without_catalog(dbt_test_helper):
99
236
  dbt_test_helper.create_model("model1", curr_sql="select 1 as c")
100
237
  adapter: DbtAdapter = dbt_test_helper.context.adapter
101
- result = adapter.get_cll_by_node_id("model1")
102
- assert not hasattr(result['nodes']['model1'], 'columns')
238
+ result = adapter.get_cll("model1")
239
+ assert not result.nodes["model1"].columns
240
+
241
+
242
+ def test_column_level_lineage(dbt_test_helper):
243
+ dbt_test_helper.create_model(
244
+ "model1", unique_id="model.model1", curr_sql="select 1 as c", curr_columns={"c": "int"}
245
+ )
246
+ dbt_test_helper.create_model(
247
+ "model2",
248
+ unique_id="model.model2",
249
+ curr_sql='select c, 2025 as y from {{ ref("model1") }}',
250
+ curr_columns={"c": "int", "y": "int"},
251
+ depends_on=["model.model1"],
252
+ )
253
+ dbt_test_helper.create_model(
254
+ "model3",
255
+ unique_id="model.model3",
256
+ curr_sql='select c from {{ ref("model2") }} where y < 2025',
257
+ curr_columns={"c": "int"},
258
+ depends_on=["model.model2"],
259
+ )
260
+ dbt_test_helper.create_model(
261
+ "model4",
262
+ unique_id="model.model4",
263
+ curr_sql='select y from {{ ref("model2") }}',
264
+ curr_columns={"y": "int"},
265
+ depends_on=["model.model2"],
266
+ )
267
+
268
+ adapter: DbtAdapter = dbt_test_helper.context.adapter
269
+
270
+ result = adapter.get_cll("model.model2", "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")])
274
+
275
+ result = adapter.get_cll("model.model2", "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=[])
279
+
280
+ result = adapter.get_cll("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")])
562
+
563
+
564
+ def test_impact_radius_by_node_no_cll(dbt_test_helper):
565
+ # non-breaking
566
+ dbt_test_helper.create_model(
567
+ "model1",
568
+ unique_id="model.model1",
569
+ curr_sql="select 1 as c",
570
+ base_sql="select 1 as c --- non-breaking",
571
+ curr_columns={"c": "int"},
572
+ base_columns={"c": "int"},
573
+ )
574
+ # breaking
575
+ dbt_test_helper.create_model(
576
+ "model2",
577
+ unique_id="model.model2",
578
+ curr_sql='select c, 2025 as y from {{ ref("model1") }}',
579
+ base_sql='select c, 2025 as y from {{ ref("model1") }} where c > 0 --- breaking',
580
+ curr_columns={"c": "int", "y": "int"},
581
+ base_columns={"c": "int", "y": "int"},
582
+ depends_on=["model.model1"],
583
+ )
584
+ dbt_test_helper.create_model(
585
+ "model3",
586
+ unique_id="model.model3",
587
+ curr_sql='select c from {{ ref("model2") }} where y < 2025',
588
+ base_sql='select c from {{ ref("model2") }} where y < 2025',
589
+ curr_columns={"c": "int"},
590
+ base_columns={"c": "int"},
591
+ depends_on=["model.model2"],
592
+ )
593
+ dbt_test_helper.create_model(
594
+ "model4",
595
+ unique_id="model.model4",
596
+ curr_sql='select y from {{ ref("model2") }}',
597
+ base_sql='select y from {{ ref("model2") }}',
598
+ curr_columns={"y": "int"},
599
+ base_columns={"y": "int"},
600
+ depends_on=["model.model2"],
601
+ )
602
+
603
+ adapter: DbtAdapter = dbt_test_helper.context.adapter
604
+
605
+ # breaking
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"])
608
+
609
+ # non-breaking
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"])
612
+
613
+
614
+ def test_impact_radius_by_node_with_cll(dbt_test_helper):
615
+ # added column
616
+ dbt_test_helper.create_model(
617
+ "model1",
618
+ unique_id="model.model1",
619
+ curr_sql="select 1 as c, 2 as d --- add d",
620
+ base_sql="select 1 as c",
621
+ curr_columns={"c": "int", "d": "int"},
622
+ base_columns={"c": "int"},
623
+ )
624
+ # modified column
625
+ dbt_test_helper.create_model(
626
+ "model2",
627
+ unique_id="model.model2",
628
+ curr_sql='select c, 2024 as y from {{ ref("model1") }} --- modify y',
629
+ base_sql='select c, 2025 as y from {{ ref("model1") }}',
630
+ curr_columns={"c": "int", "y": "int"},
631
+ base_columns={"c": "int", "y": "int"},
632
+ depends_on=["model.model1"],
633
+ )
634
+ dbt_test_helper.create_model(
635
+ "model3",
636
+ unique_id="model.model3",
637
+ curr_sql='select c from {{ ref("model2") }} where y < 2025',
638
+ base_sql='select c from {{ ref("model2") }} where y < 2025',
639
+ curr_columns={"c": "int"},
640
+ base_columns={"c": "int"},
641
+ depends_on=["model.model2"],
642
+ )
643
+ dbt_test_helper.create_model(
644
+ "model4",
645
+ unique_id="model.model4",
646
+ curr_sql='select y from {{ ref("model2") }}',
647
+ base_sql='select y from {{ ref("model2") }}',
648
+ curr_columns={"y": "int"},
649
+ base_columns={"y": "int"},
650
+ depends_on=["model.model2"],
651
+ )
652
+
653
+ adapter: DbtAdapter = dbt_test_helper.context.adapter
654
+
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")])
661
+
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")
667
+
668
+
669
+ def test_impact_radius_by_node_with_cll_2(dbt_test_helper):
670
+ # added column
671
+ dbt_test_helper.create_model(
672
+ "model1",
673
+ unique_id="model.model1",
674
+ curr_sql="select 1 as c, 2 as d --- add d",
675
+ base_sql="select 1 as c",
676
+ curr_columns={"c": "int", "d": "int"},
677
+ base_columns={"c": "int"},
678
+ )
679
+ # breaking, added column, modified column
680
+ dbt_test_helper.create_model(
681
+ "model2",
682
+ unique_id="model.model2",
683
+ curr_sql='select c, 2024 as y, d from {{ ref("model1") }} --- modify y, add d',
684
+ base_sql='select c, 2025 as y from {{ ref("model1") }} where c > 0 --- breaking',
685
+ curr_columns={"c": "int", "y": "int", "d": "int"},
686
+ base_columns={"c": "int", "y": "int"},
687
+ depends_on=["model.model1"],
688
+ )
689
+ dbt_test_helper.create_model(
690
+ "model3",
691
+ unique_id="model.model3",
692
+ curr_sql='select c from {{ ref("model2") }} where y < 2025',
693
+ base_sql='select c from {{ ref("model2") }} where y < 2025',
694
+ curr_columns={"c": "int"},
695
+ base_columns={"c": "int"},
696
+ depends_on=["model.model2"],
697
+ )
698
+ dbt_test_helper.create_model(
699
+ "model4",
700
+ unique_id="model.model4",
701
+ curr_sql='select y from {{ ref("model2") }}',
702
+ base_sql='select y from {{ ref("model2") }}',
703
+ curr_columns={"y": "int"},
704
+ base_columns={"y": "int"},
705
+ depends_on=["model.model2"],
706
+ )
707
+
708
+ adapter: DbtAdapter = dbt_test_helper.context.adapter
709
+
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")])