google-pso-data-validator 7.6.0__tar.gz → 7.6.1__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 (92) hide show
  1. {google_pso_data_validator-7.6.0/google_pso_data_validator.egg-info → google_pso_data_validator-7.6.1}/PKG-INFO +1 -1
  2. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/data_validation/combiner.py +12 -12
  3. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/data_validation/config_manager.py +19 -13
  4. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/data_validation/consts.py +17 -0
  5. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/data_validation/query_builder/query_builder.py +9 -1
  6. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/data_validation/validation_builder.py +63 -11
  7. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1/google_pso_data_validator.egg-info}/PKG-INFO +1 -1
  8. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/setup.py +1 -1
  9. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/third_party/ibis/ibis_addon/operations.py +19 -1
  10. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/third_party/ibis/ibis_postgres/client.py +7 -0
  11. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/third_party/ibis/ibis_postgres/registry.py +9 -0
  12. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/LICENSE +0 -0
  13. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/README.md +0 -0
  14. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/data_validation/__init__.py +0 -0
  15. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/data_validation/__main__.py +0 -0
  16. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/data_validation/app.py +0 -0
  17. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/data_validation/cli_tools.py +0 -0
  18. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/data_validation/client_info.py +0 -0
  19. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/data_validation/clients.py +0 -0
  20. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/data_validation/data_validation.py +0 -0
  21. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/data_validation/exceptions.py +0 -0
  22. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/data_validation/find_tables.py +0 -0
  23. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/data_validation/gcs_helper.py +0 -0
  24. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/data_validation/jellyfish_distance.py +0 -0
  25. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/data_validation/metadata.py +0 -0
  26. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/data_validation/partition_builder.py +0 -0
  27. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/data_validation/query_builder/__init__.py +0 -0
  28. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/data_validation/query_builder/partition_row_builder.py +0 -0
  29. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/data_validation/query_builder/random_row_builder.py +0 -0
  30. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/data_validation/raw_query.py +0 -0
  31. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/data_validation/result_handlers/__init__.py +0 -0
  32. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/data_validation/result_handlers/base_backend.py +0 -0
  33. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/data_validation/result_handlers/bigquery.py +0 -0
  34. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/data_validation/result_handlers/factory.py +0 -0
  35. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/data_validation/result_handlers/postgres.py +0 -0
  36. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/data_validation/result_handlers/text.py +0 -0
  37. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/data_validation/schema_validation.py +0 -0
  38. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/data_validation/secret_manager.py +0 -0
  39. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/data_validation/state_manager.py +0 -0
  40. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/data_validation/util.py +0 -0
  41. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/google_pso_data_validator.egg-info/SOURCES.txt +0 -0
  42. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/google_pso_data_validator.egg-info/dependency_links.txt +0 -0
  43. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/google_pso_data_validator.egg-info/entry_points.txt +0 -0
  44. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/google_pso_data_validator.egg-info/requires.txt +0 -0
  45. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/google_pso_data_validator.egg-info/top_level.txt +0 -0
  46. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/setup.cfg +0 -0
  47. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/third_party/ibis/ibis_addon/__init__.py +0 -0
  48. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/third_party/ibis/ibis_addon/api.py +0 -0
  49. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/third_party/ibis/ibis_biquery/__init__.py +0 -0
  50. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/third_party/ibis/ibis_biquery/api.py +0 -0
  51. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/third_party/ibis/ibis_cloud_spanner/__init__.py +0 -0
  52. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/third_party/ibis/ibis_cloud_spanner/api.py +0 -0
  53. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/third_party/ibis/ibis_cloud_spanner/client.py +0 -0
  54. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/third_party/ibis/ibis_cloud_spanner/compiler.py +0 -0
  55. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/third_party/ibis/ibis_cloud_spanner/datatypes.py +0 -0
  56. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/third_party/ibis/ibis_cloud_spanner/registry.py +0 -0
  57. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/third_party/ibis/ibis_cloud_spanner/tests/__init__.py +0 -0
  58. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/third_party/ibis/ibis_cloud_spanner/tests/conftest.py +0 -0
  59. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/third_party/ibis/ibis_cloud_spanner/to_pandas.py +0 -0
  60. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/third_party/ibis/ibis_db2/__init__.py +0 -0
  61. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/third_party/ibis/ibis_db2/api.py +0 -0
  62. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/third_party/ibis/ibis_db2/compiler.py +0 -0
  63. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/third_party/ibis/ibis_db2/datatypes.py +0 -0
  64. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/third_party/ibis/ibis_db2/registry.py +0 -0
  65. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/third_party/ibis/ibis_impala/__init__.py +0 -0
  66. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/third_party/ibis/ibis_impala/api.py +0 -0
  67. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/third_party/ibis/ibis_mssql/__init__.py +0 -0
  68. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/third_party/ibis/ibis_mssql/api.py +0 -0
  69. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/third_party/ibis/ibis_mssql/datatypes.py +0 -0
  70. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/third_party/ibis/ibis_mssql/registry.py +0 -0
  71. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/third_party/ibis/ibis_mysql/__init__.py +0 -0
  72. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/third_party/ibis/ibis_mysql/base_sql_compiler/__init__.py +0 -0
  73. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/third_party/ibis/ibis_mysql/base_sql_compiler/select_builder.py +0 -0
  74. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/third_party/ibis/ibis_mysql/compiler.py +0 -0
  75. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/third_party/ibis/ibis_oracle/__init__.py +0 -0
  76. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/third_party/ibis/ibis_oracle/api.py +0 -0
  77. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/third_party/ibis/ibis_oracle/compiler.py +0 -0
  78. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/third_party/ibis/ibis_oracle/datatypes.py +0 -0
  79. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/third_party/ibis/ibis_oracle/registry.py +0 -0
  80. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/third_party/ibis/ibis_postgres/__init__.py +0 -0
  81. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/third_party/ibis/ibis_postgres/datatypes.py +0 -0
  82. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/third_party/ibis/ibis_redshift/__init__.py +0 -0
  83. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/third_party/ibis/ibis_redshift/api.py +0 -0
  84. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/third_party/ibis/ibis_redshift/compiler.py +0 -0
  85. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/third_party/ibis/ibis_snowflake/__init__.py +0 -0
  86. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/third_party/ibis/ibis_snowflake/api.py +0 -0
  87. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/third_party/ibis/ibis_snowflake/datatypes.py +0 -0
  88. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/third_party/ibis/ibis_teradata/__init__.py +0 -0
  89. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/third_party/ibis/ibis_teradata/api.py +0 -0
  90. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/third_party/ibis/ibis_teradata/compiler.py +0 -0
  91. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/third_party/ibis/ibis_teradata/datatypes.py +0 -0
  92. {google_pso_data_validator-7.6.0 → google_pso_data_validator-7.6.1}/third_party/ibis/ibis_teradata/registry.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: google-pso-data-validator
3
- Version: 7.6.0
3
+ Version: 7.6.1
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
@@ -406,18 +406,18 @@ def _pivot_result(
406
406
  consts.AGGREGATION_TYPE
407
407
  ),
408
408
  ibis.literal(validation.get_table_name(result_type)).name(
409
- "table_name"
409
+ consts.COMBINER_TABLE_NAME
410
410
  ),
411
411
  # Cast to string to ensure types match, even when column
412
412
  # name is NULL (such as for count aggregations).
413
413
  ibis.literal(validation.get_column_name(result_type))
414
414
  .cast("string")
415
- .name("column_name"),
415
+ .name(consts.COMBINER_COLUMN_NAME),
416
416
  primary_keys,
417
417
  ibis.literal(validation.num_random_rows).name(
418
418
  consts.NUM_RANDOM_ROWS
419
419
  ),
420
- result[field].cast("string").name("agg_value"),
420
+ result[field].cast("string").name(consts.COMBINER_AGG_VALUE),
421
421
  )
422
422
  + join_on_fields
423
423
  )
@@ -469,11 +469,11 @@ def _join_pivots(
469
469
  + [
470
470
  source[consts.VALIDATION_TYPE],
471
471
  source[consts.AGGREGATION_TYPE],
472
- source[consts.CONFIG_TABLE_NAME],
473
- source["column_name"],
472
+ source[consts.COMBINER_TABLE_NAME],
473
+ source[consts.COMBINER_COLUMN_NAME],
474
474
  source[consts.CONFIG_PRIMARY_KEYS],
475
475
  source[consts.NUM_RANDOM_ROWS],
476
- source["agg_value"],
476
+ source[consts.COMBINER_AGG_VALUE],
477
477
  differences[consts.VALIDATION_DIFFERENCE],
478
478
  differences[consts.VALIDATION_PCT_DIFFERENCE],
479
479
  differences[consts.VALIDATION_PCT_THRESHOLD],
@@ -488,12 +488,12 @@ def _join_pivots(
488
488
  source_difference[consts.AGGREGATION_TYPE]
489
489
  .fillna(target[consts.AGGREGATION_TYPE])
490
490
  .name(consts.AGGREGATION_TYPE),
491
- source_difference["table_name"].name(consts.SOURCE_TABLE_NAME),
492
- source_difference["column_name"].name(consts.SOURCE_COLUMN_NAME),
493
- source_difference["agg_value"].name(consts.SOURCE_AGG_VALUE),
494
- target["table_name"].name(consts.TARGET_TABLE_NAME),
495
- target["column_name"].name(consts.TARGET_COLUMN_NAME),
496
- target["agg_value"].name(consts.TARGET_AGG_VALUE),
491
+ source_difference[consts.COMBINER_TABLE_NAME].name(consts.SOURCE_TABLE_NAME),
492
+ source_difference[consts.COMBINER_COLUMN_NAME].name(consts.SOURCE_COLUMN_NAME),
493
+ source_difference[consts.COMBINER_AGG_VALUE].name(consts.SOURCE_AGG_VALUE),
494
+ target[consts.COMBINER_TABLE_NAME].name(consts.TARGET_TABLE_NAME),
495
+ target[consts.COMBINER_COLUMN_NAME].name(consts.TARGET_COLUMN_NAME),
496
+ target[consts.COMBINER_AGG_VALUE].name(consts.TARGET_AGG_VALUE),
497
497
  group_by_columns,
498
498
  source_difference[consts.CONFIG_PRIMARY_KEYS],
499
499
  source_difference[consts.NUM_RANDOM_ROWS],
@@ -611,7 +611,7 @@ class ConfigManager(object):
611
611
  self.build_config_calculated_fields(
612
612
  [casefold_source_columns[field.casefold()]],
613
613
  [casefold_target_columns[field.casefold()]],
614
- "rstrip",
614
+ consts.CALC_FIELD_RSTRIP,
615
615
  alias,
616
616
  0,
617
617
  )
@@ -812,16 +812,16 @@ class ConfigManager(object):
812
812
  consts.CONFIG_FIELD_ALIAS
813
813
  ]
814
814
  depth = 1
815
- calc_func = "length"
815
+ calc_func = consts.CALC_FIELD_LENGTH
816
816
  elif column_type in ["string", "!string"]:
817
- calc_func = "length"
817
+ calc_func = consts.CALC_FIELD_LENGTH
818
818
 
819
819
  elif self._is_uuid(column_type, target_column_type):
820
820
  calc_func = consts.CONFIG_CAST
821
821
  cast_type = consts.CONFIG_CAST_UUID_STRING
822
822
 
823
823
  elif column_type in ["binary", "!binary"]:
824
- calc_func = "byte_length"
824
+ calc_func = consts.CALC_FIELD_BYTE_LENGTH
825
825
 
826
826
  elif column_type in ["timestamp", "!timestamp", "date", "!date"]:
827
827
  if (
@@ -841,7 +841,7 @@ class ConfigManager(object):
841
841
  ]
842
842
  depth = 1
843
843
 
844
- calc_func = "epoch_seconds"
844
+ calc_func = consts.CALC_FIELD_EPOCH_SECONDS
845
845
  if agg_type == consts.CONFIG_TYPE_SUM:
846
846
  # It is possible to exceed int64 when summing epoch_seconds therefore cast to string.
847
847
  # See issue 1391 for details.
@@ -1138,7 +1138,7 @@ class ConfigManager(object):
1138
1138
  target_table: "ibis.expr.types.Table",
1139
1139
  ) -> dict:
1140
1140
  """Mutates col_config to contain any overrides. Also returns col_config for convenience."""
1141
- if col_config["calc_type"] != "cast":
1141
+ if col_config["calc_type"] != consts.CALC_FIELD_CAST:
1142
1142
  return col_config
1143
1143
 
1144
1144
  source_table_schema = {k: v for k, v in source_table.schema().items()}
@@ -1186,13 +1186,19 @@ class ConfigManager(object):
1186
1186
 
1187
1187
  def _get_order_of_operations(self, calc_type: str) -> List[str]:
1188
1188
  """Return order of operations for row validation."""
1189
- order_of_operations = ["cast", "ifnull", "rstrip"]
1189
+ order_of_operations = [
1190
+ consts.CALC_FIELD_CAST,
1191
+ consts.CALC_FIELD_IFNULL,
1192
+ consts.CALC_FIELD_RSTRIP,
1193
+ ]
1190
1194
  if self.case_insensitive_match():
1191
- order_of_operations.append("upper")
1192
- if calc_type == "hash":
1193
- order_of_operations.extend(["concat", "hash"])
1194
- elif calc_type == "concat":
1195
- order_of_operations.append("concat")
1195
+ order_of_operations.append(consts.CALC_FIELD_UPPER)
1196
+ if calc_type == consts.CALC_FIELD_HASH:
1197
+ order_of_operations.extend(
1198
+ [consts.CALC_FIELD_CONCAT, consts.CALC_FIELD_HASH]
1199
+ )
1200
+ elif calc_type == consts.CALC_FIELD_CONCAT:
1201
+ order_of_operations.append(consts.CALC_FIELD_CONCAT)
1196
1202
 
1197
1203
  return order_of_operations
1198
1204
 
@@ -1241,7 +1247,7 @@ class ConfigManager(object):
1241
1247
  previous_level = [x for x in casefold_source_columns.keys()]
1242
1248
  else:
1243
1249
  previous_level = [k for k, v in column_aliases.items() if v == i - 1]
1244
- if calc in ["concat", "hash"]:
1250
+ if calc in [consts.CALC_FIELD_CONCAT, consts.CALC_FIELD_HASH]:
1245
1251
  col = {}
1246
1252
  col["source_reference"] = previous_level
1247
1253
  col["target_reference"] = previous_level
@@ -181,6 +181,11 @@ FAILED_SOURCE_NOT_IN_TARGET = "failed_rows_present_in_source_not_in_target"
181
181
  FAILED_TARGET_NOT_IN_SOURCE = "failed_rows_present_in_target_not_in_source"
182
182
  FAILED_PRESENT_IN_BOTH_TABLES = "failed_rows_present_in_both_source_and_target"
183
183
 
184
+ # Combiner only constants
185
+ COMBINER_TABLE_NAME = "dvt_table_name"
186
+ COMBINER_COLUMN_NAME = "dvt_column_name"
187
+ COMBINER_AGG_VALUE = "dvt_agg_value"
188
+
184
189
  # SQL Template Formatting
185
190
  # TODO: should this be managed in query_builder if that is the only place its used?
186
191
  COUNT_STAR = "{count_star}"
@@ -239,3 +244,15 @@ MAX_CONCAT_COLUMNS_DEFAULTS = {
239
244
  # Minimizing risk of: [Error 3556] Too many columns defined for this table.
240
245
  "teradata": 500,
241
246
  }
247
+
248
+ # CalculatedField expression constants.
249
+ CALC_FIELD_CAST = "cast"
250
+ CALC_FIELD_CONCAT = "concat"
251
+ CALC_FIELD_BYTE_LENGTH = "byte_length"
252
+ CALC_FIELD_EPOCH_SECONDS = "epoch_seconds"
253
+ CALC_FIELD_HASH = "hash"
254
+ CALC_FIELD_IFNULL = "ifnull"
255
+ CALC_FIELD_LENGTH = "length"
256
+ CALC_FIELD_PADDED_CHAR_LENGTH = "padded_char_length"
257
+ CALC_FIELD_RSTRIP = "rstrip"
258
+ CALC_FIELD_UPPER = "upper"
@@ -358,6 +358,14 @@ class CalculatedField(object):
358
358
  fields,
359
359
  )
360
360
 
361
+ @staticmethod
362
+ def padded_char_length(config, fields):
363
+ return CalculatedField(
364
+ ibis.expr.types.StringValue.padded_char_length,
365
+ config,
366
+ fields,
367
+ )
368
+
361
369
  @staticmethod
362
370
  def byte_length(config, fields):
363
371
  return CalculatedField(
@@ -403,7 +411,7 @@ class CalculatedField(object):
403
411
  @staticmethod
404
412
  def custom(config, fields):
405
413
  """Returns a CalculatedField instance built for any custom ibis expression
406
- i.e. 'ibis.expr.api.StringValue.replace'. For a list of supported functions,
414
+ e.g. 'ibis.expr.api.StringValue.replace'. For a list of supported functions,
407
415
  see https://github.com/ibis-project/ibis/blob/1.4.0/ibis/expr/api.py
408
416
  Args:
409
417
  expr (Str): A custom ibis expression to be used as a calc field
@@ -11,8 +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
- import logging
15
14
  from copy import deepcopy
15
+ import logging
16
+ from typing import TYPE_CHECKING
16
17
 
17
18
  from data_validation import consts, metadata
18
19
  from data_validation.clients import get_max_in_list_size
@@ -25,6 +26,9 @@ from data_validation.query_builder.query_builder import (
25
26
  QueryBuilder,
26
27
  )
27
28
 
29
+ if TYPE_CHECKING:
30
+ import ibis
31
+
28
32
 
29
33
  def list_to_sublists(id_list: list, max_size: int) -> list:
30
34
  """Return a list of items as a list of lists based on a max list length of max_size."""
@@ -87,6 +91,23 @@ class ValidationBuilder(object):
87
91
  else:
88
92
  return FilterField.isin(column_name, in_list)
89
93
 
94
+ def _is_padded_char(
95
+ self,
96
+ client: "ibis.backends.base.BaseBackend",
97
+ raw_column_metadata: dict,
98
+ column_name: str,
99
+ ) -> bool:
100
+ # Clients only need to implement _is_char_type_padded method if they support padded strings.
101
+ return (
102
+ (
103
+ client.is_char_type_padded(raw_column_metadata[column_name])
104
+ if raw_column_metadata.get(column_name)
105
+ else False
106
+ )
107
+ if hasattr(client, "is_char_type_padded") and raw_column_metadata
108
+ else False
109
+ )
110
+
90
111
  def clone(self):
91
112
  cloned_builder = ValidationBuilder(self.config_manager)
92
113
 
@@ -347,32 +368,63 @@ class ValidationBuilder(object):
347
368
  threshold=self.config_manager.threshold,
348
369
  )
349
370
 
350
- def add_calc(self, calc_field):
371
+ def _get_calc_type(
372
+ self,
373
+ calc_field: dict,
374
+ column_name: str,
375
+ client: "ibis.backends.base.BaseBackend",
376
+ raw_data_types: dict,
377
+ ) -> str:
378
+ if calc_field[
379
+ consts.CONFIG_TYPE
380
+ ] == consts.CALC_FIELD_LENGTH and self._is_padded_char(
381
+ client,
382
+ raw_data_types,
383
+ column_name,
384
+ ):
385
+ calc_type = consts.CALC_FIELD_PADDED_CHAR_LENGTH
386
+ else:
387
+ calc_type = calc_field[consts.CONFIG_TYPE]
388
+ # Check if valid calc field and return correct object.
389
+ if not hasattr(CalculatedField, calc_type):
390
+ raise Exception("Unknown Calculation Type: {}".format(calc_type))
391
+ return calc_type
392
+
393
+ def add_calc(self, calc_field: dict):
351
394
  """Add CalculatedField to Queries
352
395
 
353
396
  Args:
354
397
  calc_field (Dict): An object with source, target, and cast info
355
398
  """
356
- # prepare source and target payloads
399
+ # Prepare source and target payloads
357
400
  source_config = deepcopy(calc_field)
358
401
  source_fields = calc_field[consts.CONFIG_CALCULATED_SOURCE_COLUMNS]
359
402
  target_config = deepcopy(calc_field)
360
403
  target_fields = calc_field[consts.CONFIG_CALCULATED_TARGET_COLUMNS]
361
- # grab calc field metadata
404
+ # Grab calc field metadata
362
405
  alias = calc_field[consts.CONFIG_FIELD_ALIAS]
363
- calc_type = calc_field[consts.CONFIG_TYPE]
364
- # check if valid calc field and return correct object
365
- if not hasattr(CalculatedField, calc_type):
366
- raise Exception("Unknown Calculation Type: {}".format(calc_type))
367
- source_field = getattr(CalculatedField, calc_type)(
406
+ source_calc_type = self._get_calc_type(
407
+ calc_field,
408
+ source_fields[0],
409
+ self.source_client,
410
+ self.config_manager.get_source_raw_data_types(),
411
+ )
412
+ target_calc_type = self._get_calc_type(
413
+ calc_field,
414
+ target_fields[0],
415
+ self.target_client,
416
+ self.config_manager.get_target_raw_data_types(),
417
+ )
418
+
419
+ source_field = getattr(CalculatedField, source_calc_type)(
368
420
  config=source_config, fields=source_fields
369
421
  )
370
- target_field = getattr(CalculatedField, calc_type)(
422
+ target_field = getattr(CalculatedField, target_calc_type)(
371
423
  config=target_config, fields=target_fields
372
424
  )
373
425
  self.source_builder.add_calculated_field(source_field)
374
426
  self.target_builder.add_calculated_field(target_field)
375
- # register calc field under alias
427
+ # Register calc field under alias
376
428
  self.calculated_aliases[alias] = calc_field
377
429
 
378
430
  def get_source_query(self):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: google-pso-data-validator
3
- Version: 7.6.0
3
+ Version: 7.6.1
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
@@ -18,7 +18,7 @@ import setuptools
18
18
 
19
19
  name = "google-pso-data-validator"
20
20
  description = "A package to enable easy data validation"
21
- version = "7.6.0"
21
+ version = "7.6.1"
22
22
  release_status = "Development Status :: 4 - Beta"
23
23
 
24
24
  with open("README.md", "r") as fh:
@@ -58,7 +58,7 @@ from ibis.backends.mysql.compiler import MySQLExprTranslator
58
58
  from ibis.backends.pandas.dispatch import execute_node
59
59
  from ibis.backends.pandas.execution.temporal import execute_epoch_seconds
60
60
  from ibis.backends.postgres.compiler import PostgreSQLExprTranslator
61
- from ibis.expr.types import BinaryValue, NumericValue, TemporalValue
61
+ from ibis.expr.types import BinaryValue, NumericValue, StringValue, TemporalValue
62
62
 
63
63
  # Do not remove these lines, they trigger patching of Ibis code.
64
64
  import third_party.ibis.ibis_biquery.api # noqa
@@ -106,6 +106,12 @@ class BinaryLength(ops.Value):
106
106
  output_shape = rlz.shape_like("arg")
107
107
 
108
108
 
109
+ class PaddedCharLength(ops.Value):
110
+ arg = rlz.one_of([rlz.value(dt.String)])
111
+ output_dtype = dt.int32
112
+ output_shape = rlz.shape_like("arg")
113
+
114
+
109
115
  class ToChar(ops.Value):
110
116
  arg = rlz.one_of(
111
117
  [
@@ -128,6 +134,10 @@ def compile_binary_length(binary_value):
128
134
  return BinaryLength(binary_value).to_expr()
129
135
 
130
136
 
137
+ def compile_padded_char_length(char_value):
138
+ return PaddedCharLength(char_value).to_expr()
139
+
140
+
131
141
  def compile_to_char(numeric_value, fmt):
132
142
  return ToChar(numeric_value, fmt=fmt).to_expr()
133
143
 
@@ -598,6 +608,8 @@ execute_epoch_seconds = execute_epoch_seconds_new
598
608
 
599
609
  BinaryValue.byte_length = compile_binary_length
600
610
 
611
+ StringValue.padded_char_length = compile_padded_char_length
612
+
601
613
  NumericValue.to_char = compile_to_char
602
614
  TemporalValue.to_char = compile_to_char
603
615
 
@@ -614,6 +626,8 @@ AlchemyExprTranslator._registry[RawSQL] = format_raw_sql
614
626
  AlchemyExprTranslator._registry[ops.HashBytes] = format_hashbytes_alchemy
615
627
  ExprTranslator._registry[RawSQL] = format_raw_sql
616
628
  ExprTranslator._registry[ops.HashBytes] = format_hashbytes_base
629
+ # Base length of padded string is the same as for a standard string.
630
+ ExprTranslator._registry[PaddedCharLength] = ExprTranslator._registry[ops.StringLength]
617
631
 
618
632
  ImpalaExprTranslator._registry[ops.Cast] = sa_cast_hive
619
633
  ImpalaExprTranslator._registry[RawSQL] = format_raw_sql
@@ -639,6 +653,10 @@ PostgreSQLExprTranslator._registry[BinaryLength] = sa_format_binary_length
639
653
  PostgreSQLExprTranslator._registry[
640
654
  ops.ExtractEpochSeconds
641
655
  ] = postgres_registry.sa_epoch_seconds
656
+ PostgreSQLExprTranslator._registry[
657
+ PaddedCharLength
658
+ ] = postgres_registry.sa_format_postgres_padded_char_length
659
+
642
660
 
643
661
  MsSqlExprTranslator._registry[ops.HashBytes] = mssql_registry.sa_format_hashbytes
644
662
  MsSqlExprTranslator._registry[RawSQL] = sa_format_raw_sql
@@ -207,8 +207,15 @@ def _raw_column_metadata(
207
207
  )
208
208
 
209
209
 
210
+ def _is_char_type_padded(self, char_type: Tuple) -> bool:
211
+ """Define this method if the backend supports character/string types that are padded and returns
212
+ padded values, which DVT may want to trim"""
213
+ return char_type[0] == "character"
214
+
215
+
210
216
  PostgresBackend._metadata = _metadata
211
217
  PostgresBackend.list_databases = list_schemas
212
218
  PostgresBackend.do_connect = do_connect
213
219
  PostgresBackend.list_primary_key_columns = _list_primary_key_columns
214
220
  PostgresBackend.raw_column_metadata = _raw_column_metadata
221
+ PostgresBackend.is_char_type_padded = _is_char_type_padded
@@ -32,3 +32,12 @@ def sa_epoch_seconds(translator, op):
32
32
  sa.extract("epoch", sa.func.date_trunc(sa.sql.literal_column("'second'"), arg)),
33
33
  sa.BIGINT,
34
34
  )
35
+
36
+
37
+ def sa_format_postgres_padded_char_length(translator, op):
38
+ """Inject concat of empty string into length(bpchar) to protect trailing spaces.
39
+
40
+ Without this workaround the bpchar value is implicitly cast to varchar and loses trailing spaces.
41
+ """
42
+ arg = translator.translate(op.arg)
43
+ return sa.func.char_length(sa.func.concat(arg, sa.text("''")))