google-pso-data-validator 8.5.0__tar.gz → 8.5.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. {google_pso_data_validator-8.5.0/google_pso_data_validator.egg-info → google_pso_data_validator-8.5.2}/PKG-INFO +1 -1
  2. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/data_validation/combiner.py +7 -5
  3. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/data_validation/config_manager.py +66 -11
  4. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2/google_pso_data_validator.egg-info}/PKG-INFO +1 -1
  5. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/setup.py +1 -1
  6. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/third_party/ibis/ibis_addon/api.py +10 -1
  7. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/third_party/ibis/ibis_addon/operations.py +1 -1
  8. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/third_party/ibis/ibis_db2/__init__.py +61 -14
  9. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/third_party/ibis/ibis_db2/datatypes.py +7 -2
  10. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/third_party/ibis/ibis_db2/registry.py +38 -7
  11. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/third_party/ibis/ibis_redshift/__init__.py +9 -7
  12. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/third_party/ibis/ibis_sybase/registry.py +1 -1
  13. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/LICENSE +0 -0
  14. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/README.md +0 -0
  15. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/data_validation/__init__.py +0 -0
  16. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/data_validation/__main__.py +0 -0
  17. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/data_validation/app.py +0 -0
  18. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/data_validation/cli_tools.py +0 -0
  19. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/data_validation/client_info.py +0 -0
  20. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/data_validation/clients.py +0 -0
  21. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/data_validation/consts.py +0 -0
  22. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/data_validation/data_validation.py +0 -0
  23. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/data_validation/exceptions.py +0 -0
  24. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/data_validation/find_tables.py +0 -0
  25. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/data_validation/gcs_helper.py +0 -0
  26. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/data_validation/jellyfish_distance.py +0 -0
  27. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/data_validation/metadata.py +0 -0
  28. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/data_validation/partition_builder.py +0 -0
  29. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/data_validation/query_builder/__init__.py +0 -0
  30. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/data_validation/query_builder/partition_row_builder.py +0 -0
  31. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/data_validation/query_builder/query_builder.py +0 -0
  32. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/data_validation/query_builder/random_row_builder.py +0 -0
  33. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/data_validation/raw_query.py +0 -0
  34. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/data_validation/result_handlers/__init__.py +0 -0
  35. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/data_validation/result_handlers/base_backend.py +0 -0
  36. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/data_validation/result_handlers/bigquery.py +0 -0
  37. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/data_validation/result_handlers/factory.py +0 -0
  38. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/data_validation/result_handlers/postgres.py +0 -0
  39. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/data_validation/result_handlers/text.py +0 -0
  40. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/data_validation/schema_validation.py +0 -0
  41. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/data_validation/secret_manager.py +0 -0
  42. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/data_validation/state_manager.py +0 -0
  43. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/data_validation/util.py +0 -0
  44. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/data_validation/validation_builder.py +0 -0
  45. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/google_pso_data_validator.egg-info/SOURCES.txt +0 -0
  46. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/google_pso_data_validator.egg-info/dependency_links.txt +0 -0
  47. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/google_pso_data_validator.egg-info/entry_points.txt +0 -0
  48. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/google_pso_data_validator.egg-info/requires.txt +0 -0
  49. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/google_pso_data_validator.egg-info/top_level.txt +0 -0
  50. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/setup.cfg +0 -0
  51. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/third_party/ibis/ibis_addon/__init__.py +0 -0
  52. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/third_party/ibis/ibis_bigquery/__init__.py +0 -0
  53. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/third_party/ibis/ibis_bigquery/api.py +0 -0
  54. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/third_party/ibis/ibis_bigquery/registry.py +0 -0
  55. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/third_party/ibis/ibis_cloud_spanner/__init__.py +0 -0
  56. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/third_party/ibis/ibis_cloud_spanner/api.py +0 -0
  57. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/third_party/ibis/ibis_cloud_spanner/client.py +0 -0
  58. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/third_party/ibis/ibis_cloud_spanner/compiler.py +0 -0
  59. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/third_party/ibis/ibis_cloud_spanner/datatypes.py +0 -0
  60. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/third_party/ibis/ibis_cloud_spanner/registry.py +0 -0
  61. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/third_party/ibis/ibis_cloud_spanner/tests/__init__.py +0 -0
  62. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/third_party/ibis/ibis_cloud_spanner/tests/conftest.py +0 -0
  63. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/third_party/ibis/ibis_cloud_spanner/to_pandas.py +0 -0
  64. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/third_party/ibis/ibis_db2/api.py +0 -0
  65. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/third_party/ibis/ibis_db2/compiler.py +0 -0
  66. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/third_party/ibis/ibis_impala/__init__.py +0 -0
  67. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/third_party/ibis/ibis_impala/api.py +0 -0
  68. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/third_party/ibis/ibis_mssql/__init__.py +0 -0
  69. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/third_party/ibis/ibis_mssql/api.py +0 -0
  70. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/third_party/ibis/ibis_mssql/datatypes.py +0 -0
  71. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/third_party/ibis/ibis_mssql/registry.py +0 -0
  72. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/third_party/ibis/ibis_mysql/__init__.py +0 -0
  73. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/third_party/ibis/ibis_mysql/base_sql_compiler/__init__.py +0 -0
  74. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/third_party/ibis/ibis_mysql/base_sql_compiler/select_builder.py +0 -0
  75. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/third_party/ibis/ibis_mysql/compiler.py +0 -0
  76. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/third_party/ibis/ibis_oracle/__init__.py +0 -0
  77. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/third_party/ibis/ibis_oracle/api.py +0 -0
  78. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/third_party/ibis/ibis_oracle/compiler.py +0 -0
  79. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/third_party/ibis/ibis_oracle/datatypes.py +0 -0
  80. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/third_party/ibis/ibis_oracle/registry.py +0 -0
  81. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/third_party/ibis/ibis_postgres/__init__.py +0 -0
  82. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/third_party/ibis/ibis_postgres/client.py +0 -0
  83. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/third_party/ibis/ibis_postgres/datatypes.py +0 -0
  84. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/third_party/ibis/ibis_postgres/registry.py +0 -0
  85. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/third_party/ibis/ibis_redshift/api.py +0 -0
  86. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/third_party/ibis/ibis_redshift/compiler.py +0 -0
  87. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/third_party/ibis/ibis_snowflake/__init__.py +0 -0
  88. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/third_party/ibis/ibis_snowflake/api.py +0 -0
  89. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/third_party/ibis/ibis_snowflake/datatypes.py +0 -0
  90. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/third_party/ibis/ibis_sybase/__init__.py +0 -0
  91. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/third_party/ibis/ibis_sybase/api.py +0 -0
  92. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/third_party/ibis/ibis_sybase/compiler.py +0 -0
  93. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/third_party/ibis/ibis_sybase/datatypes.py +0 -0
  94. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/third_party/ibis/ibis_teradata/__init__.py +0 -0
  95. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/third_party/ibis/ibis_teradata/api.py +0 -0
  96. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/third_party/ibis/ibis_teradata/compiler.py +0 -0
  97. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/third_party/ibis/ibis_teradata/datatypes.py +0 -0
  98. {google_pso_data_validator-8.5.0 → google_pso_data_validator-8.5.2}/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: 8.5.0
3
+ Version: 8.5.2
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
@@ -113,11 +113,11 @@ def generate_report(
113
113
  # Get the first validation metadata object to fill source and/or target empty table names.
114
114
  first = run_metadata.validations[next(iter(run_metadata.validations))]
115
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
116
+ result_df["source_table_name"] = result_df["source_table_name"].fillna(
117
+ first.get_table_name(consts.RESULT_TYPE_SOURCE)
118
118
  )
119
- result_df.target_table_name.fillna(
120
- first.get_table_name(consts.RESULT_TYPE_TARGET), inplace=True
119
+ result_df["target_table_name"] = result_df["target_table_name"].fillna(
120
+ first.get_table_name(consts.RESULT_TYPE_TARGET)
121
121
  )
122
122
 
123
123
  _get_summary(run_metadata, result_df, source_df, target_df)
@@ -194,7 +194,9 @@ def _generate_report_slice(
194
194
  logging.debug(documented.compile())
195
195
 
196
196
  result_df = client.execute(documented)
197
- result_df.validation_status.fillna(consts.VALIDATION_STATUS_FAIL, inplace=True)
197
+ result_df["validation_status"] = result_df["validation_status"].fillna(
198
+ consts.VALIDATION_STATUS_FAIL
199
+ )
198
200
  return result_df
199
201
 
200
202
 
@@ -667,6 +667,27 @@ class ConfigManager(object):
667
667
  isinstance(source_type, dt.UUID) or isinstance(target_type, dt.UUID)
668
668
  )
669
669
 
670
+ def _is_db2_xml(self, source_column_name: str, target_column_name: str) -> bool:
671
+ """Returns True when either source or target column is Oracle LOB data type."""
672
+ return self._is_raw_data_type(
673
+ "db2", source_column_name, target_column_name, ["XML"]
674
+ )
675
+
676
+ def _is_oracle_lob(self, source_column_name: str, target_column_name: str) -> bool:
677
+ """Returns True when either source or target column is Oracle LOB data type.
678
+
679
+ Unexpectedly the raw types for for LOB types are:
680
+ BLOB: LONG_RAW
681
+ CLOB: LONG
682
+ NCLOB: LONG_NVARCHAR
683
+ """
684
+ return self._is_raw_data_type(
685
+ "oracle",
686
+ source_column_name,
687
+ target_column_name,
688
+ ["LONG", "LONG_NVARCHAR", "LONG_RAW"],
689
+ )
690
+
670
691
  def _is_sql_server_text(
671
692
  self, source_column_name: str, target_column_name: str
672
693
  ) -> bool:
@@ -693,19 +714,36 @@ class ConfigManager(object):
693
714
  self, source_column_name: str, target_column_name: str, type_list: List[str]
694
715
  ) -> bool:
695
716
  """Returns True when either source or target column is of a SQL Server type listed in type_list."""
717
+ return self._is_raw_data_type(
718
+ "mssql",
719
+ source_column_name,
720
+ target_column_name,
721
+ type_list,
722
+ )
696
723
 
724
+ def _is_raw_data_type(
725
+ self,
726
+ client_name: str,
727
+ source_column_name: str,
728
+ target_column_name: str,
729
+ type_list: List[str],
730
+ ) -> bool:
731
+ """Returns True when either source or target column is of a client & type."""
697
732
  raw_source_types = self.get_source_raw_data_types()
698
733
  raw_target_types = self.get_target_raw_data_types()
734
+ # Raw data type map uses casefold column name as the key.
699
735
  return bool(
700
736
  (
701
- self.source_client.name == "mssql"
737
+ self.source_client.name == client_name
702
738
  and raw_source_types
703
- and raw_source_types.get(source_column_name, [None])[0] in type_list
739
+ and raw_source_types.get(source_column_name.casefold(), [None])[0]
740
+ in type_list
704
741
  )
705
742
  or (
706
- self.target_client.name == "mssql"
743
+ self.target_client.name == client_name
707
744
  and raw_target_types
708
- and raw_target_types.get(target_column_name, [None])[0] in type_list
745
+ and raw_target_types.get(target_column_name.casefold(), [None])[0]
746
+ in type_list
709
747
  )
710
748
  )
711
749
 
@@ -853,6 +891,7 @@ class ConfigManager(object):
853
891
  ]
854
892
  depth = 1
855
893
  calc_func = consts.CALC_FIELD_LENGTH
894
+
856
895
  elif column_type in ["string", "!string"]:
857
896
  if self._is_sql_server_text(source_column, target_column):
858
897
  calc_func = consts.CALC_FIELD_BYTE_LENGTH
@@ -992,6 +1031,8 @@ class ConfigManager(object):
992
1031
  """Return list of aggregate objects of given agg_type."""
993
1032
 
994
1033
  def require_pre_agg_calc_field(
1034
+ source_column: str,
1035
+ target_column: str,
995
1036
  column_type: str,
996
1037
  target_column_type: str,
997
1038
  agg_type: str,
@@ -1001,8 +1042,15 @@ class ConfigManager(object):
1001
1042
  _ in ["string", "!string", "json", "!json"]
1002
1043
  for _ in [column_type, target_column_type]
1003
1044
  ):
1004
- # These data types are aggregated using their lengths.
1005
- return True
1045
+ # These data types are aggregated using their lengths, except for count().
1046
+ if agg_type == "count":
1047
+ # Oracle LOBs & SQL Server TEXT need a length before the count().
1048
+ # TODO As does Sybase TEXT, see issue-1675.
1049
+ return self._is_oracle_lob(
1050
+ source_column, target_column
1051
+ ) or self._is_sql_server_text(source_column, target_column)
1052
+ else:
1053
+ return True
1006
1054
  elif self._is_uuid(column_type, target_column_type):
1007
1055
  return True
1008
1056
  elif column_type in ["binary", "!binary"]:
@@ -1010,10 +1058,7 @@ class ConfigManager(object):
1010
1058
  # Oracle BLOB is invalid for use with SQL COUNT function.
1011
1059
  # The expression below returns True if client is Oracle which
1012
1060
  # has the effect of triggering use of byte_length transformation.
1013
- return bool(
1014
- self.source_client.name == "oracle"
1015
- or self.target_client.name == "oracle"
1016
- )
1061
+ return self._is_oracle_lob(source_column, target_column)
1017
1062
  else:
1018
1063
  # Convert to length for any min/max/sum on binary columns.
1019
1064
  return True
@@ -1101,9 +1146,19 @@ class ConfigManager(object):
1101
1146
  f"Skipping {agg_type} on {column} due to SQL Server image data type"
1102
1147
  )
1103
1148
  continue
1149
+ elif agg_type != "count" and self._is_db2_xml(column, column):
1150
+ logging.info(
1151
+ f"Skipping {agg_type} on {column} due to Db2 XML data type"
1152
+ )
1153
+ continue
1104
1154
 
1105
1155
  if require_pre_agg_calc_field(
1106
- column_type, target_column_type, agg_type, cast_to_bigint
1156
+ casefold_source_columns[column],
1157
+ casefold_target_columns[column],
1158
+ column_type,
1159
+ target_column_type,
1160
+ agg_type,
1161
+ cast_to_bigint,
1107
1162
  ):
1108
1163
  aggregate_config = self.append_pre_agg_calc_field(
1109
1164
  casefold_source_columns[column],
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: google-pso-data-validator
3
- Version: 8.5.0
3
+ Version: 8.5.2
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 = "8.5.0"
21
+ version = "8.5.2"
22
22
  release_status = "Development Status :: 4 - Beta"
23
23
 
24
24
  with open("README.md", "r") as fh:
@@ -173,6 +173,14 @@ def db2_type_string_length(
173
173
  # Timestamp when converted to string will be at least 19 characters (YYYY-MM-DD HH:MI:SS).
174
174
  return 19
175
175
  elif ibis_column.is_integer():
176
+ return ibis_integer_string_length(ibis_column)
177
+ else:
178
+ return None
179
+
180
+
181
+ def ibis_integer_string_length(ibis_column: dt.DataType) -> Optional[int]:
182
+ """Return the maximum string length of an integer column, including minus sign."""
183
+ if ibis_column.is_integer():
176
184
  if isinstance(ibis_column, dt.Int64):
177
185
  return 20
178
186
  elif isinstance(ibis_column, dt.Int32):
@@ -181,7 +189,8 @@ def db2_type_string_length(
181
189
  return 6
182
190
  elif isinstance(ibis_column, dt.Int8):
183
191
  return 4
184
- return 20
192
+ # Safe catch all length.
193
+ return 38
185
194
  else:
186
195
  return None
187
196
 
@@ -439,7 +439,7 @@ def string_to_epoch(ts: str) -> int:
439
439
 
440
440
  @execute_node.register(ops.ExtractEpochSeconds, (datetime.datetime, pd.Series))
441
441
  def execute_epoch_seconds_new(op, data, **kwargs):
442
- convert = getattr(data, "view", data.astype)
442
+ convert = data.astype
443
443
  try:
444
444
  series = convert(np.int64)
445
445
  # We need int64 below because NaT overflows int32.
@@ -11,11 +11,13 @@
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 sqlalchemy as sa
14
+
15
15
  import re
16
+ from typing import Iterable, Optional, Tuple
17
+
18
+ import sqlalchemy as sa
16
19
 
17
20
  import ibis.expr.datatypes as dt
18
- from typing import Iterable, Tuple
19
21
  from ibis.backends.base.sql.alchemy import BaseAlchemyBackend
20
22
  from third_party.ibis.ibis_db2.compiler import Db2Compiler
21
23
  from third_party.ibis.ibis_db2.datatypes import _get_type
@@ -28,11 +30,11 @@ class Backend(BaseAlchemyBackend):
28
30
  def do_connect(
29
31
  self,
30
32
  host: str = "localhost",
31
- user: str = None,
32
- password: str = None,
33
+ user: Optional[str] = None,
34
+ password: Optional[str] = None,
33
35
  port: int = 50000,
34
- database: str = None,
35
- url: str = None,
36
+ database: Optional[str] = None,
37
+ url: Optional[str] = None,
36
38
  driver: str = "ibm_db_sa",
37
39
  ) -> None:
38
40
  if url is None:
@@ -102,24 +104,69 @@ class Backend(BaseAlchemyBackend):
102
104
  )
103
105
  return [_[0] for _ in result.cursor.fetchall()]
104
106
 
105
- def raw_column_metadata_not_implemented(
107
+ def raw_column_metadata(
106
108
  self, database: str = None, table: str = None, query: str = None
107
109
  ) -> list:
108
110
  """Define this method to allow DVT to test if backend specific transformations may be needed for comparison.
109
- Partner method to _metadata that retains raw data type information instead of converting
110
- to Ibis types. This works in the same way as _metadata by running a query over the DVT
111
- source, either schema.table or a custom query, and fetching the metadata using sp_describe_first_result_set.
112
-
113
- THIS METHOD IS NOT IMPLEMENTED YET.
111
+ Partner method to _metadata that retains raw data type information instead of converting to Ibis types.
112
+ This works in the same way as _metadata by running a query over the DVT source, either schema.table or a
113
+ custom query, and fetching the first row. From the cursor we can detect data types of the row's columns.
114
114
 
115
+ NOTE: This only works for table look-ups. For custom queries the raw data types are not available to us
116
+ due to the IBM Db2 driver hiding the real data types.
115
117
 
116
118
  Returns:
117
119
  list: A list of tuples containing the standard 7 DB API fields:
118
120
  https://peps.python.org/pep-0249/#description
119
121
  """
120
- return []
122
+ assert (database and table) or query, "We should never receive all args=None"
123
+ if database and table:
124
+ # For table-based validation, query the system catalog to get the true data type.
125
+ get_column_metadata_sql = """
126
+ SELECT COLNAME, TYPENAME, LENGTH, SCALE, NULLS
127
+ FROM SYSCAT.COLUMNS
128
+ WHERE TABSCHEMA = ? AND TABNAME = ?
129
+ ORDER BY COLNO
130
+ """
131
+ with self.begin() as con:
132
+ result = con.exec_driver_sql(
133
+ get_column_metadata_sql,
134
+ parameters=(database.upper(), table.upper()),
135
+ )
136
+ # Yield a 7-tuple mimicking cursor.description, with the true typename.
137
+ for (
138
+ colname,
139
+ typename,
140
+ col_length,
141
+ col_scale,
142
+ nullable,
143
+ ) in result.cursor.fetchall():
144
+ yield (
145
+ colname,
146
+ typename,
147
+ col_length,
148
+ col_length,
149
+ col_length,
150
+ col_scale,
151
+ nullable,
152
+ )
153
+ elif query:
154
+ # For custom queries, the system catalog cannot be used. Fall back to
155
+ # cursor.description, which may not distinguish padded char types.
156
+ source = f"({query})"
157
+ with self.begin() as con:
158
+ result = con.exec_driver_sql(f"SELECT * FROM {source} t0 LIMIT 0")
159
+ cursor = result.cursor
160
+ yield from (column for column in cursor.description)
121
161
 
122
162
  def is_char_type_padded(self, char_type: Tuple) -> bool:
123
163
  """Define this method if the backend supports character/string types that are padded and returns
124
164
  padded values, which DVT may want to trim"""
125
- return char_type[0] == "CHARACTER"
165
+ type_code = char_type[0]
166
+ if isinstance(type_code, str):
167
+ return type_code.upper() == "CHARACTER"
168
+ else:
169
+ # From cursor.description for custom queries, this is a DBAPITypeObject.
170
+ # It's not possible to distinguish padded char types in this case,
171
+ # so we default to False to be safe and avoid trimming incorrectly.
172
+ return False
@@ -15,11 +15,11 @@
15
15
  import ibm_db_dbi
16
16
 
17
17
  import ibis.expr.datatypes as dt
18
- from ibis.backends.base.sql.alchemy.datatypes import ibis_type_to_sqla
19
18
  from ibm_db_sa.ibm_db import DB2Dialect_ibm_db
20
19
  import sqlalchemy as sa
21
20
  import sqlalchemy.types as sat
22
21
 
22
+ from ibis.backends.base.sql.alchemy import to_sqla_type
23
23
 
24
24
  # Types from https://github.com/ibmdb/python-ibmdb/blob/master/IBM_DB/ibm_db/ibm_db_dbi.py
25
25
  _type_mapping = {
@@ -37,7 +37,12 @@ _type_mapping = {
37
37
  ibm_db_dbi.ROWID: dt.String,
38
38
  }
39
39
 
40
- ibis_type_to_sqla[dt.String] = sa.sql.sqltypes.String(length=3000)
40
+
41
+ @to_sqla_type.register(DB2Dialect_ibm_db, dt.String)
42
+ def _string(_, itype):
43
+ """Include a limit for casts to String due to line size limits supported by Db2 concat()."""
44
+ return sa.sql.sqltypes.String(length=3000)
45
+
41
46
 
42
47
  DB2Dialect_ibm_db.ischema_names["DECFLOAT"] = sat.DOUBLE
43
48
 
@@ -33,6 +33,9 @@ from ibis.backends.base.sql.alchemy import (
33
33
  )
34
34
  from ibis.backends.base.sql.alchemy.registry import variance_reduction
35
35
 
36
+ from third_party.ibis.ibis_addon.api import ibis_integer_string_length
37
+
38
+
36
39
  operation_registry = sqlalchemy_operation_registry.copy()
37
40
  operation_registry.update(sqlalchemy_window_functions_registry)
38
41
 
@@ -121,9 +124,41 @@ def _cast(t, op):
121
124
 
122
125
  sa_arg = t.translate(arg)
123
126
 
124
- # specialize going from an integer type to a timestamp
125
- if arg_dtype.is_integer() and typ.is_timestamp():
126
- return t.integer_to_timestamp(sa_arg, tz=typ.timezone)
127
+ if arg_dtype.is_integer():
128
+ if typ.is_timestamp():
129
+ # specialize going from an integer type to a timestamp
130
+ return t.integer_to_timestamp(sa_arg, tz=typ.timezone)
131
+ elif typ.is_string():
132
+ string_length = ibis_integer_string_length(arg_dtype)
133
+ if string_length:
134
+ # Restring target string length to appropriate length for integer size.
135
+ return sa.cast(sa_arg, sa.String(string_length))
136
+ elif arg_dtype.is_decimal() and typ.is_string():
137
+ if arg_dtype.scale is not None and arg_dtype.scale > 0:
138
+ # Db2 always pads fractional part of the number out to length of scale.
139
+ # We need to remove those insignificant digits.
140
+ precision = arg_dtype.precision or 31
141
+ fmt = (
142
+ ("9" * (precision - arg_dtype.scale - 1))
143
+ + "0."
144
+ + ("9" * arg_dtype.scale)
145
+ )
146
+ return sa.func.ltrim(
147
+ sa.func.regexp_replace(
148
+ sa.func.to_char(sa_arg, fmt),
149
+ sa.literal_column("'\\.?0+$'"),
150
+ sa.literal_column("''"),
151
+ )
152
+ )
153
+ # Max expected precision 38 plus 2 for minus sign and decimal place.
154
+ return sa.cast(sa_arg, sa.String(40))
155
+ elif arg_dtype.is_floating() and typ.is_string():
156
+ # Max expected precision 38 plus 2 for minus sign and decimal place.
157
+ return sa.cast(sa_arg, sa.String(40))
158
+
159
+ if arg_dtype.is_time() and typ.is_string():
160
+ # Force colons as time separator with CHAR(column,JIS) expression.
161
+ return sa.func.char(sa_arg, sa.literal_column("JIS"))
127
162
 
128
163
  if arg_dtype.is_binary() and typ.is_string():
129
164
  # Binary to string cast is a "to hex" conversion for DVT.
@@ -297,10 +332,6 @@ def _reduction(func_name):
297
332
  return reduction_compiler
298
333
 
299
334
 
300
- def _count_start(sa_func):
301
- return sa_func
302
-
303
-
304
335
  def _reduction_count(sa_func):
305
336
  def formatter(t, op):
306
337
  *args, where = op.args
@@ -82,14 +82,16 @@ class Backend(BaseAlchemyBackend):
82
82
  return self._filter_with_like(databases, like)
83
83
 
84
84
  def _metadata(self, query: str) -> Iterable[Tuple[str, dt.DataType]]:
85
- raw_name = util.guid()
85
+ raw_name = util.guid().lower()
86
86
  name = self._quote(raw_name)
87
- type_info_sql = """\
88
- SELECT
89
- "column", "type"
90
- FROM PG_TABLE_DEF
91
- WHERE tablename = :raw_name
92
- """
87
+ type_info_sql = """
88
+ SELECT
89
+ "column_name",
90
+ "data_type"
91
+ FROM SVV_ALL_COLUMNS
92
+ WHERE table_name = :raw_name
93
+ ORDER BY ordinal_position
94
+ """
93
95
  if self.inspector.has_table(query):
94
96
  query = f"TABLE {query}"
95
97
  with self.begin() as con:
@@ -104,7 +104,7 @@ def sa_format_hashbytes(translator, op):
104
104
  Sybase ASE does not support a SHA256 algorithm:
105
105
  https://userapps.support.sap.com/sap/support/knowledge/en/3108409
106
106
 
107
- It might be possible to add a customer UDF for this, see issue-xxxx for details.
107
+ It might be possible to add a custom UDF for this, see issue-1538 for details.
108
108
  """
109
109
  arg = translator.translate(op.arg)
110
110
  cast_arg = sa.func.convert(sa.sql.literal_column("VARCHAR(16384)"), arg)