google-pso-data-validator 8.2.0__tar.gz → 8.3.0__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 (93) hide show
  1. {google_pso_data_validator-8.2.0/google_pso_data_validator.egg-info → google_pso_data_validator-8.3.0}/PKG-INFO +8 -9
  2. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/README.md +3 -4
  3. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/data_validation/__main__.py +6 -4
  4. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/data_validation/cli_tools.py +7 -12
  5. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/data_validation/config_manager.py +53 -3
  6. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/data_validation/data_validation.py +16 -9
  7. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/data_validation/find_tables.py +18 -8
  8. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/data_validation/partition_builder.py +27 -20
  9. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/data_validation/result_handlers/base_backend.py +2 -2
  10. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/data_validation/result_handlers/bigquery.py +49 -4
  11. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/data_validation/result_handlers/factory.py +34 -29
  12. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/data_validation/result_handlers/postgres.py +3 -3
  13. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/data_validation/state_manager.py +1 -4
  14. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/data_validation/util.py +18 -19
  15. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0/google_pso_data_validator.egg-info}/PKG-INFO +8 -9
  16. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/google_pso_data_validator.egg-info/requires.txt +2 -2
  17. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/setup.py +5 -5
  18. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/third_party/ibis/ibis_mssql/__init__.py +9 -4
  19. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/LICENSE +0 -0
  20. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/data_validation/__init__.py +0 -0
  21. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/data_validation/app.py +0 -0
  22. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/data_validation/client_info.py +0 -0
  23. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/data_validation/clients.py +0 -0
  24. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/data_validation/combiner.py +0 -0
  25. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/data_validation/consts.py +0 -0
  26. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/data_validation/exceptions.py +0 -0
  27. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/data_validation/gcs_helper.py +0 -0
  28. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/data_validation/jellyfish_distance.py +0 -0
  29. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/data_validation/metadata.py +0 -0
  30. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/data_validation/query_builder/__init__.py +0 -0
  31. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/data_validation/query_builder/partition_row_builder.py +0 -0
  32. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/data_validation/query_builder/query_builder.py +0 -0
  33. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/data_validation/query_builder/random_row_builder.py +0 -0
  34. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/data_validation/raw_query.py +0 -0
  35. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/data_validation/result_handlers/__init__.py +0 -0
  36. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/data_validation/result_handlers/text.py +0 -0
  37. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/data_validation/schema_validation.py +0 -0
  38. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/data_validation/secret_manager.py +0 -0
  39. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/data_validation/validation_builder.py +0 -0
  40. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/google_pso_data_validator.egg-info/SOURCES.txt +0 -0
  41. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/google_pso_data_validator.egg-info/dependency_links.txt +0 -0
  42. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/google_pso_data_validator.egg-info/entry_points.txt +0 -0
  43. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/google_pso_data_validator.egg-info/top_level.txt +0 -0
  44. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/setup.cfg +0 -0
  45. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/third_party/ibis/ibis_addon/__init__.py +0 -0
  46. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/third_party/ibis/ibis_addon/api.py +0 -0
  47. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/third_party/ibis/ibis_addon/operations.py +0 -0
  48. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/third_party/ibis/ibis_bigquery/__init__.py +0 -0
  49. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/third_party/ibis/ibis_bigquery/api.py +0 -0
  50. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/third_party/ibis/ibis_bigquery/registry.py +0 -0
  51. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/third_party/ibis/ibis_cloud_spanner/__init__.py +0 -0
  52. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/third_party/ibis/ibis_cloud_spanner/api.py +0 -0
  53. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/third_party/ibis/ibis_cloud_spanner/client.py +0 -0
  54. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/third_party/ibis/ibis_cloud_spanner/compiler.py +0 -0
  55. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/third_party/ibis/ibis_cloud_spanner/datatypes.py +0 -0
  56. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/third_party/ibis/ibis_cloud_spanner/registry.py +0 -0
  57. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/third_party/ibis/ibis_cloud_spanner/tests/__init__.py +0 -0
  58. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/third_party/ibis/ibis_cloud_spanner/tests/conftest.py +0 -0
  59. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/third_party/ibis/ibis_cloud_spanner/to_pandas.py +0 -0
  60. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/third_party/ibis/ibis_db2/__init__.py +0 -0
  61. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/third_party/ibis/ibis_db2/api.py +0 -0
  62. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/third_party/ibis/ibis_db2/compiler.py +0 -0
  63. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/third_party/ibis/ibis_db2/datatypes.py +0 -0
  64. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/third_party/ibis/ibis_db2/registry.py +0 -0
  65. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/third_party/ibis/ibis_impala/__init__.py +0 -0
  66. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/third_party/ibis/ibis_impala/api.py +0 -0
  67. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/third_party/ibis/ibis_mssql/api.py +0 -0
  68. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/third_party/ibis/ibis_mssql/datatypes.py +0 -0
  69. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/third_party/ibis/ibis_mssql/registry.py +0 -0
  70. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/third_party/ibis/ibis_mysql/__init__.py +0 -0
  71. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/third_party/ibis/ibis_mysql/base_sql_compiler/__init__.py +0 -0
  72. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/third_party/ibis/ibis_mysql/base_sql_compiler/select_builder.py +0 -0
  73. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/third_party/ibis/ibis_mysql/compiler.py +0 -0
  74. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/third_party/ibis/ibis_oracle/__init__.py +0 -0
  75. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/third_party/ibis/ibis_oracle/api.py +0 -0
  76. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/third_party/ibis/ibis_oracle/compiler.py +0 -0
  77. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/third_party/ibis/ibis_oracle/datatypes.py +0 -0
  78. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/third_party/ibis/ibis_oracle/registry.py +0 -0
  79. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/third_party/ibis/ibis_postgres/__init__.py +0 -0
  80. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/third_party/ibis/ibis_postgres/client.py +0 -0
  81. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/third_party/ibis/ibis_postgres/datatypes.py +0 -0
  82. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/third_party/ibis/ibis_postgres/registry.py +0 -0
  83. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/third_party/ibis/ibis_redshift/__init__.py +0 -0
  84. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/third_party/ibis/ibis_redshift/api.py +0 -0
  85. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/third_party/ibis/ibis_redshift/compiler.py +0 -0
  86. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/third_party/ibis/ibis_snowflake/__init__.py +0 -0
  87. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/third_party/ibis/ibis_snowflake/api.py +0 -0
  88. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/third_party/ibis/ibis_snowflake/datatypes.py +0 -0
  89. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/third_party/ibis/ibis_teradata/__init__.py +0 -0
  90. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/third_party/ibis/ibis_teradata/api.py +0 -0
  91. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/third_party/ibis/ibis_teradata/compiler.py +0 -0
  92. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/third_party/ibis/ibis_teradata/datatypes.py +0 -0
  93. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.3.0}/third_party/ibis/ibis_teradata/registry.py +0 -0
@@ -1,21 +1,21 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: google-pso-data-validator
3
- Version: 8.2.0
3
+ Version: 8.3.0
4
4
  Summary: A package to enable easy data validation
5
5
  Author: PSO DVT Engineering team
6
6
  Author-email: data-validator-eng@google.com
7
7
  Classifier: Development Status :: 4 - Beta
8
8
  Classifier: License :: OSI Approved :: Apache Software License
9
9
  Classifier: Programming Language :: Python
10
- Classifier: Programming Language :: Python :: 3.8
11
10
  Classifier: Programming Language :: Python :: 3.9
12
11
  Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
13
  Classifier: Operating System :: OS Independent
14
- Requires-Python: >=3.8
14
+ Requires-Python: >=3.9
15
15
  Description-Content-Type: text/markdown
16
16
  License-File: LICENSE
17
17
  Requires-Dist: Flask>=2.2
18
- Requires-Dist: fsspec!=2025.3.1
18
+ Requires-Dist: fsspec
19
19
  Requires-Dist: google-api-python-client>=2.144.0
20
20
  Requires-Dist: google-cloud-bigquery>=3.25.0
21
21
  Requires-Dist: google-cloud-bigquery-storage>=2.26.0
@@ -25,7 +25,7 @@ Requires-Dist: google-cloud-storage>=2.18.2
25
25
  Requires-Dist: ibis-framework==5.1.0
26
26
  Requires-Dist: impyla>=0.19.0
27
27
  Requires-Dist: jellyfish>=1.1.0
28
- Requires-Dist: pandas==2.0.3
28
+ Requires-Dist: pandas
29
29
  Requires-Dist: parsy>=2.1
30
30
  Requires-Dist: psycopg2-binary>=2.9.9
31
31
  Requires-Dist: pyarrow==14.0.1
@@ -340,7 +340,7 @@ data-validation
340
340
  Directory Path to store YAML Config Files
341
341
  GCS: Provide a full gs:// path of the target directory. Eg: `gs://<BUCKET>/partitions_dir`
342
342
  Local: Provide a relative path of the target directory. Eg: `partitions_dir`
343
- If invoked with -tbls parameter, the validations are stored in a directory named <schema>.<table>, otherwise the directory is named `custom.<random_string>`
343
+ For custom queries, the yaml files are stored in CONFIG_DIR and, for tables the yamls are stored in a directory named <source_schema>.<table> within CONFIG_DIR.
344
344
  --partition-num INT, -pn INT
345
345
  Number of partitions into which the table should be split, e.g. 1000 or 10000
346
346
  In case this value exceeds the row count of the source/target table, it will be decreased to max(source_row_count, target_row_count)
@@ -653,9 +653,8 @@ The `--config-dir` flag will specify the directory with the YAML files to be exe
653
653
 
654
654
  ### Validation Reports
655
655
 
656
- The result handlers tell DVT where to store the results of each validation. The
657
- tool can write the results of a validation run to Google BigQuery, PostgreSQL
658
- or print to stdout (default). View the schema of the results table [here](https://github.com/GoogleCloudPlatform/professional-services-data-validator/blob/develop/terraform/results_schema.json).
656
+ The result handlers tell DVT where to store the results of each validation. The tool can write the results of a validation run to Google BigQuery, PostgreSQL or print to stdout (default). See [result handler setup](https://github.com/GoogleCloudPlatform/professional-services-data-validator/blob/develop/docs/installation.md#result-handler-setup) for pre-requisites. View the schema of the BigQuery results table [here](https://github.com/GoogleCloudPlatform/professional-services-data-validator/blob/develop/terraform/results_schema.json).
657
+
659
658
 
660
659
  To output to BigQuery or PostgreSQL, simply include the `-rh` flag during a validation run including
661
660
  the schema and table name for the results.
@@ -281,7 +281,7 @@ data-validation
281
281
  Directory Path to store YAML Config Files
282
282
  GCS: Provide a full gs:// path of the target directory. Eg: `gs://<BUCKET>/partitions_dir`
283
283
  Local: Provide a relative path of the target directory. Eg: `partitions_dir`
284
- If invoked with -tbls parameter, the validations are stored in a directory named <schema>.<table>, otherwise the directory is named `custom.<random_string>`
284
+ For custom queries, the yaml files are stored in CONFIG_DIR and, for tables the yamls are stored in a directory named <source_schema>.<table> within CONFIG_DIR.
285
285
  --partition-num INT, -pn INT
286
286
  Number of partitions into which the table should be split, e.g. 1000 or 10000
287
287
  In case this value exceeds the row count of the source/target table, it will be decreased to max(source_row_count, target_row_count)
@@ -594,9 +594,8 @@ The `--config-dir` flag will specify the directory with the YAML files to be exe
594
594
 
595
595
  ### Validation Reports
596
596
 
597
- The result handlers tell DVT where to store the results of each validation. The
598
- tool can write the results of a validation run to Google BigQuery, PostgreSQL
599
- or print to stdout (default). View the schema of the results table [here](https://github.com/GoogleCloudPlatform/professional-services-data-validator/blob/develop/terraform/results_schema.json).
597
+ The result handlers tell DVT where to store the results of each validation. The tool can write the results of a validation run to Google BigQuery, PostgreSQL or print to stdout (default). See [result handler setup](https://github.com/GoogleCloudPlatform/professional-services-data-validator/blob/develop/docs/installation.md#result-handler-setup) for pre-requisites. View the schema of the BigQuery results table [here](https://github.com/GoogleCloudPlatform/professional-services-data-validator/blob/develop/terraform/results_schema.json).
598
+
600
599
 
601
600
  To output to BigQuery or PostgreSQL, simply include the `-rh` flag during a validation run including
602
601
  the schema and table name for the results.
@@ -524,8 +524,8 @@ def run_validation(config_manager: ConfigManager, dry_run=False, verbose=False):
524
524
  validation_builder=None,
525
525
  result_handler=None,
526
526
  verbose=verbose,
527
- source_client=source_client,
528
- target_client=target_client,
527
+ cached_source_client=source_client,
528
+ cached_target_client=target_client,
529
529
  ) as validator:
530
530
 
531
531
  if dry_run:
@@ -533,10 +533,12 @@ def run_validation(config_manager: ConfigManager, dry_run=False, verbose=False):
533
533
  json.dumps(
534
534
  {
535
535
  "source_query": util.ibis_table_to_sql(
536
- validator.validation_builder.get_source_query()
536
+ validator.validation_builder.get_source_query(),
537
+ source_client,
537
538
  ),
538
539
  "target_query": util.ibis_table_to_sql(
539
- validator.validation_builder.get_target_query()
540
+ validator.validation_builder.get_target_query(),
541
+ target_client,
540
542
  ),
541
543
  },
542
544
  indent=4,
@@ -1308,21 +1308,16 @@ def _get_result_handler(rc_value: str, sa_file=None) -> dict:
1308
1308
  if config[0] in connections:
1309
1309
  # We received connection_name.results_table.
1310
1310
  conn_from_file = get_connection(config[0])
1311
- if conn_from_file[consts.SOURCE_TYPE] == consts.SOURCE_TYPE_BIGQUERY:
1311
+ if conn_from_file[consts.SOURCE_TYPE] in [
1312
+ consts.SOURCE_TYPE_BIGQUERY,
1313
+ consts.SOURCE_TYPE_POSTGRES,
1314
+ ]:
1312
1315
  result_handler = {
1313
1316
  consts.RH_TYPE: conn_from_file[consts.SOURCE_TYPE],
1314
- consts.PROJECT_ID: conn_from_file["project_id"],
1315
1317
  consts.TABLE_ID: config[1],
1316
- consts.API_ENDPOINT: conn_from_file.get("api_endpoint", None),
1317
- consts.STORAGE_API_ENDPOINT: conn_from_file.get(
1318
- "storage_api_endpoint", None
1319
- ),
1320
- }
1321
- elif conn_from_file[consts.SOURCE_TYPE] == consts.SOURCE_TYPE_POSTGRES:
1322
- result_handler = {
1323
- consts.RH_TYPE: conn_from_file[consts.SOURCE_TYPE],
1324
- consts.TABLE_ID: config[1],
1325
- consts.RH_CONN: conn_from_file,
1318
+ # Only store the connection name in the result handler to avoid accidentally
1319
+ # storing credentials in config files.
1320
+ consts.RH_CONN: config[0],
1326
1321
  }
1327
1322
  # TODO Add filesytem handler too.
1328
1323
  else:
@@ -671,20 +671,38 @@ class ConfigManager(object):
671
671
 
672
672
  This is because SQL Server text does not support the len() function to get the length in characters.
673
673
  Instead we must fall back to using ByteLength."""
674
+ return self._is_sql_server_type(
675
+ source_column_name, target_column_name, ["text", "ntext"]
676
+ )
677
+
678
+ def _is_sql_server_image(
679
+ self, source_column_name: str, target_column_name: str
680
+ ) -> bool:
681
+ """Returns True when either source or target column is of SQL Server image data type.
682
+
683
+ This is because SQL Server image is deprecated and not supported by a number of SQL functions.
684
+ """
685
+ return self._is_sql_server_type(
686
+ source_column_name, target_column_name, ["image"]
687
+ )
688
+
689
+ def _is_sql_server_type(
690
+ self, source_column_name: str, target_column_name: str, type_list: List[str]
691
+ ) -> bool:
692
+ """Returns True when either source or target column is of a SQL Server type listed in type_list."""
674
693
 
675
694
  raw_source_types = self.get_source_raw_data_types()
676
695
  raw_target_types = self.get_target_raw_data_types()
677
- text_types = ("text", "ntext")
678
696
  return bool(
679
697
  (
680
698
  self.source_client.name == "mssql"
681
699
  and raw_source_types
682
- and raw_source_types.get(source_column_name, [None])[0] in text_types
700
+ and raw_source_types.get(source_column_name, [None])[0] in type_list
683
701
  )
684
702
  or (
685
703
  self.target_client.name == "mssql"
686
704
  and raw_target_types
687
- and raw_target_types.get(target_column_name, [None])[0] in text_types
705
+ and raw_target_types.get(target_column_name, [None])[0] in type_list
688
706
  )
689
707
  )
690
708
 
@@ -1075,6 +1093,11 @@ class ConfigManager(object):
1075
1093
  f"Skipping {agg_type} on {column} due to data type: {column_type}"
1076
1094
  )
1077
1095
  continue
1096
+ elif self._is_sql_server_image(column, column):
1097
+ logging.info(
1098
+ f"Skipping {agg_type} on {column} due to SQL Server image data type"
1099
+ )
1100
+ continue
1078
1101
 
1079
1102
  if require_pre_agg_calc_field(
1080
1103
  column_type, target_column_type, agg_type, cast_to_bigint
@@ -1252,8 +1275,25 @@ class ConfigManager(object):
1252
1275
  raise ValueError(
1253
1276
  "Exclude columns flag cannot be present with column list '*'"
1254
1277
  )
1278
+
1255
1279
  return casefold_columns
1256
1280
 
1281
+ def _filter_columns_by_data_type(
1282
+ self, source_columns: dict, target_columns: dict
1283
+ ) -> tuple:
1284
+ """Filter out columns with a data type that is incompatible with DVT."""
1285
+ result_source_columns = source_columns.copy()
1286
+ result_target_columns = target_columns.copy()
1287
+ for source_column, target_column in zip(source_columns, target_columns):
1288
+ if self._is_sql_server_image(source_column, target_column):
1289
+ logging.info(
1290
+ f"Skipping column {source_column} due to SQL Server image data type"
1291
+ )
1292
+ result_source_columns.pop(source_column)
1293
+ result_target_columns.pop(target_column)
1294
+
1295
+ return result_source_columns, result_target_columns
1296
+
1257
1297
  def build_dependent_aliases(self, calc_type: str, col_list=None) -> List[Dict]:
1258
1298
  """This is a utility function for determining the required depth of all fields"""
1259
1299
  source_table = self.get_source_ibis_calculated_table()
@@ -1269,6 +1309,13 @@ class ConfigManager(object):
1269
1309
  casefold_target_columns, col_list
1270
1310
  )
1271
1311
 
1312
+ (
1313
+ casefold_source_columns,
1314
+ casefold_target_columns,
1315
+ ) = self._filter_columns_by_data_type(
1316
+ casefold_source_columns, casefold_target_columns
1317
+ )
1318
+
1272
1319
  column_aliases = {}
1273
1320
  col_names = []
1274
1321
  for i, calc in enumerate(self._get_order_of_operations(calc_type)):
@@ -1325,6 +1372,9 @@ class ConfigManager(object):
1325
1372
  casefold_source_columns = self._filter_columns_by_column_list(
1326
1373
  casefold_source_columns, col_list, exclude_cols=exclude_cols
1327
1374
  )
1375
+ casefold_source_columns, _ = self._filter_columns_by_data_type(
1376
+ casefold_source_columns, casefold_source_columns
1377
+ )
1328
1378
  return casefold_source_columns
1329
1379
 
1330
1380
  def auto_list_primary_keys(self) -> list:
@@ -16,9 +16,11 @@ import json
16
16
  import logging
17
17
  import warnings
18
18
  from concurrent.futures import ThreadPoolExecutor
19
- import ibis.backends.pandas
20
- import pandas
21
19
  import uuid
20
+ from typing import TYPE_CHECKING
21
+
22
+ import ibis
23
+ import pandas
22
24
 
23
25
  from data_validation import combiner, consts, metadata, util
24
26
  from data_validation.config_manager import ConfigManager
@@ -26,6 +28,9 @@ from data_validation.query_builder.random_row_builder import RandomRowBuilder
26
28
  from data_validation.schema_validation import SchemaValidation
27
29
  from data_validation.validation_builder import ValidationBuilder
28
30
 
31
+ if TYPE_CHECKING:
32
+ from ibis.backends.base import BaseBackend
33
+
29
34
  """ The DataValidation class is where the code becomes source/target aware
30
35
 
31
36
  The class builds specific source and target clients and is likely where someone would go to
@@ -43,8 +48,8 @@ class DataValidation(object):
43
48
  schema_validator=None,
44
49
  result_handler=None,
45
50
  verbose=False,
46
- source_client: ibis.backends.base.BaseBackend = None,
47
- target_client: ibis.backends.base.BaseBackend = None,
51
+ cached_source_client: "BaseBackend" = None,
52
+ cached_target_client: "BaseBackend" = None,
48
53
  ):
49
54
  """Initialize a DataValidation client
50
55
 
@@ -54,19 +59,21 @@ class DataValidation(object):
54
59
  schema_validator (SchemaValidation): Optional instance of a SchemaValidation.
55
60
  result_handler (ResultHandler): Optional instance of as ResultHandler client.
56
61
  verbose (bool): If verbose, the Data Validation client will print the queries run.
57
- source_client: Optional client to avoid unnecessary connections,
58
- target_client: Optional client to avoid unnecessary connections,
62
+ cached_source_client: Optional client to avoid unnecessary connections,
63
+ cached_target_client: Optional client to avoid unnecessary connections,
59
64
  """
60
65
  self.verbose = verbose
61
- self._fresh_connections = not bool(source_client and target_client)
66
+ self._fresh_connections = not bool(
67
+ cached_source_client and cached_target_client
68
+ )
62
69
 
63
70
  # Data Client Management
64
71
  self.config = config
65
72
 
66
73
  self.config_manager = ConfigManager(
67
74
  config,
68
- source_client=source_client,
69
- target_client=target_client,
75
+ source_client=cached_source_client,
76
+ target_client=cached_target_client,
70
77
  verbose=self.verbose,
71
78
  )
72
79
 
@@ -60,15 +60,9 @@ def _compare_match_tables(source_table_map, target_table_map, score_cutoff=0.8)
60
60
  return table_configs
61
61
 
62
62
 
63
- def _get_table_map(
64
- client: "ibis.backends.base.BaseBackend", allowed_schemas=None, include_views=False
65
- ):
66
- """Return dict with searchable keys for table matching."""
63
+ def _get_table_map_from_obj_list(table_objs: list) -> dict:
64
+ """Convert list of schema, table tuples into dict with searchable keys for table matching."""
67
65
  table_map = {}
68
- table_objs = clients.get_all_tables(
69
- client, allowed_schemas=allowed_schemas, tables_only=(not include_views)
70
- )
71
-
72
66
  for table_obj in table_objs:
73
67
  table_key = ".".join([t for t in table_obj if t])
74
68
  table_map[table_key] = {
@@ -76,9 +70,25 @@ def _get_table_map(
76
70
  consts.CONFIG_TABLE_NAME: table_obj[1],
77
71
  }
78
72
 
73
+ # Post process table_map and lower case table_keys, if possible.
74
+ for table_key in [_ for _ in table_map if _ != _.casefold()]:
75
+ if table_key.casefold() not in table_map:
76
+ # Only lower case the key if there isn't one already.
77
+ table_map[table_key.casefold()] = table_map.pop(table_key)
78
+
79
79
  return table_map
80
80
 
81
81
 
82
+ def _get_table_map(
83
+ client: "ibis.backends.base.BaseBackend", allowed_schemas=None, include_views=False
84
+ ) -> dict:
85
+ """Return dict with searchable keys for table matching."""
86
+ table_objs = clients.get_all_tables(
87
+ client, allowed_schemas=allowed_schemas, tables_only=(not include_views)
88
+ )
89
+ return _get_table_map_from_obj_list(table_objs)
90
+
91
+
82
92
  def get_mapped_table_configs(
83
93
  source_client: "ibis.backends.base.BaseBackend",
84
94
  target_client: "ibis.backends.base.BaseBackend",
@@ -18,8 +18,6 @@ import pandas
18
18
  import logging
19
19
  import re
20
20
  import datetime
21
- import hashlib
22
- import base64
23
21
  from typing import List, Dict, TYPE_CHECKING
24
22
 
25
23
  from data_validation import cli_tools, consts, util
@@ -30,7 +28,8 @@ from data_validation.validation_builder import list_to_sublists
30
28
 
31
29
  if TYPE_CHECKING:
32
30
  from argparse import Namespace
33
- import ibis.expr.types.Table
31
+ from ibis.backends.base import BaseBackend
32
+ from ibis.expr.types.relations import Table as IbisTable
34
33
 
35
34
 
36
35
  class PartitionBuilder:
@@ -40,6 +39,12 @@ class PartitionBuilder:
40
39
  self.args = args
41
40
  self.config_dir = self._get_arg_config_dir()
42
41
 
42
+ @staticmethod
43
+ def _definitely_no_time_part(value: datetime.datetime) -> bool:
44
+ """Oracle date has a time portion - this function ensures we don't truncate the time when we really want it."""
45
+ # The function name is a bit of a misnomer, it's checking if there is a time part.
46
+ return value.hour + value.minute + value.second + value.microsecond == 0
47
+
43
48
  def _get_arg_config_dir(self) -> str:
44
49
  """Return String yaml config folder path."""
45
50
  if not self.args.config_dir:
@@ -101,7 +106,7 @@ class PartitionBuilder:
101
106
  self._store_partitions(yaml_configs_list)
102
107
 
103
108
  @staticmethod
104
- def _extract_where(table_expr: "ibis.expr.types.Table") -> str:
109
+ def _extract_where(table_expr: "IbisTable", client: "BaseBackend") -> str:
105
110
  """Given a ibis table expression with a filter (i.e. WHERE) clause, this function extracts the
106
111
  where clause in plain text.
107
112
 
@@ -111,8 +116,9 @@ class PartitionBuilder:
111
116
  # This extraction of the where clause is a bit of a hack. To extract it correctly, the SQL table
112
117
  # expression should be correctly parsed and the where clause extracted. Perhaps use something like Sqlglot.
113
118
  sql_where_expr = re.split(
114
- r"\sWHERE\s", util.ibis_table_to_sql(table_expr), flags=re.I
119
+ r"\sWHERE\s", util.ibis_table_to_sql(table_expr, client), flags=re.I
115
120
  )[-1]
121
+
116
122
  sql_string_re = re.compile(r"'(?:''|\\'|[^'])*'")
117
123
  sql_not_string_re = re.compile(r"[^']+")
118
124
  sql_where_less_ws = ""
@@ -253,7 +259,6 @@ class PartitionBuilder:
253
259
  # Up until this point, we have built the table expression, have not executed the query yet.
254
260
  # The query is now executed to find the first element of each partition
255
261
  first_elements = first_keys_table.execute().to_numpy()
256
-
257
262
  # The objective is to generate the SQL expression string that is saved in the yaml file as a
258
263
  # filters property. This SQL expression is used as a filter during validation to ensure
259
264
  # that the yaml file is only validating the specific partition. This string is backend specific as
@@ -280,13 +285,14 @@ class PartitionBuilder:
280
285
  values[0].date()
281
286
  if key_column.type().is_date()
282
287
  and isinstance(values[0], datetime.datetime)
288
+ and self._definitely_no_time_part(values[0])
283
289
  else values[0]
284
290
  )
285
291
  if len(keys) == 1:
286
- return key_column < value
292
+ return key_column < ibis.literal(value)
287
293
  else:
288
- return (key_column < value) | (
289
- (key_column == value)
294
+ return (key_column < ibis.literal(value)) | (
295
+ (key_column == ibis.literal(value))
290
296
  & less_than_value(table, keys[1:], values[1:])
291
297
  )
292
298
 
@@ -297,14 +303,16 @@ class PartitionBuilder:
297
303
  values[0].date()
298
304
  if key_column.type().is_date()
299
305
  and isinstance(values[0], datetime.datetime)
306
+ and self._definitely_no_time_part(values[0])
300
307
  else values[0]
301
308
  )
302
309
 
303
310
  if len(keys) == 1:
304
- return key_column >= value
311
+ return key_column >= ibis.literal(value)
305
312
  else:
306
- return (key_column > value) | (
307
- (key_column == value) & geq_value(table, keys[1:], values[1:])
313
+ return (key_column > ibis.literal(value)) | (
314
+ (key_column == ibis.literal(value))
315
+ & geq_value(table, keys[1:], values[1:])
308
316
  )
309
317
 
310
318
  filter_source_clause = less_than_value(
@@ -320,11 +328,13 @@ class PartitionBuilder:
320
328
  source_where_list.append(
321
329
  self._extract_where(
322
330
  source_table.filter(filter_source_clause),
331
+ config_manager.source_client,
323
332
  )
324
333
  )
325
334
  target_where_list.append(
326
335
  self._extract_where(
327
336
  target_table.filter(filter_target_clause),
337
+ config_manager.target_client,
328
338
  )
329
339
  )
330
340
 
@@ -350,11 +360,13 @@ class PartitionBuilder:
350
360
  source_where_list.append(
351
361
  self._extract_where(
352
362
  source_table.filter(filter_source_clause),
363
+ config_manager.source_client,
353
364
  )
354
365
  )
355
366
  target_where_list.append(
356
367
  self._extract_where(
357
368
  target_table.filter(filter_target_clause),
369
+ config_manager.target_client,
358
370
  )
359
371
  )
360
372
  filter_source_clause = geq_value(
@@ -370,11 +382,13 @@ class PartitionBuilder:
370
382
  source_where_list.append(
371
383
  self._extract_where(
372
384
  source_table.filter(filter_source_clause),
385
+ config_manager.source_client,
373
386
  )
374
387
  )
375
388
  target_where_list.append(
376
389
  self._extract_where(
377
390
  target_table.filter(filter_target_clause),
391
+ config_manager.target_client,
378
392
  )
379
393
  )
380
394
  master_filter_list.append([source_where_list, target_where_list])
@@ -409,14 +423,7 @@ class PartitionBuilder:
409
423
  if config_manager.source_table:
410
424
  dir_name = config_manager.full_source_table
411
425
  else:
412
- dir_name = (
413
- "custom."
414
- + base64.b64encode(
415
- hashlib.sha256(
416
- config_manager.source_query.encode("utf-8")
417
- ).digest()
418
- ).decode("ascii")[:5]
419
- )
426
+ dir_name = ""
420
427
  filter_list = partition_filters[ind]
421
428
 
422
429
  if src_config_dict.get(dir_name) is None:
@@ -13,7 +13,7 @@
13
13
  # limitations under the License.
14
14
 
15
15
  import logging
16
- from typing import TYPE_CHECKING
16
+ from typing import Optional, TYPE_CHECKING
17
17
 
18
18
  import ibis
19
19
 
@@ -58,7 +58,7 @@ class BaseBackendResultHandler:
58
58
  """Write results of data validation to a backend."""
59
59
 
60
60
  _table_id: str = None
61
- _status_list: list = None
61
+ _status_list: Optional[list] = None
62
62
  _text_format: str = None
63
63
 
64
64
  def _filter_by_status_list(self, result_df: "DataFrame") -> "DataFrame":
@@ -15,6 +15,10 @@
15
15
  """Output validation report to BigQuery tables"""
16
16
 
17
17
  import logging
18
+ from typing import Optional
19
+
20
+ import google.oauth2.service_account
21
+
18
22
 
19
23
  from data_validation import clients, consts, exceptions, util
20
24
  from data_validation.result_handlers.base_backend import BaseBackendResultHandler
@@ -24,6 +28,14 @@ BQRH_WRITE_MESSAGE = "Results written to BigQuery"
24
28
  BQRH_NO_WRITE_MESSAGE = "No results to write to BigQuery"
25
29
 
26
30
 
31
+ def credentials_from_key_path(sa_key_path):
32
+ if not sa_key_path:
33
+ return None
34
+ return google.oauth2.service_account.Credentials.from_service_account_file(
35
+ sa_key_path
36
+ )
37
+
38
+
27
39
  class BigQueryResultHandler(BaseBackendResultHandler):
28
40
  """Write results of data validation to BigQuery.
29
41
 
@@ -38,7 +50,7 @@ class BigQueryResultHandler(BaseBackendResultHandler):
38
50
  def __init__(
39
51
  self,
40
52
  bigquery_client,
41
- status_list: list = None,
53
+ status_list: Optional[list] = None,
42
54
  table_id: str = "pso_data_validator.results",
43
55
  text_format: str = consts.FORMAT_TYPE_TABLE,
44
56
  ):
@@ -50,10 +62,10 @@ class BigQueryResultHandler(BaseBackendResultHandler):
50
62
  @staticmethod
51
63
  def get_handler_for_project(
52
64
  project_id,
53
- status_list=None,
65
+ status_list: Optional[list] = None,
54
66
  table_id: str = "pso_data_validator.results",
55
- credentials=None,
56
- api_endpoint: str = None,
67
+ sa_key_path: Optional[str] = None,
68
+ api_endpoint: Optional[str] = None,
57
69
  text_format: str = consts.FORMAT_TYPE_TABLE,
58
70
  ):
59
71
  """Return BigQueryResultHandler instance for given project.
@@ -70,6 +82,39 @@ class BigQueryResultHandler(BaseBackendResultHandler):
70
82
  This allows the user to influence the text results written via logger.debug.
71
83
  See: https://github.com/GoogleCloudPlatform/professional-services-data-validator/issues/871
72
84
  """
85
+
86
+ credentials = credentials_from_key_path(sa_key_path)
87
+ client = clients.get_google_bigquery_client(
88
+ project_id, credentials=credentials, api_endpoint=api_endpoint
89
+ )
90
+ return BigQueryResultHandler(
91
+ client,
92
+ status_list=status_list,
93
+ table_id=table_id,
94
+ text_format=text_format,
95
+ )
96
+
97
+ @staticmethod
98
+ def get_handler_for_connection(
99
+ connection_config: dict,
100
+ status_list: Optional[list] = None,
101
+ table_id: str = "pso_data_validator.results",
102
+ text_format: str = consts.FORMAT_TYPE_TABLE,
103
+ ):
104
+ """Return BigQueryResultHandler instance for given connection config.
105
+
106
+ Args:
107
+ table_id (str): Table ID used for validation results.
108
+ status_list (list): provided status to filter the results with
109
+ text_format (str, optional):
110
+ This allows the user to influence the text results written via logger.debug.
111
+ See: https://github.com/GoogleCloudPlatform/professional-services-data-validator/issues/871
112
+ """
113
+ project_id = connection_config[consts.PROJECT_ID]
114
+ credentials = credentials_from_key_path(
115
+ connection_config.get(consts.GOOGLE_SERVICE_ACCOUNT_KEY_PATH)
116
+ )
117
+ api_endpoint = connection_config.get(consts.API_ENDPOINT)
73
118
  client = clients.get_google_bigquery_client(
74
119
  project_id, credentials=credentials, api_endpoint=api_endpoint
75
120
  )