frontier-runner 0.2.0__tar.gz → 0.2.2__tar.gz
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.
- {frontier_runner-0.2.0 → frontier_runner-0.2.2}/CHANGELOG.md +16 -0
- {frontier_runner-0.2.0/frontier_runner.egg-info → frontier_runner-0.2.2}/PKG-INFO +4 -4
- {frontier_runner-0.2.0 → frontier_runner-0.2.2}/README.md +3 -3
- {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/__init__.py +1 -1
- {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/adapters/base.py +19 -0
- {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/adapters/bigquery.py +17 -0
- {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/adapters/snowflake.py +162 -3
- {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/api.py +9 -0
- frontier_runner-0.2.2/frontier/certification.py +263 -0
- {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/cli.py +905 -89
- {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/comment.py +94 -12
- {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/compare.py +233 -15
- frontier_runner-0.2.2/frontier/environment.py +86 -0
- {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/execute.py +493 -11
- frontier_runner-0.2.2/frontier/filter_v1.py +2423 -0
- {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/frontier.py +27 -8
- {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/impact.py +4 -1
- frontier_runner-0.2.2/frontier/mutation.py +373 -0
- {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/onboard/permissions.py +7 -0
- {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/onboard/saas.py +1 -1
- {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/progress.py +37 -4
- {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/proof.py +209 -46
- frontier_runner-0.2.2/frontier/repair.py +394 -0
- frontier_runner-0.2.2/frontier/snapshot.py +643 -0
- {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/sql_fingerprint.py +29 -0
- {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/validation.py +8 -3
- {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/warehouse.py +185 -0
- {frontier_runner-0.2.0 → frontier_runner-0.2.2/frontier_runner.egg-info}/PKG-INFO +4 -4
- {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier_runner.egg-info/SOURCES.txt +6 -0
- {frontier_runner-0.2.0 → frontier_runner-0.2.2}/pyproject.toml +1 -1
- {frontier_runner-0.2.0 → frontier_runner-0.2.2}/LICENSE +0 -0
- {frontier_runner-0.2.0 → frontier_runner-0.2.2}/MANIFEST.in +0 -0
- {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/__main__.py +0 -0
- {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/adapters/__init__.py +0 -0
- {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/adapters/databricks.py +0 -0
- {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/adapters/postgres.py +0 -0
- {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/adapters/redshift.py +0 -0
- {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/artifacts.py +0 -0
- {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/cdc/__init__.py +0 -0
- {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/cdc/config.py +0 -0
- {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/cdc/consume.py +0 -0
- {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/cdc/normalize.py +0 -0
- {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/cdc/prove.py +0 -0
- {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/cdc/route.py +0 -0
- {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/cdc/store.py +0 -0
- {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/cdc/streams.py +0 -0
- {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/cdc/upload.py +0 -0
- {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/config.py +0 -0
- {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/credentials.py +0 -0
- {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/dbt_artifacts.py +0 -0
- {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/errors.py +0 -0
- {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/github.py +0 -0
- {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/hashing.py +0 -0
- {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/local_config.py +0 -0
- {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/onboard/__init__.py +0 -0
- {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/onboard/commands.py +0 -0
- {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/onboard/constants.py +0 -0
- {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/onboard/demo.py +0 -0
- {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/onboard/detect.py +0 -0
- {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/onboard/discover.py +0 -0
- {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/onboard/doctor.py +0 -0
- {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/onboard/github.py +0 -0
- {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/onboard/gitignore.py +0 -0
- {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/onboard/hashkey.py +0 -0
- {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/onboard/prompt.py +0 -0
- {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/onboard/routes.py +0 -0
- {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/onboard/versions.py +0 -0
- {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/semantic.py +0 -0
- {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/snowflake.py +0 -0
- {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/snowflake_sql.py +0 -0
- {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier_runner.egg-info/dependency_links.txt +0 -0
- {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier_runner.egg-info/entry_points.txt +0 -0
- {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier_runner.egg-info/requires.txt +0 -0
- {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier_runner.egg-info/top_level.txt +0 -0
- {frontier_runner-0.2.0 → frontier_runner-0.2.2}/setup.cfg +0 -0
|
@@ -5,6 +5,20 @@ All notable changes to frontier-runner are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.2.2] — 2026-09-22
|
|
9
|
+
|
|
10
|
+
- Disposable repair validation is the authoritative repaired-vs-head check. The unchanged materialized mart versus head comparison is an expected pre-repair difference and no longer fails the run.
|
|
11
|
+
- `assert_repaired_equals_reference`, mismatch counts, targeted-repair safety, GitHub comments, and run detail now follow `repairValidation`. Production apply remains not requested unless explicitly applied.
|
|
12
|
+
- Mart baseline, candidate certification, disposable repair, production apply, economics, and execution stay independent dimensions. `NOT_EVALUATED` economics does not fail a correct assessment.
|
|
13
|
+
|
|
14
|
+
## [0.2.1] — 2026-09-22
|
|
15
|
+
|
|
16
|
+
- Made filter-v1 eligibility and the RowCover candidate compiler authoritative for `compare` and `prove`. Ineligible plans stay `UNCERTIFIED` and are not reported as safe for targeted repair.
|
|
17
|
+
- Failed `prove` writes a fresh uniquely identified run artifact. `upload` refuses a stale `frontier-run.json`.
|
|
18
|
+
- Targeted SQL generation failures are classified as execution failures instead of being swallowed.
|
|
19
|
+
- Mixed compiled dbt environments fail before warehouse execution with `ENVIRONMENT_MISMATCH`.
|
|
20
|
+
- Economics stay independent of `SQL_CERTIFIED` and use measured bytes rather than a demonstrated cost saving.
|
|
21
|
+
|
|
8
22
|
## [0.2.0] — 2026-09-21
|
|
9
23
|
|
|
10
24
|
- Added browser-based CLI authentication.
|
|
@@ -86,6 +100,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
86
100
|
- SaaS commands resolve stored keychain credentials through one shared
|
|
87
101
|
resolver instead of requiring `FRONTIER_API_KEY` in the environment.
|
|
88
102
|
|
|
103
|
+
[0.2.2]: https://github.com/jadsamara/frontier-runner/compare/v0.2.1...v0.2.2
|
|
104
|
+
[0.2.1]: https://github.com/jadsamara/frontier-runner/compare/v0.2.0...v0.2.1
|
|
89
105
|
[0.2.0]: https://github.com/jadsamara/frontier-runner/compare/v0.1.3...v0.2.0
|
|
90
106
|
[0.1.3]: https://github.com/jadsamara/frontier-runner/compare/v0.1.2...v0.1.3
|
|
91
107
|
[0.1.2]: https://github.com/jadsamara/frontier-runner/compare/v0.1.1...v0.1.2
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: frontier-runner
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Customer-side Frontier CLI for dbt + Snowflake or BigQuery + GitHub impact assessments.
|
|
5
5
|
Author: Frontier
|
|
6
6
|
License: MIT License
|
|
@@ -92,7 +92,7 @@ Until the package is on PyPI, install the GitHub Release wheel for a version
|
|
|
92
92
|
tag. You do not need a commit SHA:
|
|
93
93
|
|
|
94
94
|
```bash
|
|
95
|
-
pipx install \ "frontier-runner[snowflake] @ https://github.com/jadsamara/frontier-runner/releases/download/v0.2.
|
|
95
|
+
pipx install \ "frontier-runner[snowflake] @ https://github.com/jadsamara/frontier-runner/releases/download/v0.2.2/frontier_runner-0.2.2-py3-none-any.whl"
|
|
96
96
|
pipx inject frontier-runner "snowflake-connector-python>=3.12,<4"
|
|
97
97
|
```
|
|
98
98
|
|
|
@@ -222,9 +222,9 @@ workflow uses `FRONTIER_BLOCKING=false` unless you pass `--blocking`.
|
|
|
222
222
|
Pin an immutable released version:
|
|
223
223
|
|
|
224
224
|
```bash
|
|
225
|
-
pip install "frontier-runner[snowflake]==0.2.
|
|
225
|
+
pip install "frontier-runner[snowflake]==0.2.2"
|
|
226
226
|
# or
|
|
227
|
-
pip install "frontier-runner[bigquery]==0.2.
|
|
227
|
+
pip install "frontier-runner[bigquery]==0.2.2"
|
|
228
228
|
```
|
|
229
229
|
|
|
230
230
|
Until PyPI trusted publishing is reviewed and live, install the GitHub Release
|
|
@@ -34,7 +34,7 @@ Until the package is on PyPI, install the GitHub Release wheel for a version
|
|
|
34
34
|
tag. You do not need a commit SHA:
|
|
35
35
|
|
|
36
36
|
```bash
|
|
37
|
-
pipx install \ "frontier-runner[snowflake] @ https://github.com/jadsamara/frontier-runner/releases/download/v0.2.
|
|
37
|
+
pipx install \ "frontier-runner[snowflake] @ https://github.com/jadsamara/frontier-runner/releases/download/v0.2.2/frontier_runner-0.2.2-py3-none-any.whl"
|
|
38
38
|
pipx inject frontier-runner "snowflake-connector-python>=3.12,<4"
|
|
39
39
|
```
|
|
40
40
|
|
|
@@ -164,9 +164,9 @@ workflow uses `FRONTIER_BLOCKING=false` unless you pass `--blocking`.
|
|
|
164
164
|
Pin an immutable released version:
|
|
165
165
|
|
|
166
166
|
```bash
|
|
167
|
-
pip install "frontier-runner[snowflake]==0.2.
|
|
167
|
+
pip install "frontier-runner[snowflake]==0.2.2"
|
|
168
168
|
# or
|
|
169
|
-
pip install "frontier-runner[bigquery]==0.2.
|
|
169
|
+
pip install "frontier-runner[bigquery]==0.2.2"
|
|
170
170
|
```
|
|
171
171
|
|
|
172
172
|
Until PyPI trusted publishing is reviewed and live, install the GitHub Release
|
|
@@ -75,3 +75,22 @@ class CursorAdapter:
|
|
|
75
75
|
|
|
76
76
|
def get_query_history(self, run_id: str) -> list[dict[str, Any]]:
|
|
77
77
|
return []
|
|
78
|
+
|
|
79
|
+
def capture_snapshot(self, relations: list[str] | tuple[str, ...], **kwargs: Any) -> Any:
|
|
80
|
+
from frontier.snapshot import SOURCE_SNAPSHOT_NOT_PINNED, unpinned_snapshot
|
|
81
|
+
|
|
82
|
+
del relations, kwargs
|
|
83
|
+
return unpinned_snapshot(
|
|
84
|
+
failure_code=SOURCE_SNAPSHOT_NOT_PINNED,
|
|
85
|
+
failure_reason=f"{self.warehouse_type} does not pin source snapshots",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
def bind_query_to_snapshot(self, sql: str, snapshot: Any) -> str:
|
|
89
|
+
del snapshot
|
|
90
|
+
return sql
|
|
91
|
+
|
|
92
|
+
def verify_snapshot_binding(self, sql: str, snapshot: Any) -> bool:
|
|
93
|
+
from frontier.snapshot import ASSURANCE_ADAPTER
|
|
94
|
+
|
|
95
|
+
del sql
|
|
96
|
+
return getattr(snapshot, "assurance", None) == ASSURANCE_ADAPTER and False
|
|
@@ -183,6 +183,23 @@ class BigQueryAdapter:
|
|
|
183
183
|
"location": self.location,
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
+
def capture_snapshot(self, relations: list[str] | tuple[str, ...], **kwargs: Any) -> Any:
|
|
187
|
+
from frontier.snapshot import SOURCE_SNAPSHOT_NOT_PINNED, unpinned_snapshot
|
|
188
|
+
|
|
189
|
+
del relations, kwargs
|
|
190
|
+
return unpinned_snapshot(
|
|
191
|
+
failure_code=SOURCE_SNAPSHOT_NOT_PINNED,
|
|
192
|
+
failure_reason="BigQuery source snapshots are not implemented",
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
def bind_query_to_snapshot(self, sql: str, snapshot: Any) -> str:
|
|
196
|
+
del snapshot
|
|
197
|
+
return sql
|
|
198
|
+
|
|
199
|
+
def verify_snapshot_binding(self, sql: str, snapshot: Any) -> bool:
|
|
200
|
+
del sql, snapshot
|
|
201
|
+
return False
|
|
202
|
+
|
|
186
203
|
def close(self) -> None:
|
|
187
204
|
client = self._client
|
|
188
205
|
if client is not None and hasattr(client, "close"):
|
|
@@ -124,7 +124,8 @@ class SnowflakeAdapter(CursorAdapter):
|
|
|
124
124
|
return {}
|
|
125
125
|
try:
|
|
126
126
|
rows = self.execute(
|
|
127
|
-
"select query_id, bytes_scanned,
|
|
127
|
+
"select query_id, bytes_scanned, rows_produced, total_elapsed_time, "
|
|
128
|
+
"credits_used_cloud_services "
|
|
128
129
|
"from table(information_schema.query_history()) "
|
|
129
130
|
f"where query_id = {sql_string(token)} "
|
|
130
131
|
"order by start_time desc limit 1"
|
|
@@ -134,13 +135,171 @@ class SnowflakeAdapter(CursorAdapter):
|
|
|
134
135
|
if not rows:
|
|
135
136
|
return {}
|
|
136
137
|
row = rows[0]
|
|
138
|
+
elapsed = row[3] if len(row) > 3 else None
|
|
137
139
|
return {
|
|
138
140
|
"query_id": row[0],
|
|
139
141
|
"bytes_scanned": row[1] if len(row) > 1 else None,
|
|
140
|
-
"
|
|
141
|
-
"
|
|
142
|
+
"rows_produced": row[2] if len(row) > 2 else None,
|
|
143
|
+
"elapsed_ms": elapsed,
|
|
144
|
+
"total_elapsed_ms": elapsed,
|
|
145
|
+
"cloud_services_credits": float(row[4]) if len(row) > 4 and row[4] is not None else None,
|
|
142
146
|
}
|
|
143
147
|
|
|
148
|
+
def capture_snapshot(self, relations: list[str] | tuple[str, ...], **kwargs: Any) -> Any:
|
|
149
|
+
from frontier.snapshot import capture_from_catalog, utc_now_iso
|
|
150
|
+
|
|
151
|
+
identifier = self._current_timestamp_literal()
|
|
152
|
+
catalog = self._inventory_relations(relations)
|
|
153
|
+
return capture_from_catalog(
|
|
154
|
+
relations,
|
|
155
|
+
catalog=catalog,
|
|
156
|
+
identifier=identifier,
|
|
157
|
+
captured_at=utc_now_iso(),
|
|
158
|
+
attestation_source=kwargs.get("attestation_source"),
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
def bind_query_to_snapshot(self, sql: str, snapshot: Any) -> str:
|
|
162
|
+
from frontier.snapshot import bind_sql_to_snapshot
|
|
163
|
+
|
|
164
|
+
return bind_sql_to_snapshot(sql, snapshot, dialect=self.dialect)
|
|
165
|
+
|
|
166
|
+
def verify_snapshot_binding(self, sql: str, snapshot: Any) -> bool:
|
|
167
|
+
from frontier.snapshot import verify_snapshot_binding as verify
|
|
168
|
+
|
|
169
|
+
return verify(sql, snapshot, dialect=self.dialect)
|
|
170
|
+
|
|
171
|
+
def _current_timestamp_literal(self) -> str:
|
|
172
|
+
rows = self.execute(
|
|
173
|
+
"select to_varchar(current_timestamp(), 'YYYY-MM-DD HH24:MI:SS.FF3 TZHTZM')"
|
|
174
|
+
)
|
|
175
|
+
if not rows or rows[0][0] is None:
|
|
176
|
+
from frontier.snapshot import SnapshotError, SOURCE_SNAPSHOT_NOT_PINNED
|
|
177
|
+
|
|
178
|
+
raise SnapshotError(SOURCE_SNAPSHOT_NOT_PINNED, "Snowflake did not return a snapshot timestamp")
|
|
179
|
+
return str(rows[0][0]).strip()
|
|
180
|
+
|
|
181
|
+
def _inventory_relations(self, relations: list[str] | tuple[str, ...]) -> dict[str, dict[str, Any]]:
|
|
182
|
+
from frontier.snapshot import DYNAMIC_TABLE, EXTERNAL_TABLE, MATERIALIZED_VIEW, VIEW
|
|
183
|
+
from frontier.warehouse import split_relation_parts
|
|
184
|
+
|
|
185
|
+
catalog: dict[str, dict[str, Any]] = {}
|
|
186
|
+
pending = [str(item).strip() for item in relations if str(item).strip()]
|
|
187
|
+
seen: set[str] = set()
|
|
188
|
+
while pending:
|
|
189
|
+
name = pending.pop(0)
|
|
190
|
+
key = name.lower()
|
|
191
|
+
if key in seen:
|
|
192
|
+
continue
|
|
193
|
+
seen.add(key)
|
|
194
|
+
database, schema, table = split_relation_parts(name)
|
|
195
|
+
if not table:
|
|
196
|
+
continue
|
|
197
|
+
entry = self._lookup_table(database, schema, table)
|
|
198
|
+
if entry is None:
|
|
199
|
+
continue
|
|
200
|
+
kind = str(entry.get("kind") or "")
|
|
201
|
+
if kind == VIEW:
|
|
202
|
+
definition = self._view_definition(database, schema, table)
|
|
203
|
+
if definition:
|
|
204
|
+
entry["view_sql"] = definition
|
|
205
|
+
from frontier.snapshot import collect_source_relations
|
|
206
|
+
|
|
207
|
+
pending.extend(collect_source_relations(definition, dialect="snowflake"))
|
|
208
|
+
elif kind in {MATERIALIZED_VIEW, EXTERNAL_TABLE, DYNAMIC_TABLE}:
|
|
209
|
+
entry["kind"] = kind
|
|
210
|
+
catalog[name] = entry
|
|
211
|
+
dynamic = self._is_dynamic_table(database, schema, table)
|
|
212
|
+
if dynamic:
|
|
213
|
+
catalog[name]["kind"] = DYNAMIC_TABLE
|
|
214
|
+
return catalog
|
|
215
|
+
|
|
216
|
+
def _lookup_table(
|
|
217
|
+
self,
|
|
218
|
+
database: str | None,
|
|
219
|
+
schema: str | None,
|
|
220
|
+
table: str,
|
|
221
|
+
) -> dict[str, Any] | None:
|
|
222
|
+
sql = (
|
|
223
|
+
"select table_catalog, table_schema, table_name, table_type, "
|
|
224
|
+
"is_transient, retention_time "
|
|
225
|
+
"from information_schema.tables "
|
|
226
|
+
f"where lower(table_name) = lower({sql_string(table)})"
|
|
227
|
+
)
|
|
228
|
+
if schema:
|
|
229
|
+
sql += f" and lower(table_schema) = lower({sql_string(schema)})"
|
|
230
|
+
if database:
|
|
231
|
+
sql += f" and lower(table_catalog) = lower({sql_string(database)})"
|
|
232
|
+
sql += " limit 1"
|
|
233
|
+
try:
|
|
234
|
+
rows = self.execute(sql)
|
|
235
|
+
except Exception:
|
|
236
|
+
return None
|
|
237
|
+
if not rows:
|
|
238
|
+
return None
|
|
239
|
+
row = rows[0]
|
|
240
|
+
table_type = str(row[3] or "").strip().upper()
|
|
241
|
+
is_transient = str(row[4] or "").strip().upper() == "YES"
|
|
242
|
+
retention = row[5] if len(row) > 5 else None
|
|
243
|
+
kind = table_type.lower()
|
|
244
|
+
if table_type == "BASE TABLE" and is_transient:
|
|
245
|
+
kind = "transient"
|
|
246
|
+
elif table_type == "BASE TABLE":
|
|
247
|
+
kind = "permanent_table"
|
|
248
|
+
return {
|
|
249
|
+
"kind": kind,
|
|
250
|
+
"retention_days": int(retention) if retention is not None else None,
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
def _view_definition(
|
|
254
|
+
self,
|
|
255
|
+
database: str | None,
|
|
256
|
+
schema: str | None,
|
|
257
|
+
table: str,
|
|
258
|
+
) -> str | None:
|
|
259
|
+
sql = (
|
|
260
|
+
"select view_definition from information_schema.views "
|
|
261
|
+
f"where lower(table_name) = lower({sql_string(table)})"
|
|
262
|
+
)
|
|
263
|
+
if schema:
|
|
264
|
+
sql += f" and lower(table_schema) = lower({sql_string(schema)})"
|
|
265
|
+
if database:
|
|
266
|
+
sql += f" and lower(table_catalog) = lower({sql_string(database)})"
|
|
267
|
+
sql += " limit 1"
|
|
268
|
+
try:
|
|
269
|
+
rows = self.execute(sql)
|
|
270
|
+
except Exception:
|
|
271
|
+
return None
|
|
272
|
+
if not rows or rows[0][0] is None:
|
|
273
|
+
return None
|
|
274
|
+
text = str(rows[0][0]).strip().rstrip(";")
|
|
275
|
+
if text.lower().startswith("create "):
|
|
276
|
+
lowered = text.lower()
|
|
277
|
+
marker = " as "
|
|
278
|
+
index = lowered.find(marker)
|
|
279
|
+
if index != -1:
|
|
280
|
+
text = text[index + len(marker) :].strip()
|
|
281
|
+
return text or None
|
|
282
|
+
|
|
283
|
+
def _is_dynamic_table(
|
|
284
|
+
self,
|
|
285
|
+
database: str | None,
|
|
286
|
+
schema: str | None,
|
|
287
|
+
table: str,
|
|
288
|
+
) -> bool:
|
|
289
|
+
sql = (
|
|
290
|
+
"select 1 from information_schema.dynamic_tables "
|
|
291
|
+
f"where lower(table_name) = lower({sql_string(table)})"
|
|
292
|
+
)
|
|
293
|
+
if schema:
|
|
294
|
+
sql += f" and lower(table_schema) = lower({sql_string(schema)})"
|
|
295
|
+
if database:
|
|
296
|
+
sql += f" and lower(table_catalog) = lower({sql_string(database)})"
|
|
297
|
+
sql += " limit 1"
|
|
298
|
+
try:
|
|
299
|
+
return bool(self.execute(sql))
|
|
300
|
+
except Exception:
|
|
301
|
+
return False
|
|
302
|
+
|
|
144
303
|
def describe(self) -> dict[str, Any]:
|
|
145
304
|
if self._config is None:
|
|
146
305
|
return {"warehouse_type": self.warehouse_type}
|
|
@@ -73,6 +73,9 @@ def build_ingest_payload(
|
|
|
73
73
|
semantic_manifest_version: int | None = None,
|
|
74
74
|
semantic_manifest_fingerprint: str | None = None,
|
|
75
75
|
manifest_source: str | None = None,
|
|
76
|
+
runner_version: str | None = None,
|
|
77
|
+
dbt_target: str | None = None,
|
|
78
|
+
assessment_identity: dict[str, Any] | None = None,
|
|
76
79
|
) -> dict[str, Any]:
|
|
77
80
|
payload = {
|
|
78
81
|
"externalRunId": external_run_id,
|
|
@@ -121,6 +124,12 @@ def build_ingest_payload(
|
|
|
121
124
|
payload["semanticManifestFingerprint"] = semantic_manifest_fingerprint
|
|
122
125
|
if manifest_source:
|
|
123
126
|
payload["manifestSource"] = manifest_source
|
|
127
|
+
if runner_version:
|
|
128
|
+
payload["runnerVersion"] = runner_version
|
|
129
|
+
if dbt_target:
|
|
130
|
+
payload["dbtTarget"] = dbt_target
|
|
131
|
+
if assessment_identity:
|
|
132
|
+
payload["assessmentIdentity"] = assessment_identity
|
|
124
133
|
assert_payload_has_no_secrets(payload)
|
|
125
134
|
assert_no_raw_rows(payload)
|
|
126
135
|
return payload
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""Separated assessment result dimensions for filter-v1.
|
|
2
|
+
|
|
3
|
+
Certification, validation, economics, and execution are independent
|
|
4
|
+
claims. Warehouse execution failure, economic fallback, and
|
|
5
|
+
full-reference validation must not overwrite certification status.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from frontier.snapshot import (
|
|
13
|
+
ASSURANCE_ADAPTER,
|
|
14
|
+
MODE_NONE,
|
|
15
|
+
RULE_SET_VERSION,
|
|
16
|
+
SOURCE_SNAPSHOT_NOT_PINNED,
|
|
17
|
+
SourceSnapshot,
|
|
18
|
+
unpinned_snapshot,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
SQL_CERTIFIED = "SQL_CERTIFIED"
|
|
22
|
+
CONTRACT_CERTIFIED = "CONTRACT_CERTIFIED"
|
|
23
|
+
UNCERTIFIED = "UNCERTIFIED"
|
|
24
|
+
_LEGACY_UNCIFIED = "UNCIFIED"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def normalize_certification_status(value: str | None) -> str:
|
|
28
|
+
"""Accept the 21A UNCIFIED misspelling; emit only UNCERTIFIED."""
|
|
29
|
+
if value == _LEGACY_UNCIFIED:
|
|
30
|
+
return UNCERTIFIED
|
|
31
|
+
if value in {SQL_CERTIFIED, CONTRACT_CERTIFIED, UNCERTIFIED}:
|
|
32
|
+
return value
|
|
33
|
+
return UNCERTIFIED
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
VALIDATION_NOT_RUN = "NOT_RUN"
|
|
37
|
+
CANDIDATES_CONFIRMED = "CANDIDATES_CONFIRMED"
|
|
38
|
+
FULL_REFERENCE_VALIDATED = "FULL_REFERENCE_VALIDATED"
|
|
39
|
+
VALIDATION_FAILED = "FAILED"
|
|
40
|
+
|
|
41
|
+
TARGETED_REPAIR_RECOMMENDED = "TARGETED_REPAIR_RECOMMENDED"
|
|
42
|
+
FULL_REBUILD_RECOMMENDED = "FULL_REBUILD_RECOMMENDED"
|
|
43
|
+
ECONOMICS_NOT_EVALUATED = "NOT_EVALUATED"
|
|
44
|
+
|
|
45
|
+
EXECUTION_PENDING = "PENDING"
|
|
46
|
+
EXECUTION_SUCCEEDED = "SUCCEEDED"
|
|
47
|
+
EXECUTION_FAILED = "FAILED"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def certification_status(
|
|
51
|
+
snapshot: SourceSnapshot | None,
|
|
52
|
+
*,
|
|
53
|
+
static_certified: bool = False,
|
|
54
|
+
contract_certified: bool = False,
|
|
55
|
+
) -> tuple[str, str | None]:
|
|
56
|
+
"""Return (status, failure_code). Never inspects execution or validation."""
|
|
57
|
+
pinned = snapshot if snapshot is not None else unpinned_snapshot()
|
|
58
|
+
if not pinned.allows_sql_certified():
|
|
59
|
+
code = pinned.failure_code or SOURCE_SNAPSHOT_NOT_PINNED
|
|
60
|
+
return UNCERTIFIED, code
|
|
61
|
+
if static_certified:
|
|
62
|
+
return SQL_CERTIFIED, None
|
|
63
|
+
if contract_certified:
|
|
64
|
+
return CONTRACT_CERTIFIED, None
|
|
65
|
+
return UNCERTIFIED, None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def validation_status(
|
|
69
|
+
*,
|
|
70
|
+
full_reference_validated: bool = False,
|
|
71
|
+
full_reference_failed: bool = False,
|
|
72
|
+
candidates_confirmed: bool = False,
|
|
73
|
+
confirmation_failed: bool = False,
|
|
74
|
+
) -> str:
|
|
75
|
+
if full_reference_validated:
|
|
76
|
+
return FULL_REFERENCE_VALIDATED
|
|
77
|
+
if full_reference_failed or confirmation_failed:
|
|
78
|
+
return VALIDATION_FAILED
|
|
79
|
+
if candidates_confirmed:
|
|
80
|
+
return CANDIDATES_CONFIRMED
|
|
81
|
+
return VALIDATION_NOT_RUN
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def economics_decision(
|
|
85
|
+
*,
|
|
86
|
+
full_rebuild_recommended: bool = False,
|
|
87
|
+
targeted_ran: bool = False,
|
|
88
|
+
frontier_bytes: int | None = None,
|
|
89
|
+
full_comparison_bytes: int | None = None,
|
|
90
|
+
warehouse_credits: float | None = None,
|
|
91
|
+
) -> str:
|
|
92
|
+
"""Bytes and wall time are not billed credits. Do not recommend targeted repair from running alone."""
|
|
93
|
+
del targeted_ran
|
|
94
|
+
if full_rebuild_recommended:
|
|
95
|
+
return FULL_REBUILD_RECOMMENDED
|
|
96
|
+
if frontier_bytes is not None and full_comparison_bytes is not None:
|
|
97
|
+
if frontier_bytes >= full_comparison_bytes:
|
|
98
|
+
return FULL_REBUILD_RECOMMENDED
|
|
99
|
+
if warehouse_credits is None:
|
|
100
|
+
return ECONOMICS_NOT_EVALUATED
|
|
101
|
+
return TARGETED_REPAIR_RECOMMENDED
|
|
102
|
+
return ECONOMICS_NOT_EVALUATED
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def execution_status(
|
|
106
|
+
*,
|
|
107
|
+
execution_failed: bool = False,
|
|
108
|
+
execution_ran: bool = False,
|
|
109
|
+
) -> str:
|
|
110
|
+
if execution_failed:
|
|
111
|
+
return EXECUTION_FAILED
|
|
112
|
+
if execution_ran:
|
|
113
|
+
return EXECUTION_SUCCEEDED
|
|
114
|
+
return EXECUTION_PENDING
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def build_assessment_dimensions(
|
|
118
|
+
*,
|
|
119
|
+
snapshot: SourceSnapshot | None,
|
|
120
|
+
static_certified: bool = False,
|
|
121
|
+
contract_certified: bool = False,
|
|
122
|
+
execution_failed: bool = False,
|
|
123
|
+
execution_ran: bool = False,
|
|
124
|
+
full_rebuild_recommended: bool = False,
|
|
125
|
+
targeted_ran: bool = False,
|
|
126
|
+
frontier_bytes: int | None = None,
|
|
127
|
+
full_comparison_bytes: int | None = None,
|
|
128
|
+
warehouse_credits: float | None = None,
|
|
129
|
+
candidates_confirmed: bool = False,
|
|
130
|
+
confirmation_failed: bool = False,
|
|
131
|
+
full_reference_validated: bool = False,
|
|
132
|
+
full_reference_failed: bool = False,
|
|
133
|
+
failure_phase: str | None = None,
|
|
134
|
+
failure_code: str | None = None,
|
|
135
|
+
failure_reason: str | None = None,
|
|
136
|
+
) -> dict[str, Any]:
|
|
137
|
+
cert_status, cert_code = certification_status(
|
|
138
|
+
snapshot,
|
|
139
|
+
static_certified=static_certified,
|
|
140
|
+
contract_certified=contract_certified,
|
|
141
|
+
)
|
|
142
|
+
if cert_status == SQL_CERTIFIED and (snapshot is None or not snapshot.allows_sql_certified()):
|
|
143
|
+
cert_status = UNCERTIFIED
|
|
144
|
+
cert_code = SOURCE_SNAPSHOT_NOT_PINNED
|
|
145
|
+
certification: dict[str, Any] = {
|
|
146
|
+
"status": cert_status,
|
|
147
|
+
"ruleSetVersion": RULE_SET_VERSION,
|
|
148
|
+
}
|
|
149
|
+
if cert_code:
|
|
150
|
+
certification["failureCode"] = cert_code
|
|
151
|
+
source = snapshot or unpinned_snapshot()
|
|
152
|
+
upload = source.to_upload_payload()
|
|
153
|
+
economics = {
|
|
154
|
+
"decision": economics_decision(
|
|
155
|
+
full_rebuild_recommended=full_rebuild_recommended,
|
|
156
|
+
targeted_ran=targeted_ran,
|
|
157
|
+
frontier_bytes=frontier_bytes,
|
|
158
|
+
full_comparison_bytes=full_comparison_bytes,
|
|
159
|
+
warehouse_credits=warehouse_credits,
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
if frontier_bytes is not None:
|
|
163
|
+
economics["frontierBytesScanned"] = int(frontier_bytes)
|
|
164
|
+
if full_comparison_bytes is not None:
|
|
165
|
+
economics["fullComparisonBytesScanned"] = int(full_comparison_bytes)
|
|
166
|
+
execution: dict[str, Any] = {
|
|
167
|
+
"status": execution_status(
|
|
168
|
+
execution_failed=execution_failed,
|
|
169
|
+
execution_ran=execution_ran,
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
if failure_phase:
|
|
173
|
+
execution["failurePhase"] = failure_phase[:64]
|
|
174
|
+
if failure_code:
|
|
175
|
+
execution["failureCode"] = failure_code[:64]
|
|
176
|
+
if failure_reason:
|
|
177
|
+
execution["failureReason"] = failure_reason[:512]
|
|
178
|
+
return {
|
|
179
|
+
"certification": certification,
|
|
180
|
+
"validation": {"status": validation_status(
|
|
181
|
+
full_reference_validated=full_reference_validated,
|
|
182
|
+
full_reference_failed=full_reference_failed,
|
|
183
|
+
candidates_confirmed=candidates_confirmed,
|
|
184
|
+
confirmation_failed=confirmation_failed,
|
|
185
|
+
)},
|
|
186
|
+
"economics": economics,
|
|
187
|
+
"execution": execution,
|
|
188
|
+
"sourceSnapshot": upload,
|
|
189
|
+
"baselineBoundary": dict(BASELINE_BOUNDARY),
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
BASELINE_BOUNDARY = {
|
|
194
|
+
"comparesSqlVersionsAtPinnedSnapshot": True,
|
|
195
|
+
"existingMaterializedMartRepresentsSnapshot": False,
|
|
196
|
+
"safeInPlaceProductionRepair": False,
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def filter_v1_sql_certified(
|
|
201
|
+
*,
|
|
202
|
+
eligible: bool,
|
|
203
|
+
compiled: bool,
|
|
204
|
+
confirmed: bool,
|
|
205
|
+
execution_failed: bool,
|
|
206
|
+
) -> bool:
|
|
207
|
+
"""All four 21C gates except snapshot, which certification_status still checks."""
|
|
208
|
+
return bool(eligible and compiled and confirmed and not execution_failed)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def enrich_certification_record(
|
|
212
|
+
dimensions: dict[str, Any],
|
|
213
|
+
*,
|
|
214
|
+
eligibility: dict[str, Any] | None,
|
|
215
|
+
snapshot: SourceSnapshot | None,
|
|
216
|
+
candidate_fingerprint: str | None = None,
|
|
217
|
+
execution_failed: bool = False,
|
|
218
|
+
) -> dict[str, Any]:
|
|
219
|
+
"""Attach plan, taint, assumption, and snapshot identity to certification."""
|
|
220
|
+
certification = dict(dimensions.get("certification") or {})
|
|
221
|
+
eligibility = eligibility or {}
|
|
222
|
+
for src, dest in (
|
|
223
|
+
("oldPlanFingerprint", "oldPlanFingerprint"),
|
|
224
|
+
("newPlanFingerprint", "newPlanFingerprint"),
|
|
225
|
+
("changedFilterNodeId", "changedFilterNodeId"),
|
|
226
|
+
("coveredNodeIds", "coveredNodeIds"),
|
|
227
|
+
("operators", "operators"),
|
|
228
|
+
("joinTaint", "joinTaint"),
|
|
229
|
+
("checkedAssumptions", "checkedAssumptions"),
|
|
230
|
+
("manifestDependencies", "manifestDependencies"),
|
|
231
|
+
("candidateFingerprint", "candidateQueryFingerprint"),
|
|
232
|
+
):
|
|
233
|
+
value = eligibility.get(src)
|
|
234
|
+
if value:
|
|
235
|
+
certification[dest] = value
|
|
236
|
+
if candidate_fingerprint:
|
|
237
|
+
certification["candidateQueryFingerprint"] = candidate_fingerprint
|
|
238
|
+
source = snapshot or unpinned_snapshot()
|
|
239
|
+
if source.identifier:
|
|
240
|
+
certification["snapshotIdentifier"] = source.identifier
|
|
241
|
+
if eligibility.get("eligible") is False:
|
|
242
|
+
certification["status"] = UNCERTIFIED
|
|
243
|
+
certification["failureCode"] = eligibility.get("reasonCode") or UNCERTIFIED
|
|
244
|
+
elif execution_failed:
|
|
245
|
+
certification["status"] = UNCERTIFIED
|
|
246
|
+
if snapshot is not None and (
|
|
247
|
+
snapshot.assurance == ASSURANCE_ADAPTER or snapshot.allows_sql_certified()
|
|
248
|
+
):
|
|
249
|
+
certification["failureCode"] = EXECUTION_FAILED
|
|
250
|
+
dimensions["certification"] = certification
|
|
251
|
+
dimensions["baselineBoundary"] = dict(BASELINE_BOUNDARY)
|
|
252
|
+
return dimensions
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def display_evidence_level(evidence_level: str, validation: dict[str, Any] | None) -> str:
|
|
256
|
+
"""Never label empirically validated unless full-reference actually ran."""
|
|
257
|
+
if (
|
|
258
|
+
validation
|
|
259
|
+
and validation.get("status") != FULL_REFERENCE_VALIDATED
|
|
260
|
+
and evidence_level == "empirically_validated"
|
|
261
|
+
):
|
|
262
|
+
return "aggregates"
|
|
263
|
+
return evidence_level
|