google-pso-data-validator 7.2.0__tar.gz → 7.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 (90) hide show
  1. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/PKG-INFO +3 -1
  2. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/README.md +2 -0
  3. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/data_validation/__main__.py +8 -6
  4. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/data_validation/cli_tools.py +17 -7
  5. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/data_validation/combiner.py +135 -48
  6. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/data_validation/config_manager.py +19 -11
  7. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/data_validation/consts.py +33 -15
  8. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/data_validation/data_validation.py +6 -3
  9. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/data_validation/partition_builder.py +6 -4
  10. google_pso_data_validator-7.4.0/data_validation/raw_query.py +74 -0
  11. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/data_validation/result_handlers/bigquery.py +4 -4
  12. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/data_validation/result_handlers/text.py +6 -4
  13. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/data_validation/secret_manager.py +6 -2
  14. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/data_validation/util.py +18 -0
  15. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/google_pso_data_validator.egg-info/PKG-INFO +3 -1
  16. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/setup.py +1 -1
  17. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/third_party/ibis/ibis_oracle/__init__.py +16 -1
  18. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/third_party/ibis/ibis_postgres/registry.py +8 -2
  19. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/third_party/ibis/ibis_snowflake/api.py +4 -1
  20. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/third_party/ibis/ibis_teradata/__init__.py +3 -10
  21. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/third_party/ibis/ibis_teradata/api.py +5 -0
  22. google_pso_data_validator-7.2.0/data_validation/raw_query.py +0 -47
  23. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/LICENSE +0 -0
  24. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/data_validation/__init__.py +0 -0
  25. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/data_validation/app.py +0 -0
  26. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/data_validation/client_info.py +0 -0
  27. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/data_validation/clients.py +0 -0
  28. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/data_validation/exceptions.py +0 -0
  29. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/data_validation/find_tables.py +0 -0
  30. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/data_validation/gcs_helper.py +0 -0
  31. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/data_validation/jellyfish_distance.py +0 -0
  32. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/data_validation/metadata.py +0 -0
  33. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/data_validation/query_builder/__init__.py +0 -0
  34. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/data_validation/query_builder/partition_row_builder.py +0 -0
  35. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/data_validation/query_builder/query_builder.py +0 -0
  36. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/data_validation/query_builder/random_row_builder.py +0 -0
  37. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/data_validation/result_handlers/__init__.py +0 -0
  38. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/data_validation/schema_validation.py +0 -0
  39. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/data_validation/state_manager.py +0 -0
  40. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/data_validation/validation_builder.py +0 -0
  41. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/google_pso_data_validator.egg-info/SOURCES.txt +0 -0
  42. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/google_pso_data_validator.egg-info/dependency_links.txt +0 -0
  43. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/google_pso_data_validator.egg-info/entry_points.txt +0 -0
  44. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/google_pso_data_validator.egg-info/requires.txt +0 -0
  45. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/google_pso_data_validator.egg-info/top_level.txt +0 -0
  46. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/setup.cfg +0 -0
  47. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/third_party/ibis/ibis_addon/__init__.py +0 -0
  48. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/third_party/ibis/ibis_addon/api.py +0 -0
  49. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/third_party/ibis/ibis_addon/operations.py +0 -0
  50. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/third_party/ibis/ibis_biquery/__init__.py +0 -0
  51. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/third_party/ibis/ibis_biquery/api.py +0 -0
  52. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/third_party/ibis/ibis_cloud_spanner/__init__.py +0 -0
  53. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/third_party/ibis/ibis_cloud_spanner/api.py +0 -0
  54. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/third_party/ibis/ibis_cloud_spanner/client.py +0 -0
  55. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/third_party/ibis/ibis_cloud_spanner/compiler.py +0 -0
  56. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/third_party/ibis/ibis_cloud_spanner/datatypes.py +0 -0
  57. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/third_party/ibis/ibis_cloud_spanner/registry.py +0 -0
  58. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/third_party/ibis/ibis_cloud_spanner/tests/__init__.py +0 -0
  59. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/third_party/ibis/ibis_cloud_spanner/tests/conftest.py +0 -0
  60. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/third_party/ibis/ibis_cloud_spanner/to_pandas.py +0 -0
  61. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/third_party/ibis/ibis_db2/__init__.py +0 -0
  62. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/third_party/ibis/ibis_db2/api.py +0 -0
  63. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/third_party/ibis/ibis_db2/compiler.py +0 -0
  64. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/third_party/ibis/ibis_db2/datatypes.py +0 -0
  65. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/third_party/ibis/ibis_db2/registry.py +0 -0
  66. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/third_party/ibis/ibis_impala/__init__.py +0 -0
  67. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/third_party/ibis/ibis_impala/api.py +0 -0
  68. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/third_party/ibis/ibis_mssql/__init__.py +0 -0
  69. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/third_party/ibis/ibis_mssql/api.py +0 -0
  70. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/third_party/ibis/ibis_mssql/datatypes.py +0 -0
  71. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/third_party/ibis/ibis_mssql/registry.py +0 -0
  72. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/third_party/ibis/ibis_mysql/__init__.py +0 -0
  73. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/third_party/ibis/ibis_mysql/base_sql_compiler/__init__.py +0 -0
  74. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/third_party/ibis/ibis_mysql/base_sql_compiler/select_builder.py +0 -0
  75. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/third_party/ibis/ibis_mysql/compiler.py +0 -0
  76. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/third_party/ibis/ibis_oracle/api.py +0 -0
  77. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/third_party/ibis/ibis_oracle/compiler.py +0 -0
  78. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/third_party/ibis/ibis_oracle/datatypes.py +0 -0
  79. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/third_party/ibis/ibis_oracle/registry.py +0 -0
  80. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/third_party/ibis/ibis_postgres/__init__.py +0 -0
  81. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/third_party/ibis/ibis_postgres/client.py +0 -0
  82. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/third_party/ibis/ibis_postgres/datatypes.py +0 -0
  83. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/third_party/ibis/ibis_redshift/__init__.py +0 -0
  84. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/third_party/ibis/ibis_redshift/api.py +0 -0
  85. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/third_party/ibis/ibis_redshift/compiler.py +0 -0
  86. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/third_party/ibis/ibis_snowflake/__init__.py +0 -0
  87. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/third_party/ibis/ibis_snowflake/datatypes.py +0 -0
  88. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/third_party/ibis/ibis_teradata/compiler.py +0 -0
  89. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/third_party/ibis/ibis_teradata/datatypes.py +0 -0
  90. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.4.0}/third_party/ibis/ibis_teradata/registry.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: google-pso-data-validator
3
- Version: 7.2.0
3
+ Version: 7.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
@@ -651,6 +651,8 @@ data-validation query
651
651
  The connection name to be queried
652
652
  --query or -q QUERY
653
653
  The raw query to run against the supplied connection
654
+ [--format or -f {minimal,python}]
655
+ Format for query output (default: python)
654
656
  ```
655
657
 
656
658
  ### Building Matched Table Lists
@@ -593,6 +593,8 @@ data-validation query
593
593
  The connection name to be queried
594
594
  --query or -q QUERY
595
595
  The raw query to run against the supplied connection
596
+ [--format or -f {minimal,python}]
597
+ Format for query output (default: python)
596
598
  ```
597
599
 
598
600
  ### Building Matched Table Lists
@@ -16,9 +16,9 @@ import json
16
16
  import logging
17
17
  import os
18
18
  import sys
19
+ from typing import TYPE_CHECKING
19
20
 
20
21
  from yaml import Dumper, dump
21
- from argparse import Namespace
22
22
  from typing import List
23
23
  from data_validation import (
24
24
  cli_tools,
@@ -34,6 +34,9 @@ from data_validation.data_validation import DataValidation
34
34
  from data_validation.find_tables import find_tables_using_string_matching
35
35
  from data_validation.partition_builder import PartitionBuilder
36
36
 
37
+ if TYPE_CHECKING:
38
+ from argparse import Namespace
39
+
37
40
  # by default yaml dumps lists as pointers. This disables that feature
38
41
  Dumper.ignore_aliases = lambda *args: True
39
42
 
@@ -246,7 +249,7 @@ def _get_comparison_config(
246
249
  return config_manager.build_config_comparison_fields(comparison_fields)
247
250
 
248
251
 
249
- def build_config_from_args(args: Namespace, config_manager: ConfigManager):
252
+ def build_config_from_args(args: "Namespace", config_manager: ConfigManager):
250
253
  """This function is used to append build configs to the config manager for all validation commands and generate-table-partitions.
251
254
  Instead of having two separate commands, e.g. validate row and validate custom-query row, generate-table-partitions
252
255
  uses implicit choice of table or custom-query. A user can specify either tables or source/target query/file,
@@ -337,7 +340,7 @@ def build_config_from_args(args: Namespace, config_manager: ConfigManager):
337
340
 
338
341
 
339
342
  def build_config_managers_from_args(
340
- args: Namespace, validate_cmd: str = None
343
+ args: "Namespace", validate_cmd: str = None
341
344
  ) -> List[ConfigManager]:
342
345
  """Return a list of config managers ready to execute."""
343
346
 
@@ -570,7 +573,6 @@ def run_validations(args, config_managers):
570
573
  Args:
571
574
  config_managers (list[ConfigManager]): List of config manager instances.
572
575
  """
573
- # TODO(issue/31): Add parallel execution logic
574
576
  for config_manager in config_managers:
575
577
  run_validation(config_manager, dry_run=args.dry_run, verbose=args.verbose)
576
578
 
@@ -597,7 +599,7 @@ def store_json_config_file(args, config_managers):
597
599
  cli_tools.store_validation(config_file_path, json_config)
598
600
 
599
601
 
600
- def partition_and_store_config_files(args: Namespace) -> None:
602
+ def partition_and_store_config_files(args: "Namespace") -> None:
601
603
  """Build multiple YAML Config files using user specified partition logic
602
604
 
603
605
  Args:
@@ -692,7 +694,7 @@ def main():
692
694
  print(find_tables_using_string_matching(args))
693
695
  elif args.command == "query":
694
696
  raw_query.print_raw_query_output(
695
- raw_query.run_raw_query_against_connection(args)
697
+ raw_query.run_raw_query_against_connection(args), format=args.output_format
696
698
  )
697
699
  elif args.command == "validate":
698
700
  validate(args)
@@ -52,8 +52,7 @@ import sys
52
52
  import uuid
53
53
  import os
54
54
  import math
55
- from argparse import Namespace
56
- from typing import Dict, List, Optional
55
+ from typing import Dict, List, Optional, TYPE_CHECKING
57
56
  from yaml import Dumper, Loader, dump, load
58
57
 
59
58
  from data_validation import (
@@ -66,6 +65,9 @@ from data_validation import (
66
65
  )
67
66
  from data_validation.validation_builder import list_to_sublists
68
67
 
68
+ if TYPE_CHECKING:
69
+ from argparse import Namespace
70
+
69
71
 
70
72
  CONNECTION_SOURCE_FIELDS = {
71
73
  "BigQuery": [
@@ -191,7 +193,7 @@ VALIDATE_SCHEMA_HELP_TEXT = "Run a schema validation"
191
193
  VALIDATE_CUSTOM_QUERY_HELP_TEXT = "Run a custom query validation"
192
194
 
193
195
 
194
- def _check_custom_query_args(parser: argparse.ArgumentParser, parsed_args: Namespace):
196
+ def _check_custom_query_args(parser: argparse.ArgumentParser, parsed_args: "Namespace"):
195
197
  # This is where we make additional checks if the arguments provided are what we expect
196
198
  # For example, only one of -tbls and custom query options can be provided
197
199
  if hasattr(parsed_args, "tables_list") and hasattr(
@@ -223,7 +225,7 @@ def _check_custom_query_args(parser: argparse.ArgumentParser, parsed_args: Names
223
225
  return # old format - only one of them is present
224
226
 
225
227
 
226
- def get_parsed_args() -> Namespace:
228
+ def get_parsed_args() -> "Namespace":
227
229
  """Return ArgParser with configured CLI arguments."""
228
230
  parser = configure_arg_parser()
229
231
  args = ["--help"] if len(sys.argv) == 1 else None
@@ -393,6 +395,14 @@ def _configure_raw_query(subparsers):
393
395
  )
394
396
  query_parser.add_argument("--conn", "-c", help="Connection name to query")
395
397
  query_parser.add_argument("--query", "-q", help="Raw query to execute")
398
+ query_parser.add_argument(
399
+ "--format",
400
+ "-f",
401
+ dest="output_format",
402
+ choices=consts.RAW_QUERY_FORMAT_TYPES,
403
+ default=consts.FORMAT_TYPE_PYTHON,
404
+ help=f"Format for query output (default: {consts.FORMAT_TYPE_PYTHON})",
405
+ )
396
406
 
397
407
 
398
408
  def _configure_validation_config_parser(subparsers):
@@ -1027,7 +1037,7 @@ def _add_common_arguments(
1027
1037
  optional_arguments.add_argument(
1028
1038
  "--format",
1029
1039
  "-fmt",
1030
- default="table",
1040
+ default=consts.FORMAT_TYPE_TABLE,
1031
1041
  help="Set the format for printing command output, Supported formats are (text, csv, json, table). Defaults "
1032
1042
  "to table",
1033
1043
  )
@@ -1443,7 +1453,7 @@ def _concat_column_count_configs(
1443
1453
  return return_list
1444
1454
 
1445
1455
 
1446
- def get_pre_build_configs(args: Namespace, validate_cmd: str) -> List[Dict]:
1456
+ def get_pre_build_configs(args: "Namespace", validate_cmd: str) -> List[Dict]:
1447
1457
  """Return a dict of configurations to build ConfigManager object"""
1448
1458
 
1449
1459
  def cols_from_arg(concat_arg: str, client, table_obj: dict, query_str: str) -> list:
@@ -1503,7 +1513,7 @@ def get_pre_build_configs(args: Namespace, validate_cmd: str) -> List[Dict]:
1503
1513
  target_client = clients.get_data_client(mgr.get_connection_config(args.target_conn))
1504
1514
 
1505
1515
  # Get format: text, csv, json, table. Default is table
1506
- format = args.format if args.format else "table"
1516
+ format = args.format if args.format else consts.FORMAT_TYPE_TABLE
1507
1517
 
1508
1518
  # Get random row arguments. Only in row validations these attributes can be present.
1509
1519
  # Bad coding here, but keeping it so as not to introduce a breaking change. See
@@ -25,17 +25,20 @@ import logging
25
25
  import ibis
26
26
  import ibis.expr.datatypes as dt
27
27
 
28
+ from typing import TYPE_CHECKING
28
29
  from data_validation import consts
29
30
 
30
- DEFAULT_SOURCE = "source"
31
- DEFAULT_TARGET = "target"
31
+ if TYPE_CHECKING:
32
+ from pandas import DataFrame
33
+ import ibis.expr.types.relations.table as IbisTable
34
+ from data_validation.metadata import RunMetadata, ValidationMetadata
32
35
 
33
36
 
34
37
  def generate_report(
35
38
  client,
36
- run_metadata,
37
- source,
38
- target,
39
+ run_metadata: "RunMetadata",
40
+ source: "IbisTable",
41
+ target: "IbisTable",
39
42
  join_on_fields=(),
40
43
  is_value_comparison=False,
41
44
  verbose=False,
@@ -44,8 +47,7 @@ def generate_report(
44
47
 
45
48
  Args:
46
49
  client (ibis.client.Client): Ibis client used to combine results.
47
- run_metadata (data_validation.metadata.RunMetadata):
48
- Metadata about the run and validations.
50
+ run_metadata: Metadata about the run and validations.
49
51
  source (ibis.QUERY): Ibis query / table object.
50
52
  target (ibis.QUERY): Ibis query / table object.
51
53
  join_on_fields (Sequence[str]):
@@ -69,7 +71,7 @@ def generate_report(
69
71
  if source_names != target_names:
70
72
  raise ValueError(
71
73
  "Expected source and target to have same schema, got "
72
- f"source: {source_names} target: {target_names}"
74
+ f"{consts.RESULT_TYPE_SOURCE}: {source_names}; {consts.RESULT_TYPE_TARGET}: {target_names}"
73
75
  )
74
76
  differences_pivot = _calculate_differences(
75
77
  source, target, join_on_fields, run_metadata.validations, is_value_comparison
@@ -87,13 +89,17 @@ def generate_report(
87
89
  target_df = client.execute(target_pivot)
88
90
 
89
91
  con = ibis.pandas.connect(
90
- {"source": source_df, "differences": differences_df, "target": target_df}
92
+ {
93
+ consts.RESULT_TYPE_SOURCE: source_df,
94
+ "differences": differences_df,
95
+ consts.RESULT_TYPE_TARGET: target_df,
96
+ }
91
97
  )
92
98
  joined = _join_pivots(
93
99
  con.tables.source, con.tables.target, con.tables.differences, join_on_fields
94
100
  )
95
101
 
96
- documented = _add_metadata(joined, run_metadata)
102
+ documented, run_metadata = _add_metadata(joined, run_metadata)
97
103
 
98
104
  if verbose:
99
105
  logging.debug("-- ** Combiner Query ** --")
@@ -112,15 +118,17 @@ def generate_report(
112
118
  first.get_table_name(consts.RESULT_TYPE_TARGET), inplace=True
113
119
  )
114
120
 
121
+ _get_summary(run_metadata, result_df, source_df, target_df)
122
+
115
123
  return result_df
116
124
 
117
125
 
118
126
  def _calculate_difference(
119
- field_differences,
127
+ field_differences: "IbisTable",
120
128
  datatype: dt.DataType,
121
129
  target_type: dt.DataType,
122
- validation,
123
- is_value_comparison,
130
+ validation: "ValidationMetadata",
131
+ is_value_comparison: bool,
124
132
  ):
125
133
  pct_threshold = ibis.literal(validation.threshold)
126
134
  if datatype.is_timestamp() or datatype.is_date():
@@ -210,12 +218,16 @@ def _calculate_difference(
210
218
  difference.name("difference"),
211
219
  pct_difference.name("pct_difference"),
212
220
  pct_threshold.name("pct_threshold"),
213
- validation_status.name("validation_status"),
221
+ validation_status.name(consts.VALIDATION_STATUS),
214
222
  )
215
223
 
216
224
 
217
225
  def _calculate_differences(
218
- source, target, join_on_fields, validations, is_value_comparison
226
+ source: "IbisTable",
227
+ target: "IbisTable",
228
+ join_on_fields: tuple,
229
+ validations: "dict[ValidationMetadata]",
230
+ is_value_comparison: bool,
219
231
  ):
220
232
  """Calculate differences between source and target fields.
221
233
 
@@ -267,7 +279,12 @@ def _calculate_differences(
267
279
  return differences_pivot
268
280
 
269
281
 
270
- def _pivot_result(result, join_on_fields, validations, result_type):
282
+ def _pivot_result(
283
+ result: "IbisTable",
284
+ join_on_fields: tuple,
285
+ validations: "dict[ValidationMetadata]",
286
+ result_type: str,
287
+ ):
271
288
  all_fields = frozenset(result.schema().names)
272
289
  validation_fields = (
273
290
  all_fields - frozenset(join_on_fields)
@@ -285,19 +302,21 @@ def _pivot_result(result, join_on_fields, validations, result_type):
285
302
  ibis.literal("{")
286
303
  + ibis.literal(", ").join(validation.primary_keys)
287
304
  + ibis.literal("}")
288
- ).name("primary_keys")
305
+ ).name(consts.CONFIG_PRIMARY_KEYS)
289
306
  else:
290
- primary_keys = ibis.literal(None).cast("string").name("primary_keys")
307
+ primary_keys = (
308
+ ibis.literal(None).cast("string").name(consts.CONFIG_PRIMARY_KEYS)
309
+ )
291
310
 
292
311
  pivots.append(
293
312
  result.projection(
294
313
  (
295
314
  ibis.literal(field).name("validation_name"),
296
315
  ibis.literal(validation.validation_type).name(
297
- "validation_type"
316
+ consts.VALIDATION_TYPE
298
317
  ),
299
318
  ibis.literal(validation.aggregation_type).name(
300
- "aggregation_type"
319
+ consts.AGGREGATION_TYPE
301
320
  ),
302
321
  ibis.literal(validation.get_table_name(result_type)).name(
303
322
  "table_name"
@@ -333,7 +352,12 @@ def _as_json(expr):
333
352
  )
334
353
 
335
354
 
336
- def _join_pivots(source, target, differences, join_on_fields):
355
+ def _join_pivots(
356
+ source: "IbisTable",
357
+ target: "IbisTable",
358
+ differences: "IbisTable",
359
+ join_on_fields: tuple,
360
+ ):
337
361
  if join_on_fields:
338
362
  join_values = []
339
363
  for field in join_on_fields:
@@ -346,62 +370,125 @@ def _join_pivots(source, target, differences, join_on_fields):
346
370
 
347
371
  group_by_columns = (
348
372
  ibis.literal("{") + ibis.literal(", ").join(join_values) + ibis.literal("}")
349
- ).name("group_by_columns")
373
+ ).name(consts.GROUP_BY_COLUMNS)
350
374
  else:
351
- group_by_columns = ibis.literal(None).cast("string").name("group_by_columns")
375
+ group_by_columns = (
376
+ ibis.literal(None).cast("string").name(consts.GROUP_BY_COLUMNS)
377
+ )
352
378
 
353
379
  join_keys = ("validation_name",) + join_on_fields
354
380
  source_difference = source.join(differences, join_keys, how="outer")[
355
381
  [source[field] for field in join_keys]
356
382
  + [
357
- source["validation_type"],
358
- source["aggregation_type"],
359
- source["table_name"],
383
+ source[consts.VALIDATION_TYPE],
384
+ source[consts.AGGREGATION_TYPE],
385
+ source[consts.CONFIG_TABLE_NAME],
360
386
  source["column_name"],
361
- source["primary_keys"],
387
+ source[consts.CONFIG_PRIMARY_KEYS],
362
388
  source["num_random_rows"],
363
389
  source["agg_value"],
364
390
  differences["difference"],
365
391
  differences["pct_difference"],
366
392
  differences["pct_threshold"],
367
- differences["validation_status"],
393
+ differences[consts.VALIDATION_STATUS],
368
394
  ]
369
395
  ]
370
396
  joined = source_difference.join(target, join_keys, how="outer")[
371
397
  source_difference["validation_name"],
372
- source_difference["validation_type"]
373
- .fillna(target["validation_type"])
374
- .name("validation_type"),
375
- source_difference["aggregation_type"]
376
- .fillna(target["aggregation_type"])
377
- .name("aggregation_type"),
378
- source_difference["table_name"].name("source_table_name"),
379
- source_difference["column_name"].name("source_column_name"),
380
- source_difference["agg_value"].name("source_agg_value"),
381
- target["table_name"].name("target_table_name"),
382
- target["column_name"].name("target_column_name"),
383
- target["agg_value"].name("target_agg_value"),
398
+ source_difference[consts.VALIDATION_TYPE]
399
+ .fillna(target[consts.VALIDATION_TYPE])
400
+ .name(consts.VALIDATION_TYPE),
401
+ source_difference[consts.AGGREGATION_TYPE]
402
+ .fillna(target[consts.AGGREGATION_TYPE])
403
+ .name(consts.AGGREGATION_TYPE),
404
+ source_difference["table_name"].name(consts.SOURCE_TABLE_NAME),
405
+ source_difference["column_name"].name(consts.SOURCE_COLUMN_NAME),
406
+ source_difference["agg_value"].name(consts.SOURCE_AGG_VALUE),
407
+ target["table_name"].name(consts.TARGET_TABLE_NAME),
408
+ target["column_name"].name(consts.TARGET_COLUMN_NAME),
409
+ target["agg_value"].name(consts.TARGET_AGG_VALUE),
384
410
  group_by_columns,
385
- source_difference["primary_keys"],
411
+ source_difference[consts.CONFIG_PRIMARY_KEYS],
386
412
  source_difference["num_random_rows"],
387
413
  source_difference["difference"],
388
414
  source_difference["pct_difference"],
389
415
  source_difference["pct_threshold"],
390
- source_difference["validation_status"],
416
+ source_difference[consts.VALIDATION_STATUS],
391
417
  ]
392
418
  return joined
393
419
 
394
420
 
395
- def _add_metadata(joined, run_metadata):
421
+ def _add_metadata(joined: "IbisTable", run_metadata: "RunMetadata"):
396
422
  # TODO: Add source and target queries to metadata
397
423
  run_metadata.end_time = datetime.datetime.now(datetime.timezone.utc)
398
424
 
399
425
  joined = joined[
400
426
  joined,
401
- ibis.literal(run_metadata.run_id).name("run_id"),
402
- ibis.literal(run_metadata.labels).name("labels"),
403
- ibis.literal(run_metadata.start_time).name("start_time"),
404
- ibis.literal(run_metadata.end_time).name("end_time"),
427
+ ibis.literal(run_metadata.run_id).name(consts.CONFIG_RUN_ID),
428
+ ibis.literal(run_metadata.labels).name(consts.CONFIG_LABELS),
429
+ ibis.literal(run_metadata.start_time).name(consts.CONFIG_START_TIME),
430
+ ibis.literal(run_metadata.end_time).name(consts.CONFIG_END_TIME),
405
431
  ]
406
432
 
407
- return joined
433
+ return (joined, run_metadata)
434
+
435
+
436
+ def _get_summary(
437
+ run_metadata: "RunMetadata",
438
+ result_df: "DataFrame",
439
+ source_df: "DataFrame",
440
+ target_df: "DataFrame",
441
+ ):
442
+ """Logs a summary report/stats of row validation results."""
443
+ try:
444
+ if result_df.loc[0, consts.VALIDATION_TYPE] == consts.ROW_VALIDATION:
445
+ # Vectorized calculations for all counts
446
+ success_condition = (
447
+ result_df[consts.VALIDATION_STATUS] == consts.VALIDATION_STATUS_SUCCESS
448
+ )
449
+ fail_condition = ~success_condition # Invert success for fail condition
450
+
451
+ source_not_in_target = (
452
+ result_df[consts.SOURCE_AGG_VALUE].notnull()
453
+ & result_df[consts.TARGET_AGG_VALUE].isnull()
454
+ )
455
+ target_not_in_source = (
456
+ result_df[consts.SOURCE_AGG_VALUE].isnull()
457
+ & result_df[consts.TARGET_AGG_VALUE].notnull()
458
+ )
459
+ present_in_both_tables = (
460
+ result_df[consts.SOURCE_AGG_VALUE].notnull()
461
+ & result_df[consts.TARGET_AGG_VALUE].notnull()
462
+ )
463
+
464
+ logging.info(
465
+ {
466
+ consts.CONFIG_RUN_ID: run_metadata.run_id,
467
+ consts.CONFIG_START_TIME: run_metadata.start_time.strftime(
468
+ "%Y-%m-%d %H:%M:%S %Z"
469
+ ),
470
+ consts.CONFIG_END_TIME: run_metadata.end_time.strftime(
471
+ "%Y-%m-%d %H:%M:%S %Z"
472
+ ),
473
+ consts.TOTAL_SOURCE_ROWS: source_df.shape[0],
474
+ consts.TOTAL_TARGET_ROWS: target_df.shape[0],
475
+ consts.TOTAL_ROWS_VALIDATED: result_df.shape[0],
476
+ # Using .sum() on boolean Series for much faster counting
477
+ consts.TOTAL_ROWS_SUCCESS: success_condition.sum(),
478
+ consts.TOTAL_ROWS_FAIL: fail_condition.sum(),
479
+ consts.FAILED_SOURCE_NOT_IN_TARGET: (
480
+ fail_condition & source_not_in_target
481
+ ).sum(),
482
+ consts.FAILED_TARGET_NOT_IN_SOURCE: (
483
+ fail_condition & target_not_in_source
484
+ ).sum(),
485
+ consts.FAILED_PRESENT_IN_BOTH_TABLES: (
486
+ fail_condition & present_in_both_tables
487
+ ).sum(),
488
+ }
489
+ )
490
+ except Exception as e:
491
+ logging.warning(
492
+ f"Error while generating summary report of row validation results: {e}",
493
+ exc_info=True,
494
+ )
@@ -459,7 +459,7 @@ class ConfigManager(object):
459
459
  cols_filter_list = consts.COLUMN_FILTER_LIST
460
460
  # handler that display results either to output or in a file
461
461
  return TextResultHandler(
462
- self._config.get(consts.CONFIG_FORMAT, "table"),
462
+ self._config.get(consts.CONFIG_FORMAT, consts.FORMAT_TYPE_TABLE),
463
463
  self.filter_status,
464
464
  cols_filter_list,
465
465
  )
@@ -486,7 +486,9 @@ class ConfigManager(object):
486
486
  table_id=table_id,
487
487
  credentials=credentials,
488
488
  api_endpoint=api_endpoint,
489
- text_format=self._config.get(consts.CONFIG_FORMAT, "table"),
489
+ text_format=self._config.get(
490
+ consts.CONFIG_FORMAT, consts.FORMAT_TYPE_TABLE
491
+ ),
490
492
  )
491
493
  else:
492
494
  raise ValueError(f"Unknown ResultHandler Class: {result_type}")
@@ -874,7 +876,7 @@ class ConfigManager(object):
874
876
  target_column_ibis_type: dt.DataType,
875
877
  margin: int = 0,
876
878
  ) -> bool:
877
- """Identifies Decimal columns that will cause problems in a Pandas Dataframe.
879
+ """Identifies numeric columns that will cause problems in a Pandas Dataframe.
878
880
 
879
881
  i.e. are of greater precision than a 64bit int/real can hold.
880
882
 
@@ -883,17 +885,23 @@ class ConfigManager(object):
883
885
  """
884
886
  return bool(
885
887
  (
886
- isinstance(source_column_ibis_type, dt.Decimal)
887
- and (
888
- source_column_ibis_type.precision is None
889
- or source_column_ibis_type.precision > (18 - margin)
888
+ (isinstance(source_column_ibis_type, dt.Int64) and margin > 0)
889
+ or (
890
+ isinstance(source_column_ibis_type, dt.Decimal)
891
+ and (
892
+ source_column_ibis_type.precision is None
893
+ or source_column_ibis_type.precision > (18 - margin)
894
+ )
890
895
  )
891
896
  )
892
897
  and (
893
- isinstance(target_column_ibis_type, dt.Decimal)
894
- and (
895
- target_column_ibis_type.precision is None
896
- or target_column_ibis_type.precision > (18 - margin)
898
+ (isinstance(target_column_ibis_type, dt.Int64) and margin > 0)
899
+ or (
900
+ isinstance(target_column_ibis_type, dt.Decimal)
901
+ and (
902
+ target_column_ibis_type.precision is None
903
+ or target_column_ibis_type.precision > (18 - margin)
904
+ )
897
905
  )
898
906
  )
899
907
  )
@@ -56,6 +56,8 @@ CONFIG_CASE_INSENSITIVE_MATCH = "case_insensitive_match"
56
56
  CONFIG_ROW_CONCAT = "concat"
57
57
  CONFIG_ROW_HASH = "hash"
58
58
  CONFIG_RUN_ID = "run_id"
59
+ CONFIG_START_TIME = "start_time"
60
+ CONFIG_END_TIME = "end_time"
59
61
  CONFIG_SOURCE_COLUMN = "source_column"
60
62
  CONFIG_TARGET_COLUMN = "target_column"
61
63
  CONFIG_THRESHOLD = "threshold"
@@ -142,12 +144,21 @@ TARGET_AGG_VALUE = "target_agg_value"
142
144
  VALIDATION_STATUS = "validation_status"
143
145
  VALIDATION_STATUS_SUCCESS = "success"
144
146
  VALIDATION_STATUS_FAIL = "fail"
145
- # TODO: update if we start to support other statuses
146
147
  VALIDATION_STATUSES = [
147
148
  VALIDATION_STATUS_SUCCESS,
148
149
  VALIDATION_STATUS_FAIL,
149
150
  ]
150
151
 
152
+ # Summary stats of Row Validation results
153
+ TOTAL_SOURCE_ROWS = "total_source_rows"
154
+ TOTAL_TARGET_ROWS = "total_target_rows"
155
+ TOTAL_ROWS_VALIDATED = "total_rows_validated"
156
+ TOTAL_ROWS_SUCCESS = "total_rows_success_validation_status"
157
+ TOTAL_ROWS_FAIL = "total_rows_fail_validation_status"
158
+ FAILED_SOURCE_NOT_IN_TARGET = "failed_rows_present_in_source_not_in_target"
159
+ FAILED_TARGET_NOT_IN_SOURCE = "failed_rows_present_in_target_not_in_source"
160
+ FAILED_PRESENT_IN_BOTH_TABLES = "failed_rows_present_in_both_source_and_target"
161
+
151
162
  # SQL Template Formatting
152
163
  # TODO: should this be managed in query_builder if that is the only place its used?
153
164
  COUNT_STAR = "{count_star}"
@@ -156,29 +167,36 @@ COUNT_STAR = "{count_star}"
156
167
  RESULT_TYPE_SOURCE = "source"
157
168
  RESULT_TYPE_TARGET = "target"
158
169
 
159
- FORMAT_TYPES = ["csv", "json", "table", "text"]
170
+ FORMAT_TYPE_CSV = "csv"
171
+ FORMAT_TYPE_JSON = "json"
172
+ FORMAT_TYPE_MINIMAL = "minimal"
173
+ FORMAT_TYPE_PYTHON = "python"
174
+ FORMAT_TYPE_TABLE = "table"
175
+ FORMAT_TYPE_TEXT = "text"
176
+ FORMAT_TYPES = [FORMAT_TYPE_CSV, FORMAT_TYPE_JSON, FORMAT_TYPE_TABLE, FORMAT_TYPE_TEXT]
177
+ RAW_QUERY_FORMAT_TYPES = [FORMAT_TYPE_MINIMAL, FORMAT_TYPE_PYTHON]
160
178
 
161
179
  # Text Result Handler column filter list
162
180
  COLUMN_FILTER_LIST = [
163
- "aggregation_type",
164
- "end_time",
165
- "labels",
181
+ AGGREGATION_TYPE,
182
+ CONFIG_END_TIME,
183
+ CONFIG_LABELS,
166
184
  "pct_threshold",
167
- "start_time",
168
- "target_table_name",
169
- "target_column_name",
185
+ CONFIG_START_TIME,
186
+ TARGET_TABLE_NAME,
187
+ TARGET_COLUMN_NAME,
170
188
  "difference",
171
- "primary_keys",
172
- "group_by_columns",
189
+ CONFIG_PRIMARY_KEYS,
190
+ GROUP_BY_COLUMNS,
173
191
  "num_random_rows",
174
192
  ]
175
193
  SCHEMA_VALIDATION_COLUMN_FILTER_LIST = [
176
- "start_time",
177
- "end_time",
178
- "aggregation_type",
194
+ CONFIG_START_TIME,
195
+ CONFIG_END_TIME,
196
+ AGGREGATION_TYPE,
179
197
  "difference",
180
- "primary_keys",
181
- "group_by_columns",
198
+ CONFIG_PRIMARY_KEYS,
199
+ GROUP_BY_COLUMNS,
182
200
  "num_random_rows",
183
201
  "pct_threshold",
184
202
  ]
@@ -353,7 +353,10 @@ class DataValidation(object):
353
353
  target_df = futures[1].result()
354
354
 
355
355
  pandas_client = ibis.pandas.connect(
356
- {combiner.DEFAULT_SOURCE: source_df, combiner.DEFAULT_TARGET: target_df}
356
+ {
357
+ consts.RESULT_TYPE_SOURCE: source_df,
358
+ consts.RESULT_TYPE_TARGET: target_df,
359
+ }
357
360
  )
358
361
 
359
362
  try:
@@ -362,8 +365,8 @@ class DataValidation(object):
362
365
  combiner.generate_report,
363
366
  pandas_client,
364
367
  self.run_metadata,
365
- pandas_client.table(combiner.DEFAULT_SOURCE),
366
- pandas_client.table(combiner.DEFAULT_TARGET),
368
+ pandas_client.table(consts.RESULT_TYPE_SOURCE),
369
+ pandas_client.table(consts.RESULT_TYPE_TARGET),
367
370
  join_on_fields=join_on_fields,
368
371
  is_value_comparison=is_value_comparison,
369
372
  verbose=self.verbose,