tablevalidator-databricks 0.1.0__tar.gz → 0.1.1__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.
- {tablevalidator_databricks-0.1.0 → tablevalidator_databricks-0.1.1}/PKG-INFO +14 -4
- {tablevalidator_databricks-0.1.0 → tablevalidator_databricks-0.1.1}/README.md +13 -3
- {tablevalidator_databricks-0.1.0 → tablevalidator_databricks-0.1.1}/tablevalidator_databricks/cli/main.py +5 -3
- tablevalidator_databricks-0.1.1/tablevalidator_databricks/templates/widget_notebook.py.tmpl +249 -0
- {tablevalidator_databricks-0.1.0 → tablevalidator_databricks-0.1.1}/tablevalidator_databricks.egg-info/PKG-INFO +14 -4
- {tablevalidator_databricks-0.1.0 → tablevalidator_databricks-0.1.1}/tablevalidator_databricks.egg-info/SOURCES.txt +1 -0
- {tablevalidator_databricks-0.1.0 → tablevalidator_databricks-0.1.1}/tablevalidator_databricks.egg-info/scm_file_list.json +1 -0
- {tablevalidator_databricks-0.1.0 → tablevalidator_databricks-0.1.1}/tablevalidator_databricks.egg-info/scm_version.json +2 -2
- tablevalidator_databricks-0.1.1/tests/test_generated_notebook_execution.py +231 -0
- {tablevalidator_databricks-0.1.0 → tablevalidator_databricks-0.1.1}/tests/test_generator.py +16 -0
- tablevalidator_databricks-0.1.0/tablevalidator_databricks/templates/widget_notebook.py.tmpl +0 -178
- {tablevalidator_databricks-0.1.0 → tablevalidator_databricks-0.1.1}/pyproject.toml +0 -0
- {tablevalidator_databricks-0.1.0 → tablevalidator_databricks-0.1.1}/setup.cfg +0 -0
- {tablevalidator_databricks-0.1.0 → tablevalidator_databricks-0.1.1}/tablevalidator_databricks/__init__.py +0 -0
- {tablevalidator_databricks-0.1.0 → tablevalidator_databricks-0.1.1}/tablevalidator_databricks/cli/__init__.py +0 -0
- {tablevalidator_databricks-0.1.0 → tablevalidator_databricks-0.1.1}/tablevalidator_databricks/generator/__init__.py +0 -0
- {tablevalidator_databricks-0.1.0 → tablevalidator_databricks-0.1.1}/tablevalidator_databricks/generator/generator.py +0 -0
- {tablevalidator_databricks-0.1.0 → tablevalidator_databricks-0.1.1}/tablevalidator_databricks.egg-info/dependency_links.txt +0 -0
- {tablevalidator_databricks-0.1.0 → tablevalidator_databricks-0.1.1}/tablevalidator_databricks.egg-info/entry_points.txt +0 -0
- {tablevalidator_databricks-0.1.0 → tablevalidator_databricks-0.1.1}/tablevalidator_databricks.egg-info/requires.txt +0 -0
- {tablevalidator_databricks-0.1.0 → tablevalidator_databricks-0.1.1}/tablevalidator_databricks.egg-info/top_level.txt +0 -0
- {tablevalidator_databricks-0.1.0 → tablevalidator_databricks-0.1.1}/tests/test_cli.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tablevalidator-databricks
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: Generates a widget-driven Databricks notebook UI on top of the table-validator package - no code required to run a comparison.
|
|
5
5
|
License: MIT
|
|
6
6
|
Requires-Python: >=3.9
|
|
@@ -72,9 +72,19 @@ markers and opens it as a real notebook.
|
|
|
72
72
|
|
|
73
73
|
**Cascading dropdowns**: Databricks widgets don't auto-refresh when an
|
|
74
74
|
upstream selection changes. After changing Source/Target Catalog or
|
|
75
|
-
Schema, re-run the notebook's "Create widgets" cell once to
|
|
76
|
-
dropdowns below it before running the rest of the notebook.
|
|
77
|
-
Databricks widget limitation, not a bug in the generated
|
|
75
|
+
Schema, re-run the notebook's "Create / refresh widgets" cell once to
|
|
76
|
+
repopulate the dropdowns below it before running the rest of the notebook.
|
|
77
|
+
This is a Databricks widget limitation, not a bug in the generated
|
|
78
|
+
notebook. If you skip this and run validation anyway with a stale Schema/
|
|
79
|
+
Table selection, the notebook fails with a clear error telling you which
|
|
80
|
+
cell to re-run, rather than silently comparing the wrong table.
|
|
81
|
+
|
|
82
|
+
**Can't find a name in a dropdown?** Every Catalog/Schema/Table dropdown
|
|
83
|
+
has a matching "(manual override)" text widget next to it. Type the exact
|
|
84
|
+
name there and it's used instead of the dropdown - useful if something
|
|
85
|
+
isn't showing up (a permissions lag, sort order, anything). It's used
|
|
86
|
+
as-is, not checked against the live catalog listing. Leave every override
|
|
87
|
+
blank to just use the dropdowns as normal.
|
|
78
88
|
|
|
79
89
|
## Why a separate package
|
|
80
90
|
|
|
@@ -58,9 +58,19 @@ markers and opens it as a real notebook.
|
|
|
58
58
|
|
|
59
59
|
**Cascading dropdowns**: Databricks widgets don't auto-refresh when an
|
|
60
60
|
upstream selection changes. After changing Source/Target Catalog or
|
|
61
|
-
Schema, re-run the notebook's "Create widgets" cell once to
|
|
62
|
-
dropdowns below it before running the rest of the notebook.
|
|
63
|
-
Databricks widget limitation, not a bug in the generated
|
|
61
|
+
Schema, re-run the notebook's "Create / refresh widgets" cell once to
|
|
62
|
+
repopulate the dropdowns below it before running the rest of the notebook.
|
|
63
|
+
This is a Databricks widget limitation, not a bug in the generated
|
|
64
|
+
notebook. If you skip this and run validation anyway with a stale Schema/
|
|
65
|
+
Table selection, the notebook fails with a clear error telling you which
|
|
66
|
+
cell to re-run, rather than silently comparing the wrong table.
|
|
67
|
+
|
|
68
|
+
**Can't find a name in a dropdown?** Every Catalog/Schema/Table dropdown
|
|
69
|
+
has a matching "(manual override)" text widget next to it. Type the exact
|
|
70
|
+
name there and it's used instead of the dropdown - useful if something
|
|
71
|
+
isn't showing up (a permissions lag, sort order, anything). It's used
|
|
72
|
+
as-is, not checked against the live catalog listing. Leave every override
|
|
73
|
+
blank to just use the dropdowns as normal.
|
|
64
74
|
|
|
65
75
|
## Why a separate package
|
|
66
76
|
|
|
@@ -88,9 +88,11 @@ def info() -> None:
|
|
|
88
88
|
" 3. Fill in the widgets and Run All\n"
|
|
89
89
|
" Pick Source/Target Catalog, Schema, Table, and which checks "
|
|
90
90
|
"to run, then run the notebook top to bottom. Re-run the "
|
|
91
|
-
"'Create widgets' cell after changing a Catalog/Schema
|
|
92
|
-
"to repopulate the dropdowns below it (a Databricks
|
|
93
|
-
"limitation, not a bug)
|
|
91
|
+
"'Create / refresh widgets' cell after changing a Catalog/Schema "
|
|
92
|
+
"selection, to repopulate the dropdowns below it (a Databricks "
|
|
93
|
+
"widget limitation, not a bug). Can't find a name in a dropdown? "
|
|
94
|
+
"Each dropdown has a '(manual override)' text widget next to it - "
|
|
95
|
+
"type the exact name there instead.\n"
|
|
94
96
|
"\n"
|
|
95
97
|
"This package contains no validation logic of its own - every "
|
|
96
98
|
"generated notebook calls directly into table-validator's own "
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
# Databricks notebook source
|
|
2
|
+
# MAGIC %md
|
|
3
|
+
# MAGIC # Table Validator
|
|
4
|
+
# MAGIC Generated by `tablevalidator-databricks init --mode {{MODE}}`.
|
|
5
|
+
# MAGIC
|
|
6
|
+
# MAGIC Fill in the widgets above (Source/Target Catalog, Schema, Table, and
|
|
7
|
+
# MAGIC which checks to run), then **Run All**. No code needs to be written or
|
|
8
|
+
# MAGIC edited - every cell below just reads the widgets and calls the
|
|
9
|
+
# MAGIC `table_validator` package's `validate_tables()` API.
|
|
10
|
+
# MAGIC
|
|
11
|
+
# MAGIC **Changing Catalog or Schema after the dropdowns are already built?**
|
|
12
|
+
# MAGIC Databricks widgets have no on-change event, so picking a new Catalog
|
|
13
|
+
# MAGIC does not repopulate the Schema dropdown by itself (same for Schema ->
|
|
14
|
+
# MAGIC Table) - only re-running the **"Create / refresh widgets"** cell does.
|
|
15
|
+
# MAGIC After changing Source/Target Catalog or Schema, re-run just that one
|
|
16
|
+
# MAGIC cell (Shift+Enter) to refresh everything below it, then continue. This
|
|
17
|
+
# MAGIC is a Databricks platform limitation, not a bug in this notebook.
|
|
18
|
+
# MAGIC
|
|
19
|
+
# MAGIC **Can't find a name in a dropdown?** Each Catalog/Schema/Table dropdown
|
|
20
|
+
# MAGIC has a matching "(manual override)" text box right next to it. Type the
|
|
21
|
+
# MAGIC exact name there and it's used INSTEAD of the dropdown - useful if
|
|
22
|
+
# MAGIC something isn't showing up (a permissions lag, a name that doesn't sort
|
|
23
|
+
# MAGIC into view, etc.). Leave every override blank to just use the dropdowns
|
|
24
|
+
# MAGIC as normal.
|
|
25
|
+
|
|
26
|
+
# COMMAND ----------
|
|
27
|
+
|
|
28
|
+
# MAGIC %pip install table-validator
|
|
29
|
+
|
|
30
|
+
# COMMAND ----------
|
|
31
|
+
|
|
32
|
+
dbutils.library.restartPython()
|
|
33
|
+
|
|
34
|
+
# COMMAND ----------
|
|
35
|
+
|
|
36
|
+
from table_validator import validate_tables
|
|
37
|
+
from table_validator.config.schema import ValidationType
|
|
38
|
+
|
|
39
|
+
MODE = "{{MODE}}"
|
|
40
|
+
SHOW_OPTIONAL_WIDGETS = {{SHOW_OPTIONAL_WIDGETS}}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _list_catalogs():
|
|
44
|
+
return sorted(r["catalog"] for r in spark.sql("SHOW CATALOGS").collect())
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _list_schemas(catalog: str):
|
|
48
|
+
if not catalog:
|
|
49
|
+
return []
|
|
50
|
+
rows = spark.sql(f"SHOW SCHEMAS IN `{catalog}`").collect()
|
|
51
|
+
return sorted(r["databaseName"] for r in rows)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _list_tables(catalog: str, schema: str):
|
|
55
|
+
if not catalog or not schema:
|
|
56
|
+
return []
|
|
57
|
+
rows = spark.sql(f"SHOW TABLES IN `{catalog}`.`{schema}`").collect()
|
|
58
|
+
return sorted(r["tableName"] for r in rows)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _current_or_first(options, current):
|
|
62
|
+
"""Keep a widget's existing selection across a re-run of this cell if
|
|
63
|
+
it's still valid for the (possibly new) upstream selection, otherwise
|
|
64
|
+
fall back to the first available option (or "" if there are none)."""
|
|
65
|
+
if current in options:
|
|
66
|
+
return current
|
|
67
|
+
return options[0] if options else ""
|
|
68
|
+
|
|
69
|
+
# COMMAND ----------
|
|
70
|
+
|
|
71
|
+
# MAGIC %md ### Create / refresh widgets
|
|
72
|
+
# MAGIC **Run this cell (Shift+Enter) every time you change Source/Target
|
|
73
|
+
# MAGIC Catalog or Schema**, to repopulate the Schema/Table dropdowns below
|
|
74
|
+
# MAGIC them - Databricks widgets don't refresh automatically. Re-running it
|
|
75
|
+
# MAGIC is always safe: your current Catalog/Schema/Table picks are kept if
|
|
76
|
+
# MAGIC still valid, and only stale downstream selections reset.
|
|
77
|
+
|
|
78
|
+
# COMMAND ----------
|
|
79
|
+
|
|
80
|
+
def _get_widget(name: str, default: str = "") -> str:
|
|
81
|
+
try:
|
|
82
|
+
return dbutils.widgets.get(name)
|
|
83
|
+
except Exception:
|
|
84
|
+
return default
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _effective_value(prefix: str) -> str:
|
|
88
|
+
"""The manual-override text widget wins whenever it's non-blank,
|
|
89
|
+
otherwise fall back to the dropdown's current value - lets a name
|
|
90
|
+
that isn't showing up in a dropdown (permissions lag, sort order,
|
|
91
|
+
anything) be typed in directly instead. Used as-is, not validated
|
|
92
|
+
against the live catalog/schema/table list - same convention as
|
|
93
|
+
row_filter elsewhere in this notebook."""
|
|
94
|
+
override = _get_widget(f"{prefix}_override").strip()
|
|
95
|
+
return override or _get_widget(prefix)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _build_cascading_widgets(prefix: str, label_prefix: str) -> None:
|
|
99
|
+
"""Build/refresh one side's (source or target) Catalog -> Schema ->
|
|
100
|
+
Table dropdowns from Unity Catalog, in one pass - each level is
|
|
101
|
+
re-queried from the CURRENT value of the level above, so this single
|
|
102
|
+
cell run always reflects whatever Catalog/Schema is selected right
|
|
103
|
+
now, not a stale snapshot from an earlier run. Also creates a
|
|
104
|
+
"(manual override)" text widget next to each dropdown, for a name
|
|
105
|
+
that isn't showing up in the dropdown for any reason - see
|
|
106
|
+
_effective_value() below for how the override takes priority."""
|
|
107
|
+
catalog_current = _current_or_first(catalogs, _effective_value(f"{prefix}_catalog"))
|
|
108
|
+
schemas = _list_schemas(catalog_current)
|
|
109
|
+
schema_current = _current_or_first(schemas, _effective_value(f"{prefix}_schema"))
|
|
110
|
+
tables = _list_tables(catalog_current, schema_current)
|
|
111
|
+
table_current = _current_or_first(tables, _effective_value(f"{prefix}_table"))
|
|
112
|
+
|
|
113
|
+
dbutils.widgets.dropdown(f"{prefix}_catalog", catalog_current, catalogs or [""], f"{label_prefix} Catalog")
|
|
114
|
+
dbutils.widgets.text(f"{prefix}_catalog_override", "", f"{label_prefix} Catalog (manual override)")
|
|
115
|
+
dbutils.widgets.dropdown(f"{prefix}_schema", schema_current, schemas or [""], f"{label_prefix} Schema")
|
|
116
|
+
dbutils.widgets.text(f"{prefix}_schema_override", "", f"{label_prefix} Schema (manual override)")
|
|
117
|
+
dbutils.widgets.dropdown(f"{prefix}_table", table_current, tables or [""], f"{label_prefix} Table")
|
|
118
|
+
dbutils.widgets.text(f"{prefix}_table_override", "", f"{label_prefix} Table (manual override)")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
catalogs = _list_catalogs()
|
|
122
|
+
_build_cascading_widgets("source", "Source")
|
|
123
|
+
_build_cascading_widgets("target", "Target")
|
|
124
|
+
|
|
125
|
+
print(
|
|
126
|
+
"Widgets refreshed for the CURRENT Catalog/Schema selection.\n"
|
|
127
|
+
"Changed Catalog or Schema just now? Run this cell again (Shift+Enter) "
|
|
128
|
+
"before moving on - Databricks widgets don't repopulate on their own."
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# COMMAND ----------
|
|
132
|
+
|
|
133
|
+
# MAGIC %md ### Validation checks
|
|
134
|
+
|
|
135
|
+
# COMMAND ----------
|
|
136
|
+
|
|
137
|
+
_default_checks = ["Catalog & Schema", "Column", "Row"] if MODE != "schema" else ["Catalog & Schema"]
|
|
138
|
+
dbutils.widgets.multiselect(
|
|
139
|
+
"checks", ",".join(_default_checks),
|
|
140
|
+
["Catalog & Schema", "Column", "Row"], "Validation Checks",
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
if SHOW_OPTIONAL_WIDGETS:
|
|
144
|
+
dbutils.widgets.text("only_columns", "", "Only compare these columns (comma-separated, optional)")
|
|
145
|
+
dbutils.widgets.text("ignore_columns", "", "Skip these columns entirely (comma-separated, optional)")
|
|
146
|
+
dbutils.widgets.text("row_filter", "", "Row filter - SQL WHERE-fragment (optional)")
|
|
147
|
+
dbutils.widgets.text("primary_key", "", "Primary key column(s) (comma-separated, optional)")
|
|
148
|
+
|
|
149
|
+
# COMMAND ----------
|
|
150
|
+
|
|
151
|
+
# MAGIC %md ### Run validation
|
|
152
|
+
|
|
153
|
+
# COMMAND ----------
|
|
154
|
+
|
|
155
|
+
def _validate_current_selection(prefix: str, label: str) -> None:
|
|
156
|
+
"""Guard against a stale Schema/Table dropdown: if the Schema/Table
|
|
157
|
+
widget's CURRENT value isn't actually in that catalog/schema right
|
|
158
|
+
now (e.g. the user picked a new Catalog but never re-ran the "Create
|
|
159
|
+
/ refresh widgets" cell above), fail loudly here with a clear fix
|
|
160
|
+
instead of silently validating the wrong table. A field with a
|
|
161
|
+
manual override set is trusted as typed and skipped here - the whole
|
|
162
|
+
point of the override is to name something the live catalog listing
|
|
163
|
+
doesn't (currently) show, so it can never pass this check by
|
|
164
|
+
definition."""
|
|
165
|
+
catalog = _effective_value(f"{prefix}_catalog")
|
|
166
|
+
schema_overridden = bool(_get_widget(f"{prefix}_schema_override").strip())
|
|
167
|
+
table_overridden = bool(_get_widget(f"{prefix}_table_override").strip())
|
|
168
|
+
schema = _effective_value(f"{prefix}_schema")
|
|
169
|
+
table = _effective_value(f"{prefix}_table")
|
|
170
|
+
|
|
171
|
+
if not schema_overridden:
|
|
172
|
+
live_schemas = _list_schemas(catalog)
|
|
173
|
+
if schema not in live_schemas:
|
|
174
|
+
raise ValueError(
|
|
175
|
+
f"{label} Schema widget is set to {schema!r}, which is not "
|
|
176
|
+
f"a schema in catalog {catalog!r} right now. You likely "
|
|
177
|
+
f"changed the {label} Catalog without refreshing - go back "
|
|
178
|
+
"and re-run the 'Create / refresh widgets' cell, then "
|
|
179
|
+
"re-pick the Schema and Table before running this cell "
|
|
180
|
+
"again. (Or, if this schema genuinely isn't showing up in "
|
|
181
|
+
"the dropdown, type it into the Schema manual override "
|
|
182
|
+
"widget instead.)"
|
|
183
|
+
)
|
|
184
|
+
if not table_overridden:
|
|
185
|
+
live_tables = _list_tables(catalog, schema)
|
|
186
|
+
if table not in live_tables:
|
|
187
|
+
raise ValueError(
|
|
188
|
+
f"{label} Table widget is set to {table!r}, which is not a "
|
|
189
|
+
f"table in {catalog}.{schema} right now. You likely changed "
|
|
190
|
+
f"the {label} Schema without refreshing - go back and "
|
|
191
|
+
"re-run the 'Create / refresh widgets' cell, then re-pick "
|
|
192
|
+
"the Table before running this cell again. (Or, if this "
|
|
193
|
+
"table genuinely isn't showing up in the dropdown, type it "
|
|
194
|
+
"into the Table manual override widget instead.)"
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
_validate_current_selection("source", "Source")
|
|
199
|
+
_validate_current_selection("target", "Target")
|
|
200
|
+
|
|
201
|
+
source = f"{_effective_value('source_catalog')}.{_effective_value('source_schema')}.{_effective_value('source_table')}"
|
|
202
|
+
target = f"{_effective_value('target_catalog')}.{_effective_value('target_schema')}.{_effective_value('target_table')}"
|
|
203
|
+
|
|
204
|
+
_CHECK_MAP = {
|
|
205
|
+
"Catalog & Schema": {ValidationType.CATALOG, ValidationType.SCHEMA},
|
|
206
|
+
"Column": {ValidationType.COLUMN},
|
|
207
|
+
"Row": {ValidationType.ROW},
|
|
208
|
+
}
|
|
209
|
+
selected_checks = [c for c in dbutils.widgets.get("checks").split(",") if c]
|
|
210
|
+
enabled_validations = set()
|
|
211
|
+
for check in selected_checks:
|
|
212
|
+
enabled_validations |= _CHECK_MAP.get(check, set())
|
|
213
|
+
if not enabled_validations:
|
|
214
|
+
enabled_validations = None # fall back to validate_tables()'s own "run everything" default
|
|
215
|
+
|
|
216
|
+
kwargs = {"enabled_validations": enabled_validations}
|
|
217
|
+
|
|
218
|
+
if SHOW_OPTIONAL_WIDGETS:
|
|
219
|
+
only_columns = [c.strip() for c in dbutils.widgets.get("only_columns").split(",") if c.strip()]
|
|
220
|
+
ignore_columns = [c.strip() for c in dbutils.widgets.get("ignore_columns").split(",") if c.strip()]
|
|
221
|
+
row_filter = dbutils.widgets.get("row_filter").strip()
|
|
222
|
+
primary_key = [c.strip() for c in dbutils.widgets.get("primary_key").split(",") if c.strip()]
|
|
223
|
+
|
|
224
|
+
if only_columns:
|
|
225
|
+
kwargs["only_columns"] = only_columns
|
|
226
|
+
if ignore_columns:
|
|
227
|
+
kwargs["ignore_columns"] = ignore_columns
|
|
228
|
+
if row_filter:
|
|
229
|
+
kwargs["row_filter"] = row_filter
|
|
230
|
+
if primary_key:
|
|
231
|
+
kwargs["primary_key"] = primary_key
|
|
232
|
+
|
|
233
|
+
result = validate_tables(source, target, **kwargs)
|
|
234
|
+
|
|
235
|
+
# COMMAND ----------
|
|
236
|
+
|
|
237
|
+
# MAGIC %md ### Results
|
|
238
|
+
|
|
239
|
+
# COMMAND ----------
|
|
240
|
+
|
|
241
|
+
print(result)
|
|
242
|
+
display(result.table_validation.to_dataframe())
|
|
243
|
+
|
|
244
|
+
# Other sheets, mirroring the Excel report's own tabs - uncomment as needed:
|
|
245
|
+
# display(result.column_validation.to_dataframe())
|
|
246
|
+
# display(result.data_mismatches.to_dataframe())
|
|
247
|
+
# display(result.row_hash_mismatches.to_dataframe())
|
|
248
|
+
# display(result.mismatch_categories.to_dataframe())
|
|
249
|
+
# display(result.suggestions.to_dataframe())
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tablevalidator-databricks
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: Generates a widget-driven Databricks notebook UI on top of the table-validator package - no code required to run a comparison.
|
|
5
5
|
License: MIT
|
|
6
6
|
Requires-Python: >=3.9
|
|
@@ -72,9 +72,19 @@ markers and opens it as a real notebook.
|
|
|
72
72
|
|
|
73
73
|
**Cascading dropdowns**: Databricks widgets don't auto-refresh when an
|
|
74
74
|
upstream selection changes. After changing Source/Target Catalog or
|
|
75
|
-
Schema, re-run the notebook's "Create widgets" cell once to
|
|
76
|
-
dropdowns below it before running the rest of the notebook.
|
|
77
|
-
Databricks widget limitation, not a bug in the generated
|
|
75
|
+
Schema, re-run the notebook's "Create / refresh widgets" cell once to
|
|
76
|
+
repopulate the dropdowns below it before running the rest of the notebook.
|
|
77
|
+
This is a Databricks widget limitation, not a bug in the generated
|
|
78
|
+
notebook. If you skip this and run validation anyway with a stale Schema/
|
|
79
|
+
Table selection, the notebook fails with a clear error telling you which
|
|
80
|
+
cell to re-run, rather than silently comparing the wrong table.
|
|
81
|
+
|
|
82
|
+
**Can't find a name in a dropdown?** Every Catalog/Schema/Table dropdown
|
|
83
|
+
has a matching "(manual override)" text widget next to it. Type the exact
|
|
84
|
+
name there and it's used instead of the dropdown - useful if something
|
|
85
|
+
isn't showing up (a permissions lag, sort order, anything). It's used
|
|
86
|
+
as-is, not checked against the live catalog listing. Leave every override
|
|
87
|
+
blank to just use the dropdowns as normal.
|
|
78
88
|
|
|
79
89
|
## Why a separate package
|
|
80
90
|
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""Executes the generated notebook's Python cells against a fake
|
|
2
|
+
Databricks runtime (dbutils/spark/display stubs), to catch real bugs in
|
|
3
|
+
the widget wiring - not just that the file is syntactically valid.
|
|
4
|
+
|
|
5
|
+
Strips the Databricks-magic-only lines (the notebook source header,
|
|
6
|
+
`# COMMAND ----------` cell separators, `# MAGIC` lines) and execs the
|
|
7
|
+
rest as one script, same approach used to hand-verify this template
|
|
8
|
+
during development.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from unittest.mock import MagicMock
|
|
15
|
+
|
|
16
|
+
import pytest
|
|
17
|
+
|
|
18
|
+
from tablevalidator_databricks.generator.generator import generate_notebook
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _code_from_notebook(path: Path) -> str:
|
|
22
|
+
lines = path.read_text(encoding="utf-8").splitlines()
|
|
23
|
+
code_lines = [
|
|
24
|
+
line for line in lines
|
|
25
|
+
if line.strip() != "# Databricks notebook source"
|
|
26
|
+
and line.strip() != "# COMMAND ----------"
|
|
27
|
+
and not line.strip().startswith("# MAGIC")
|
|
28
|
+
]
|
|
29
|
+
return "\n".join(code_lines)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class _FakeWidgets:
|
|
33
|
+
def __init__(self, initial=None):
|
|
34
|
+
self._values = dict(initial or {})
|
|
35
|
+
|
|
36
|
+
def get(self, name):
|
|
37
|
+
return self._values.get(name, "")
|
|
38
|
+
|
|
39
|
+
def text(self, name, default, label):
|
|
40
|
+
self._values.setdefault(name, default)
|
|
41
|
+
|
|
42
|
+
def dropdown(self, name, default, choices, label):
|
|
43
|
+
# Real dbutils.widgets.dropdown does NOT overwrite an existing
|
|
44
|
+
# value just because the cell re-ran with a different `default` -
|
|
45
|
+
# it only sets it the first time. Match that.
|
|
46
|
+
self._values.setdefault(name, default)
|
|
47
|
+
|
|
48
|
+
def multiselect(self, name, default, choices, label):
|
|
49
|
+
self._values.setdefault(name, default)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class _FakeRow(dict):
|
|
53
|
+
def __getitem__(self, key):
|
|
54
|
+
return dict.__getitem__(self, key)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _fake_spark(catalogs, schemas_by_catalog, tables_by_schema):
|
|
58
|
+
def sql(query):
|
|
59
|
+
result = MagicMock()
|
|
60
|
+
if "SHOW CATALOGS" in query:
|
|
61
|
+
result.collect.return_value = [_FakeRow(catalog=c) for c in catalogs]
|
|
62
|
+
elif "SHOW SCHEMAS IN" in query:
|
|
63
|
+
catalog = query.split("`")[1]
|
|
64
|
+
result.collect.return_value = [
|
|
65
|
+
_FakeRow(databaseName=s) for s in schemas_by_catalog.get(catalog, [])
|
|
66
|
+
]
|
|
67
|
+
elif "SHOW TABLES IN" in query:
|
|
68
|
+
parts = query.split("`")
|
|
69
|
+
catalog, schema = parts[1], parts[3]
|
|
70
|
+
result.collect.return_value = [
|
|
71
|
+
_FakeRow(tableName=t) for t in tables_by_schema.get((catalog, schema), [])
|
|
72
|
+
]
|
|
73
|
+
else:
|
|
74
|
+
result.collect.return_value = []
|
|
75
|
+
return result
|
|
76
|
+
|
|
77
|
+
spark = MagicMock()
|
|
78
|
+
spark.sql.side_effect = sql
|
|
79
|
+
return spark
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _run_notebook(path: Path, widget_values, spark, validate_tables_mock):
|
|
83
|
+
code = _code_from_notebook(path)
|
|
84
|
+
|
|
85
|
+
fake_dbutils = MagicMock()
|
|
86
|
+
fake_dbutils.widgets = _FakeWidgets(initial=widget_values)
|
|
87
|
+
fake_dbutils.library.restartPython = MagicMock()
|
|
88
|
+
|
|
89
|
+
globs = {
|
|
90
|
+
"dbutils": fake_dbutils,
|
|
91
|
+
"spark": spark,
|
|
92
|
+
"display": MagicMock(),
|
|
93
|
+
"__name__": "__main__",
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
import table_validator
|
|
97
|
+
import table_validator.notebook as nb_mod
|
|
98
|
+
|
|
99
|
+
table_validator.validate_tables = validate_tables_mock
|
|
100
|
+
nb_mod.validate_tables = validate_tables_mock
|
|
101
|
+
|
|
102
|
+
exec(compile(code, str(path), "exec"), globs)
|
|
103
|
+
return globs
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@pytest.fixture
|
|
107
|
+
def notebook_path(tmp_path: Path) -> Path:
|
|
108
|
+
path = tmp_path / "TableValidator.py"
|
|
109
|
+
generate_notebook(path, "full")
|
|
110
|
+
return path
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_happy_path_calls_validate_tables_with_resolved_names(notebook_path):
|
|
114
|
+
spark = _fake_spark(
|
|
115
|
+
catalogs=["main"],
|
|
116
|
+
schemas_by_catalog={"main": ["sales"]},
|
|
117
|
+
tables_by_schema={("main", "sales"): ["orders"]},
|
|
118
|
+
)
|
|
119
|
+
captured = {}
|
|
120
|
+
|
|
121
|
+
def fake_validate_tables(source, target, **kwargs):
|
|
122
|
+
captured["source"] = source
|
|
123
|
+
captured["target"] = target
|
|
124
|
+
captured["kwargs"] = kwargs
|
|
125
|
+
m = MagicMock()
|
|
126
|
+
m.__str__ = MagicMock(return_value="Overall status: PASS")
|
|
127
|
+
return m
|
|
128
|
+
|
|
129
|
+
_run_notebook(notebook_path, widget_values={}, spark=spark, validate_tables_mock=fake_validate_tables)
|
|
130
|
+
|
|
131
|
+
assert captured["source"] == "main.sales.orders"
|
|
132
|
+
assert captured["target"] == "main.sales.orders"
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def test_stale_schema_selection_raises_clear_error(notebook_path):
|
|
136
|
+
"""Regression test for a real user-reported issue: after picking a
|
|
137
|
+
new Source Catalog, the Source Schema/Table widgets kept their value
|
|
138
|
+
from the PREVIOUS catalog because the 'Create / refresh widgets'
|
|
139
|
+
cell wasn't re-run. Must fail loudly instead of silently validating
|
|
140
|
+
against the wrong table."""
|
|
141
|
+
spark = _fake_spark(
|
|
142
|
+
catalogs=["for_validation1", "for_validation2"],
|
|
143
|
+
schemas_by_catalog={"for_validation1": ["alteryx_silver_source"]},
|
|
144
|
+
tables_by_schema={("for_validation1", "alteryx_silver_source"): ["from_bronze_us_counties"]},
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
with pytest.raises(ValueError, match="not a schema in catalog"):
|
|
148
|
+
_run_notebook(
|
|
149
|
+
notebook_path,
|
|
150
|
+
widget_values={
|
|
151
|
+
"source_catalog": "for_validation1",
|
|
152
|
+
"source_schema": "some_stale_schema_from_before",
|
|
153
|
+
"source_table": "some_stale_table",
|
|
154
|
+
"target_catalog": "for_validation1",
|
|
155
|
+
"target_schema": "alteryx_silver_source",
|
|
156
|
+
"target_table": "from_bronze_us_counties",
|
|
157
|
+
},
|
|
158
|
+
spark=spark,
|
|
159
|
+
validate_tables_mock=MagicMock(),
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def test_manual_override_bypasses_dropdown_and_stale_check(notebook_path):
|
|
164
|
+
"""A table that isn't showing up in the dropdown must still be
|
|
165
|
+
usable by typing it into the "(manual override)" text widget - and
|
|
166
|
+
the override, not the dropdown, is what gets sent to
|
|
167
|
+
validate_tables()."""
|
|
168
|
+
spark = _fake_spark(
|
|
169
|
+
catalogs=["main"],
|
|
170
|
+
schemas_by_catalog={"main": ["sales"]},
|
|
171
|
+
# "hidden_table" deliberately absent - simulating a table not
|
|
172
|
+
# showing up in the dropdown.
|
|
173
|
+
tables_by_schema={("main", "sales"): ["orders"]},
|
|
174
|
+
)
|
|
175
|
+
captured = {}
|
|
176
|
+
|
|
177
|
+
def fake_validate_tables(source, target, **kwargs):
|
|
178
|
+
captured["source"] = source
|
|
179
|
+
captured["target"] = target
|
|
180
|
+
m = MagicMock()
|
|
181
|
+
m.__str__ = MagicMock(return_value="Overall status: PASS")
|
|
182
|
+
return m
|
|
183
|
+
|
|
184
|
+
_run_notebook(
|
|
185
|
+
notebook_path,
|
|
186
|
+
widget_values={
|
|
187
|
+
"source_catalog": "main", "source_schema": "sales", "source_table": "orders",
|
|
188
|
+
"target_catalog": "main", "target_schema": "sales",
|
|
189
|
+
"target_table_override": "hidden_table",
|
|
190
|
+
},
|
|
191
|
+
spark=spark,
|
|
192
|
+
validate_tables_mock=fake_validate_tables,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
assert captured["target"] == "main.sales.hidden_table"
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def test_catalog_override_drives_schema_and_table_lookup(notebook_path):
|
|
199
|
+
"""A manually-overridden Catalog must be used when querying for
|
|
200
|
+
Schemas/Tables too, not just at final source/target string
|
|
201
|
+
construction - otherwise typing a catalog override would leave the
|
|
202
|
+
Schema/Table dropdowns empty."""
|
|
203
|
+
spark = _fake_spark(
|
|
204
|
+
catalogs=["main"], # "hidden_catalog" deliberately absent
|
|
205
|
+
schemas_by_catalog={"hidden_catalog": ["sales"], "main": ["sales"]},
|
|
206
|
+
tables_by_schema={
|
|
207
|
+
("hidden_catalog", "sales"): ["orders"],
|
|
208
|
+
("main", "sales"): ["orders"],
|
|
209
|
+
},
|
|
210
|
+
)
|
|
211
|
+
captured = {}
|
|
212
|
+
|
|
213
|
+
def fake_validate_tables(source, target, **kwargs):
|
|
214
|
+
captured["source"] = source
|
|
215
|
+
captured["target"] = target
|
|
216
|
+
m = MagicMock()
|
|
217
|
+
m.__str__ = MagicMock(return_value="Overall status: PASS")
|
|
218
|
+
return m
|
|
219
|
+
|
|
220
|
+
_run_notebook(
|
|
221
|
+
notebook_path,
|
|
222
|
+
widget_values={
|
|
223
|
+
"source_catalog_override": "hidden_catalog",
|
|
224
|
+
"source_schema": "sales", "source_table": "orders",
|
|
225
|
+
"target_catalog": "main", "target_schema": "sales", "target_table": "orders",
|
|
226
|
+
},
|
|
227
|
+
spark=spark,
|
|
228
|
+
validate_tables_mock=fake_validate_tables,
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
assert captured["source"] == "hidden_catalog.sales.orders"
|
|
@@ -63,6 +63,22 @@ def test_generate_notebook_creates_parent_directories(tmp_path: Path) -> None:
|
|
|
63
63
|
assert output.exists()
|
|
64
64
|
|
|
65
65
|
|
|
66
|
+
def test_generate_notebook_includes_manual_override_widgets(tmp_path: Path) -> None:
|
|
67
|
+
"""Every Catalog/Schema/Table dropdown gets a matching manual-override
|
|
68
|
+
text widget, for a name that isn't showing up in the dropdown. Widget
|
|
69
|
+
names are built at runtime via f-strings (f"{prefix}_catalog_override"),
|
|
70
|
+
not literal per-side text, so check for the shared template fragments
|
|
71
|
+
rather than the fully-resolved names."""
|
|
72
|
+
output = tmp_path / "TableValidator.py"
|
|
73
|
+
generate_notebook(output, "full")
|
|
74
|
+
content = output.read_text(encoding="utf-8")
|
|
75
|
+
|
|
76
|
+
assert '"{prefix}_catalog_override"' in content
|
|
77
|
+
assert '"{prefix}_schema_override"' in content
|
|
78
|
+
assert '"{prefix}_table_override"' in content
|
|
79
|
+
assert "(manual override)" in content
|
|
80
|
+
|
|
81
|
+
|
|
66
82
|
def test_generate_notebook_no_leftover_placeholders(tmp_path: Path) -> None:
|
|
67
83
|
"""Regression guard: every {{...}} placeholder in the template must be
|
|
68
84
|
substituted - a leftover placeholder would silently ship broken Python
|
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
# Databricks notebook source
|
|
2
|
-
# MAGIC %md
|
|
3
|
-
# MAGIC # Table Validator
|
|
4
|
-
# MAGIC Generated by `tablevalidator-databricks init --mode {{MODE}}`.
|
|
5
|
-
# MAGIC
|
|
6
|
-
# MAGIC Fill in the widgets above (Source/Target Catalog, Schema, Table, and
|
|
7
|
-
# MAGIC which checks to run), then **Run All**. No code needs to be written or
|
|
8
|
-
# MAGIC edited - every cell below just reads the widgets and calls the
|
|
9
|
-
# MAGIC `table_validator` package's `validate_tables()` API.
|
|
10
|
-
# MAGIC
|
|
11
|
-
# MAGIC **Cascading dropdowns**: Databricks widgets don't auto-refresh when an
|
|
12
|
-
# MAGIC upstream selection changes. After picking a different Source/Target
|
|
13
|
-
# MAGIC Catalog, re-run the "Create widgets" cell below to repopulate the Schema
|
|
14
|
-
# MAGIC dropdown for that catalog; likewise after changing Schema, re-run it
|
|
15
|
-
# MAGIC again to repopulate Table. This is a real Databricks widget limitation,
|
|
16
|
-
# MAGIC not a bug in this notebook.
|
|
17
|
-
|
|
18
|
-
# COMMAND ----------
|
|
19
|
-
|
|
20
|
-
# MAGIC %pip install table-validator
|
|
21
|
-
|
|
22
|
-
# COMMAND ----------
|
|
23
|
-
|
|
24
|
-
dbutils.library.restartPython()
|
|
25
|
-
|
|
26
|
-
# COMMAND ----------
|
|
27
|
-
|
|
28
|
-
from table_validator import validate_tables
|
|
29
|
-
from table_validator.config.schema import ValidationType
|
|
30
|
-
|
|
31
|
-
MODE = "{{MODE}}"
|
|
32
|
-
SHOW_OPTIONAL_WIDGETS = {{SHOW_OPTIONAL_WIDGETS}}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def _list_catalogs():
|
|
36
|
-
return sorted(r["catalog"] for r in spark.sql("SHOW CATALOGS").collect())
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def _list_schemas(catalog: str):
|
|
40
|
-
if not catalog:
|
|
41
|
-
return []
|
|
42
|
-
rows = spark.sql(f"SHOW SCHEMAS IN `{catalog}`").collect()
|
|
43
|
-
return sorted(r["databaseName"] for r in rows)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def _list_tables(catalog: str, schema: str):
|
|
47
|
-
if not catalog or not schema:
|
|
48
|
-
return []
|
|
49
|
-
rows = spark.sql(f"SHOW TABLES IN `{catalog}`.`{schema}`").collect()
|
|
50
|
-
return sorted(r["tableName"] for r in rows)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def _current_or_first(options, current):
|
|
54
|
-
"""Keep a widget's existing selection across a re-run of this cell if
|
|
55
|
-
it's still valid for the (possibly new) upstream selection, otherwise
|
|
56
|
-
fall back to the first available option (or "" if there are none)."""
|
|
57
|
-
if current in options:
|
|
58
|
-
return current
|
|
59
|
-
return options[0] if options else ""
|
|
60
|
-
|
|
61
|
-
# COMMAND ----------
|
|
62
|
-
|
|
63
|
-
# MAGIC %md ### Create widgets
|
|
64
|
-
# MAGIC Re-run this cell after changing Source/Target Catalog or Schema, to
|
|
65
|
-
# MAGIC repopulate the dropdowns below them.
|
|
66
|
-
|
|
67
|
-
# COMMAND ----------
|
|
68
|
-
|
|
69
|
-
def _get_widget(name: str, default: str = "") -> str:
|
|
70
|
-
try:
|
|
71
|
-
return dbutils.widgets.get(name)
|
|
72
|
-
except Exception:
|
|
73
|
-
return default
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
catalogs = _list_catalogs()
|
|
77
|
-
|
|
78
|
-
source_catalog_current = _current_or_first(catalogs, _get_widget("source_catalog"))
|
|
79
|
-
target_catalog_current = _current_or_first(catalogs, _get_widget("target_catalog"))
|
|
80
|
-
|
|
81
|
-
source_schemas = _list_schemas(source_catalog_current)
|
|
82
|
-
target_schemas = _list_schemas(target_catalog_current)
|
|
83
|
-
|
|
84
|
-
source_schema_current = _current_or_first(source_schemas, _get_widget("source_schema"))
|
|
85
|
-
target_schema_current = _current_or_first(target_schemas, _get_widget("target_schema"))
|
|
86
|
-
|
|
87
|
-
source_tables = _list_tables(source_catalog_current, source_schema_current)
|
|
88
|
-
target_tables = _list_tables(target_catalog_current, target_schema_current)
|
|
89
|
-
|
|
90
|
-
source_table_current = _current_or_first(source_tables, _get_widget("source_table"))
|
|
91
|
-
target_table_current = _current_or_first(target_tables, _get_widget("target_table"))
|
|
92
|
-
|
|
93
|
-
dbutils.widgets.dropdown("source_catalog", source_catalog_current, catalogs or [""], "Source Catalog")
|
|
94
|
-
dbutils.widgets.dropdown("source_schema", source_schema_current, source_schemas or [""], "Source Schema")
|
|
95
|
-
dbutils.widgets.dropdown("source_table", source_table_current, source_tables or [""], "Source Table")
|
|
96
|
-
|
|
97
|
-
dbutils.widgets.dropdown("target_catalog", target_catalog_current, catalogs or [""], "Target Catalog")
|
|
98
|
-
dbutils.widgets.dropdown("target_schema", target_schema_current, target_schemas or [""], "Target Schema")
|
|
99
|
-
dbutils.widgets.dropdown("target_table", target_table_current, target_tables or [""], "Target Table")
|
|
100
|
-
|
|
101
|
-
print(
|
|
102
|
-
"Widgets created. If you just changed Source/Target Catalog or Schema, "
|
|
103
|
-
"re-run this cell once more to repopulate the dropdowns below it."
|
|
104
|
-
)
|
|
105
|
-
|
|
106
|
-
# COMMAND ----------
|
|
107
|
-
|
|
108
|
-
# MAGIC %md ### Validation checks
|
|
109
|
-
|
|
110
|
-
# COMMAND ----------
|
|
111
|
-
|
|
112
|
-
_default_checks = ["Catalog & Schema", "Column", "Row"] if MODE != "schema" else ["Catalog & Schema"]
|
|
113
|
-
dbutils.widgets.multiselect(
|
|
114
|
-
"checks", ",".join(_default_checks),
|
|
115
|
-
["Catalog & Schema", "Column", "Row"], "Validation Checks",
|
|
116
|
-
)
|
|
117
|
-
|
|
118
|
-
if SHOW_OPTIONAL_WIDGETS:
|
|
119
|
-
dbutils.widgets.text("only_columns", "", "Only compare these columns (comma-separated, optional)")
|
|
120
|
-
dbutils.widgets.text("ignore_columns", "", "Skip these columns entirely (comma-separated, optional)")
|
|
121
|
-
dbutils.widgets.text("row_filter", "", "Row filter - SQL WHERE-fragment (optional)")
|
|
122
|
-
dbutils.widgets.text("primary_key", "", "Primary key column(s) (comma-separated, optional)")
|
|
123
|
-
|
|
124
|
-
# COMMAND ----------
|
|
125
|
-
|
|
126
|
-
# MAGIC %md ### Run validation
|
|
127
|
-
|
|
128
|
-
# COMMAND ----------
|
|
129
|
-
|
|
130
|
-
source = f"{dbutils.widgets.get('source_catalog')}.{dbutils.widgets.get('source_schema')}.{dbutils.widgets.get('source_table')}"
|
|
131
|
-
target = f"{dbutils.widgets.get('target_catalog')}.{dbutils.widgets.get('target_schema')}.{dbutils.widgets.get('target_table')}"
|
|
132
|
-
|
|
133
|
-
_CHECK_MAP = {
|
|
134
|
-
"Catalog & Schema": {ValidationType.CATALOG, ValidationType.SCHEMA},
|
|
135
|
-
"Column": {ValidationType.COLUMN},
|
|
136
|
-
"Row": {ValidationType.ROW},
|
|
137
|
-
}
|
|
138
|
-
selected_checks = [c for c in dbutils.widgets.get("checks").split(",") if c]
|
|
139
|
-
enabled_validations = set()
|
|
140
|
-
for check in selected_checks:
|
|
141
|
-
enabled_validations |= _CHECK_MAP.get(check, set())
|
|
142
|
-
if not enabled_validations:
|
|
143
|
-
enabled_validations = None # fall back to validate_tables()'s own "run everything" default
|
|
144
|
-
|
|
145
|
-
kwargs = {"enabled_validations": enabled_validations}
|
|
146
|
-
|
|
147
|
-
if SHOW_OPTIONAL_WIDGETS:
|
|
148
|
-
only_columns = [c.strip() for c in dbutils.widgets.get("only_columns").split(",") if c.strip()]
|
|
149
|
-
ignore_columns = [c.strip() for c in dbutils.widgets.get("ignore_columns").split(",") if c.strip()]
|
|
150
|
-
row_filter = dbutils.widgets.get("row_filter").strip()
|
|
151
|
-
primary_key = [c.strip() for c in dbutils.widgets.get("primary_key").split(",") if c.strip()]
|
|
152
|
-
|
|
153
|
-
if only_columns:
|
|
154
|
-
kwargs["only_columns"] = only_columns
|
|
155
|
-
if ignore_columns:
|
|
156
|
-
kwargs["ignore_columns"] = ignore_columns
|
|
157
|
-
if row_filter:
|
|
158
|
-
kwargs["row_filter"] = row_filter
|
|
159
|
-
if primary_key:
|
|
160
|
-
kwargs["primary_key"] = primary_key
|
|
161
|
-
|
|
162
|
-
result = validate_tables(source, target, **kwargs)
|
|
163
|
-
|
|
164
|
-
# COMMAND ----------
|
|
165
|
-
|
|
166
|
-
# MAGIC %md ### Results
|
|
167
|
-
|
|
168
|
-
# COMMAND ----------
|
|
169
|
-
|
|
170
|
-
print(result)
|
|
171
|
-
display(result.table_validation.to_dataframe())
|
|
172
|
-
|
|
173
|
-
# Other sheets, mirroring the Excel report's own tabs - uncomment as needed:
|
|
174
|
-
# display(result.column_validation.to_dataframe())
|
|
175
|
-
# display(result.data_mismatches.to_dataframe())
|
|
176
|
-
# display(result.row_hash_mismatches.to_dataframe())
|
|
177
|
-
# display(result.mismatch_categories.to_dataframe())
|
|
178
|
-
# display(result.suggestions.to_dataframe())
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|