google-pso-data-validator 7.4.0__tar.gz → 7.5.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.
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/PKG-INFO +3 -2
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/data_validation/combiner.py +156 -63
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/data_validation/config_manager.py +48 -5
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/data_validation/consts.py +1 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/data_validation/data_validation.py +37 -65
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/data_validation/partition_builder.py +15 -10
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/google_pso_data_validator.egg-info/PKG-INFO +3 -2
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/setup.py +1 -1
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_oracle/__init__.py +35 -1
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_postgres/client.py +51 -1
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_teradata/__init__.py +30 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/LICENSE +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/README.md +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/data_validation/__init__.py +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/data_validation/__main__.py +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/data_validation/app.py +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/data_validation/cli_tools.py +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/data_validation/client_info.py +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/data_validation/clients.py +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/data_validation/exceptions.py +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/data_validation/find_tables.py +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/data_validation/gcs_helper.py +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/data_validation/jellyfish_distance.py +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/data_validation/metadata.py +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/data_validation/query_builder/__init__.py +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/data_validation/query_builder/partition_row_builder.py +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/data_validation/query_builder/query_builder.py +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/data_validation/query_builder/random_row_builder.py +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/data_validation/raw_query.py +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/data_validation/result_handlers/__init__.py +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/data_validation/result_handlers/bigquery.py +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/data_validation/result_handlers/text.py +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/data_validation/schema_validation.py +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/data_validation/secret_manager.py +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/data_validation/state_manager.py +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/data_validation/util.py +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/data_validation/validation_builder.py +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/google_pso_data_validator.egg-info/SOURCES.txt +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/google_pso_data_validator.egg-info/dependency_links.txt +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/google_pso_data_validator.egg-info/entry_points.txt +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/google_pso_data_validator.egg-info/requires.txt +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/google_pso_data_validator.egg-info/top_level.txt +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/setup.cfg +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_addon/__init__.py +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_addon/api.py +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_addon/operations.py +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_biquery/__init__.py +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_biquery/api.py +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_cloud_spanner/__init__.py +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_cloud_spanner/api.py +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_cloud_spanner/client.py +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_cloud_spanner/compiler.py +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_cloud_spanner/datatypes.py +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_cloud_spanner/registry.py +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_cloud_spanner/tests/__init__.py +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_cloud_spanner/tests/conftest.py +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_cloud_spanner/to_pandas.py +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_db2/__init__.py +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_db2/api.py +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_db2/compiler.py +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_db2/datatypes.py +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_db2/registry.py +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_impala/__init__.py +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_impala/api.py +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_mssql/__init__.py +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_mssql/api.py +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_mssql/datatypes.py +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_mssql/registry.py +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_mysql/__init__.py +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_mysql/base_sql_compiler/__init__.py +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_mysql/base_sql_compiler/select_builder.py +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_mysql/compiler.py +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_oracle/api.py +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_oracle/compiler.py +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_oracle/datatypes.py +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_oracle/registry.py +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_postgres/__init__.py +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_postgres/datatypes.py +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_postgres/registry.py +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_redshift/__init__.py +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_redshift/api.py +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_redshift/compiler.py +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_snowflake/__init__.py +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_snowflake/api.py +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_snowflake/datatypes.py +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_teradata/api.py +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_teradata/compiler.py +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_teradata/datatypes.py +0 -0
- {google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_teradata/registry.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: google-pso-data-validator
|
|
3
|
-
Version: 7.
|
|
3
|
+
Version: 7.5.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
|
|
@@ -51,6 +51,7 @@ Dynamic: author-email
|
|
|
51
51
|
Dynamic: classifier
|
|
52
52
|
Dynamic: description
|
|
53
53
|
Dynamic: description-content-type
|
|
54
|
+
Dynamic: license-file
|
|
54
55
|
Dynamic: provides-extra
|
|
55
56
|
Dynamic: requires-dist
|
|
56
57
|
Dynamic: requires-python
|
{google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/data_validation/combiner.py
RENAMED
|
@@ -22,10 +22,12 @@ import datetime
|
|
|
22
22
|
import functools
|
|
23
23
|
import json
|
|
24
24
|
import logging
|
|
25
|
+
from typing import TYPE_CHECKING
|
|
26
|
+
|
|
25
27
|
import ibis
|
|
26
28
|
import ibis.expr.datatypes as dt
|
|
29
|
+
import pandas
|
|
27
30
|
|
|
28
|
-
from typing import TYPE_CHECKING
|
|
29
31
|
from data_validation import consts
|
|
30
32
|
|
|
31
33
|
if TYPE_CHECKING:
|
|
@@ -34,22 +36,109 @@ if TYPE_CHECKING:
|
|
|
34
36
|
from data_validation.metadata import RunMetadata, ValidationMetadata
|
|
35
37
|
|
|
36
38
|
|
|
39
|
+
# At around 140 columns we hit RecursionError when unioning Ibis subqueries.
|
|
40
|
+
# This constant is a threshold at which we slice up the input Dataframes
|
|
41
|
+
# and stitch them back together again after Ibis processing.
|
|
42
|
+
COMBINER_COLUMN_SLICE_WIDTH = 120
|
|
43
|
+
|
|
44
|
+
COMBINER_GET_SUMMARY_EXC_TEXT = (
|
|
45
|
+
"Error while generating summary report of row validation results"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
37
49
|
def generate_report(
|
|
38
|
-
client,
|
|
39
50
|
run_metadata: "RunMetadata",
|
|
40
|
-
|
|
41
|
-
|
|
51
|
+
source_df: "DataFrame",
|
|
52
|
+
target_df: "DataFrame",
|
|
42
53
|
join_on_fields=(),
|
|
43
54
|
is_value_comparison=False,
|
|
44
55
|
verbose=False,
|
|
45
|
-
):
|
|
56
|
+
) -> "DataFrame":
|
|
57
|
+
"""Combine results into a report.
|
|
58
|
+
|
|
59
|
+
This function is a wrapper around _generate_report_slice(). _generate_report_slice() does the main work, this
|
|
60
|
+
wrapper simply manages the input columns and stitches the results back together.
|
|
61
|
+
This is because validations of > 140(ish) columns trigger a RecursionError when unioning Ibis subqueries.
|
|
62
|
+
In this method we pass in column slices of the incoming Dataframes and combine the results.
|
|
63
|
+
|
|
64
|
+
It is a bit of a hack but I cannot find a way to optimize the Ibis processing. It appears to be
|
|
65
|
+
inefficient in that we create a subquery for each validation (column) in _calculate_differences() and
|
|
66
|
+
then union them all. We then do the same on the source/target table expressions to join it all back
|
|
67
|
+
together again. I (nj1973) spent a singificant amount of time trying to understand/optimize the Ibis
|
|
68
|
+
processing but fell back on this simpler (less risky) workaround.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
pandas.DataFrame:
|
|
72
|
+
A pandas DataFrame with the results of the validation in the same
|
|
73
|
+
schema as the report table.
|
|
74
|
+
"""
|
|
75
|
+
_check_schema_names(source_df, target_df)
|
|
76
|
+
|
|
77
|
+
join_on_fields = tuple(join_on_fields)
|
|
78
|
+
|
|
79
|
+
validation_columns = run_metadata.validations.keys()
|
|
80
|
+
# slice_thresholds is a list of points at which we should break up the Dataframe by column.
|
|
81
|
+
# e.g. [10, 20, 30] would mean process columns 0-9, 10-19 and 20-the max column.
|
|
82
|
+
# 1. len(...) / COMBINER_COLUMN_SLICE_WIDTH: Divides total columns by the slice width to get the number of slices.
|
|
83
|
+
# 2. int(...) + 1: int()+1 is effectively ceil() which is what we want to get the actual whole number of slices
|
|
84
|
+
# 3. _ * COMBINER_COLUMN_SLICE_WIDTH: Multiplies each number by the slice width to get actual column counts for each slice.
|
|
85
|
+
slice_thresholds = [
|
|
86
|
+
(_ * COMBINER_COLUMN_SLICE_WIDTH)
|
|
87
|
+
for _ in range(int(len(validation_columns) / COMBINER_COLUMN_SLICE_WIDTH) + 1)
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
result_df = None
|
|
91
|
+
# Process the input Dataframes in slices of columns to avoid "RecursionError"s.
|
|
92
|
+
for slice_start in slice_thresholds:
|
|
93
|
+
columns_in_vertical_slice = list(validation_columns)[
|
|
94
|
+
slice_start : slice_start + COMBINER_COLUMN_SLICE_WIDTH
|
|
95
|
+
]
|
|
96
|
+
# Ensure any join columns are included in the column slice.
|
|
97
|
+
columns_in_vertical_slice.extend(
|
|
98
|
+
set(join_on_fields) - set(columns_in_vertical_slice)
|
|
99
|
+
)
|
|
100
|
+
interim_result_df = _generate_report_slice(
|
|
101
|
+
run_metadata,
|
|
102
|
+
source_df[columns_in_vertical_slice],
|
|
103
|
+
target_df[columns_in_vertical_slice],
|
|
104
|
+
join_on_fields=join_on_fields,
|
|
105
|
+
is_value_comparison=is_value_comparison,
|
|
106
|
+
verbose=verbose,
|
|
107
|
+
)
|
|
108
|
+
if result_df is None:
|
|
109
|
+
result_df = interim_result_df
|
|
110
|
+
else:
|
|
111
|
+
result_df = pandas.concat([result_df, interim_result_df])
|
|
112
|
+
|
|
113
|
+
# Get the first validation metadata object to fill source and/or target empty table names.
|
|
114
|
+
first = run_metadata.validations[next(iter(run_metadata.validations))]
|
|
115
|
+
if first.validation_type != consts.CUSTOM_QUERY:
|
|
116
|
+
result_df.source_table_name.fillna(
|
|
117
|
+
first.get_table_name(consts.RESULT_TYPE_SOURCE), inplace=True
|
|
118
|
+
)
|
|
119
|
+
result_df.target_table_name.fillna(
|
|
120
|
+
first.get_table_name(consts.RESULT_TYPE_TARGET), inplace=True
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
_get_summary(run_metadata, result_df, source_df, target_df)
|
|
124
|
+
|
|
125
|
+
return result_df
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _generate_report_slice(
|
|
129
|
+
run_metadata: "RunMetadata",
|
|
130
|
+
source_df: "DataFrame",
|
|
131
|
+
target_df: "DataFrame",
|
|
132
|
+
join_on_fields=(),
|
|
133
|
+
is_value_comparison=False,
|
|
134
|
+
verbose=False,
|
|
135
|
+
) -> "DataFrame":
|
|
46
136
|
"""Combine results into a report.
|
|
47
137
|
|
|
48
138
|
Args:
|
|
49
|
-
client (ibis.client.Client): Ibis client used to combine results.
|
|
50
139
|
run_metadata: Metadata about the run and validations.
|
|
51
|
-
|
|
52
|
-
|
|
140
|
+
source_df: Dataframe contains results of source query.
|
|
141
|
+
target_df: Dataframe contains results of target query.
|
|
53
142
|
join_on_fields (Sequence[str]):
|
|
54
143
|
A collection of column names to use to join source and target.
|
|
55
144
|
These are the columns that both the source and target queries
|
|
@@ -63,16 +152,15 @@ def generate_report(
|
|
|
63
152
|
A pandas DataFrame with the results of the validation in the same
|
|
64
153
|
schema as the report table.
|
|
65
154
|
"""
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
155
|
+
client = ibis.pandas.connect(
|
|
156
|
+
{
|
|
157
|
+
consts.RESULT_TYPE_SOURCE: source_df,
|
|
158
|
+
consts.RESULT_TYPE_TARGET: target_df,
|
|
159
|
+
}
|
|
160
|
+
)
|
|
161
|
+
source = client.table(consts.RESULT_TYPE_SOURCE)
|
|
162
|
+
target = client.table(consts.RESULT_TYPE_TARGET)
|
|
70
163
|
|
|
71
|
-
if source_names != target_names:
|
|
72
|
-
raise ValueError(
|
|
73
|
-
"Expected source and target to have same schema, got "
|
|
74
|
-
f"{consts.RESULT_TYPE_SOURCE}: {source_names}; {consts.RESULT_TYPE_TARGET}: {target_names}"
|
|
75
|
-
)
|
|
76
164
|
differences_pivot = _calculate_differences(
|
|
77
165
|
source, target, join_on_fields, run_metadata.validations, is_value_comparison
|
|
78
166
|
)
|
|
@@ -81,18 +169,18 @@ def generate_report(
|
|
|
81
169
|
source_pivot = _pivot_result(
|
|
82
170
|
source, join_on_fields, run_metadata.validations, consts.RESULT_TYPE_SOURCE
|
|
83
171
|
)
|
|
84
|
-
|
|
172
|
+
source_pivot_df = client.execute(source_pivot)
|
|
85
173
|
|
|
86
174
|
target_pivot = _pivot_result(
|
|
87
175
|
target, join_on_fields, run_metadata.validations, consts.RESULT_TYPE_TARGET
|
|
88
176
|
)
|
|
89
|
-
|
|
177
|
+
target_pivot_df = client.execute(target_pivot)
|
|
90
178
|
|
|
91
179
|
con = ibis.pandas.connect(
|
|
92
180
|
{
|
|
93
|
-
consts.RESULT_TYPE_SOURCE:
|
|
94
|
-
|
|
95
|
-
consts.RESULT_TYPE_TARGET:
|
|
181
|
+
consts.RESULT_TYPE_SOURCE: source_pivot_df,
|
|
182
|
+
consts.RESULT_TYPE_DIFFERENCES: differences_df,
|
|
183
|
+
consts.RESULT_TYPE_TARGET: target_pivot_df,
|
|
96
184
|
}
|
|
97
185
|
)
|
|
98
186
|
joined = _join_pivots(
|
|
@@ -107,19 +195,6 @@ def generate_report(
|
|
|
107
195
|
|
|
108
196
|
result_df = client.execute(documented)
|
|
109
197
|
result_df.validation_status.fillna(consts.VALIDATION_STATUS_FAIL, inplace=True)
|
|
110
|
-
|
|
111
|
-
# get the first validation metadata object to fill source and/or target empty table names
|
|
112
|
-
first = run_metadata.validations[next(iter(run_metadata.validations))]
|
|
113
|
-
if first.validation_type != consts.CUSTOM_QUERY:
|
|
114
|
-
result_df.source_table_name.fillna(
|
|
115
|
-
first.get_table_name(consts.RESULT_TYPE_SOURCE), inplace=True
|
|
116
|
-
)
|
|
117
|
-
result_df.target_table_name.fillna(
|
|
118
|
-
first.get_table_name(consts.RESULT_TYPE_TARGET), inplace=True
|
|
119
|
-
)
|
|
120
|
-
|
|
121
|
-
_get_summary(run_metadata, result_df, source_df, target_df)
|
|
122
|
-
|
|
123
198
|
return result_df
|
|
124
199
|
|
|
125
200
|
|
|
@@ -279,6 +354,18 @@ def _calculate_differences(
|
|
|
279
354
|
return differences_pivot
|
|
280
355
|
|
|
281
356
|
|
|
357
|
+
def _check_schema_names(source_df, target_df):
|
|
358
|
+
"""Check that the two input Dataframes have matching column names."""
|
|
359
|
+
source_names = tuple(source_df.columns)
|
|
360
|
+
target_names = tuple(target_df.columns)
|
|
361
|
+
|
|
362
|
+
if source_names != target_names:
|
|
363
|
+
raise ValueError(
|
|
364
|
+
"Expected source and target to have same schema, got "
|
|
365
|
+
f"{consts.RESULT_TYPE_SOURCE}: {source_names}; {consts.RESULT_TYPE_TARGET}: {target_names}"
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
|
|
282
369
|
def _pivot_result(
|
|
283
370
|
result: "IbisTable",
|
|
284
371
|
join_on_fields: tuple,
|
|
@@ -441,12 +528,19 @@ def _get_summary(
|
|
|
441
528
|
):
|
|
442
529
|
"""Logs a summary report/stats of row validation results."""
|
|
443
530
|
try:
|
|
444
|
-
if result_df.
|
|
445
|
-
|
|
531
|
+
if result_df.empty:
|
|
532
|
+
return
|
|
533
|
+
|
|
534
|
+
if (result_df.loc[0, consts.VALIDATION_TYPE] == consts.ROW_VALIDATION) or (
|
|
535
|
+
# Check for custom-query row validation which always should have primary keys (not null)
|
|
536
|
+
result_df.loc[0, consts.VALIDATION_TYPE] == consts.CUSTOM_QUERY
|
|
537
|
+
and result_df.loc[0, consts.CONFIG_PRIMARY_KEYS]
|
|
538
|
+
):
|
|
539
|
+
# Vectorized calculations for all counts.
|
|
446
540
|
success_condition = (
|
|
447
541
|
result_df[consts.VALIDATION_STATUS] == consts.VALIDATION_STATUS_SUCCESS
|
|
448
542
|
)
|
|
449
|
-
fail_condition = ~success_condition # Invert success for fail condition
|
|
543
|
+
fail_condition = ~success_condition # Invert success for fail condition.
|
|
450
544
|
|
|
451
545
|
source_not_in_target = (
|
|
452
546
|
result_df[consts.SOURCE_AGG_VALUE].notnull()
|
|
@@ -462,33 +556,32 @@ def _get_summary(
|
|
|
462
556
|
)
|
|
463
557
|
|
|
464
558
|
logging.info(
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
}
|
|
559
|
+
json.dumps(
|
|
560
|
+
{
|
|
561
|
+
consts.CONFIG_RUN_ID: run_metadata.run_id,
|
|
562
|
+
consts.CONFIG_START_TIME: run_metadata.start_time.isoformat(),
|
|
563
|
+
consts.CONFIG_END_TIME: run_metadata.end_time.isoformat(),
|
|
564
|
+
# Explicit conversion of numpy's int64 values to int for JSON serializability
|
|
565
|
+
consts.TOTAL_SOURCE_ROWS: int(source_df.shape[0]),
|
|
566
|
+
consts.TOTAL_TARGET_ROWS: int(target_df.shape[0]),
|
|
567
|
+
consts.TOTAL_ROWS_VALIDATED: int(result_df.shape[0]),
|
|
568
|
+
# Using .sum() on boolean Series for much faster counting
|
|
569
|
+
consts.TOTAL_ROWS_SUCCESS: int(success_condition.sum()),
|
|
570
|
+
consts.TOTAL_ROWS_FAIL: int(fail_condition.sum()),
|
|
571
|
+
consts.FAILED_SOURCE_NOT_IN_TARGET: int(
|
|
572
|
+
(fail_condition & source_not_in_target).sum()
|
|
573
|
+
),
|
|
574
|
+
consts.FAILED_TARGET_NOT_IN_SOURCE: int(
|
|
575
|
+
(fail_condition & target_not_in_source).sum()
|
|
576
|
+
),
|
|
577
|
+
consts.FAILED_PRESENT_IN_BOTH_TABLES: int(
|
|
578
|
+
(fail_condition & present_in_both_tables).sum()
|
|
579
|
+
),
|
|
580
|
+
}
|
|
581
|
+
)
|
|
489
582
|
)
|
|
490
583
|
except Exception as e:
|
|
491
584
|
logging.warning(
|
|
492
|
-
f"
|
|
585
|
+
f"{COMBINER_GET_SUMMARY_EXC_TEXT}: {e}",
|
|
493
586
|
exc_info=True,
|
|
494
587
|
)
|
|
@@ -16,7 +16,7 @@ import copy
|
|
|
16
16
|
import logging
|
|
17
17
|
import string
|
|
18
18
|
import random
|
|
19
|
-
from typing import TYPE_CHECKING, Optional, Union,
|
|
19
|
+
from typing import TYPE_CHECKING, Dict, List, Optional, Union, Tuple
|
|
20
20
|
|
|
21
21
|
import google.oauth2.service_account
|
|
22
22
|
import ibis.expr.datatypes as dt
|
|
@@ -66,6 +66,9 @@ class ConfigManager(object):
|
|
|
66
66
|
if self.validation_type not in consts.CONFIG_TYPES:
|
|
67
67
|
raise ValueError(f"Unknown Configuration Type: {self.validation_type}")
|
|
68
68
|
self._comparison_max_col_length = None
|
|
69
|
+
# For some engines we need to know the actual raw data type rather than the Ibis canonical type.
|
|
70
|
+
self._source_raw_data_types = None
|
|
71
|
+
self._target_raw_data_types = None
|
|
69
72
|
|
|
70
73
|
@property
|
|
71
74
|
def config(self):
|
|
@@ -94,6 +97,50 @@ class ConfigManager(object):
|
|
|
94
97
|
|
|
95
98
|
return self._target_conn
|
|
96
99
|
|
|
100
|
+
def get_source_raw_data_types(self) -> Dict[str, Tuple]:
|
|
101
|
+
"""Return raw data type information from source system.
|
|
102
|
+
|
|
103
|
+
The raw data type is the source/target engine type, for example it might
|
|
104
|
+
be "NCLOB" or "char" when the Ibis type simply states "string".
|
|
105
|
+
The data is cached in state when fetched for the first time.
|
|
106
|
+
The retuen value is keyed on the casefolded column name and the tuple is
|
|
107
|
+
the remaining 6 elements of the DB API cursor description specification."""
|
|
108
|
+
if self._source_raw_data_types is None:
|
|
109
|
+
if hasattr(self.source_client, "raw_column_metadata"):
|
|
110
|
+
raw_data_types = self.source_client.raw_column_metadata(
|
|
111
|
+
database=self.source_schema,
|
|
112
|
+
table=self.source_table,
|
|
113
|
+
query=self.source_query,
|
|
114
|
+
)
|
|
115
|
+
self._source_raw_data_types = {
|
|
116
|
+
_[0].casefold(): _[1:] for _ in raw_data_types
|
|
117
|
+
}
|
|
118
|
+
else:
|
|
119
|
+
self._source_raw_data_types = {}
|
|
120
|
+
return self._source_raw_data_types
|
|
121
|
+
|
|
122
|
+
def get_target_raw_data_types(self) -> Dict[str, Tuple]:
|
|
123
|
+
"""Return raw data type information from target system.
|
|
124
|
+
|
|
125
|
+
The raw data type is the source/target engine type, for example it might
|
|
126
|
+
be "NCLOB" or "char" when the Ibis type simply states "string".
|
|
127
|
+
The data is cached in state when fetched for the first time.
|
|
128
|
+
The retuen value is keyed on the casefolded column name and the tuple is
|
|
129
|
+
the remaining 6 elements of the DB API cursor description specification."""
|
|
130
|
+
if self._target_raw_data_types is None:
|
|
131
|
+
if hasattr(self.target_client, "raw_column_metadata"):
|
|
132
|
+
raw_data_types = self.target_client.raw_column_metadata(
|
|
133
|
+
database=self.target_schema,
|
|
134
|
+
table=self.target_table,
|
|
135
|
+
query=self.target_query,
|
|
136
|
+
)
|
|
137
|
+
self._target_raw_data_types = {
|
|
138
|
+
_[0].casefold(): _[1:] for _ in raw_data_types
|
|
139
|
+
}
|
|
140
|
+
else:
|
|
141
|
+
self._target_raw_data_types = {}
|
|
142
|
+
return self._target_raw_data_types
|
|
143
|
+
|
|
97
144
|
def close_client_connections(self):
|
|
98
145
|
"""Attempt to clean up any source/target connections, based on the client types.
|
|
99
146
|
|
|
@@ -138,10 +185,6 @@ class ConfigManager(object):
|
|
|
138
185
|
"""Return if the validation should perform a case insensitive match."""
|
|
139
186
|
return self._config.get(consts.CONFIG_CASE_INSENSITIVE_MATCH) or False
|
|
140
187
|
|
|
141
|
-
def process_in_memory(self):
|
|
142
|
-
"""Return whether to process in memory or on a remote platform."""
|
|
143
|
-
return True
|
|
144
|
-
|
|
145
188
|
@property
|
|
146
189
|
def max_recursive_query_size(self):
|
|
147
190
|
"""Return Aggregates from Config"""
|
|
@@ -115,9 +115,7 @@ class DataValidation(object):
|
|
|
115
115
|
"Schema validation", self.schema_validator.execute
|
|
116
116
|
)
|
|
117
117
|
else:
|
|
118
|
-
result_df = self._execute_validation(
|
|
119
|
-
self.validation_builder, process_in_memory=True
|
|
120
|
-
)
|
|
118
|
+
result_df = self._execute_validation(self.validation_builder)
|
|
121
119
|
|
|
122
120
|
# Call Result Handler to Manage Results
|
|
123
121
|
return self.result_handler.execute(result_df)
|
|
@@ -227,13 +225,10 @@ class DataValidation(object):
|
|
|
227
225
|
clause recursively until the individual row differences can be
|
|
228
226
|
identified.
|
|
229
227
|
"""
|
|
230
|
-
process_in_memory = self.config_manager.process_in_memory()
|
|
231
228
|
past_results = []
|
|
232
229
|
if len(grouped_fields) > 0:
|
|
233
230
|
validation_builder.add_query_group(grouped_fields[0])
|
|
234
|
-
result_df = self._execute_validation(
|
|
235
|
-
validation_builder, process_in_memory=process_in_memory
|
|
236
|
-
)
|
|
231
|
+
result_df = self._execute_validation(validation_builder)
|
|
237
232
|
|
|
238
233
|
for grouped_key in result_df[consts.GROUP_BY_COLUMNS].unique():
|
|
239
234
|
# Validations are viewed separtely, but queried together.
|
|
@@ -267,11 +262,7 @@ class DataValidation(object):
|
|
|
267
262
|
)
|
|
268
263
|
)
|
|
269
264
|
elif self.config_manager.primary_keys and len(grouped_fields) == 0:
|
|
270
|
-
past_results.append(
|
|
271
|
-
self._execute_validation(
|
|
272
|
-
validation_builder, process_in_memory=process_in_memory
|
|
273
|
-
)
|
|
274
|
-
)
|
|
265
|
+
past_results.append(self._execute_validation(validation_builder))
|
|
275
266
|
|
|
276
267
|
# elif self.config_manager.primary_keys:
|
|
277
268
|
# validation_builder.add_config_query_groups(self.config_manager.primary_keys)
|
|
@@ -302,7 +293,7 @@ class DataValidation(object):
|
|
|
302
293
|
}
|
|
303
294
|
validation_builder.add_filter(filter_field)
|
|
304
295
|
|
|
305
|
-
def _execute_validation(self, validation_builder
|
|
296
|
+
def _execute_validation(self, validation_builder):
|
|
306
297
|
"""Execute Against a Supplied Validation Builder"""
|
|
307
298
|
|
|
308
299
|
self.run_metadata.validations = validation_builder.get_metadata()
|
|
@@ -329,67 +320,48 @@ class DataValidation(object):
|
|
|
329
320
|
)
|
|
330
321
|
)
|
|
331
322
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
source_query,
|
|
342
|
-
)
|
|
323
|
+
futures = []
|
|
324
|
+
with ThreadPoolExecutor() as executor:
|
|
325
|
+
# Submit the two query network calls concurrently
|
|
326
|
+
futures.append(
|
|
327
|
+
executor.submit(
|
|
328
|
+
util.timed_call,
|
|
329
|
+
"Source query",
|
|
330
|
+
self.config_manager.source_client.execute,
|
|
331
|
+
source_query,
|
|
343
332
|
)
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
333
|
+
)
|
|
334
|
+
futures.append(
|
|
335
|
+
executor.submit(
|
|
336
|
+
util.timed_call,
|
|
337
|
+
"Target query",
|
|
338
|
+
self.config_manager.target_client.execute,
|
|
339
|
+
target_query,
|
|
351
340
|
)
|
|
352
|
-
source_df = futures[0].result()
|
|
353
|
-
target_df = futures[1].result()
|
|
354
|
-
|
|
355
|
-
pandas_client = ibis.pandas.connect(
|
|
356
|
-
{
|
|
357
|
-
consts.RESULT_TYPE_SOURCE: source_df,
|
|
358
|
-
consts.RESULT_TYPE_TARGET: target_df,
|
|
359
|
-
}
|
|
360
341
|
)
|
|
342
|
+
source_df = futures[0].result()
|
|
343
|
+
target_df = futures[1].result()
|
|
361
344
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
pandas_client,
|
|
367
|
-
self.run_metadata,
|
|
368
|
-
pandas_client.table(consts.RESULT_TYPE_SOURCE),
|
|
369
|
-
pandas_client.table(consts.RESULT_TYPE_TARGET),
|
|
370
|
-
join_on_fields=join_on_fields,
|
|
371
|
-
is_value_comparison=is_value_comparison,
|
|
372
|
-
verbose=self.verbose,
|
|
373
|
-
)
|
|
374
|
-
except Exception as e:
|
|
375
|
-
if self.verbose:
|
|
376
|
-
logging.error("-- ** Logging Source DF ** --")
|
|
377
|
-
logging.error(source_df.dtypes)
|
|
378
|
-
logging.error(source_df)
|
|
379
|
-
logging.error("-- ** Logging Target DF ** --")
|
|
380
|
-
logging.error(target_df.dtypes)
|
|
381
|
-
logging.error(target_df)
|
|
382
|
-
raise e
|
|
383
|
-
else:
|
|
384
|
-
result_df = combiner.generate_report(
|
|
385
|
-
self.config_manager.source_client,
|
|
345
|
+
try:
|
|
346
|
+
result_df = util.timed_call(
|
|
347
|
+
"Generate report",
|
|
348
|
+
combiner.generate_report,
|
|
386
349
|
self.run_metadata,
|
|
387
|
-
|
|
388
|
-
|
|
350
|
+
source_df,
|
|
351
|
+
target_df,
|
|
389
352
|
join_on_fields=join_on_fields,
|
|
390
353
|
is_value_comparison=is_value_comparison,
|
|
391
354
|
verbose=self.verbose,
|
|
392
355
|
)
|
|
356
|
+
except Exception as e:
|
|
357
|
+
if self.verbose:
|
|
358
|
+
logging.error("-- ** Logging Source DF ** --")
|
|
359
|
+
logging.error(source_df.dtypes)
|
|
360
|
+
logging.error(source_df)
|
|
361
|
+
logging.error("-- ** Logging Target DF ** --")
|
|
362
|
+
logging.error(target_df.dtypes)
|
|
363
|
+
logging.error(target_df)
|
|
364
|
+
raise e
|
|
393
365
|
|
|
394
366
|
return result_df
|
|
395
367
|
|
|
@@ -17,6 +17,7 @@ import ibis
|
|
|
17
17
|
import pandas
|
|
18
18
|
import logging
|
|
19
19
|
import re
|
|
20
|
+
import datetime
|
|
20
21
|
from typing import List, Dict, TYPE_CHECKING
|
|
21
22
|
|
|
22
23
|
from data_validation import cli_tools, consts
|
|
@@ -253,12 +254,13 @@ class PartitionBuilder:
|
|
|
253
254
|
|
|
254
255
|
def less_than_value(table, keys, values):
|
|
255
256
|
key_column = table.__getattr__(keys[0])
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
257
|
+
# Due to issue 1474, the type can be datetime.datetime or datetime.date
|
|
258
|
+
value = (
|
|
259
|
+
values[0].date()
|
|
260
|
+
if key_column.type().is_date()
|
|
261
|
+
and isinstance(values[0], datetime.datetime)
|
|
262
|
+
else values[0]
|
|
263
|
+
)
|
|
262
264
|
if len(keys) == 1:
|
|
263
265
|
return key_column < value
|
|
264
266
|
else:
|
|
@@ -269,10 +271,13 @@ class PartitionBuilder:
|
|
|
269
271
|
|
|
270
272
|
def geq_value(table, keys, values):
|
|
271
273
|
key_column = table.__getattr__(keys[0])
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
274
|
+
# Due to issue 1474, the type can be datetime.datetime or datetime.date
|
|
275
|
+
value = (
|
|
276
|
+
values[0].date()
|
|
277
|
+
if key_column.type().is_date()
|
|
278
|
+
and isinstance(values[0], datetime.datetime)
|
|
279
|
+
else values[0]
|
|
280
|
+
)
|
|
276
281
|
|
|
277
282
|
if len(keys) == 1:
|
|
278
283
|
return key_column >= value
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: google-pso-data-validator
|
|
3
|
-
Version: 7.
|
|
3
|
+
Version: 7.5.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
|
|
@@ -51,6 +51,7 @@ Dynamic: author-email
|
|
|
51
51
|
Dynamic: classifier
|
|
52
52
|
Dynamic: description
|
|
53
53
|
Dynamic: description-content-type
|
|
54
|
+
Dynamic: license-file
|
|
54
55
|
Dynamic: provides-extra
|
|
55
56
|
Dynamic: requires-dist
|
|
56
57
|
Dynamic: requires-python
|
|
@@ -11,6 +11,9 @@
|
|
|
11
11
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from typing import Iterable, Literal, Tuple
|
|
16
|
+
|
|
14
17
|
import sqlalchemy as sa
|
|
15
18
|
from sqlalchemy.dialects.oracle.base import (
|
|
16
19
|
OracleIdentifierPreparer,
|
|
@@ -20,7 +23,6 @@ from sqlalchemy.dialects.oracle.cx_oracle import OracleDialect_cx_oracle
|
|
|
20
23
|
import re
|
|
21
24
|
|
|
22
25
|
import ibis.expr.datatypes as dt
|
|
23
|
-
from typing import Iterable, Literal, Tuple
|
|
24
26
|
from ibis.backends.base.sql.alchemy import BaseAlchemyBackend
|
|
25
27
|
from third_party.ibis.ibis_oracle.compiler import OracleCompiler
|
|
26
28
|
from third_party.ibis.ibis_oracle.datatypes import _get_type
|
|
@@ -178,3 +180,35 @@ class Backend(BaseAlchemyBackend):
|
|
|
178
180
|
list_pk_col_sql, parameters=(database.upper(), table.upper())
|
|
179
181
|
)
|
|
180
182
|
return [_[0] for _ in result.cursor.fetchall()]
|
|
183
|
+
|
|
184
|
+
def raw_column_metadata(
|
|
185
|
+
self, database: str = None, table: str = None, query: str = None
|
|
186
|
+
) -> Iterable[Tuple]:
|
|
187
|
+
"""Partner method to _metadata that retains raw data type information instead of converting to Ibis types.
|
|
188
|
+
This works in the same way as _metadata by running a query over the DVT source, either schema.table or a
|
|
189
|
+
custom query, and fetching the first row. From the cursor we can detect data types of the row's columns.
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
list: A list of tuples containing the standard 7 DB API fields:
|
|
193
|
+
https://peps.python.org/pep-0249/#description
|
|
194
|
+
"""
|
|
195
|
+
|
|
196
|
+
def strip_prefix(s: str):
|
|
197
|
+
if s.startswith("DB_TYPE_"):
|
|
198
|
+
return s[8:]
|
|
199
|
+
else:
|
|
200
|
+
return s
|
|
201
|
+
|
|
202
|
+
assert (database and table) or query, "We should never receive all args=None"
|
|
203
|
+
if database and table:
|
|
204
|
+
source = f'"{database}"."{table}"'.upper()
|
|
205
|
+
elif query:
|
|
206
|
+
source = f"({query})"
|
|
207
|
+
|
|
208
|
+
with self.begin() as con:
|
|
209
|
+
result = con.exec_driver_sql(f"SELECT * FROM {source} t0 WHERE ROWNUM <= 1")
|
|
210
|
+
cursor = result.cursor
|
|
211
|
+
yield from (
|
|
212
|
+
(column[0], strip_prefix(column[1].name), *column[2:])
|
|
213
|
+
for column in cursor.description
|
|
214
|
+
)
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
|
|
15
|
-
from typing import Literal
|
|
15
|
+
from typing import Iterable, Literal, Tuple
|
|
16
16
|
|
|
17
17
|
import ibis.expr.datatypes as dt
|
|
18
18
|
import ibis.expr.schema as sch
|
|
@@ -156,7 +156,57 @@ def _list_primary_key_columns(self, database: str, table: str) -> list:
|
|
|
156
156
|
return [_[0] for _ in result.cursor.fetchall()]
|
|
157
157
|
|
|
158
158
|
|
|
159
|
+
def _raw_column_metadata(
|
|
160
|
+
self, database: str = None, table: str = None, query: str = None
|
|
161
|
+
) -> Iterable[Tuple]:
|
|
162
|
+
"""Partner method to _metadata that retains raw data type information instead of converting to Ibis types.
|
|
163
|
+
This works in the same way as _metadata by running a query over the DVT source, either schema.table or a
|
|
164
|
+
custom query, and fetching the first row. From the cursor we can detect data types of the row's columns.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
list: A list of tuples containing the standard 7 DB API fields:
|
|
168
|
+
https://peps.python.org/pep-0249/#description
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
assert (database and table) or query, "We should never receive all args=None"
|
|
172
|
+
if database and table:
|
|
173
|
+
source = f'"{database}"."{table}"'
|
|
174
|
+
elif query:
|
|
175
|
+
source = f"({query})"
|
|
176
|
+
|
|
177
|
+
with self.begin() as con:
|
|
178
|
+
cur = con.exec_driver_sql(f"SELECT * FROM {source} t0 LIMIT 0")
|
|
179
|
+
qry_cols = [
|
|
180
|
+
f"('{column.name}'::text,"
|
|
181
|
+
+ f"{column.type_code},"
|
|
182
|
+
+ f"{column.display_size if column.display_size else 'NULL'}::int,"
|
|
183
|
+
+ f"{column.internal_size if column.internal_size else 'NULL'}::int,"
|
|
184
|
+
+ f"{column.precision if column.precision else 'NULL'}::int,"
|
|
185
|
+
+ f"{column.scale if column.scale is not None else 'NULL'}::int,"
|
|
186
|
+
+ f"{column.null_ok if column.null_ok else 'NULL'}::int, {idx})"
|
|
187
|
+
for idx, column in enumerate(cur.cursor.description)
|
|
188
|
+
]
|
|
189
|
+
sql = f"""SELECT name, format_type(t0.type_code, NULL) AS data_type
|
|
190
|
+
, display_size, internal_size, precision, scale, null_ok
|
|
191
|
+
FROM UNNEST(array[{','.join(qry_cols)}])
|
|
192
|
+
AS t0(name text, type_code int, display_size int, internal_size int, precision int, scale int, null_ok int, col_order int)
|
|
193
|
+
ORDER BY col_order"""
|
|
194
|
+
yield from (
|
|
195
|
+
(
|
|
196
|
+
column.name,
|
|
197
|
+
column.data_type,
|
|
198
|
+
column.display_size,
|
|
199
|
+
column.internal_size,
|
|
200
|
+
column.precision,
|
|
201
|
+
column.scale,
|
|
202
|
+
column.null_ok,
|
|
203
|
+
)
|
|
204
|
+
for column in con.exec_driver_sql(sql)
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
|
|
159
208
|
PostgresBackend._metadata = _metadata
|
|
160
209
|
PostgresBackend.list_databases = list_schemas
|
|
161
210
|
PostgresBackend.do_connect = do_connect
|
|
162
211
|
PostgresBackend.list_primary_key_columns = _list_primary_key_columns
|
|
212
|
+
PostgresBackend.raw_column_metadata = _raw_column_metadata
|
|
@@ -11,6 +11,9 @@
|
|
|
11
11
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from typing import Iterable, Tuple
|
|
16
|
+
|
|
14
17
|
import pandas
|
|
15
18
|
import warnings
|
|
16
19
|
|
|
@@ -272,6 +275,33 @@ class Backend(BaseSQLBackend):
|
|
|
272
275
|
)
|
|
273
276
|
return list(tables_df.ColumnName.str.rstrip())
|
|
274
277
|
|
|
278
|
+
def raw_column_metadata(
|
|
279
|
+
self, database: str = None, table: str = None, query: str = None
|
|
280
|
+
) -> Iterable[Tuple]:
|
|
281
|
+
"""Partner method to _get_schema_using_query that retains raw data type information instead of converting
|
|
282
|
+
to Ibis types. This works in the same way as _get_schema_using_query by running a query over the DVT
|
|
283
|
+
source, either schema.table or a custom query, and fetching the first row. From the cursor we can detect
|
|
284
|
+
data types of the row's columns.
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
list: A list of tuples containing the standard 7 DB API fields:
|
|
288
|
+
https://peps.python.org/pep-0249/#description
|
|
289
|
+
"""
|
|
290
|
+
|
|
291
|
+
assert (database and table) or query, "We should never receive all args=None"
|
|
292
|
+
if database and table:
|
|
293
|
+
source = f'"{database}"."{table}"'.upper()
|
|
294
|
+
elif query:
|
|
295
|
+
source = f"({query})"
|
|
296
|
+
|
|
297
|
+
cur = self.raw_sql(f"SELECT TOP 1 * FROM {source} t0")
|
|
298
|
+
# resets the state of the cursor and closes operation
|
|
299
|
+
cur.fetchall()
|
|
300
|
+
yield from (
|
|
301
|
+
(column[0], raw_type, *column[2:])
|
|
302
|
+
for column, raw_type in zip(cur.description, cur.columntypename)
|
|
303
|
+
)
|
|
304
|
+
|
|
275
305
|
# Methods we need to implement for BaseSQLBackend
|
|
276
306
|
def create_table(self):
|
|
277
307
|
pass
|
|
File without changes
|
|
File without changes
|
{google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/data_validation/__init__.py
RENAMED
|
File without changes
|
{google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/data_validation/__main__.py
RENAMED
|
File without changes
|
|
File without changes
|
{google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/data_validation/cli_tools.py
RENAMED
|
File without changes
|
{google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/data_validation/client_info.py
RENAMED
|
File without changes
|
{google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/data_validation/clients.py
RENAMED
|
File without changes
|
{google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/data_validation/exceptions.py
RENAMED
|
File without changes
|
{google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/data_validation/find_tables.py
RENAMED
|
File without changes
|
{google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/data_validation/gcs_helper.py
RENAMED
|
File without changes
|
|
File without changes
|
{google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/data_validation/metadata.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/data_validation/raw_query.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/data_validation/state_manager.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{google_pso_data_validator-7.4.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_db2/api.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|