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.
- recce/VERSION +1 -1
- recce/__init__.py +27 -22
- recce/adapter/base.py +11 -14
- recce/adapter/dbt_adapter/__init__.py +810 -480
- recce/adapter/dbt_adapter/dbt_version.py +3 -0
- recce/adapter/sqlmesh_adapter.py +24 -35
- recce/apis/check_api.py +39 -28
- recce/apis/check_func.py +33 -27
- recce/apis/run_api.py +25 -19
- recce/apis/run_func.py +29 -23
- recce/artifact.py +119 -51
- recce/cli.py +1299 -323
- recce/config.py +42 -33
- recce/connect_to_cloud.py +138 -0
- recce/core.py +55 -47
- recce/data/404.html +1 -1
- recce/data/__next.__PAGE__.txt +10 -0
- recce/data/__next._full.txt +23 -0
- recce/data/__next._head.txt +8 -0
- recce/data/__next._index.txt +8 -0
- recce/data/__next._tree.txt +5 -0
- recce/data/_next/static/52aV_JrNUZU6dMFgvTQEO/_buildManifest.js +11 -0
- recce/data/_next/static/52aV_JrNUZU6dMFgvTQEO/_clientMiddlewareManifest.json +1 -0
- recce/data/_next/static/chunks/02b996c7f6a29a06.js +4 -0
- recce/data/_next/static/chunks/19c10d219a6a21ff.js +1 -0
- recce/data/_next/static/chunks/2df9ec28a061971d.js +11 -0
- recce/data/_next/static/chunks/3098c987393bda15.js +1 -0
- recce/data/_next/static/chunks/393dc43e483f717a.css +2 -0
- recce/data/_next/static/chunks/399e8d91a7e45073.js +2 -0
- recce/data/_next/static/chunks/4d0186f631230245.js +1 -0
- recce/data/_next/static/chunks/5794ba9e10a9c060.js +11 -0
- recce/data/_next/static/chunks/715761c929a3f28b.js +110 -0
- recce/data/_next/static/chunks/71f88fcc615bf282.js +1 -0
- recce/data/_next/static/chunks/80d2a95eaf1201ea.js +1 -0
- recce/data/_next/static/chunks/9979c6109bbbee35.js +1 -0
- recce/data/_next/static/chunks/99d638224186c118.js +1 -0
- recce/data/_next/static/chunks/d003eb36240e92f3.js +1 -0
- recce/data/_next/static/chunks/d3167cdfec4fc351.js +1 -0
- recce/data/_next/static/chunks/e124bccf574a3361.css +1 -0
- recce/data/_next/static/chunks/f40141db1bdb46f0.css +6 -0
- recce/data/_next/static/chunks/fcc53a88741a52f9.js +1 -0
- recce/data/_next/static/chunks/turbopack-b1920d28cfb1f28d.js +3 -0
- recce/data/_next/static/media/favicon.a8d38d84.ico +0 -0
- recce/data/_next/static/media/montserrat-cyrillic-800-normal.d80d830d.woff2 +0 -0
- recce/data/_next/static/media/montserrat-cyrillic-800-normal.f9d58125.woff +0 -0
- recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.076c2a93.woff2 +0 -0
- recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.a4fa76b5.woff +0 -0
- recce/data/_next/static/media/montserrat-latin-800-normal.cde454cc.woff2 +0 -0
- recce/data/_next/static/media/montserrat-latin-800-normal.d5761935.woff +0 -0
- recce/data/_next/static/media/montserrat-latin-ext-800-normal.40ec0659.woff2 +0 -0
- recce/data/_next/static/media/montserrat-latin-ext-800-normal.b671449b.woff +0 -0
- recce/data/_next/static/media/montserrat-vietnamese-800-normal.9f7b8541.woff +0 -0
- recce/data/_next/static/media/montserrat-vietnamese-800-normal.f9eb854e.woff2 +0 -0
- recce/data/_next/static/media/reload-image.7aa931c7.svg +4 -0
- recce/data/_not-found/__next._full.txt +17 -0
- recce/data/_not-found/__next._head.txt +8 -0
- recce/data/_not-found/__next._index.txt +8 -0
- recce/data/_not-found/__next._not-found.__PAGE__.txt +5 -0
- recce/data/_not-found/__next._not-found.txt +4 -0
- recce/data/_not-found/__next._tree.txt +3 -0
- recce/data/_not-found.html +1 -0
- recce/data/_not-found.txt +17 -0
- recce/data/auth_callback.html +68 -0
- recce/data/imgs/reload-image.svg +4 -0
- recce/data/index.html +1 -27
- recce/data/index.txt +23 -7
- recce/diff.py +6 -12
- recce/event/__init__.py +86 -74
- recce/event/collector.py +33 -22
- recce/event/track.py +49 -27
- recce/exceptions.py +1 -1
- recce/git.py +7 -7
- recce/github.py +57 -53
- recce/mcp_server.py +716 -0
- recce/models/__init__.py +4 -1
- recce/models/check.py +6 -7
- recce/models/run.py +1 -0
- recce/models/types.py +131 -28
- recce/pull_request.py +27 -25
- recce/run.py +165 -121
- recce/server.py +303 -111
- recce/state/__init__.py +31 -0
- recce/state/cloud.py +632 -0
- recce/state/const.py +26 -0
- recce/state/local.py +56 -0
- recce/state/state.py +119 -0
- recce/state/state_loader.py +174 -0
- recce/summary.py +188 -143
- recce/tasks/__init__.py +19 -3
- recce/tasks/core.py +11 -13
- recce/tasks/dataframe.py +82 -18
- recce/tasks/histogram.py +69 -34
- recce/tasks/lineage.py +2 -2
- recce/tasks/profile.py +152 -86
- recce/tasks/query.py +139 -87
- recce/tasks/rowcount.py +37 -31
- recce/tasks/schema.py +18 -15
- recce/tasks/top_k.py +35 -35
- recce/tasks/valuediff.py +216 -152
- recce/util/__init__.py +3 -0
- recce/util/api_token.py +80 -0
- recce/util/breaking.py +87 -85
- recce/util/cll.py +274 -219
- recce/util/io.py +22 -17
- recce/util/lineage.py +65 -16
- recce/util/logger.py +1 -1
- recce/util/onboarding_state.py +45 -0
- recce/util/perf_tracking.py +85 -0
- recce/util/recce_cloud.py +322 -72
- recce/util/singleton.py +4 -4
- recce/yaml/__init__.py +7 -10
- recce_cloud/__init__.py +24 -0
- recce_cloud/api/__init__.py +17 -0
- recce_cloud/api/base.py +111 -0
- recce_cloud/api/client.py +150 -0
- recce_cloud/api/exceptions.py +26 -0
- recce_cloud/api/factory.py +63 -0
- recce_cloud/api/github.py +76 -0
- recce_cloud/api/gitlab.py +82 -0
- recce_cloud/artifact.py +57 -0
- recce_cloud/ci_providers/__init__.py +9 -0
- recce_cloud/ci_providers/base.py +82 -0
- recce_cloud/ci_providers/detector.py +147 -0
- recce_cloud/ci_providers/github_actions.py +136 -0
- recce_cloud/ci_providers/gitlab_ci.py +130 -0
- recce_cloud/cli.py +245 -0
- recce_cloud/upload.py +214 -0
- {recce_nightly-1.2.0.20250506.dist-info → recce_nightly-1.26.0.20251124.dist-info}/METADATA +68 -37
- recce_nightly-1.26.0.20251124.dist-info/RECORD +180 -0
- {recce_nightly-1.2.0.20250506.dist-info → recce_nightly-1.26.0.20251124.dist-info}/WHEEL +1 -1
- {recce_nightly-1.2.0.20250506.dist-info → recce_nightly-1.26.0.20251124.dist-info}/top_level.txt +1 -0
- tests/adapter/dbt_adapter/conftest.py +9 -5
- tests/adapter/dbt_adapter/dbt_test_helper.py +37 -22
- tests/adapter/dbt_adapter/test_dbt_adapter.py +0 -15
- tests/adapter/dbt_adapter/test_dbt_cll.py +656 -41
- tests/adapter/dbt_adapter/test_selector.py +22 -21
- tests/recce_cloud/__init__.py +0 -0
- tests/recce_cloud/test_ci_providers.py +351 -0
- tests/recce_cloud/test_cli.py +372 -0
- tests/recce_cloud/test_client.py +273 -0
- tests/recce_cloud/test_platform_clients.py +333 -0
- tests/tasks/conftest.py +1 -1
- tests/tasks/test_histogram.py +58 -66
- tests/tasks/test_lineage.py +36 -23
- tests/tasks/test_preset_checks.py +45 -31
- tests/tasks/test_profile.py +339 -15
- tests/tasks/test_query.py +46 -46
- tests/tasks/test_row_count.py +65 -46
- tests/tasks/test_schema.py +65 -42
- tests/tasks/test_top_k.py +22 -18
- tests/tasks/test_valuediff.py +43 -32
- tests/test_cli.py +174 -60
- tests/test_cli_mcp_optional.py +45 -0
- tests/test_cloud_listing_cli.py +324 -0
- tests/test_config.py +7 -9
- tests/test_connect_to_cloud.py +82 -0
- tests/test_core.py +151 -4
- tests/test_dbt.py +7 -7
- tests/test_mcp_server.py +332 -0
- tests/test_pull_request.py +1 -1
- tests/test_server.py +25 -19
- tests/test_summary.py +29 -17
- recce/data/_next/static/Kcbs3GEIyH2LxgLYat0es/_buildManifest.js +0 -1
- recce/data/_next/static/chunks/1f229bf6-d9fe92e56db8d93b.js +0 -1
- recce/data/_next/static/chunks/29e3cc0d-8c150e37dff9631b.js +0 -1
- recce/data/_next/static/chunks/368-7587b306577df275.js +0 -65
- recce/data/_next/static/chunks/36e1c10d-bb0210cbd6573a8d.js +0 -1
- recce/data/_next/static/chunks/3998a672-eaad84bdd88cc73e.js +0 -1
- recce/data/_next/static/chunks/3a92ee20-3b5d922d4157af5e.js +0 -1
- recce/data/_next/static/chunks/450c323b-1bb5db526e54435a.js +0 -1
- recce/data/_next/static/chunks/47d8844f-79a1b53c66a7d7ec.js +0 -1
- recce/data/_next/static/chunks/6dc81886-c94b9b91bc2c3caf.js +0 -1
- recce/data/_next/static/chunks/6ef81909-694dc38134099299.js +0 -1
- recce/data/_next/static/chunks/700-3b65fc3666820d00.js +0 -2
- recce/data/_next/static/chunks/7a8a3e83-d7fa409d97b38b2b.js +0 -1
- recce/data/_next/static/chunks/7f27ae6c-413f6b869a04183a.js +0 -1
- recce/data/_next/static/chunks/8d700b6a-f0b1f6b9e0d97ce2.js +0 -1
- recce/data/_next/static/chunks/9746af58-d74bef4d03eea6ab.js +0 -1
- recce/data/_next/static/chunks/a30376cd-7d806e1602f2dc3a.js +0 -1
- recce/data/_next/static/chunks/app/_not-found/page-8a886fa0855c3105.js +0 -1
- recce/data/_next/static/chunks/app/layout-9102e22cb73f74d6.js +0 -1
- recce/data/_next/static/chunks/app/page-cee661090afbd6aa.js +0 -1
- recce/data/_next/static/chunks/b63b1b3f-7395c74e11a14e95.js +0 -1
- recce/data/_next/static/chunks/c132bf7d-8102037f9ccf372a.js +0 -1
- recce/data/_next/static/chunks/c1ceaa8b-a1e442154d23515e.js +0 -1
- recce/data/_next/static/chunks/cd9f8d63-cf0d5a7b0f7a92e8.js +0 -54
- recce/data/_next/static/chunks/ce84277d-f42c2c58049cea2d.js +0 -1
- recce/data/_next/static/chunks/e24bf851-0f8cbc99656833e7.js +0 -1
- recce/data/_next/static/chunks/fee69bc6-f17d36c080742e74.js +0 -1
- recce/data/_next/static/chunks/framework-ded83d71b51ce901.js +0 -1
- recce/data/_next/static/chunks/main-a0859f1f36d0aa6c.js +0 -1
- recce/data/_next/static/chunks/main-app-0225a2255968e566.js +0 -1
- recce/data/_next/static/chunks/pages/_app-d5672bf3d8b6371b.js +0 -1
- recce/data/_next/static/chunks/pages/_error-ed75be3f25588548.js +0 -1
- recce/data/_next/static/chunks/webpack-567d72f0bc0820d5.js +0 -1
- recce/data/_next/static/css/c9ecb46a4b21c126.css +0 -14
- recce/data/_next/static/media/montserrat-cyrillic-800-normal.22628180.woff2 +0 -0
- recce/data/_next/static/media/montserrat-cyrillic-800-normal.31d693bb.woff +0 -0
- recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.7e2c1e62.woff +0 -0
- recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.94a63aea.woff2 +0 -0
- recce/data/_next/static/media/montserrat-latin-800-normal.6f8fa298.woff2 +0 -0
- recce/data/_next/static/media/montserrat-latin-800-normal.97e20d5e.woff +0 -0
- recce/data/_next/static/media/montserrat-latin-ext-800-normal.013b84f9.woff2 +0 -0
- recce/data/_next/static/media/montserrat-latin-ext-800-normal.aff52ab0.woff +0 -0
- recce/data/_next/static/media/montserrat-vietnamese-800-normal.5f21869b.woff +0 -0
- recce/data/_next/static/media/montserrat-vietnamese-800-normal.c0035377.woff2 +0 -0
- recce/state.py +0 -753
- recce_nightly-1.2.0.20250506.dist-info/RECORD +0 -142
- tests/test_state.py +0 -123
- /recce/data/_next/static/{Kcbs3GEIyH2LxgLYat0es → 52aV_JrNUZU6dMFgvTQEO}/_ssgManifest.js +0 -0
- /recce/data/_next/static/chunks/{polyfills-42372ed130431b0a.js → a6dad97d9634a72d.js} +0 -0
- {recce_nightly-1.2.0.20250506.dist-info → recce_nightly-1.26.0.20251124.dist-info}/entry_points.txt +0 -0
- {recce_nightly-1.2.0.20250506.dist-info → recce_nightly-1.26.0.20251124.dist-info}/licenses/LICENSE +0 -0
recce/tasks/query.py
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import typing
|
|
2
|
-
from typing import Optional, Tuple
|
|
2
|
+
from typing import List, Optional, Tuple
|
|
3
3
|
|
|
4
4
|
from pydantic import BaseModel
|
|
5
5
|
|
|
6
|
-
from .core import Task, TaskResultDiffer, CheckValidator
|
|
7
|
-
from .dataframe import DataFrame
|
|
8
|
-
from .valuediff import ValueDiffMixin
|
|
9
6
|
from ..core import default_context
|
|
10
7
|
from ..exceptions import RecceException
|
|
11
8
|
from ..models import Check
|
|
9
|
+
from .core import CheckValidator, Task, TaskResultDiffer
|
|
10
|
+
from .dataframe import DataFrame
|
|
11
|
+
from .valuediff import ValueDiffMixin
|
|
12
12
|
|
|
13
13
|
QUERY_LIMIT = 2000
|
|
14
14
|
|
|
@@ -19,11 +19,8 @@ if typing.TYPE_CHECKING:
|
|
|
19
19
|
class QueryMixin:
|
|
20
20
|
@classmethod
|
|
21
21
|
def execute_sql_with_limit(
|
|
22
|
-
cls,
|
|
23
|
-
|
|
24
|
-
base: bool = False,
|
|
25
|
-
limit: Optional[int] = None
|
|
26
|
-
) -> Tuple['agate.Table', bool]:
|
|
22
|
+
cls, sql_template, base: bool = False, limit: Optional[int] = None
|
|
23
|
+
) -> Tuple["agate.Table", bool]:
|
|
27
24
|
"""
|
|
28
25
|
Execute a SQL template and return the result as an agate table.
|
|
29
26
|
:param sql_template: SQL template to execute
|
|
@@ -32,8 +29,10 @@ class QueryMixin:
|
|
|
32
29
|
:return: Tuple of agate table and whether there are more rows to fetch
|
|
33
30
|
"""
|
|
34
31
|
from jinja2.exceptions import TemplateSyntaxError
|
|
32
|
+
|
|
35
33
|
dbt_adapter = default_context().adapter
|
|
36
34
|
from dbt.exceptions import TargetNotFoundError
|
|
35
|
+
|
|
37
36
|
try:
|
|
38
37
|
sql = dbt_adapter.generate_sql(sql_template, base)
|
|
39
38
|
|
|
@@ -51,7 +50,7 @@ class QueryMixin:
|
|
|
51
50
|
raise RecceException(f"Jinja template error: line {e.lineno}: {str(e)}")
|
|
52
51
|
|
|
53
52
|
@classmethod
|
|
54
|
-
def execute_sql(cls, sql_template, base: bool = False) ->
|
|
53
|
+
def execute_sql(cls, sql_template, base: bool = False) -> "agate.Table":
|
|
55
54
|
result, _ = cls.execute_sql_with_limit(sql_template, base)
|
|
56
55
|
return result
|
|
57
56
|
|
|
@@ -87,6 +86,7 @@ class QueryTask(Task, QueryMixin):
|
|
|
87
86
|
|
|
88
87
|
def execute_dbt(self):
|
|
89
88
|
from recce.adapter.dbt_adapter import DbtAdapter
|
|
89
|
+
|
|
90
90
|
dbt_adapter: DbtAdapter = default_context().adapter
|
|
91
91
|
|
|
92
92
|
limit = QUERY_LIMIT
|
|
@@ -101,9 +101,10 @@ class QueryTask(Task, QueryMixin):
|
|
|
101
101
|
|
|
102
102
|
def execute_sqlmesh(self):
|
|
103
103
|
from ..adapter.sqlmesh_adapter import SqlmeshAdapter
|
|
104
|
+
|
|
104
105
|
sqlmesh_adapter: SqlmeshAdapter = default_context().adapter
|
|
105
106
|
|
|
106
|
-
sql = self.params.get(
|
|
107
|
+
sql = self.params.get("sql_template")
|
|
107
108
|
limit = QUERY_LIMIT
|
|
108
109
|
df, more = sqlmesh_adapter.fetchdf_with_limit(sql, base=self.is_base, limit=limit)
|
|
109
110
|
return DataFrame.from_pandas(df, limit=limit, more=more)
|
|
@@ -111,7 +112,7 @@ class QueryTask(Task, QueryMixin):
|
|
|
111
112
|
def execute(self):
|
|
112
113
|
context = default_context()
|
|
113
114
|
|
|
114
|
-
if context.adapter_type ==
|
|
115
|
+
if context.adapter_type == "sqlmesh":
|
|
115
116
|
return self.execute_sqlmesh()
|
|
116
117
|
else:
|
|
117
118
|
return self.execute_dbt()
|
|
@@ -139,8 +140,13 @@ class QueryDiffTask(Task, QueryMixin, ValueDiffMixin):
|
|
|
139
140
|
self.connection = None
|
|
140
141
|
self.legacy_surrogate_key = True
|
|
141
142
|
|
|
142
|
-
def _query_diff(
|
|
143
|
-
|
|
143
|
+
def _query_diff(
|
|
144
|
+
self,
|
|
145
|
+
dbt_adapter,
|
|
146
|
+
sql_template: str,
|
|
147
|
+
base_sql_template: Optional[str] = None,
|
|
148
|
+
preview_change: bool = False,
|
|
149
|
+
):
|
|
144
150
|
limit = QUERY_LIMIT
|
|
145
151
|
|
|
146
152
|
self.connection = dbt_adapter.get_thread_connection()
|
|
@@ -155,40 +161,76 @@ class QueryDiffTask(Task, QueryMixin, ValueDiffMixin):
|
|
|
155
161
|
|
|
156
162
|
return QueryDiffResult(
|
|
157
163
|
base=DataFrame.from_agate(base, limit=limit, more=base_more),
|
|
158
|
-
current=DataFrame.from_agate(current, limit=limit, more=current_more)
|
|
164
|
+
current=DataFrame.from_agate(current, limit=limit, more=current_more),
|
|
159
165
|
)
|
|
160
166
|
|
|
161
|
-
def _query_diff_join(
|
|
162
|
-
|
|
167
|
+
def _query_diff_join(
|
|
168
|
+
self,
|
|
169
|
+
dbt_adapter,
|
|
170
|
+
sql_template: str,
|
|
171
|
+
primary_keys: List[str],
|
|
172
|
+
base_sql_template: Optional[str] = None,
|
|
173
|
+
preview_change: bool = False,
|
|
174
|
+
):
|
|
163
175
|
|
|
164
176
|
query_template = r"""
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
177
|
+
with a_query as (
|
|
178
|
+
{{ base_query }}
|
|
179
|
+
),
|
|
180
|
+
|
|
181
|
+
b_query as (
|
|
182
|
+
{{ current_query }}
|
|
183
|
+
),
|
|
184
|
+
|
|
185
|
+
a_intersect_b as (
|
|
186
|
+
select * from a_query
|
|
187
|
+
{{ dbt.intersect() }}
|
|
188
|
+
select * from b_query
|
|
189
|
+
),
|
|
190
|
+
|
|
191
|
+
a_except_b as (
|
|
192
|
+
select * from a_query
|
|
193
|
+
{{ dbt.except() }}
|
|
194
|
+
select * from b_query
|
|
195
|
+
),
|
|
196
|
+
|
|
197
|
+
b_except_a as (
|
|
198
|
+
select * from b_query
|
|
199
|
+
{{ dbt.except() }}
|
|
200
|
+
select * from a_query
|
|
201
|
+
),
|
|
202
|
+
|
|
203
|
+
all_records as (
|
|
204
|
+
select
|
|
205
|
+
*,
|
|
206
|
+
true as in_a,
|
|
207
|
+
true as in_b
|
|
208
|
+
from a_intersect_b
|
|
209
|
+
|
|
210
|
+
union all
|
|
211
|
+
|
|
212
|
+
select
|
|
213
|
+
*,
|
|
214
|
+
true as in_a,
|
|
215
|
+
false as in_b
|
|
216
|
+
from a_except_b
|
|
217
|
+
|
|
218
|
+
union all
|
|
219
|
+
|
|
220
|
+
select
|
|
221
|
+
*,
|
|
222
|
+
false as in_a,
|
|
223
|
+
true as in_b
|
|
224
|
+
from b_except_a
|
|
225
|
+
)
|
|
184
226
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
227
|
+
select * from all_records
|
|
228
|
+
where not (in_a and in_b)
|
|
229
|
+
order by {{ primary_keys | join(',\n') }}, in_a desc, in_b desc
|
|
230
|
+
limit {{ limit }}
|
|
231
|
+
"""
|
|
232
|
+
|
|
233
|
+
self.check_cancel()
|
|
192
234
|
|
|
193
235
|
if preview_change:
|
|
194
236
|
base_query = dbt_adapter.generate_sql(base_sql_template, base=False)
|
|
@@ -196,19 +238,20 @@ class QueryDiffTask(Task, QueryMixin, ValueDiffMixin):
|
|
|
196
238
|
base_query = dbt_adapter.generate_sql(base_sql_template or sql_template, base=True)
|
|
197
239
|
current_query = dbt_adapter.generate_sql(sql_template, base=False)
|
|
198
240
|
|
|
199
|
-
sql = dbt_adapter.generate_sql(
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
241
|
+
sql = dbt_adapter.generate_sql(
|
|
242
|
+
query_template,
|
|
243
|
+
context=dict(
|
|
244
|
+
base_query=base_query,
|
|
245
|
+
current_query=current_query,
|
|
246
|
+
primary_keys=primary_keys,
|
|
247
|
+
limit=QUERY_LIMIT,
|
|
248
|
+
),
|
|
249
|
+
)
|
|
205
250
|
|
|
206
251
|
_, table = dbt_adapter.execute(sql, fetch=True)
|
|
207
252
|
self.check_cancel()
|
|
208
253
|
|
|
209
|
-
return QueryDiffResult(
|
|
210
|
-
diff=DataFrame.from_agate(table)
|
|
211
|
-
)
|
|
254
|
+
return QueryDiffResult(diff=DataFrame.from_agate(table))
|
|
212
255
|
|
|
213
256
|
@staticmethod
|
|
214
257
|
def _select_single_model(model_name):
|
|
@@ -216,6 +259,7 @@ class QueryDiffTask(Task, QueryMixin, ValueDiffMixin):
|
|
|
216
259
|
|
|
217
260
|
def execute_dbt(self):
|
|
218
261
|
from recce.adapter.dbt_adapter import DbtAdapter
|
|
262
|
+
|
|
219
263
|
dbt_adapter: DbtAdapter = default_context().adapter
|
|
220
264
|
|
|
221
265
|
with dbt_adapter.connection_named("query"):
|
|
@@ -228,11 +272,20 @@ class QueryDiffTask(Task, QueryMixin, ValueDiffMixin):
|
|
|
228
272
|
preview_change = True
|
|
229
273
|
|
|
230
274
|
if primary_keys:
|
|
231
|
-
return self._query_diff_join(
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
275
|
+
return self._query_diff_join(
|
|
276
|
+
dbt_adapter,
|
|
277
|
+
sql_template,
|
|
278
|
+
primary_keys,
|
|
279
|
+
base_sql_template=base_sql_template,
|
|
280
|
+
preview_change=preview_change,
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
return self._query_diff(
|
|
284
|
+
dbt_adapter,
|
|
285
|
+
sql_template,
|
|
286
|
+
base_sql_template=base_sql_template,
|
|
287
|
+
preview_change=preview_change,
|
|
288
|
+
)
|
|
236
289
|
|
|
237
290
|
def _sqlmesh_query_diff(self, sql, base_sql=None):
|
|
238
291
|
from ..adapter.sqlmesh_adapter import SqlmeshAdapter
|
|
@@ -244,7 +297,7 @@ class QueryDiffTask(Task, QueryMixin, ValueDiffMixin):
|
|
|
244
297
|
curr, curr_more = sqlmesh_adapter.fetchdf_with_limit(sql, base=False, limit=limit)
|
|
245
298
|
return QueryDiffResult(
|
|
246
299
|
base=DataFrame.from_pandas(base, limit=limit, more=base_more),
|
|
247
|
-
current=DataFrame.from_pandas(curr, limit=limit, more=curr_more)
|
|
300
|
+
current=DataFrame.from_pandas(curr, limit=limit, more=curr_more),
|
|
248
301
|
)
|
|
249
302
|
|
|
250
303
|
def _sqlmesh_query_diff_join(self, sql, primary_keys, base_sql=None):
|
|
@@ -257,21 +310,18 @@ class QueryDiffTask(Task, QueryMixin, ValueDiffMixin):
|
|
|
257
310
|
expr_curr = sqlmesh_adapter.replace_virtual_tables(sql, base=False)
|
|
258
311
|
import sqlglot as g
|
|
259
312
|
|
|
260
|
-
expr =
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
).with_(
|
|
273
|
-
'all_records',
|
|
274
|
-
as_='''
|
|
313
|
+
expr = (
|
|
314
|
+
g.select(
|
|
315
|
+
"*",
|
|
316
|
+
)
|
|
317
|
+
.with_("a", as_=expr_base)
|
|
318
|
+
.with_("b", as_=expr_curr)
|
|
319
|
+
.with_("a_interset_b", as_="select * from a intersect select * from b")
|
|
320
|
+
.with_("a_except_b", as_="select * from a except select * from b")
|
|
321
|
+
.with_("b_except_a", as_="select * from b except select * from a")
|
|
322
|
+
.with_(
|
|
323
|
+
"all_records",
|
|
324
|
+
as_="""
|
|
275
325
|
SELECT
|
|
276
326
|
*,
|
|
277
327
|
TRUE AS in_a,
|
|
@@ -289,19 +339,21 @@ class QueryDiffTask(Task, QueryMixin, ValueDiffMixin):
|
|
|
289
339
|
FALSE AS in_a,
|
|
290
340
|
TRUE AS in_b
|
|
291
341
|
FROM b_except_a
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
342
|
+
""",
|
|
343
|
+
)
|
|
344
|
+
.with_(
|
|
345
|
+
"final",
|
|
346
|
+
as_=f"""
|
|
296
347
|
select * from all_records
|
|
297
348
|
where not (in_a and in_b)
|
|
298
349
|
order by {", ".join(primary_keys)}, in_a desc, in_b desc
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
diff=DataFrame.from_pandas(diff, limit=limit, more=diff_more)
|
|
350
|
+
""",
|
|
351
|
+
)
|
|
352
|
+
.from_("final")
|
|
353
|
+
.limit(1000)
|
|
304
354
|
)
|
|
355
|
+
diff, diff_more = sqlmesh_adapter.fetchdf_with_limit(expr, limit=limit)
|
|
356
|
+
return QueryDiffResult(diff=DataFrame.from_pandas(diff, limit=limit, more=diff_more))
|
|
305
357
|
|
|
306
358
|
def execute_sqlmesh(self):
|
|
307
359
|
sql = self.params.sql_template
|
|
@@ -316,7 +368,7 @@ class QueryDiffTask(Task, QueryMixin, ValueDiffMixin):
|
|
|
316
368
|
def execute(self):
|
|
317
369
|
context = default_context()
|
|
318
370
|
|
|
319
|
-
if context.adapter_type ==
|
|
371
|
+
if context.adapter_type == "sqlmesh":
|
|
320
372
|
return self.execute_sqlmesh()
|
|
321
373
|
else:
|
|
322
374
|
return self.execute_dbt()
|
|
@@ -329,14 +381,14 @@ class QueryDiffTask(Task, QueryMixin, ValueDiffMixin):
|
|
|
329
381
|
|
|
330
382
|
class QueryDiffResultDiffer(TaskResultDiffer):
|
|
331
383
|
def _check_result_changed_fn(self, result):
|
|
332
|
-
base = result.get(
|
|
333
|
-
current = result.get(
|
|
334
|
-
diff = result.get(
|
|
384
|
+
base = result.get("base")
|
|
385
|
+
current = result.get("current")
|
|
386
|
+
diff = result.get("diff")
|
|
335
387
|
|
|
336
388
|
if diff is None:
|
|
337
389
|
return TaskResultDiffer.diff(base, current)
|
|
338
390
|
else:
|
|
339
|
-
diff_data = diff.get(
|
|
391
|
+
diff_data = diff.get("data")
|
|
340
392
|
if diff_data is None or len(diff_data) == 0:
|
|
341
393
|
return None
|
|
342
394
|
|
recce/tasks/rowcount.py
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
from typing import
|
|
1
|
+
from typing import List, Literal, Optional, Union
|
|
2
2
|
|
|
3
3
|
from pydantic import BaseModel
|
|
4
4
|
|
|
5
5
|
from recce.core import default_context
|
|
6
6
|
from recce.models import Check
|
|
7
7
|
from recce.tasks import Task
|
|
8
|
-
from recce.tasks.core import
|
|
8
|
+
from recce.tasks.core import CheckValidator, TaskResultDiffer
|
|
9
9
|
from recce.tasks.query import QueryMixin
|
|
10
10
|
|
|
11
11
|
|
|
@@ -25,10 +25,10 @@ class RowCountTask(Task, QueryMixin):
|
|
|
25
25
|
if node is None:
|
|
26
26
|
return None
|
|
27
27
|
|
|
28
|
-
if node.resource_type !=
|
|
28
|
+
if node.resource_type != "model" and node.resource_type != "snapshot":
|
|
29
29
|
return None
|
|
30
30
|
|
|
31
|
-
if node.config and node.config.materialized not in [
|
|
31
|
+
if node.config and node.config.materialized not in ["table", "view", "incremental", "snapshot"]:
|
|
32
32
|
return None
|
|
33
33
|
|
|
34
34
|
relation = dbt_adapter.create_relation(model_name, base=base)
|
|
@@ -54,8 +54,9 @@ class RowCountTask(Task, QueryMixin):
|
|
|
54
54
|
for node in self.params.node_names or []:
|
|
55
55
|
query_candidates.append(node)
|
|
56
56
|
else:
|
|
57
|
+
|
|
57
58
|
def countable(unique_id):
|
|
58
|
-
return unique_id.startswith(
|
|
59
|
+
return unique_id.startswith("model") or unique_id.startswith("snapshot") or unique_id.startswith("seed")
|
|
59
60
|
|
|
60
61
|
node_ids = dbt_adapter.select_nodes(
|
|
61
62
|
select=self.params.select,
|
|
@@ -80,7 +81,7 @@ class RowCountTask(Task, QueryMixin):
|
|
|
80
81
|
row_count = self._query_row_count(dbt_adapter, node, base=False)
|
|
81
82
|
self.check_cancel()
|
|
82
83
|
result[node] = {
|
|
83
|
-
|
|
84
|
+
"curr": row_count,
|
|
84
85
|
}
|
|
85
86
|
completed += 1
|
|
86
87
|
|
|
@@ -98,7 +99,7 @@ class RowCountDiffParams(BaseModel):
|
|
|
98
99
|
select: Optional[str] = None
|
|
99
100
|
exclude: Optional[str] = None
|
|
100
101
|
packages: Optional[list[str]] = None
|
|
101
|
-
view_mode: Optional[Literal[
|
|
102
|
+
view_mode: Optional[Literal["all", "changed_models"]] = None
|
|
102
103
|
|
|
103
104
|
|
|
104
105
|
class RowCountDiffTask(Task, QueryMixin):
|
|
@@ -112,10 +113,10 @@ class RowCountDiffTask(Task, QueryMixin):
|
|
|
112
113
|
if node is None:
|
|
113
114
|
return None
|
|
114
115
|
|
|
115
|
-
if node.resource_type !=
|
|
116
|
+
if node.resource_type != "model" and node.resource_type != "snapshot":
|
|
116
117
|
return None
|
|
117
118
|
|
|
118
|
-
if node.config and node.config.materialized not in [
|
|
119
|
+
if node.config and node.config.materialized not in ["table", "view", "incremental", "snapshot"]:
|
|
119
120
|
return None
|
|
120
121
|
|
|
121
122
|
relation = dbt_adapter.create_relation(model_name, base=base)
|
|
@@ -141,8 +142,9 @@ class RowCountDiffTask(Task, QueryMixin):
|
|
|
141
142
|
for node in self.params.node_names or []:
|
|
142
143
|
query_candidates.append(node)
|
|
143
144
|
else:
|
|
145
|
+
|
|
144
146
|
def countable(unique_id):
|
|
145
|
-
return unique_id.startswith(
|
|
147
|
+
return unique_id.startswith("model") or unique_id.startswith("snapshot") or unique_id.startswith("seed")
|
|
146
148
|
|
|
147
149
|
node_ids = dbt_adapter.select_nodes(
|
|
148
150
|
select=self.params.select,
|
|
@@ -169,8 +171,8 @@ class RowCountDiffTask(Task, QueryMixin):
|
|
|
169
171
|
curr_row_count = self._query_row_count(dbt_adapter, node, base=False)
|
|
170
172
|
self.check_cancel()
|
|
171
173
|
result[node] = {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
+
"base": base_row_count,
|
|
175
|
+
"curr": curr_row_count,
|
|
174
176
|
}
|
|
175
177
|
completed += 1
|
|
176
178
|
|
|
@@ -187,6 +189,7 @@ class RowCountDiffTask(Task, QueryMixin):
|
|
|
187
189
|
query_candidates.append(node_name)
|
|
188
190
|
|
|
189
191
|
from recce.adapter.sqlmesh_adapter import SqlmeshAdapter
|
|
192
|
+
|
|
190
193
|
sqlmesh_adapter: SqlmeshAdapter = default_context().adapter
|
|
191
194
|
|
|
192
195
|
for name in query_candidates:
|
|
@@ -194,28 +197,28 @@ class RowCountDiffTask(Task, QueryMixin):
|
|
|
194
197
|
curr_row_count = None
|
|
195
198
|
|
|
196
199
|
try:
|
|
197
|
-
df, _ = sqlmesh_adapter.fetchdf_with_limit(f
|
|
200
|
+
df, _ = sqlmesh_adapter.fetchdf_with_limit(f"select count(*) from {name}", base=True)
|
|
198
201
|
base_row_count = int(df.iloc[0, 0])
|
|
199
202
|
except Exception:
|
|
200
203
|
pass
|
|
201
204
|
self.check_cancel()
|
|
202
205
|
|
|
203
206
|
try:
|
|
204
|
-
df, _ = sqlmesh_adapter.fetchdf_with_limit(f
|
|
207
|
+
df, _ = sqlmesh_adapter.fetchdf_with_limit(f"select count(*) from {name}", base=False)
|
|
205
208
|
curr_row_count = int(df.iloc[0, 0])
|
|
206
209
|
except Exception:
|
|
207
210
|
pass
|
|
208
211
|
self.check_cancel()
|
|
209
212
|
result[name] = {
|
|
210
|
-
|
|
211
|
-
|
|
213
|
+
"base": base_row_count,
|
|
214
|
+
"curr": curr_row_count,
|
|
212
215
|
}
|
|
213
216
|
|
|
214
217
|
return result
|
|
215
218
|
|
|
216
219
|
def execute(self):
|
|
217
220
|
context = default_context()
|
|
218
|
-
if context.adapter_type ==
|
|
221
|
+
if context.adapter_type == "dbt":
|
|
219
222
|
return self.execute_dbt()
|
|
220
223
|
else:
|
|
221
224
|
return self.execute_sqlmesh()
|
|
@@ -232,8 +235,8 @@ class RowCountDiffResultDiffer(TaskResultDiffer):
|
|
|
232
235
|
current = {}
|
|
233
236
|
|
|
234
237
|
for node, row_counts in result.items():
|
|
235
|
-
base[node] = row_counts[
|
|
236
|
-
current[node] = row_counts[
|
|
238
|
+
base[node] = row_counts["base"]
|
|
239
|
+
current[node] = row_counts["curr"]
|
|
237
240
|
|
|
238
241
|
return TaskResultDiffer.diff(base, current)
|
|
239
242
|
|
|
@@ -243,24 +246,27 @@ class RowCountDiffResultDiffer(TaskResultDiffer):
|
|
|
243
246
|
Should be implemented by subclass.
|
|
244
247
|
"""
|
|
245
248
|
params = self.run.params
|
|
246
|
-
if params.get(
|
|
247
|
-
return [TaskResultDiffer.get_node_id_by_name(params.get(
|
|
248
|
-
elif params.get(
|
|
249
|
-
names = params.get(
|
|
249
|
+
if params.get("model"):
|
|
250
|
+
return [TaskResultDiffer.get_node_id_by_name(params.get("model"))]
|
|
251
|
+
elif params.get("node_names"):
|
|
252
|
+
names = params.get("node_names", [])
|
|
250
253
|
return [TaskResultDiffer.get_node_id_by_name(name) for name in names]
|
|
251
|
-
elif params.get(
|
|
252
|
-
return params.get(
|
|
254
|
+
elif params.get("node_ids"):
|
|
255
|
+
return params.get("node_ids", [])
|
|
253
256
|
else:
|
|
254
257
|
return TaskResultDiffer.get_node_ids_by_selector(
|
|
255
|
-
select=params.get(
|
|
256
|
-
exclude=params.get(
|
|
257
|
-
packages=params.get(
|
|
258
|
-
view_mode=params.get(
|
|
258
|
+
select=params.get("select"),
|
|
259
|
+
exclude=params.get("exclude"),
|
|
260
|
+
packages=params.get("packages"),
|
|
261
|
+
view_mode=params.get("view_mode"),
|
|
259
262
|
)
|
|
260
263
|
|
|
261
264
|
def _get_changed_nodes(self) -> Union[List[str], None]:
|
|
262
265
|
if self.changes:
|
|
263
|
-
|
|
266
|
+
# Both affected_root_keys of deepdiff v7 (OrderedSet) and v8 (SetOrdered) are iterable
|
|
267
|
+
# Convert to list directly
|
|
268
|
+
return list(self.changes.affected_root_keys)
|
|
269
|
+
return None
|
|
264
270
|
|
|
265
271
|
|
|
266
272
|
class RowCountDiffCheckValidator(CheckValidator):
|
|
@@ -268,4 +274,4 @@ class RowCountDiffCheckValidator(CheckValidator):
|
|
|
268
274
|
try:
|
|
269
275
|
RowCountDiffParams(**check.params)
|
|
270
276
|
except Exception as e:
|
|
271
|
-
raise ValueError(f
|
|
277
|
+
raise ValueError(f"Invalid params: str{e}")
|
recce/tasks/schema.py
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
from typing import
|
|
1
|
+
from typing import List, Literal, Optional, Union
|
|
2
2
|
|
|
3
3
|
from pydantic import BaseModel
|
|
4
4
|
|
|
5
5
|
from recce.models import Check
|
|
6
|
-
from recce.tasks.core import
|
|
6
|
+
from recce.tasks.core import CheckValidator, TaskResultDiffer
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class SchemaDiffResultDiffer:
|
|
@@ -17,35 +17,38 @@ class SchemaDiffResultDiffer:
|
|
|
17
17
|
|
|
18
18
|
def _get_related_node_ids(self) -> Union[List[str], None]:
|
|
19
19
|
params = self.check.params
|
|
20
|
-
if params.get(
|
|
21
|
-
return params.get(
|
|
20
|
+
if params.get("node_id"):
|
|
21
|
+
return params.get("node_id") if isinstance(params.get("node_id"), list) else [params.get("node_id")]
|
|
22
22
|
else:
|
|
23
23
|
return TaskResultDiffer.get_node_ids_by_selector(
|
|
24
|
-
select=params.get(
|
|
25
|
-
exclude=params.get(
|
|
26
|
-
packages=params.get(
|
|
27
|
-
view_mode=params.get(
|
|
24
|
+
select=params.get("select"),
|
|
25
|
+
exclude=params.get("exclude"),
|
|
26
|
+
packages=params.get("packages"),
|
|
27
|
+
view_mode=params.get("view_mode"),
|
|
28
28
|
)
|
|
29
29
|
|
|
30
30
|
def _check_result_changed_fn(self, base_lineage, curr_lineage):
|
|
31
31
|
base = {}
|
|
32
32
|
current = {}
|
|
33
|
-
base_nodes = base_lineage.get(
|
|
34
|
-
curr_nodes = curr_lineage.get(
|
|
33
|
+
base_nodes = base_lineage.get("nodes", {})
|
|
34
|
+
curr_nodes = curr_lineage.get("nodes", {})
|
|
35
35
|
for node_id in self.related_node_ids:
|
|
36
36
|
node = curr_nodes.get(node_id) or base_nodes.get(node_id)
|
|
37
37
|
if not node:
|
|
38
38
|
continue
|
|
39
39
|
|
|
40
|
-
node_name = node.get(
|
|
41
|
-
base[node_name] = base_nodes.get(node_id, {}).get(
|
|
42
|
-
current[node_name] = curr_nodes.get(node_id, {}).get(
|
|
40
|
+
node_name = node.get("name")
|
|
41
|
+
base[node_name] = base_nodes.get(node_id, {}).get("columns", {})
|
|
42
|
+
current[node_name] = curr_nodes.get(node_id, {}).get("columns", {})
|
|
43
43
|
|
|
44
44
|
return TaskResultDiffer.diff(base, current)
|
|
45
45
|
|
|
46
46
|
def _get_changed_nodes(self) -> Union[List[str], None]:
|
|
47
47
|
if self.changes:
|
|
48
|
-
|
|
48
|
+
# Both affected_root_keys of deepdiff v7 (OrderedSet) and v8 (SetOrdered) are iterable
|
|
49
|
+
# Convert to list directly
|
|
50
|
+
return list(self.changes.affected_root_keys)
|
|
51
|
+
return None
|
|
49
52
|
|
|
50
53
|
|
|
51
54
|
class SchemaDiffParams(BaseModel):
|
|
@@ -53,7 +56,7 @@ class SchemaDiffParams(BaseModel):
|
|
|
53
56
|
select: Optional[str] = None
|
|
54
57
|
exclude: Optional[str] = None
|
|
55
58
|
packages: Optional[list[str]] = None
|
|
56
|
-
view_mode: Optional[Literal[
|
|
59
|
+
view_mode: Optional[Literal["all", "changed_models"]] = None
|
|
57
60
|
|
|
58
61
|
|
|
59
62
|
class SchemaDiffCheckValidator(CheckValidator):
|