table-validator 0.1.16__tar.gz → 0.1.18__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.
- {table_validator-0.1.16/table_validator.egg-info → table_validator-0.1.18}/PKG-INFO +1 -1
- {table_validator-0.1.16 → table_validator-0.1.18}/table_validator/cli/main.py +11 -0
- {table_validator-0.1.16 → table_validator-0.1.18}/table_validator/cli/wizard.py +58 -3
- {table_validator-0.1.16 → table_validator-0.1.18}/table_validator/config/schema.py +32 -0
- {table_validator-0.1.16 → table_validator-0.1.18}/table_validator/connectors/base_sql_connector.py +20 -2
- {table_validator-0.1.16 → table_validator-0.1.18/table_validator.egg-info}/PKG-INFO +1 -1
- {table_validator-0.1.16 → table_validator-0.1.18}/table_validator.egg-info/scm_version.json +2 -2
- {table_validator-0.1.16 → table_validator-0.1.18}/tests/test_base_sql_connector.py +73 -0
- {table_validator-0.1.16 → table_validator-0.1.18}/tests/test_cli.py +59 -0
- {table_validator-0.1.16 → table_validator-0.1.18}/tests/test_wizard.py +13 -0
- {table_validator-0.1.16 → table_validator-0.1.18}/LICENSE +0 -0
- {table_validator-0.1.16 → table_validator-0.1.18}/README.md +0 -0
- {table_validator-0.1.16 → table_validator-0.1.18}/pyproject.toml +0 -0
- {table_validator-0.1.16 → table_validator-0.1.18}/setup.cfg +0 -0
- {table_validator-0.1.16 → table_validator-0.1.18}/table_validator/__init__.py +0 -0
- {table_validator-0.1.16 → table_validator-0.1.18}/table_validator/auth/__init__.py +0 -0
- {table_validator-0.1.16 → table_validator-0.1.18}/table_validator/auth/azure_auth.py +0 -0
- {table_validator-0.1.16 → table_validator-0.1.18}/table_validator/auth/databricks_auth.py +0 -0
- {table_validator-0.1.16 → table_validator-0.1.18}/table_validator/cli/__init__.py +0 -0
- {table_validator-0.1.16 → table_validator-0.1.18}/table_validator/cli/partition_prompt.py +0 -0
- {table_validator-0.1.16 → table_validator-0.1.18}/table_validator/cli/summary_table.py +0 -0
- {table_validator-0.1.16 → table_validator-0.1.18}/table_validator/config/__init__.py +0 -0
- {table_validator-0.1.16 → table_validator-0.1.18}/table_validator/config/manager.py +0 -0
- {table_validator-0.1.16 → table_validator-0.1.18}/table_validator/connectors/__init__.py +0 -0
- {table_validator-0.1.16 → table_validator-0.1.18}/table_validator/connectors/azure_connector.py +0 -0
- {table_validator-0.1.16 → table_validator-0.1.18}/table_validator/connectors/databricks_connector.py +0 -0
- {table_validator-0.1.16 → table_validator-0.1.18}/table_validator/connectors/spark_connector.py +0 -0
- {table_validator-0.1.16 → table_validator-0.1.18}/table_validator/engine/__init__.py +0 -0
- {table_validator-0.1.16 → table_validator-0.1.18}/table_validator/engine/comparison_engine.py +0 -0
- {table_validator-0.1.16 → table_validator-0.1.18}/table_validator/models.py +0 -0
- {table_validator-0.1.16 → table_validator-0.1.18}/table_validator/notebook.py +0 -0
- {table_validator-0.1.16 → table_validator-0.1.18}/table_validator/reports/__init__.py +0 -0
- {table_validator-0.1.16 → table_validator-0.1.18}/table_validator/reports/excel_report.py +0 -0
- {table_validator-0.1.16 → table_validator-0.1.18}/table_validator/validators/__init__.py +0 -0
- {table_validator-0.1.16 → table_validator-0.1.18}/table_validator/validators/blob_discovery.py +0 -0
- {table_validator-0.1.16 → table_validator-0.1.18}/table_validator/validators/catalog_validator.py +0 -0
- {table_validator-0.1.16 → table_validator-0.1.18}/table_validator/validators/mismatch_classifier.py +0 -0
- {table_validator-0.1.16 → table_validator-0.1.18}/table_validator/validators/row_validator.py +0 -0
- {table_validator-0.1.16 → table_validator-0.1.18}/table_validator.egg-info/SOURCES.txt +0 -0
- {table_validator-0.1.16 → table_validator-0.1.18}/table_validator.egg-info/dependency_links.txt +0 -0
- {table_validator-0.1.16 → table_validator-0.1.18}/table_validator.egg-info/entry_points.txt +0 -0
- {table_validator-0.1.16 → table_validator-0.1.18}/table_validator.egg-info/requires.txt +0 -0
- {table_validator-0.1.16 → table_validator-0.1.18}/table_validator.egg-info/scm_file_list.json +0 -0
- {table_validator-0.1.16 → table_validator-0.1.18}/table_validator.egg-info/top_level.txt +0 -0
- {table_validator-0.1.16 → table_validator-0.1.18}/tests/__init__.py +0 -0
- {table_validator-0.1.16 → table_validator-0.1.18}/tests/test_blob_discovery.py +0 -0
- {table_validator-0.1.16 → table_validator-0.1.18}/tests/test_catalog_validator.py +0 -0
- {table_validator-0.1.16 → table_validator-0.1.18}/tests/test_databricks_connector.py +0 -0
- {table_validator-0.1.16 → table_validator-0.1.18}/tests/test_excel_report.py +0 -0
- {table_validator-0.1.16 → table_validator-0.1.18}/tests/test_mismatch_classifier.py +0 -0
- {table_validator-0.1.16 → table_validator-0.1.18}/tests/test_notebook.py +0 -0
- {table_validator-0.1.16 → table_validator-0.1.18}/tests/test_package_import_without_pyspark.py +0 -0
- {table_validator-0.1.16 → table_validator-0.1.18}/tests/test_partition_prompt.py +0 -0
- {table_validator-0.1.16 → table_validator-0.1.18}/tests/test_report_command.py +0 -0
- {table_validator-0.1.16 → table_validator-0.1.18}/tests/test_row_validator.py +0 -0
- {table_validator-0.1.16 → table_validator-0.1.18}/tests/test_spark_connector.py +0 -0
|
@@ -507,6 +507,17 @@ def _run_databricks_validation(
|
|
|
507
507
|
column_map=config.column_map,
|
|
508
508
|
)
|
|
509
509
|
|
|
510
|
+
# Row-filter predicate - lives purely as connector instance state
|
|
511
|
+
# (BaseSqlConnector.set_row_filters/_scoped_table), same mechanism as
|
|
512
|
+
# the notebook-native validate_tables() API, not a CatalogValidationRequest
|
|
513
|
+
# field. A no-op unless the user configured at least one of the three.
|
|
514
|
+
if config.row_filter or config.source_row_filter or config.target_row_filter:
|
|
515
|
+
databricks.set_row_filters(
|
|
516
|
+
common=config.row_filter,
|
|
517
|
+
source=config.source_row_filter,
|
|
518
|
+
target=config.target_row_filter,
|
|
519
|
+
)
|
|
520
|
+
|
|
510
521
|
partition_prompt = build_partition_prompt(yes=yes)
|
|
511
522
|
validator = CatalogValidator(databricks, partition_prompt=partition_prompt)
|
|
512
523
|
return validator.compare_catalogs(request)
|
|
@@ -192,6 +192,57 @@ def _prompt_column_customization(config: ValidatorConfig) -> None:
|
|
|
192
192
|
) or []
|
|
193
193
|
|
|
194
194
|
|
|
195
|
+
def _prompt_row_filter(config: ValidatorConfig) -> None:
|
|
196
|
+
"""Optional row-filter predicate, asked right after column
|
|
197
|
+
customization - same single-table scope, Databricks source only
|
|
198
|
+
(AzureSqlConnector has no row-filter mechanism, unlike
|
|
199
|
+
BaseSqlConnector's set_row_filters/_scoped_table that the Databricks
|
|
200
|
+
connector and the notebook-native validate_tables() API both share).
|
|
201
|
+
Skipped entirely (leaving any existing row_filter/source_row_filter/
|
|
202
|
+
target_row_filter untouched) unless the user opts in, so declining
|
|
203
|
+
gives identical behavior to before this feature existed."""
|
|
204
|
+
customize = questionary.confirm(
|
|
205
|
+
"Filter to a subset of rows? (only rows matching a condition are "
|
|
206
|
+
"compared, e.g. \"id > 10 and id < 30\" or \"gender = 'male'\")",
|
|
207
|
+
default=False,
|
|
208
|
+
).ask()
|
|
209
|
+
|
|
210
|
+
if not customize:
|
|
211
|
+
return
|
|
212
|
+
|
|
213
|
+
same_condition = questionary.confirm(
|
|
214
|
+
"Apply the same condition to both source and target?",
|
|
215
|
+
default=True,
|
|
216
|
+
).ask()
|
|
217
|
+
|
|
218
|
+
# Mutually exclusive in this UI - picking one must clear whatever
|
|
219
|
+
# stale value the OTHER carried over from an earlier configure run,
|
|
220
|
+
# same convention as _prompt_schema_scoping below.
|
|
221
|
+
if same_condition:
|
|
222
|
+
config.row_filter = _ask(
|
|
223
|
+
questionary.text(
|
|
224
|
+
"Row filter condition (SQL WHERE-fragment):",
|
|
225
|
+
default=config.row_filter or "",
|
|
226
|
+
)
|
|
227
|
+
) or None
|
|
228
|
+
config.source_row_filter = None
|
|
229
|
+
config.target_row_filter = None
|
|
230
|
+
else:
|
|
231
|
+
config.row_filter = None
|
|
232
|
+
config.source_row_filter = _ask(
|
|
233
|
+
questionary.text(
|
|
234
|
+
"Source row filter condition:",
|
|
235
|
+
default=config.source_row_filter or "",
|
|
236
|
+
)
|
|
237
|
+
) or None
|
|
238
|
+
config.target_row_filter = _ask(
|
|
239
|
+
questionary.text(
|
|
240
|
+
"Target row filter condition:",
|
|
241
|
+
default=config.target_row_filter or "",
|
|
242
|
+
)
|
|
243
|
+
) or None
|
|
244
|
+
|
|
245
|
+
|
|
195
246
|
def _prompt_schema_scoping(config: ValidatorConfig) -> None:
|
|
196
247
|
"""
|
|
197
248
|
Optional schema-wide scoping, asked only when a schema is named on
|
|
@@ -725,10 +776,11 @@ def run_configure_wizard() -> None:
|
|
|
725
776
|
# identical behavior to before this feature existed.
|
|
726
777
|
typer.echo("\n== Customize validation (optional) ==")
|
|
727
778
|
_prompt_column_customization(config)
|
|
728
|
-
#
|
|
729
|
-
# only, since column_map/
|
|
730
|
-
# Azure Blob/SQL source paths.
|
|
779
|
+
# Row-filter predicate and column-name mapping (renamed columns) -
|
|
780
|
+
# Databricks-to-Databricks only, since set_row_filters/column_map/
|
|
781
|
+
# CatalogValidator don't apply to the Azure Blob/SQL source paths.
|
|
731
782
|
if config.source_type == SourceType.DATABRICKS:
|
|
783
|
+
_prompt_row_filter(config)
|
|
732
784
|
_prompt_column_mapping(config, secrets)
|
|
733
785
|
else:
|
|
734
786
|
config.primary_key = None
|
|
@@ -736,6 +788,9 @@ def run_configure_wizard() -> None:
|
|
|
736
788
|
config.ignore_columns = []
|
|
737
789
|
config.ignore_datatype_columns = []
|
|
738
790
|
config.column_map = {}
|
|
791
|
+
config.row_filter = None
|
|
792
|
+
config.source_row_filter = None
|
|
793
|
+
config.target_row_filter = None
|
|
739
794
|
|
|
740
795
|
# Schema-wide sweep scoping - only meaningful when a schema is named
|
|
741
796
|
# on both sides but no specific table is (the single-table branch
|
|
@@ -314,6 +314,38 @@ class ValidatorConfig(BaseModel):
|
|
|
314
314
|
),
|
|
315
315
|
)
|
|
316
316
|
|
|
317
|
+
row_filter: Optional[str] = Field(
|
|
318
|
+
default=None,
|
|
319
|
+
description=(
|
|
320
|
+
"Optional SQL WHERE-fragment restricting comparison to matching "
|
|
321
|
+
"rows on BOTH sides (row count, statistics, row-hash/column "
|
|
322
|
+
"diff) - e.g. 'id > 20 and id < 100' or \"gender = 'male'\". "
|
|
323
|
+
"Only meaningful for the single named table in source_table/"
|
|
324
|
+
"target_table (Databricks source only). Combined with "
|
|
325
|
+
"source_row_filter/target_row_filter via AND if both are set. "
|
|
326
|
+
"Used as-is, not parsed or validated - a typo surfaces as a "
|
|
327
|
+
"normal SQL error when the query runs."
|
|
328
|
+
),
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
source_row_filter: Optional[str] = Field(
|
|
332
|
+
default=None,
|
|
333
|
+
description=(
|
|
334
|
+
"Optional SQL WHERE-fragment applied only to the source side, "
|
|
335
|
+
"ANDed with row_filter if both are set. Use this (with "
|
|
336
|
+
"target_row_filter) instead of row_filter when the condition "
|
|
337
|
+
"must differ per side."
|
|
338
|
+
),
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
target_row_filter: Optional[str] = Field(
|
|
342
|
+
default=None,
|
|
343
|
+
description=(
|
|
344
|
+
"Optional SQL WHERE-fragment applied only to the target side, "
|
|
345
|
+
"ANDed with row_filter if both are set."
|
|
346
|
+
),
|
|
347
|
+
)
|
|
348
|
+
|
|
317
349
|
blob_source: BlobSourceConfig = Field(default_factory=BlobSourceConfig)
|
|
318
350
|
sql_source: SqlSourceConfig = Field(default_factory=SqlSourceConfig)
|
|
319
351
|
synapse_source: SynapseSourceConfig = Field(default_factory=SynapseSourceConfig)
|
{table_validator-0.1.16 → table_validator-0.1.18}/table_validator/connectors/base_sql_connector.py
RENAMED
|
@@ -528,6 +528,24 @@ class BaseSqlConnector:
|
|
|
528
528
|
return None
|
|
529
529
|
return int(value)
|
|
530
530
|
|
|
531
|
+
def _nan_to_none(value: Any) -> Any:
|
|
532
|
+
"""
|
|
533
|
+
MIN/MAX over ZERO matching rows (e.g. both sides legitimately
|
|
534
|
+
empty under the same row_filter) is SQL NULL, which arrives
|
|
535
|
+
here as float NaN after the spark.sql(...).toPandas() round-
|
|
536
|
+
trip - same underlying issue as _to_int_or_none above, but
|
|
537
|
+
min/max must stay their original type (a string, date, etc.)
|
|
538
|
+
when a real value IS present, so this only strips NaN rather
|
|
539
|
+
than casting to int. Without this, two equally-empty sides
|
|
540
|
+
each report NaN for min/max, and compare_min_max's `==`
|
|
541
|
+
check treats NaN != NaN (the standard IEEE 754 quirk) as a
|
|
542
|
+
real difference - a false FAIL on a table with nothing to
|
|
543
|
+
compare, not an actual mismatch.
|
|
544
|
+
"""
|
|
545
|
+
if isinstance(value, float) and value != value:
|
|
546
|
+
return None
|
|
547
|
+
return value
|
|
548
|
+
|
|
531
549
|
for col in columns:
|
|
532
550
|
entry: Dict[str, Any] = {
|
|
533
551
|
"null_count": _to_int_or_none(row.get(f"{col}__nulls")),
|
|
@@ -536,8 +554,8 @@ class BaseSqlConnector:
|
|
|
536
554
|
"max": None,
|
|
537
555
|
}
|
|
538
556
|
if col.lower() in min_max_set:
|
|
539
|
-
entry["min"] = row.get(f"{col}__min")
|
|
540
|
-
entry["max"] = row.get(f"{col}__max")
|
|
557
|
+
entry["min"] = _nan_to_none(row.get(f"{col}__min"))
|
|
558
|
+
entry["max"] = _nan_to_none(row.get(f"{col}__max"))
|
|
541
559
|
result[col] = entry
|
|
542
560
|
|
|
543
561
|
return result
|
|
@@ -198,3 +198,76 @@ def test_get_column_statistics_still_converts_real_values_to_int():
|
|
|
198
198
|
assert stats["id"]["distinct_count"] == 42
|
|
199
199
|
assert isinstance(stats["id"]["null_count"], int)
|
|
200
200
|
assert isinstance(stats["id"]["distinct_count"], int)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def test_get_column_statistics_min_max_nan_becomes_none():
|
|
204
|
+
"""Real bug found via live user testing: comparing a table against
|
|
205
|
+
ITSELF with the exact same row_filter on both sides (matching zero
|
|
206
|
+
rows) still reported Overall Status FAIL, specifically Min/Max FAIL -
|
|
207
|
+
MIN/MAX over zero rows is SQL NULL -> NaN after the pandas round-
|
|
208
|
+
trip, and NaN != NaN (IEEE 754), so compare_min_max's `==` check
|
|
209
|
+
treated two equally-empty, equally-NaN sides as a real difference.
|
|
210
|
+
min/max must come back as None (not NaN) so two empty sides compare
|
|
211
|
+
equal."""
|
|
212
|
+
fake_spark = MagicMock()
|
|
213
|
+
fake_spark.sql.return_value.toPandas.return_value = pd.DataFrame(
|
|
214
|
+
{"id__nulls": [float("nan")], "id__distinct": [float("nan")],
|
|
215
|
+
"id__min": [float("nan")], "id__max": [float("nan")]}
|
|
216
|
+
)
|
|
217
|
+
connector = SparkConnector(spark=fake_spark)
|
|
218
|
+
|
|
219
|
+
stats = connector.get_column_statistics(
|
|
220
|
+
"cat", "sch", "tbl", ["id"], min_max_columns=["id"],
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
assert stats["id"]["min"] is None
|
|
224
|
+
assert stats["id"]["max"] is None
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def test_get_column_statistics_min_max_real_values_pass_through_unchanged():
|
|
228
|
+
"""Regression guard alongside the NaN fix above - a genuine min/max
|
|
229
|
+
result (of any type - string, date, number) must pass through
|
|
230
|
+
completely unchanged, not be mistaken for NaN."""
|
|
231
|
+
fake_spark = MagicMock()
|
|
232
|
+
fake_spark.sql.return_value.toPandas.return_value = pd.DataFrame(
|
|
233
|
+
{"name__nulls": [0], "name__distinct": [5],
|
|
234
|
+
"name__min": ["Alice"], "name__max": ["Zoe"]}
|
|
235
|
+
)
|
|
236
|
+
connector = SparkConnector(spark=fake_spark)
|
|
237
|
+
|
|
238
|
+
stats = connector.get_column_statistics(
|
|
239
|
+
"cat", "sch", "tbl", ["name"], min_max_columns=["name"],
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
assert stats["name"]["min"] == "Alice"
|
|
243
|
+
assert stats["name"]["max"] == "Zoe"
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def test_two_identically_filtered_empty_sides_compare_as_pass():
|
|
247
|
+
"""End-to-end regression test for the exact reported scenario: the
|
|
248
|
+
SAME table compared against itself with the SAME row_filter matching
|
|
249
|
+
zero rows on both sides must report min/max as equal (both None),
|
|
250
|
+
not a false mismatch."""
|
|
251
|
+
from table_validator.validators.catalog_validator import CatalogValidator
|
|
252
|
+
|
|
253
|
+
fake_spark = MagicMock()
|
|
254
|
+
connector = SparkConnector(spark=fake_spark)
|
|
255
|
+
connector.set_row_filters(common="gender = 'male'")
|
|
256
|
+
|
|
257
|
+
fake_spark.sql.return_value.toPandas.return_value = pd.DataFrame(
|
|
258
|
+
{"id__nulls": [float("nan")], "id__distinct": [float("nan")],
|
|
259
|
+
"id__min": [float("nan")], "id__max": [float("nan")]}
|
|
260
|
+
)
|
|
261
|
+
source_stats = connector.get_column_statistics(
|
|
262
|
+
"cat", "sch", "src_tbl", ["id"], min_max_columns=["id"], side="source",
|
|
263
|
+
)
|
|
264
|
+
target_stats = connector.get_column_statistics(
|
|
265
|
+
"cat", "sch", "tgt_tbl", ["id"], min_max_columns=["id"], side="target",
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
validator = CatalogValidator(connector)
|
|
269
|
+
status = validator.compare_min_max(
|
|
270
|
+
source_stats["id"]["min"], source_stats["id"]["max"],
|
|
271
|
+
target_stats["id"]["min"], target_stats["id"]["max"],
|
|
272
|
+
)
|
|
273
|
+
assert status.value == "PASS"
|
|
@@ -1437,6 +1437,65 @@ def test_validate_databricks_wires_only_columns_ignore_columns_and_ignore_dataty
|
|
|
1437
1437
|
assert request.column_map == {"cust_id": "customer_id"}
|
|
1438
1438
|
|
|
1439
1439
|
|
|
1440
|
+
def test_validate_databricks_wires_row_filters_to_connector(tmp_path: Path) -> None:
|
|
1441
|
+
"""config.row_filter/source_row_filter/target_row_filter must reach the
|
|
1442
|
+
Databricks connector via set_row_filters() before compare_catalogs runs -
|
|
1443
|
+
these live purely as connector instance state (same mechanism as the
|
|
1444
|
+
notebook-native validate_tables() API), never as CatalogValidationRequest
|
|
1445
|
+
fields."""
|
|
1446
|
+
config_path = _full_config(tmp_path)
|
|
1447
|
+
config = load_config(config_path)
|
|
1448
|
+
config.row_filter = "status = 'active'"
|
|
1449
|
+
config.source_row_filter = "id > 20"
|
|
1450
|
+
config.target_row_filter = "id > 15"
|
|
1451
|
+
save_config(config, config_path)
|
|
1452
|
+
output_path = tmp_path / "validation_report.xlsx"
|
|
1453
|
+
|
|
1454
|
+
mock_connector = _mock_databricks_connector()
|
|
1455
|
+
|
|
1456
|
+
with patch(
|
|
1457
|
+
"table_validator.cli.main.DatabricksConnector", return_value=mock_connector
|
|
1458
|
+
), patch(
|
|
1459
|
+
"table_validator.cli.main.get_databricks_token", return_value="dapi_fake"
|
|
1460
|
+
), patch(
|
|
1461
|
+
"table_validator.cli.main.get_azure_credential", return_value=None
|
|
1462
|
+
):
|
|
1463
|
+
runner.invoke(
|
|
1464
|
+
app,
|
|
1465
|
+
["validate", "--config-path", str(config_path), "--output", str(output_path)],
|
|
1466
|
+
)
|
|
1467
|
+
|
|
1468
|
+
mock_connector.set_row_filters.assert_called_once_with(
|
|
1469
|
+
common="status = 'active'", source="id > 20", target="id > 15",
|
|
1470
|
+
)
|
|
1471
|
+
|
|
1472
|
+
|
|
1473
|
+
def test_validate_databricks_skips_set_row_filters_when_none_configured(
|
|
1474
|
+
tmp_path: Path,
|
|
1475
|
+
) -> None:
|
|
1476
|
+
"""A config with no row filters set must never call set_row_filters at
|
|
1477
|
+
all - regression guard so a table without this feature configured
|
|
1478
|
+
behaves identically to before the feature existed."""
|
|
1479
|
+
config_path = _full_config(tmp_path)
|
|
1480
|
+
output_path = tmp_path / "validation_report.xlsx"
|
|
1481
|
+
|
|
1482
|
+
mock_connector = _mock_databricks_connector()
|
|
1483
|
+
|
|
1484
|
+
with patch(
|
|
1485
|
+
"table_validator.cli.main.DatabricksConnector", return_value=mock_connector
|
|
1486
|
+
), patch(
|
|
1487
|
+
"table_validator.cli.main.get_databricks_token", return_value="dapi_fake"
|
|
1488
|
+
), patch(
|
|
1489
|
+
"table_validator.cli.main.get_azure_credential", return_value=None
|
|
1490
|
+
):
|
|
1491
|
+
runner.invoke(
|
|
1492
|
+
app,
|
|
1493
|
+
["validate", "--config-path", str(config_path), "--output", str(output_path)],
|
|
1494
|
+
)
|
|
1495
|
+
|
|
1496
|
+
mock_connector.set_row_filters.assert_not_called()
|
|
1497
|
+
|
|
1498
|
+
|
|
1440
1499
|
def test_validate_mode_stats_stops_before_fingerprint_and_row_hash(tmp_path: Path) -> None:
|
|
1441
1500
|
"""--mode stats is the CLI-flag equivalent of the tiered funnel's
|
|
1442
1501
|
"statistical only" prompt: it must stop after Tier 1 even though the
|
|
@@ -343,6 +343,7 @@ def test_deselecting_row_only_omits_row(tmp_path, monkeypatch):
|
|
|
343
343
|
"tgt_cat", "silver", "customers",
|
|
344
344
|
"", # primary key (blank)
|
|
345
345
|
False, # customize validation? (declined)
|
|
346
|
+
False, # filter to a subset of rows? (declined)
|
|
346
347
|
["catalog & schema", "column"], # validations checkbox: row unchecked
|
|
347
348
|
])
|
|
348
349
|
|
|
@@ -417,6 +418,7 @@ def test_databricks_source_type_prompts_for_source_table(tmp_path, monkeypatch):
|
|
|
417
418
|
"tgt_cat", "silver", "customers", # target table
|
|
418
419
|
"", # primary key (left blank)
|
|
419
420
|
False, # customize validation? (declined)
|
|
421
|
+
False, # filter to a subset of rows? (declined)
|
|
420
422
|
[], # validations
|
|
421
423
|
])
|
|
422
424
|
|
|
@@ -442,6 +444,7 @@ def test_databricks_source_type_prompts_for_primary_key_when_both_tables_named(
|
|
|
442
444
|
"tgt_cat", "silver", "customers",
|
|
443
445
|
"id, region_id ", # primary key, comma-separated with stray spaces
|
|
444
446
|
False, # customize validation? (declined)
|
|
447
|
+
False, # filter to a subset of rows? (declined)
|
|
445
448
|
[],
|
|
446
449
|
])
|
|
447
450
|
|
|
@@ -462,6 +465,7 @@ def test_databricks_wizard_declining_customization_leaves_defaults_untouched(
|
|
|
462
465
|
"tgt_cat", "silver", "customers",
|
|
463
466
|
"", # primary key (left blank)
|
|
464
467
|
False, # customize validation? (declined)
|
|
468
|
+
False, # filter to a subset of rows? (declined)
|
|
465
469
|
[],
|
|
466
470
|
])
|
|
467
471
|
|
|
@@ -486,6 +490,7 @@ def test_databricks_wizard_customize_populates_all_three_column_lists(
|
|
|
486
490
|
"id, name ", # only_columns, stray spaces
|
|
487
491
|
"updated_at", # ignore_columns
|
|
488
492
|
"legacy_flag, notes", # ignore_datatype_columns
|
|
493
|
+
False, # filter to a subset of rows? (declined)
|
|
489
494
|
[],
|
|
490
495
|
])
|
|
491
496
|
|
|
@@ -508,6 +513,7 @@ def test_databricks_wizard_customize_all_three_left_blank(tmp_path, monkeypatch)
|
|
|
508
513
|
"", # only_columns left blank
|
|
509
514
|
"", # ignore_columns left blank
|
|
510
515
|
"", # ignore_datatype_columns left blank
|
|
516
|
+
False, # filter to a subset of rows? (declined)
|
|
511
517
|
[],
|
|
512
518
|
])
|
|
513
519
|
|
|
@@ -811,6 +817,7 @@ def test_column_mapping_skipped_when_no_token_available(tmp_path, monkeypatch):
|
|
|
811
817
|
"tgt_cat", "silver", "customers",
|
|
812
818
|
"id",
|
|
813
819
|
False, # customize validation? (declined)
|
|
820
|
+
False, # filter to a subset of rows? (declined)
|
|
814
821
|
[],
|
|
815
822
|
])
|
|
816
823
|
|
|
@@ -834,6 +841,7 @@ def test_column_mapping_live_connection_success_populates_map(tmp_path, monkeypa
|
|
|
834
841
|
"tgt_cat", "silver", "customers",
|
|
835
842
|
"id",
|
|
836
843
|
False, # customize validation? (declined)
|
|
844
|
+
False, # filter to a subset of rows? (declined)
|
|
837
845
|
"customer_id", # column mapping: cust_id -> customer_id
|
|
838
846
|
[],
|
|
839
847
|
],
|
|
@@ -859,6 +867,7 @@ def test_column_mapping_live_connection_failure_skips_gracefully(tmp_path, monke
|
|
|
859
867
|
"tgt_cat", "silver", "customers",
|
|
860
868
|
"id",
|
|
861
869
|
False, # customize validation? (declined)
|
|
870
|
+
False, # filter to a subset of rows? (declined)
|
|
862
871
|
[],
|
|
863
872
|
],
|
|
864
873
|
databricks_token="dapi_fake",
|
|
@@ -889,6 +898,7 @@ def test_column_mapping_only_asks_about_unmatched_source_columns(tmp_path, monke
|
|
|
889
898
|
"tgt_cat", "silver", "customers",
|
|
890
899
|
"id",
|
|
891
900
|
False,
|
|
901
|
+
False, # filter to a subset of rows? (declined)
|
|
892
902
|
"customer_id", # only ONE column-mapping answer, for cust_id
|
|
893
903
|
[],
|
|
894
904
|
],
|
|
@@ -916,6 +926,7 @@ def test_column_mapping_skip_leaves_column_unmapped(tmp_path, monkeypatch):
|
|
|
916
926
|
"tgt_cat", "silver", "customers",
|
|
917
927
|
"id",
|
|
918
928
|
False,
|
|
929
|
+
False, # filter to a subset of rows? (declined)
|
|
919
930
|
"(skip this column)",
|
|
920
931
|
[],
|
|
921
932
|
],
|
|
@@ -941,6 +952,7 @@ def test_column_mapping_no_unmatched_columns_asks_nothing(tmp_path, monkeypatch)
|
|
|
941
952
|
"tgt_cat", "silver", "customers",
|
|
942
953
|
"id",
|
|
943
954
|
False,
|
|
955
|
+
False, # filter to a subset of rows? (declined)
|
|
944
956
|
# no column-mapping answer - none should be consumed
|
|
945
957
|
[],
|
|
946
958
|
],
|
|
@@ -993,6 +1005,7 @@ def test_schema_scoping_prompt_skipped_when_a_table_is_named(tmp_path, monkeypat
|
|
|
993
1005
|
"tgt_cat", "silver", "customers",
|
|
994
1006
|
"", # primary key (blank)
|
|
995
1007
|
False, # customize validation? (declined)
|
|
1008
|
+
False, # filter to a subset of rows? (declined)
|
|
996
1009
|
# no schema-scoping answer here - none should be consumed
|
|
997
1010
|
[],
|
|
998
1011
|
])
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{table_validator-0.1.16 → table_validator-0.1.18}/table_validator/connectors/azure_connector.py
RENAMED
|
File without changes
|
{table_validator-0.1.16 → table_validator-0.1.18}/table_validator/connectors/databricks_connector.py
RENAMED
|
File without changes
|
{table_validator-0.1.16 → table_validator-0.1.18}/table_validator/connectors/spark_connector.py
RENAMED
|
File without changes
|
|
File without changes
|
{table_validator-0.1.16 → table_validator-0.1.18}/table_validator/engine/comparison_engine.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{table_validator-0.1.16 → table_validator-0.1.18}/table_validator/validators/blob_discovery.py
RENAMED
|
File without changes
|
{table_validator-0.1.16 → table_validator-0.1.18}/table_validator/validators/catalog_validator.py
RENAMED
|
File without changes
|
{table_validator-0.1.16 → table_validator-0.1.18}/table_validator/validators/mismatch_classifier.py
RENAMED
|
File without changes
|
{table_validator-0.1.16 → table_validator-0.1.18}/table_validator/validators/row_validator.py
RENAMED
|
File without changes
|
|
File without changes
|
{table_validator-0.1.16 → table_validator-0.1.18}/table_validator.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{table_validator-0.1.16 → table_validator-0.1.18}/table_validator.egg-info/scm_file_list.json
RENAMED
|
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
|
{table_validator-0.1.16 → table_validator-0.1.18}/tests/test_package_import_without_pyspark.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|