google-pso-data-validator 8.2.0__tar.gz → 8.4.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 (98) hide show
  1. {google_pso_data_validator-8.2.0/google_pso_data_validator.egg-info → google_pso_data_validator-8.4.0}/PKG-INFO +12 -11
  2. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/README.md +5 -5
  3. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/data_validation/__main__.py +7 -4
  4. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/data_validation/cli_tools.py +33 -24
  5. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/data_validation/clients.py +42 -13
  6. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/data_validation/config_manager.py +55 -5
  7. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/data_validation/consts.py +5 -0
  8. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/data_validation/data_validation.py +16 -9
  9. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/data_validation/exceptions.py +4 -0
  10. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/data_validation/find_tables.py +18 -8
  11. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/data_validation/partition_builder.py +37 -21
  12. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/data_validation/query_builder/random_row_builder.py +2 -1
  13. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/data_validation/result_handlers/base_backend.py +2 -2
  14. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/data_validation/result_handlers/bigquery.py +49 -4
  15. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/data_validation/result_handlers/factory.py +34 -29
  16. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/data_validation/result_handlers/postgres.py +3 -3
  17. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/data_validation/state_manager.py +1 -4
  18. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/data_validation/util.py +18 -19
  19. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0/google_pso_data_validator.egg-info}/PKG-INFO +12 -11
  20. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/google_pso_data_validator.egg-info/SOURCES.txt +5 -0
  21. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/google_pso_data_validator.egg-info/requires.txt +4 -3
  22. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/setup.py +7 -6
  23. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/third_party/ibis/ibis_addon/operations.py +15 -0
  24. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/third_party/ibis/ibis_bigquery/__init__.py +20 -10
  25. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/third_party/ibis/ibis_mssql/__init__.py +9 -4
  26. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/third_party/ibis/ibis_mssql/registry.py +1 -1
  27. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/third_party/ibis/ibis_oracle/datatypes.py +1 -1
  28. google_pso_data_validator-8.4.0/third_party/ibis/ibis_sybase/__init__.py +186 -0
  29. google_pso_data_validator-8.4.0/third_party/ibis/ibis_sybase/api.py +42 -0
  30. google_pso_data_validator-8.4.0/third_party/ibis/ibis_sybase/compiler.py +101 -0
  31. google_pso_data_validator-8.4.0/third_party/ibis/ibis_sybase/datatypes.py +96 -0
  32. google_pso_data_validator-8.4.0/third_party/ibis/ibis_sybase/registry.py +209 -0
  33. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/LICENSE +0 -0
  34. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/data_validation/__init__.py +0 -0
  35. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/data_validation/app.py +0 -0
  36. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/data_validation/client_info.py +0 -0
  37. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/data_validation/combiner.py +0 -0
  38. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/data_validation/gcs_helper.py +0 -0
  39. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/data_validation/jellyfish_distance.py +0 -0
  40. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/data_validation/metadata.py +0 -0
  41. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/data_validation/query_builder/__init__.py +0 -0
  42. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/data_validation/query_builder/partition_row_builder.py +0 -0
  43. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/data_validation/query_builder/query_builder.py +0 -0
  44. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/data_validation/raw_query.py +0 -0
  45. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/data_validation/result_handlers/__init__.py +0 -0
  46. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/data_validation/result_handlers/text.py +0 -0
  47. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/data_validation/schema_validation.py +0 -0
  48. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/data_validation/secret_manager.py +0 -0
  49. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/data_validation/validation_builder.py +0 -0
  50. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/google_pso_data_validator.egg-info/dependency_links.txt +0 -0
  51. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/google_pso_data_validator.egg-info/entry_points.txt +0 -0
  52. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/google_pso_data_validator.egg-info/top_level.txt +0 -0
  53. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/setup.cfg +0 -0
  54. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/third_party/ibis/ibis_addon/__init__.py +0 -0
  55. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/third_party/ibis/ibis_addon/api.py +0 -0
  56. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/third_party/ibis/ibis_bigquery/api.py +0 -0
  57. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/third_party/ibis/ibis_bigquery/registry.py +0 -0
  58. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/third_party/ibis/ibis_cloud_spanner/__init__.py +0 -0
  59. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/third_party/ibis/ibis_cloud_spanner/api.py +0 -0
  60. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/third_party/ibis/ibis_cloud_spanner/client.py +0 -0
  61. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/third_party/ibis/ibis_cloud_spanner/compiler.py +0 -0
  62. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/third_party/ibis/ibis_cloud_spanner/datatypes.py +0 -0
  63. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/third_party/ibis/ibis_cloud_spanner/registry.py +0 -0
  64. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/third_party/ibis/ibis_cloud_spanner/tests/__init__.py +0 -0
  65. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/third_party/ibis/ibis_cloud_spanner/tests/conftest.py +0 -0
  66. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/third_party/ibis/ibis_cloud_spanner/to_pandas.py +0 -0
  67. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/third_party/ibis/ibis_db2/__init__.py +0 -0
  68. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/third_party/ibis/ibis_db2/api.py +0 -0
  69. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/third_party/ibis/ibis_db2/compiler.py +0 -0
  70. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/third_party/ibis/ibis_db2/datatypes.py +0 -0
  71. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/third_party/ibis/ibis_db2/registry.py +0 -0
  72. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/third_party/ibis/ibis_impala/__init__.py +0 -0
  73. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/third_party/ibis/ibis_impala/api.py +0 -0
  74. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/third_party/ibis/ibis_mssql/api.py +0 -0
  75. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/third_party/ibis/ibis_mssql/datatypes.py +0 -0
  76. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/third_party/ibis/ibis_mysql/__init__.py +0 -0
  77. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/third_party/ibis/ibis_mysql/base_sql_compiler/__init__.py +0 -0
  78. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/third_party/ibis/ibis_mysql/base_sql_compiler/select_builder.py +0 -0
  79. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/third_party/ibis/ibis_mysql/compiler.py +0 -0
  80. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/third_party/ibis/ibis_oracle/__init__.py +0 -0
  81. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/third_party/ibis/ibis_oracle/api.py +0 -0
  82. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/third_party/ibis/ibis_oracle/compiler.py +0 -0
  83. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/third_party/ibis/ibis_oracle/registry.py +0 -0
  84. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/third_party/ibis/ibis_postgres/__init__.py +0 -0
  85. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/third_party/ibis/ibis_postgres/client.py +0 -0
  86. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/third_party/ibis/ibis_postgres/datatypes.py +0 -0
  87. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/third_party/ibis/ibis_postgres/registry.py +0 -0
  88. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/third_party/ibis/ibis_redshift/__init__.py +0 -0
  89. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/third_party/ibis/ibis_redshift/api.py +0 -0
  90. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/third_party/ibis/ibis_redshift/compiler.py +0 -0
  91. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/third_party/ibis/ibis_snowflake/__init__.py +0 -0
  92. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/third_party/ibis/ibis_snowflake/api.py +0 -0
  93. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/third_party/ibis/ibis_snowflake/datatypes.py +0 -0
  94. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/third_party/ibis/ibis_teradata/__init__.py +0 -0
  95. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/third_party/ibis/ibis_teradata/api.py +0 -0
  96. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/third_party/ibis/ibis_teradata/compiler.py +0 -0
  97. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.0}/third_party/ibis/ibis_teradata/datatypes.py +0 -0
  98. {google_pso_data_validator-8.2.0 → google_pso_data_validator-8.4.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.4.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,10 +25,10 @@ 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
- Requires-Dist: pyarrow==14.0.1
31
+ Requires-Dist: pyarrow
32
32
  Requires-Dist: pydata-google-auth>=1.8.2
33
33
  Requires-Dist: PyMySQL>=1.1.1
34
34
  Requires-Dist: PyYAML>=6.0.2
@@ -45,6 +45,7 @@ Requires-Dist: freezegun; extra == "develop"
45
45
  Requires-Dist: pyfakefs; extra == "develop"
46
46
  Requires-Dist: pytest; extra == "develop"
47
47
  Requires-Dist: pytest-cov; extra == "develop"
48
+ Requires-Dist: pytest-rerunfailures; extra == "develop"
48
49
  Requires-Dist: pytest-timeout; extra == "develop"
49
50
  Dynamic: author
50
51
  Dynamic: author-email
@@ -92,8 +93,9 @@ DVT supports the following connection types:
92
93
  * [Postgres](https://github.com/GoogleCloudPlatform/professional-services-data-validator/blob/develop/docs/connections.md#postgres)
93
94
  * [Redshift](https://github.com/GoogleCloudPlatform/professional-services-data-validator/blob/develop/docs/connections.md#redshift)
94
95
  * [Spanner](https://github.com/GoogleCloudPlatform/professional-services-data-validator/blob/develop/docs/connections.md#google-spanner)
95
- * [Teradata](https://github.com/GoogleCloudPlatform/professional-services-data-validator/blob/develop/docs/connections.md#teradata)
96
96
  * [Snowflake](https://github.com/GoogleCloudPlatform/professional-services-data-validator/blob/develop/docs/connections.md#snowflake)
97
+ * [Sybase ASE](https://github.com/GoogleCloudPlatform/professional-services-data-validator/blob/develop/docs/connections.md#sybase)
98
+ * [Teradata](https://github.com/GoogleCloudPlatform/professional-services-data-validator/blob/develop/docs/connections.md#teradata)
97
99
 
98
100
  The [Connections](https://github.com/GoogleCloudPlatform/professional-services-data-validator/blob/develop/docs/connections.md) page provides details about how to create
99
101
  and list connections for the validation tool.
@@ -340,7 +342,7 @@ data-validation
340
342
  Directory Path to store YAML Config Files
341
343
  GCS: Provide a full gs:// path of the target directory. Eg: `gs://<BUCKET>/partitions_dir`
342
344
  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>`
345
+ 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
346
  --partition-num INT, -pn INT
345
347
  Number of partitions into which the table should be split, e.g. 1000 or 10000
346
348
  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 +655,8 @@ The `--config-dir` flag will specify the directory with the YAML files to be exe
653
655
 
654
656
  ### Validation Reports
655
657
 
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).
658
+ 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).
659
+
659
660
 
660
661
  To output to BigQuery or PostgreSQL, simply include the `-rh` flag during a validation run including
661
662
  the schema and table name for the results.
@@ -33,8 +33,9 @@ DVT supports the following connection types:
33
33
  * [Postgres](https://github.com/GoogleCloudPlatform/professional-services-data-validator/blob/develop/docs/connections.md#postgres)
34
34
  * [Redshift](https://github.com/GoogleCloudPlatform/professional-services-data-validator/blob/develop/docs/connections.md#redshift)
35
35
  * [Spanner](https://github.com/GoogleCloudPlatform/professional-services-data-validator/blob/develop/docs/connections.md#google-spanner)
36
- * [Teradata](https://github.com/GoogleCloudPlatform/professional-services-data-validator/blob/develop/docs/connections.md#teradata)
37
36
  * [Snowflake](https://github.com/GoogleCloudPlatform/professional-services-data-validator/blob/develop/docs/connections.md#snowflake)
37
+ * [Sybase ASE](https://github.com/GoogleCloudPlatform/professional-services-data-validator/blob/develop/docs/connections.md#sybase)
38
+ * [Teradata](https://github.com/GoogleCloudPlatform/professional-services-data-validator/blob/develop/docs/connections.md#teradata)
38
39
 
39
40
  The [Connections](https://github.com/GoogleCloudPlatform/professional-services-data-validator/blob/develop/docs/connections.md) page provides details about how to create
40
41
  and list connections for the validation tool.
@@ -281,7 +282,7 @@ data-validation
281
282
  Directory Path to store YAML Config Files
282
283
  GCS: Provide a full gs:// path of the target directory. Eg: `gs://<BUCKET>/partitions_dir`
283
284
  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>`
285
+ 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
286
  --partition-num INT, -pn INT
286
287
  Number of partitions into which the table should be split, e.g. 1000 or 10000
287
288
  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 +595,8 @@ The `--config-dir` flag will specify the directory with the YAML files to be exe
594
595
 
595
596
  ### Validation Reports
596
597
 
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).
598
+ 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).
599
+
600
600
 
601
601
  To output to BigQuery or PostgreSQL, simply include the `-rh` flag during a validation run including
602
602
  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,
@@ -594,6 +596,7 @@ def partition_and_store_config_files(args: "Namespace") -> None:
594
596
  setattr(args, "custom_query_type", "row")
595
597
  config_managers = build_config_managers_from_args(args, consts.CUSTOM_QUERY)
596
598
  partition_builder = PartitionBuilder(config_managers, args)
599
+ partition_builder.check_partition_configs()
597
600
  partition_builder.partition_configs()
598
601
 
599
602
 
@@ -73,6 +73,10 @@ if TYPE_CHECKING:
73
73
  CONNECTION_SOURCE_FIELDS = {
74
74
  consts.SOURCE_TYPE_BIGQUERY: [
75
75
  ["project_id", "GCP Project to use for BigQuery"],
76
+ [
77
+ "client_project_id",
78
+ "(Optional) BigQuery job/billing project (can differ from data project)",
79
+ ],
76
80
  ["google_service_account_key_path", "(Optional) GCP SA Key Path"],
77
81
  [
78
82
  "api_endpoint",
@@ -84,7 +88,7 @@ CONNECTION_SOURCE_FIELDS = {
84
88
  ],
85
89
  ],
86
90
  consts.SOURCE_TYPE_TERADATA: [
87
- ["host", "Desired Teradata host"],
91
+ ["host", "Teradata host"],
88
92
  ["port", "Teradata port to connect on"],
89
93
  ["user_name", "User used to connect"],
90
94
  ["password", "Password for supplied user"],
@@ -93,7 +97,7 @@ CONNECTION_SOURCE_FIELDS = {
93
97
  ["json_params", "(Optional) Additional teradatasql JSON string parameters"],
94
98
  ],
95
99
  consts.SOURCE_TYPE_ORACLE: [
96
- ["host", "Desired Oracle host"],
100
+ ["host", "Oracle host"],
97
101
  ["port", "Oracle port to connect on"],
98
102
  ["user", "User used to connect"],
99
103
  ["thick_mode", "Flag indicating thick_mode"],
@@ -103,7 +107,7 @@ CONNECTION_SOURCE_FIELDS = {
103
107
  ["connect_args", "(Optional) Additional connection argument mapping"],
104
108
  ],
105
109
  consts.SOURCE_TYPE_MSSQL: [
106
- ["host", "Desired SQL Server host (default localhost)"],
110
+ ["host", "SQL Server host (default localhost)"],
107
111
  ["port", "SQL Server port to connect on (default 1433)"],
108
112
  ["user", "User used to connect"],
109
113
  ["password", "Password for supplied user"],
@@ -112,7 +116,7 @@ CONNECTION_SOURCE_FIELDS = {
112
116
  ["url", "SQL Server SQLAlchemy connection URL"],
113
117
  ],
114
118
  consts.SOURCE_TYPE_MYSQL: [
115
- ["host", "Desired MySQL host (default localhost)"],
119
+ ["host", "MySQL host (default localhost)"],
116
120
  ["port", "MySQL port to connect on (default 3306)"],
117
121
  ["user", "User used to connect"],
118
122
  ["password", "Password for supplied user"],
@@ -126,14 +130,14 @@ CONNECTION_SOURCE_FIELDS = {
126
130
  ["connect_args", "(Optional) Additional connection argument mapping"],
127
131
  ],
128
132
  consts.SOURCE_TYPE_POSTGRES: [
129
- ["host", "Desired PostgreSQL host."],
133
+ ["host", "PostgreSQL host."],
130
134
  ["port", "PostgreSQL port to connect on (e.g. 5432)"],
131
135
  ["user", "Username to connect to"],
132
136
  ["password", "Password for authentication of user"],
133
137
  ["database", "Database in PostgreSQL to connect to (default postgres)"],
134
138
  ],
135
139
  consts.SOURCE_TYPE_REDSHIFT: [
136
- ["host", "Desired Redshift host."],
140
+ ["host", "Redshift host."],
137
141
  ["port", "Redshift port to connect on (e.g. 5439)"],
138
142
  ["user", "Username to connect to"],
139
143
  ["password", "Password for authentication of user"],
@@ -149,16 +153,26 @@ CONNECTION_SOURCE_FIELDS = {
149
153
  '(Optional) GCP Spanner API endpoint (e.g. "https://spanner-mypsc.p.googleapis.com")',
150
154
  ],
151
155
  ],
156
+ consts.SOURCE_TYPE_SYBASE: [
157
+ ["host", "Sybase host (default localhost)"],
158
+ ["port", "Sybase port to connect on (default 5000)"],
159
+ ["user", "User used to connect"],
160
+ ["password", "Password for supplied user"],
161
+ ["database", "Database to connect to (default master)"],
162
+ ["odbc_driver", "ODBC driver name"],
163
+ ["query", "Connection query parameters"],
164
+ ["url", "Sybase SQLAlchemy connection URL"],
165
+ ],
152
166
  consts.SOURCE_TYPE_FILESYSTEM: [
153
167
  ["table_name", "Table name to use as reference for file data"],
154
168
  ["file_path", "The local, s3, or GCS file path to the data"],
155
169
  ["file_type", "The file type of the file. 'csv', 'orc', 'parquet' or 'json'"],
156
170
  ],
157
171
  consts.SOURCE_TYPE_IMPALA: [
158
- ["host", "Desired Impala host"],
159
- ["port", "Desired Impala port (10000 if not provided)"],
160
- ["database", "Desired Impala database (default if not provided)"],
161
- ["auth_mechanism", "Desired Impala auth mechanism (PLAIN if not provided)"],
172
+ ["host", "Impala host"],
173
+ ["port", "Impala port (10000 if not provided)"],
174
+ ["database", "Impala database (default if not provided)"],
175
+ ["auth_mechanism", "Impala auth mechanism (PLAIN if not provided)"],
162
176
  [
163
177
  "kerberos_service_name",
164
178
  "Desired Kerberos service name ('impala' if not provided)",
@@ -183,8 +197,8 @@ CONNECTION_SOURCE_FIELDS = {
183
197
  ["http_path", "URL path of HTTP proxy"],
184
198
  ],
185
199
  consts.SOURCE_TYPE_DB2: [
186
- ["host", "Desired DB2 host"],
187
- ["port", "Desired DB2 port (50000 if not provided)"],
200
+ ["host", "DB2 host"],
201
+ ["port", "DB2 port (50000 if not provided)"],
188
202
  ["user", "Username to connect to"],
189
203
  ["password", "Password for authentication of user"],
190
204
  ["database", "Database in DB2 to connect to"],
@@ -1308,21 +1322,16 @@ def _get_result_handler(rc_value: str, sa_file=None) -> dict:
1308
1322
  if config[0] in connections:
1309
1323
  # We received connection_name.results_table.
1310
1324
  conn_from_file = get_connection(config[0])
1311
- if conn_from_file[consts.SOURCE_TYPE] == consts.SOURCE_TYPE_BIGQUERY:
1312
- result_handler = {
1313
- consts.RH_TYPE: conn_from_file[consts.SOURCE_TYPE],
1314
- consts.PROJECT_ID: conn_from_file["project_id"],
1315
- 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:
1325
+ if conn_from_file[consts.SOURCE_TYPE] in [
1326
+ consts.SOURCE_TYPE_BIGQUERY,
1327
+ consts.SOURCE_TYPE_POSTGRES,
1328
+ ]:
1322
1329
  result_handler = {
1323
1330
  consts.RH_TYPE: conn_from_file[consts.SOURCE_TYPE],
1324
1331
  consts.TABLE_ID: config[1],
1325
- consts.RH_CONN: conn_from_file,
1332
+ # Only store the connection name in the result handler to avoid accidentally
1333
+ # storing credentials in config files.
1334
+ consts.RH_CONN: config[0],
1326
1335
  }
1327
1336
  # TODO Add filesytem handler too.
1328
1337
  else:
@@ -16,7 +16,7 @@
16
16
  from contextlib import contextmanager
17
17
  import copy
18
18
  import logging
19
- from typing import TYPE_CHECKING
19
+ from typing import TYPE_CHECKING, Optional
20
20
  import warnings
21
21
 
22
22
  import google.oauth2.service_account
@@ -56,6 +56,7 @@ IBIS_ALCHEMY_BACKENDS = [
56
56
  "mssql",
57
57
  "redshift",
58
58
  "snowflake",
59
+ "sybase",
59
60
  ]
60
61
 
61
62
 
@@ -93,19 +94,32 @@ try:
93
94
  except ImportError:
94
95
  db2_connect = _raise_missing_client_error("pip install ibm_db_sa")
95
96
 
97
+ # Sybase requires sqlalchemy_sybase package.
98
+ try:
99
+ from third_party.ibis.ibis_sybase.api import sybase_connect
100
+ except ImportError:
101
+ sybase_connect = _raise_missing_client_error("pip install sqlalchemy_sybase")
102
+
96
103
 
97
104
  def get_google_bigquery_client(
98
- project_id: str, credentials=None, api_endpoint: str = None
105
+ project_id: str,
106
+ credentials=None,
107
+ api_endpoint: Optional[str] = None,
108
+ quota_project_id: Optional[str] = None,
99
109
  ):
100
110
  info = client_info.get_http_client_info()
101
111
  job_config = bigquery.QueryJobConfig(
102
112
  connection_properties=[bigquery.ConnectionProperty("time_zone", "UTC")]
103
113
  )
114
+ effective_project = quota_project_id or project_id
104
115
  options = None
105
- if api_endpoint:
106
- options = client_options.ClientOptions(api_endpoint=api_endpoint)
116
+ if api_endpoint or quota_project_id:
117
+ options = client_options.ClientOptions(
118
+ api_endpoint=api_endpoint,
119
+ quota_project_id=quota_project_id if quota_project_id else None,
120
+ )
107
121
  return bigquery.Client(
108
- project=project_id,
122
+ project=effective_project,
109
123
  client_info=info,
110
124
  credentials=credentials,
111
125
  default_query_job_config=job_config,
@@ -113,10 +127,17 @@ def get_google_bigquery_client(
113
127
  )
114
128
 
115
129
 
116
- def _get_google_bqstorage_client(credentials=None, api_endpoint: str = None):
130
+ def _get_google_bqstorage_client(
131
+ credentials=None,
132
+ api_endpoint: Optional[str] = None,
133
+ quota_project_id: Optional[str] = None,
134
+ ):
117
135
  options = None
118
- if api_endpoint:
119
- options = client_options.ClientOptions(api_endpoint=api_endpoint)
136
+ if api_endpoint or quota_project_id:
137
+ options = client_options.ClientOptions(
138
+ api_endpoint=api_endpoint,
139
+ quota_project_id=quota_project_id if quota_project_id else None,
140
+ )
120
141
  from google.cloud import bigquery_storage_v1 as bigquery_storage
121
142
 
122
143
  return bigquery_storage.BigQueryReadClient(
@@ -129,20 +150,26 @@ def get_bigquery_client(
129
150
  project_id: str,
130
151
  dataset_id: str = "",
131
152
  credentials=None,
132
- api_endpoint: str = None,
133
- storage_api_endpoint: str = None,
153
+ api_endpoint: Optional[str] = None,
154
+ storage_api_endpoint: Optional[str] = None,
155
+ client_project_id: Optional[str] = None,
134
156
  ):
135
157
  google_client = get_google_bigquery_client(
136
- project_id, credentials=credentials, api_endpoint=api_endpoint
158
+ project_id,
159
+ credentials=credentials,
160
+ api_endpoint=api_endpoint,
161
+ quota_project_id=client_project_id,
137
162
  )
138
163
  bqstorage_client = None
139
164
  if storage_api_endpoint:
140
165
  bqstorage_client = _get_google_bqstorage_client(
141
- credentials=credentials, api_endpoint=storage_api_endpoint
166
+ credentials=credentials,
167
+ api_endpoint=storage_api_endpoint,
168
+ quota_project_id=client_project_id,
142
169
  )
143
170
 
144
171
  return bigquery_connect(
145
- project_id=project_id,
172
+ project_id=project_id or client_project_id,
146
173
  dataset_id=dataset_id,
147
174
  credentials=credentials,
148
175
  bigquery_client=google_client,
@@ -202,6 +229,7 @@ def get_ibis_table(client, schema_name, table_name, database_name=None):
202
229
  "db2",
203
230
  "mssql",
204
231
  "redshift",
232
+ "sybase",
205
233
  ]:
206
234
  return client.table(table_name, database=database_name, schema=schema_name)
207
235
  elif client.name == "pandas":
@@ -400,5 +428,6 @@ CLIENT_LOOKUP = {
400
428
  consts.SOURCE_TYPE_MSSQL: mssql_connect,
401
429
  consts.SOURCE_TYPE_SNOWFLAKE: snowflake_connect,
402
430
  consts.SOURCE_TYPE_SPANNER: spanner_connect,
431
+ consts.SOURCE_TYPE_SYBASE: sybase_connect,
403
432
  consts.SOURCE_TYPE_DB2: db2_connect,
404
433
  }
@@ -71,7 +71,7 @@ class ConfigManager(object):
71
71
  """Return config object."""
72
72
  return self._config
73
73
 
74
- def get_source_connection(self):
74
+ def get_source_connection(self) -> dict:
75
75
  """Return source connection object."""
76
76
  if not self._source_conn:
77
77
  if self._config.get(consts.CONFIG_SOURCE_CONN):
@@ -82,7 +82,7 @@ class ConfigManager(object):
82
82
 
83
83
  return self._source_conn
84
84
 
85
- def get_target_connection(self):
85
+ def get_target_connection(self) -> dict:
86
86
  """Return target connection object."""
87
87
  if not self._target_conn:
88
88
  if self._config.get(consts.CONFIG_TARGET_CONN):
@@ -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:
@@ -139,6 +139,7 @@ SOURCE_TYPE_POSTGRES = "Postgres"
139
139
  SOURCE_TYPE_REDSHIFT = "Redshift"
140
140
  SOURCE_TYPE_SNOWFLAKE = "Snowflake"
141
141
  SOURCE_TYPE_SPANNER = "Spanner"
142
+ SOURCE_TYPE_SYBASE = "Sybase"
142
143
  SOURCE_TYPE_TERADATA = "Teradata"
143
144
 
144
145
  # BigQuery Result Handler Configs
@@ -149,6 +150,7 @@ TABLE_ID = "table_id"
149
150
  GOOGLE_SERVICE_ACCOUNT_KEY_PATH = "google_service_account_key_path"
150
151
  API_ENDPOINT = "api_endpoint"
151
152
  STORAGE_API_ENDPOINT = "storage_api_endpoint"
153
+ CLIENT_PROJECT_ID = "client_project_id"
152
154
 
153
155
  # Result Handler Output Table Fields
154
156
  VALIDATION_TYPE = "validation_type"
@@ -262,3 +264,6 @@ CALC_FIELD_LENGTH = "length"
262
264
  CALC_FIELD_PADDED_CHAR_LENGTH = "padded_char_length"
263
265
  CALC_FIELD_RSTRIP = "rstrip"
264
266
  CALC_FIELD_UPPER = "upper"
267
+
268
+ # Backends that do not support window functions.
269
+ NO_WINDOW_FUNCTION_SUPPORT = [SOURCE_TYPE_SYBASE]
@@ -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
 
@@ -27,3 +27,7 @@ class ResultHandlerException(Exception):
27
27
 
28
28
  class SchemaValidationException(ValidationException):
29
29
  pass
30
+
31
+
32
+ class PartitionBuilderException(ValidationException):
33
+ pass
@@ -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",