table-validator 0.1.9__tar.gz → 0.1.10__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.
Files changed (51) hide show
  1. {table_validator-0.1.9/table_validator.egg-info → table_validator-0.1.10}/PKG-INFO +8 -5
  2. {table_validator-0.1.9 → table_validator-0.1.10}/README.md +6 -4
  3. {table_validator-0.1.9 → table_validator-0.1.10}/pyproject.toml +1 -0
  4. {table_validator-0.1.9 → table_validator-0.1.10}/table_validator/auth/azure_auth.py +22 -5
  5. {table_validator-0.1.9 → table_validator-0.1.10}/table_validator/cli/main.py +155 -1
  6. {table_validator-0.1.9 → table_validator-0.1.10}/table_validator/cli/wizard.py +257 -12
  7. {table_validator-0.1.9 → table_validator-0.1.10}/table_validator/config/schema.py +97 -0
  8. {table_validator-0.1.9 → table_validator-0.1.10}/table_validator/connectors/azure_connector.py +97 -15
  9. {table_validator-0.1.9 → table_validator-0.1.10}/table_validator/connectors/databricks_connector.py +35 -4
  10. {table_validator-0.1.9 → table_validator-0.1.10}/table_validator/models.py +9 -0
  11. {table_validator-0.1.9 → table_validator-0.1.10}/table_validator/reports/excel_report.py +448 -11
  12. {table_validator-0.1.9 → table_validator-0.1.10}/table_validator/validators/catalog_validator.py +21 -15
  13. table_validator-0.1.10/table_validator/validators/mismatch_classifier.py +221 -0
  14. {table_validator-0.1.9 → table_validator-0.1.10/table_validator.egg-info}/PKG-INFO +8 -5
  15. {table_validator-0.1.9 → table_validator-0.1.10}/table_validator.egg-info/SOURCES.txt +2 -0
  16. {table_validator-0.1.9 → table_validator-0.1.10}/table_validator.egg-info/requires.txt +1 -0
  17. {table_validator-0.1.9 → table_validator-0.1.10}/table_validator.egg-info/scm_file_list.json +2 -0
  18. table_validator-0.1.10/table_validator.egg-info/scm_version.json +8 -0
  19. {table_validator-0.1.9 → table_validator-0.1.10}/tests/test_catalog_validator.py +96 -1
  20. {table_validator-0.1.9 → table_validator-0.1.10}/tests/test_cli.py +342 -0
  21. {table_validator-0.1.9 → table_validator-0.1.10}/tests/test_databricks_connector.py +89 -0
  22. table_validator-0.1.10/tests/test_excel_report.py +1158 -0
  23. table_validator-0.1.10/tests/test_mismatch_classifier.py +170 -0
  24. {table_validator-0.1.9 → table_validator-0.1.10}/tests/test_row_validator.py +88 -0
  25. {table_validator-0.1.9 → table_validator-0.1.10}/tests/test_wizard.py +304 -0
  26. table_validator-0.1.9/table_validator.egg-info/scm_version.json +0 -8
  27. table_validator-0.1.9/tests/test_excel_report.py +0 -417
  28. {table_validator-0.1.9 → table_validator-0.1.10}/LICENSE +0 -0
  29. {table_validator-0.1.9 → table_validator-0.1.10}/setup.cfg +0 -0
  30. {table_validator-0.1.9 → table_validator-0.1.10}/table_validator/__init__.py +0 -0
  31. {table_validator-0.1.9 → table_validator-0.1.10}/table_validator/auth/__init__.py +0 -0
  32. {table_validator-0.1.9 → table_validator-0.1.10}/table_validator/auth/databricks_auth.py +0 -0
  33. {table_validator-0.1.9 → table_validator-0.1.10}/table_validator/cli/__init__.py +0 -0
  34. {table_validator-0.1.9 → table_validator-0.1.10}/table_validator/cli/partition_prompt.py +0 -0
  35. {table_validator-0.1.9 → table_validator-0.1.10}/table_validator/cli/summary_table.py +0 -0
  36. {table_validator-0.1.9 → table_validator-0.1.10}/table_validator/config/__init__.py +0 -0
  37. {table_validator-0.1.9 → table_validator-0.1.10}/table_validator/config/manager.py +0 -0
  38. {table_validator-0.1.9 → table_validator-0.1.10}/table_validator/connectors/__init__.py +0 -0
  39. {table_validator-0.1.9 → table_validator-0.1.10}/table_validator/engine/__init__.py +0 -0
  40. {table_validator-0.1.9 → table_validator-0.1.10}/table_validator/engine/comparison_engine.py +0 -0
  41. {table_validator-0.1.9 → table_validator-0.1.10}/table_validator/reports/__init__.py +0 -0
  42. {table_validator-0.1.9 → table_validator-0.1.10}/table_validator/validators/__init__.py +0 -0
  43. {table_validator-0.1.9 → table_validator-0.1.10}/table_validator/validators/blob_discovery.py +0 -0
  44. {table_validator-0.1.9 → table_validator-0.1.10}/table_validator/validators/row_validator.py +0 -0
  45. {table_validator-0.1.9 → table_validator-0.1.10}/table_validator.egg-info/dependency_links.txt +0 -0
  46. {table_validator-0.1.9 → table_validator-0.1.10}/table_validator.egg-info/entry_points.txt +0 -0
  47. {table_validator-0.1.9 → table_validator-0.1.10}/table_validator.egg-info/top_level.txt +0 -0
  48. {table_validator-0.1.9 → table_validator-0.1.10}/tests/__init__.py +0 -0
  49. {table_validator-0.1.9 → table_validator-0.1.10}/tests/test_blob_discovery.py +0 -0
  50. {table_validator-0.1.9 → table_validator-0.1.10}/tests/test_partition_prompt.py +0 -0
  51. {table_validator-0.1.9 → table_validator-0.1.10}/tests/test_report_command.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: table-validator
3
- Version: 0.1.9
3
+ Version: 0.1.10
4
4
  Summary: CLI tool for validating data migrations between Azure (Blob Storage / SQL Database) and Databricks Delta Lake catalogs
5
5
  License: MIT
6
6
  Requires-Python: >=3.9
@@ -15,6 +15,7 @@ Requires-Dist: databricks-sql-connector>=3.0
15
15
  Requires-Dist: pandas>=2.0
16
16
  Requires-Dist: numpy>=1.24
17
17
  Requires-Dist: azure-storage-blob>=12.14
18
+ Requires-Dist: azure-identity>=1.15
18
19
  Requires-Dist: openpyxl>=3.1
19
20
  Requires-Dist: pyarrow>=14.0
20
21
  Requires-Dist: PyYAML>=6.0
@@ -38,6 +39,7 @@ Currently supported sources (target is always Databricks):
38
39
  - Databricks catalog → Databricks catalog
39
40
  - Azure Blob Storage (CSV / Excel / Parquet) → Databricks table
40
41
  - Azure SQL Database → Databricks catalog
42
+ - Azure Synapse SQL pool (dedicated or serverless) → Databricks catalog
41
43
 
42
44
  More source platforms can be added behind the same connector/validator
43
45
  interfaces described below.
@@ -80,10 +82,11 @@ Interactive wizard that walks you through:
80
82
 
81
83
  1. Azure Storage account + container (optional, skip if you don't have a Blob source) and account key
82
84
  2. Azure SQL server + database (optional, skip if you don't have a SQL source) and username/password
83
- 3. Databricks workspace URL, SQL Warehouse HTTP path, and personal access token
84
- 4. Source table (catalog / schema / table)
85
- 5. Target table (catalog / schema / table)
86
- 6. Which validations to run (catalog / schema / column / row)
85
+ 3. Azure Synapse SQL pool endpoint + database (optional, skip if you don't have a Synapse source), then either a SQL login (username/password) or a Microsoft Entra ID service principal (tenant ID + client ID + client secret)
86
+ 4. Databricks workspace URL, SQL Warehouse HTTP path, and personal access token
87
+ 5. Source table (catalog / schema / table)
88
+ 6. Target table (catalog / schema / table)
89
+ 7. Which validations to run (catalog / schema / column / row)
87
90
 
88
91
  ```bash
89
92
  tablevalidator validate
@@ -11,6 +11,7 @@ Currently supported sources (target is always Databricks):
11
11
  - Databricks catalog → Databricks catalog
12
12
  - Azure Blob Storage (CSV / Excel / Parquet) → Databricks table
13
13
  - Azure SQL Database → Databricks catalog
14
+ - Azure Synapse SQL pool (dedicated or serverless) → Databricks catalog
14
15
 
15
16
  More source platforms can be added behind the same connector/validator
16
17
  interfaces described below.
@@ -53,10 +54,11 @@ Interactive wizard that walks you through:
53
54
 
54
55
  1. Azure Storage account + container (optional, skip if you don't have a Blob source) and account key
55
56
  2. Azure SQL server + database (optional, skip if you don't have a SQL source) and username/password
56
- 3. Databricks workspace URL, SQL Warehouse HTTP path, and personal access token
57
- 4. Source table (catalog / schema / table)
58
- 5. Target table (catalog / schema / table)
59
- 6. Which validations to run (catalog / schema / column / row)
57
+ 3. Azure Synapse SQL pool endpoint + database (optional, skip if you don't have a Synapse source), then either a SQL login (username/password) or a Microsoft Entra ID service principal (tenant ID + client ID + client secret)
58
+ 4. Databricks workspace URL, SQL Warehouse HTTP path, and personal access token
59
+ 5. Source table (catalog / schema / table)
60
+ 6. Target table (catalog / schema / table)
61
+ 7. Which validations to run (catalog / schema / column / row)
60
62
 
61
63
  ```bash
62
64
  tablevalidator validate
@@ -19,6 +19,7 @@ dependencies = [
19
19
  "pandas>=2.0",
20
20
  "numpy>=1.24",
21
21
  "azure-storage-blob>=12.14",
22
+ "azure-identity>=1.15",
22
23
  "openpyxl>=3.1",
23
24
  "pyarrow>=14.0",
24
25
  "PyYAML>=6.0",
@@ -26,22 +26,36 @@ class AzureCredential:
26
26
 
27
27
  storage_account_key authenticates AzureConnector (Blob Storage);
28
28
  sql_username/sql_password authenticate AzureSqlConnector (Azure SQL
29
- Database). Both are optional here since a given validation run may
30
- only need one of the two.
29
+ Database); synapse_username/synapse_password authenticate the same
30
+ AzureSqlConnector class when pointed at a Synapse SQL pool instead
31
+ (Synapse SQL is protocol-identical T-SQL/ODBC, so no separate
32
+ connector class exists - only a separate credential pair, since a
33
+ Synapse SQL login is a distinct principal from an Azure SQL DB one).
34
+ synapse_client_secret is the alternative to that pair: the Entra ID
35
+ service-principal secret used when config.azure.synapse_auth_mode is
36
+ 'entra_service_principal', paired with the non-secret tenant_id /
37
+ synapse_client_id that live in config.yaml.
38
+ All fields are optional here since a given validation run may only
39
+ need one pair.
31
40
  """
32
41
 
33
42
  storage_account_key: Optional[str] = None
34
43
  sql_username: Optional[str] = None
35
44
  sql_password: Optional[str] = None
45
+ synapse_username: Optional[str] = None
46
+ synapse_password: Optional[str] = None
47
+ synapse_client_secret: Optional[str] = None
36
48
 
37
49
 
38
50
  def get_azure_credential(config: ValidatorConfig, env_path: Path = ENV_PATH) -> AzureCredential:
39
51
  """
40
52
  Resolve Azure credentials for the given config.
41
53
 
42
- Phase 1: reads AZURE_STORAGE_KEY / AZURE_SQL_USERNAME / AZURE_SQL_PASSWORD
43
- from ~/.table_validator/.env. A later phase can swap this body for
44
- Azure CLI / Service Principal auth without changing any caller.
54
+ Phase 1: reads AZURE_STORAGE_KEY / AZURE_SQL_USERNAME /
55
+ AZURE_SQL_PASSWORD / SYNAPSE_USERNAME / SYNAPSE_PASSWORD /
56
+ SYNAPSE_CLIENT_SECRET from ~/.table_validator/.env. A later phase can
57
+ swap this body for Azure CLI / managed-identity auth without changing
58
+ any caller.
45
59
  """
46
60
  values = dotenv_values(env_path) if env_path.exists() else {}
47
61
 
@@ -49,4 +63,7 @@ def get_azure_credential(config: ValidatorConfig, env_path: Path = ENV_PATH) ->
49
63
  storage_account_key=values.get("AZURE_STORAGE_KEY") or None,
50
64
  sql_username=values.get("AZURE_SQL_USERNAME") or None,
51
65
  sql_password=values.get("AZURE_SQL_PASSWORD") or None,
66
+ synapse_username=values.get("SYNAPSE_USERNAME") or None,
67
+ synapse_password=values.get("SYNAPSE_PASSWORD") or None,
68
+ synapse_client_secret=values.get("SYNAPSE_CLIENT_SECRET") or None,
52
69
  )
@@ -19,7 +19,12 @@ from table_validator.cli.summary_table import (
19
19
  from table_validator.cli.partition_prompt import build_partition_prompt
20
20
  from table_validator.cli.wizard import run_configure_wizard
21
21
  from table_validator.config.manager import CONFIG_PATH, ConfigNotFoundError, require_config
22
- from table_validator.config.schema import SourceType, ValidationType, ValidatorConfig
22
+ from table_validator.config.schema import (
23
+ SourceType,
24
+ SynapseAuthMode,
25
+ ValidationType,
26
+ ValidatorConfig,
27
+ )
23
28
  from table_validator.connectors.azure_connector import AzureConnector, AzureSqlConnector
24
29
  from table_validator.connectors.databricks_connector import DatabricksConnector
25
30
  from table_validator.models import (
@@ -207,6 +212,18 @@ def validate(
207
212
  "prompt on its own, but --yes makes that explicit."
208
213
  ),
209
214
  ),
215
+ skip_category_summary: bool = typer.Option(
216
+ False,
217
+ "--skip-category-summary",
218
+ help=(
219
+ "Omit the 'Mismatch Categories' sheet (root-cause "
220
+ "classification + plain-English summary of Data Mismatches) "
221
+ "from the Excel report. Every other sheet, including Data "
222
+ "Mismatches itself, is unaffected - this recovers the exact "
223
+ "report shape from before the mismatch-categorization "
224
+ "feature existed, for anyone who doesn't want it."
225
+ ),
226
+ ),
210
227
  ) -> None:
211
228
  """Run validation and produce an Excel validation report."""
212
229
 
@@ -281,6 +298,8 @@ def validate(
281
298
  result = _run_blob_validation(config, azure_credential, databricks)
282
299
  elif config.source_type == SourceType.AZURE_SQL:
283
300
  result = _run_sql_validation(config, azure_credential, databricks)
301
+ elif config.source_type == SourceType.SYNAPSE:
302
+ result = _run_synapse_validation(config, azure_credential, databricks)
284
303
  else:
285
304
  result = _run_databricks_validation(config, databricks, mode=mode, yes=yes)
286
305
  except ValueError as exc:
@@ -310,6 +329,7 @@ def validate(
310
329
  result, str(output),
311
330
  source_type=config.source_type.value,
312
331
  enabled_validations=report_enabled_validations,
332
+ skip_category_summary=skip_category_summary,
313
333
  )
314
334
  except PermissionError:
315
335
  typer.secho(
@@ -444,6 +464,17 @@ def _run_databricks_validation(
444
464
  tables_restriction = [config.source_table.table]
445
465
  if config.source_table.table.lower() != config.target_table.table.lower():
446
466
  table_map = {config.source_table.table: config.target_table.table}
467
+ else:
468
+ # Schema-wide sweep (no single table named on both sides) - apply
469
+ # the wizard's optional only_tables allowlist / table_map renaming
470
+ # for this schema, if the user set either. Both are no-ops
471
+ # (tables_restriction stays None, table_map stays {}) unless the
472
+ # user actually configured them, so a plain schema sweep behaves
473
+ # exactly as before this feature existed.
474
+ if config.only_tables:
475
+ tables_restriction = list(config.only_tables)
476
+ if config.table_map:
477
+ table_map = dict(config.table_map)
447
478
 
448
479
  # A configured primary key only applies to the single named table
449
480
  # (not a catalog-wide sweep) - CatalogValidator looks it up by
@@ -603,6 +634,113 @@ def _run_sql_validation(
603
634
  return validator.validate(request)
604
635
 
605
636
 
637
+ def _run_synapse_validation(
638
+ config: ValidatorConfig,
639
+ azure_credential,
640
+ databricks: DatabricksConnector,
641
+ ) -> CatalogValidationResponse:
642
+ """source_type == synapse: Azure Synapse SQL pool (dedicated or
643
+ serverless) against a Databricks catalog. Synapse SQL is
644
+ protocol-identical T-SQL over the same ODBC driver Azure SQL Database
645
+ uses, so this reuses AzureSqlConnector/AzureSqlValidator UNCHANGED,
646
+ just constructed with the Synapse server/database/credentials instead
647
+ of an Azure SQL DB's - no new connector or validator class, mirrors
648
+ _run_sql_validation's logic exactly (see that function for the
649
+ schema_map/table_map/primary_keys/FULL-mode rationale, identical
650
+ here).
651
+
652
+ Auth is the one place Synapse genuinely diverges from the Azure SQL
653
+ path: config.azure.synapse_auth_mode selects between a SQL login
654
+ (SYNAPSE_USERNAME/SYNAPSE_PASSWORD) and a Microsoft Entra ID service
655
+ principal (azure.tenant_id + azure.synapse_client_id +
656
+ SYNAPSE_CLIENT_SECRET). Entra is what a workspace with SQL
657
+ authentication disabled requires - the connector fetches an access
658
+ token and passes it via the ODBC access-token attribute rather than
659
+ sending UID/PWD at all.
660
+
661
+ Not yet verified against a live Synapse SQL pool for pool-specific
662
+ T-SQL quirks (dedicated pools have historically had some system-view/
663
+ HASHBYTES-adjacent gaps vs. Azure SQL DB) - flagged here rather than
664
+ assumed away; report back if a specific query fails against your pool
665
+ and it can be special-cased in AzureSqlConnector the same way
666
+ decimal_as_integer/float formatting already are for Databricks
667
+ compatibility.
668
+ """
669
+ if not config.azure.synapse_server or not config.azure.synapse_database:
670
+ raise ValueError(
671
+ "azure.synapse_server and azure.synapse_database are required "
672
+ "for a Synapse source. Run 'tablevalidator configure' first."
673
+ )
674
+
675
+ if config.azure.synapse_auth_mode == SynapseAuthMode.ENTRA_SERVICE_PRINCIPAL:
676
+ missing_entra = []
677
+ if not config.azure.tenant_id:
678
+ missing_entra.append("azure.tenant_id (in config.yaml)")
679
+ if not config.azure.synapse_client_id:
680
+ missing_entra.append("azure.synapse_client_id (in config.yaml)")
681
+ if not azure_credential.synapse_client_secret:
682
+ missing_entra.append("SYNAPSE_CLIENT_SECRET (in ~/.table_validator/.env)")
683
+ if missing_entra:
684
+ raise ValueError(
685
+ "Synapse is configured for Entra service-principal auth but "
686
+ "these are missing: " + ", ".join(missing_entra)
687
+ + ". Run 'tablevalidator configure' first."
688
+ )
689
+
690
+ synapse = AzureSqlConnector(
691
+ server=config.azure.synapse_server,
692
+ database=config.azure.synapse_database,
693
+ tenant_id=config.azure.tenant_id,
694
+ client_id=config.azure.synapse_client_id,
695
+ client_secret=azure_credential.synapse_client_secret,
696
+ )
697
+ else:
698
+ if not azure_credential.synapse_username or not azure_credential.synapse_password:
699
+ raise ValueError(
700
+ "No Synapse username/password found in ~/.table_validator/.env. "
701
+ "Run 'tablevalidator configure' first."
702
+ )
703
+
704
+ synapse = AzureSqlConnector(
705
+ server=config.azure.synapse_server,
706
+ database=config.azure.synapse_database,
707
+ username=azure_credential.synapse_username,
708
+ password=azure_credential.synapse_password,
709
+ )
710
+
711
+ schema_map: Dict[str, str] = {}
712
+ schemas_restriction = None
713
+ if config.synapse_source.schema_name and config.target_table.schema_name:
714
+ schema_map = {config.synapse_source.schema_name: config.target_table.schema_name}
715
+ schemas_restriction = [config.synapse_source.schema_name]
716
+
717
+ table_map: Dict[str, str] = {}
718
+ tables_restriction = None
719
+ if config.synapse_source.table and config.target_table.table:
720
+ table_map = {config.synapse_source.table: config.target_table.table}
721
+ tables_restriction = [config.synapse_source.table]
722
+
723
+ primary_keys: Dict[str, list] = {}
724
+ if config.primary_key and config.target_table.table:
725
+ primary_keys[config.target_table.table] = config.primary_key
726
+ if config.target_table.schema_name:
727
+ key = f"{config.target_table.schema_name}.{config.target_table.table}"
728
+ primary_keys[key] = config.primary_key
729
+
730
+ request = AzureSqlValidationRequest(
731
+ target_catalog=config.target_table.catalog or "",
732
+ schemas=schemas_restriction,
733
+ schema_map=schema_map,
734
+ tables=tables_restriction,
735
+ table_map=table_map,
736
+ primary_keys=primary_keys,
737
+ data_compare_mode=DataCompareMode.FULL,
738
+ )
739
+
740
+ validator = AzureSqlValidator(synapse, databricks)
741
+ return validator.validate(request)
742
+
743
+
606
744
  # ---------------------------------------------------------------------------
607
745
  # Helpers
608
746
  # ---------------------------------------------------------------------------
@@ -632,6 +770,11 @@ def _missing_config_fields(config: ValidatorConfig) -> list:
632
770
  missing.append("azure.sql_server")
633
771
  if not config.azure.sql_database:
634
772
  missing.append("azure.sql_database")
773
+ elif config.source_type == SourceType.SYNAPSE:
774
+ if not config.azure.synapse_server:
775
+ missing.append("azure.synapse_server")
776
+ if not config.azure.synapse_database:
777
+ missing.append("azure.synapse_database")
635
778
  else:
636
779
  if not config.source_table.catalog:
637
780
  missing.append("source_table.catalog")
@@ -696,6 +839,17 @@ def _describe_scope(config: ValidatorConfig) -> str:
696
839
  source_desc = f"sql:{database}.{config.sql_source.schema_name} (all tables)"
697
840
  else:
698
841
  source_desc = f"sql:{database}.{config.sql_source.schema_name}.{config.sql_source.table}"
842
+ elif config.source_type == SourceType.SYNAPSE:
843
+ database = config.azure.synapse_database or "?"
844
+ if not config.synapse_source.schema_name:
845
+ source_desc = f"synapse:{database} (all schemas)"
846
+ elif not config.synapse_source.table:
847
+ source_desc = f"synapse:{database}.{config.synapse_source.schema_name} (all tables)"
848
+ else:
849
+ source_desc = (
850
+ f"synapse:{database}.{config.synapse_source.schema_name}."
851
+ f"{config.synapse_source.table}"
852
+ )
699
853
  else:
700
854
  source_desc = _side(config.source_table)
701
855