google-pso-data-validator 8.1.0__tar.gz → 8.2.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 (93) hide show
  1. {google_pso_data_validator-8.1.0/google_pso_data_validator.egg-info → google_pso_data_validator-8.2.0}/PKG-INFO +3 -1
  2. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/README.md +2 -0
  3. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/data_validation/__init__.py +2 -2
  4. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/data_validation/__main__.py +1 -4
  5. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/data_validation/cli_tools.py +5 -0
  6. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/data_validation/clients.py +4 -5
  7. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/data_validation/config_manager.py +28 -1
  8. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0/google_pso_data_validator.egg-info}/PKG-INFO +3 -1
  9. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/google_pso_data_validator.egg-info/SOURCES.txt +1 -0
  10. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/setup.py +1 -1
  11. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/third_party/ibis/ibis_addon/operations.py +8 -51
  12. google_pso_data_validator-8.2.0/third_party/ibis/ibis_bigquery/registry.py +57 -0
  13. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/third_party/ibis/ibis_db2/__init__.py +2 -2
  14. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/third_party/ibis/ibis_mssql/registry.py +23 -1
  15. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/third_party/ibis/ibis_oracle/datatypes.py +4 -0
  16. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/third_party/ibis/ibis_oracle/registry.py +2 -0
  17. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/third_party/ibis/ibis_teradata/registry.py +2 -0
  18. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/LICENSE +0 -0
  19. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/data_validation/app.py +0 -0
  20. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/data_validation/client_info.py +0 -0
  21. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/data_validation/combiner.py +0 -0
  22. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/data_validation/consts.py +0 -0
  23. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/data_validation/data_validation.py +0 -0
  24. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/data_validation/exceptions.py +0 -0
  25. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/data_validation/find_tables.py +0 -0
  26. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/data_validation/gcs_helper.py +0 -0
  27. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/data_validation/jellyfish_distance.py +0 -0
  28. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/data_validation/metadata.py +0 -0
  29. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/data_validation/partition_builder.py +0 -0
  30. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/data_validation/query_builder/__init__.py +0 -0
  31. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/data_validation/query_builder/partition_row_builder.py +0 -0
  32. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/data_validation/query_builder/query_builder.py +0 -0
  33. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/data_validation/query_builder/random_row_builder.py +0 -0
  34. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/data_validation/raw_query.py +0 -0
  35. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/data_validation/result_handlers/__init__.py +0 -0
  36. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/data_validation/result_handlers/base_backend.py +0 -0
  37. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/data_validation/result_handlers/bigquery.py +0 -0
  38. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/data_validation/result_handlers/factory.py +0 -0
  39. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/data_validation/result_handlers/postgres.py +0 -0
  40. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/data_validation/result_handlers/text.py +0 -0
  41. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/data_validation/schema_validation.py +0 -0
  42. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/data_validation/secret_manager.py +0 -0
  43. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/data_validation/state_manager.py +0 -0
  44. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/data_validation/util.py +0 -0
  45. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/data_validation/validation_builder.py +0 -0
  46. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/google_pso_data_validator.egg-info/dependency_links.txt +0 -0
  47. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/google_pso_data_validator.egg-info/entry_points.txt +0 -0
  48. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/google_pso_data_validator.egg-info/requires.txt +0 -0
  49. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/google_pso_data_validator.egg-info/top_level.txt +0 -0
  50. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/setup.cfg +0 -0
  51. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/third_party/ibis/ibis_addon/__init__.py +0 -0
  52. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/third_party/ibis/ibis_addon/api.py +0 -0
  53. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/third_party/ibis/ibis_bigquery/__init__.py +0 -0
  54. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/third_party/ibis/ibis_bigquery/api.py +0 -0
  55. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/third_party/ibis/ibis_cloud_spanner/__init__.py +0 -0
  56. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/third_party/ibis/ibis_cloud_spanner/api.py +0 -0
  57. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/third_party/ibis/ibis_cloud_spanner/client.py +0 -0
  58. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/third_party/ibis/ibis_cloud_spanner/compiler.py +0 -0
  59. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/third_party/ibis/ibis_cloud_spanner/datatypes.py +0 -0
  60. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/third_party/ibis/ibis_cloud_spanner/registry.py +0 -0
  61. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/third_party/ibis/ibis_cloud_spanner/tests/__init__.py +0 -0
  62. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/third_party/ibis/ibis_cloud_spanner/tests/conftest.py +0 -0
  63. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/third_party/ibis/ibis_cloud_spanner/to_pandas.py +0 -0
  64. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/third_party/ibis/ibis_db2/api.py +0 -0
  65. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/third_party/ibis/ibis_db2/compiler.py +0 -0
  66. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/third_party/ibis/ibis_db2/datatypes.py +0 -0
  67. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/third_party/ibis/ibis_db2/registry.py +0 -0
  68. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/third_party/ibis/ibis_impala/__init__.py +0 -0
  69. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/third_party/ibis/ibis_impala/api.py +0 -0
  70. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/third_party/ibis/ibis_mssql/__init__.py +0 -0
  71. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/third_party/ibis/ibis_mssql/api.py +0 -0
  72. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/third_party/ibis/ibis_mssql/datatypes.py +0 -0
  73. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/third_party/ibis/ibis_mysql/__init__.py +0 -0
  74. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/third_party/ibis/ibis_mysql/base_sql_compiler/__init__.py +0 -0
  75. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/third_party/ibis/ibis_mysql/base_sql_compiler/select_builder.py +0 -0
  76. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/third_party/ibis/ibis_mysql/compiler.py +0 -0
  77. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/third_party/ibis/ibis_oracle/__init__.py +0 -0
  78. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/third_party/ibis/ibis_oracle/api.py +0 -0
  79. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/third_party/ibis/ibis_oracle/compiler.py +0 -0
  80. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/third_party/ibis/ibis_postgres/__init__.py +0 -0
  81. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/third_party/ibis/ibis_postgres/client.py +0 -0
  82. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/third_party/ibis/ibis_postgres/datatypes.py +0 -0
  83. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/third_party/ibis/ibis_postgres/registry.py +0 -0
  84. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/third_party/ibis/ibis_redshift/__init__.py +0 -0
  85. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/third_party/ibis/ibis_redshift/api.py +0 -0
  86. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/third_party/ibis/ibis_redshift/compiler.py +0 -0
  87. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/third_party/ibis/ibis_snowflake/__init__.py +0 -0
  88. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/third_party/ibis/ibis_snowflake/api.py +0 -0
  89. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/third_party/ibis/ibis_snowflake/datatypes.py +0 -0
  90. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/third_party/ibis/ibis_teradata/__init__.py +0 -0
  91. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/third_party/ibis/ibis_teradata/api.py +0 -0
  92. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/third_party/ibis/ibis_teradata/compiler.py +0 -0
  93. {google_pso_data_validator-8.1.0 → google_pso_data_validator-8.2.0}/third_party/ibis/ibis_teradata/datatypes.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: google-pso-data-validator
3
- Version: 8.1.0
3
+ Version: 8.2.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
@@ -449,6 +449,8 @@ data-validation
449
449
  Either --target-query or --target-query-file must be provided
450
450
  --target-query-file TARGET_QUERY_FILE, -tqf TARGET_QUERY_FILE
451
451
  File containing the target sql command. Supports GCS and local paths.
452
+ [--grouped-columns or -gc GROUPED_COLUMNS]
453
+ Comma separated list of columns for Group By i.e col_a,col_b
452
454
  [--count COLUMNS] Comma separated list of columns for count or * for all columns
453
455
  [--sum COLUMNS] Comma separated list of columns for sum or * for all numeric
454
456
  [--min COLUMNS] Comma separated list of columns for min or * for all numeric
@@ -390,6 +390,8 @@ data-validation
390
390
  Either --target-query or --target-query-file must be provided
391
391
  --target-query-file TARGET_QUERY_FILE, -tqf TARGET_QUERY_FILE
392
392
  File containing the target sql command. Supports GCS and local paths.
393
+ [--grouped-columns or -gc GROUPED_COLUMNS]
394
+ Comma separated list of columns for Group By i.e col_a,col_b
393
395
  [--count COLUMNS] Comma separated list of columns for count or * for all columns
394
396
  [--sum COLUMNS] Comma separated list of columns for sum or * for all numeric
395
397
  [--min COLUMNS] Comma separated list of columns for min or * for all numeric
@@ -12,8 +12,8 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
- from pkg_resources import get_distribution
15
+ from importlib.metadata import version
16
16
 
17
17
 
18
- __version__ = get_distribution("google-pso-data-validator").version
18
+ __version__ = version("google-pso-data-validator")
19
19
  __all__ = ["__version__"]
@@ -294,10 +294,7 @@ def build_config_from_args(args: "Namespace", config_manager: ConfigManager):
294
294
  and args.custom_query_type == consts.COLUMN_VALIDATION.lower()
295
295
  ):
296
296
  config_manager.append_aggregates(get_aggregate_config(args, config_manager))
297
- if (
298
- config_manager.validation_type == consts.COLUMN_VALIDATION
299
- and args.grouped_columns # grouped_columns not supported in custom queries - at least now.
300
- ):
297
+ if args.grouped_columns:
301
298
  grouped_columns = cli_tools.get_arg_list(args.grouped_columns)
302
299
  config_manager.append_query_groups(
303
300
  config_manager.build_column_configs(grouped_columns)
@@ -948,6 +948,11 @@ def _configure_custom_query_column_parser(custom_query_column_parser):
948
948
  equivalent) that are comparable across all other supported SQL engines. This option may produce
949
949
  unreliable results.""",
950
950
  )
951
+ optional_arguments.add_argument(
952
+ "--grouped-columns",
953
+ "-gc",
954
+ help="Comma separated list of columns to group results by, e.g. 'col_a,col_b'. Applies to column validations, including custom-query column.",
955
+ )
951
956
  optional_arguments.add_argument(
952
957
  "--exclude-columns",
953
958
  "-ec",
@@ -69,20 +69,20 @@ def _raise_missing_client_error(msg):
69
69
  # Teradata requires teradatasql and licensing
70
70
  try:
71
71
  from third_party.ibis.ibis_teradata.api import teradata_connect
72
- except Exception:
72
+ except ImportError:
73
73
  msg = "pip install teradatasql (requires Teradata licensing)"
74
74
  teradata_connect = _raise_missing_client_error(msg)
75
75
 
76
76
  # Oracle requires python-oracldb driver
77
77
  try:
78
78
  from third_party.ibis.ibis_oracle.api import oracle_connect
79
- except Exception:
79
+ except ImportError:
80
80
  oracle_connect = _raise_missing_client_error("pip install oracledb")
81
81
 
82
82
  # Snowflake requires snowflake-connector-python and snowflake-sqlalchemy
83
83
  try:
84
84
  from third_party.ibis.ibis_snowflake.api import snowflake_connect
85
- except Exception:
85
+ except ImportError:
86
86
  snowflake_connect = _raise_missing_client_error(
87
87
  "pip install snowflake-connector-python && pip install snowflake-sqlalchemy"
88
88
  )
@@ -90,8 +90,7 @@ except Exception:
90
90
  # DB2 requires ibm_db_sa
91
91
  try:
92
92
  from third_party.ibis.ibis_db2.api import db2_connect
93
- except Exception as e:
94
- logging.error(f"Exception {str(e)} while importing db2_connect")
93
+ except ImportError:
95
94
  db2_connect = _raise_missing_client_error("pip install ibm_db_sa")
96
95
 
97
96
 
@@ -664,6 +664,30 @@ class ConfigManager(object):
664
664
  isinstance(source_type, dt.UUID) or isinstance(target_type, dt.UUID)
665
665
  )
666
666
 
667
+ def _is_sql_server_text(
668
+ self, source_column_name: str, target_column_name: str
669
+ ) -> bool:
670
+ """Returns True when either source or target column is of SQL Server text data type.
671
+
672
+ This is because SQL Server text does not support the len() function to get the length in characters.
673
+ Instead we must fall back to using ByteLength."""
674
+
675
+ raw_source_types = self.get_source_raw_data_types()
676
+ raw_target_types = self.get_target_raw_data_types()
677
+ text_types = ("text", "ntext")
678
+ return bool(
679
+ (
680
+ self.source_client.name == "mssql"
681
+ and raw_source_types
682
+ and raw_source_types.get(source_column_name, [None])[0] in text_types
683
+ )
684
+ or (
685
+ self.target_client.name == "mssql"
686
+ and raw_target_types
687
+ and raw_target_types.get(target_column_name, [None])[0] in text_types
688
+ )
689
+ )
690
+
667
691
  def build_config_comparison_fields(self, fields, depth=None):
668
692
  """Return list of field config objects."""
669
693
  field_configs = []
@@ -809,7 +833,10 @@ class ConfigManager(object):
809
833
  depth = 1
810
834
  calc_func = consts.CALC_FIELD_LENGTH
811
835
  elif column_type in ["string", "!string"]:
812
- calc_func = consts.CALC_FIELD_LENGTH
836
+ if self._is_sql_server_text(source_column, target_column):
837
+ calc_func = consts.CALC_FIELD_BYTE_LENGTH
838
+ else:
839
+ calc_func = consts.CALC_FIELD_LENGTH
813
840
 
814
841
  elif self._is_uuid(column_type, target_column_type):
815
842
  calc_func = consts.CONFIG_CAST
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: google-pso-data-validator
3
- Version: 8.1.0
3
+ Version: 8.2.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
@@ -449,6 +449,8 @@ data-validation
449
449
  Either --target-query or --target-query-file must be provided
450
450
  --target-query-file TARGET_QUERY_FILE, -tqf TARGET_QUERY_FILE
451
451
  File containing the target sql command. Supports GCS and local paths.
452
+ [--grouped-columns or -gc GROUPED_COLUMNS]
453
+ Comma separated list of columns for Group By i.e col_a,col_b
452
454
  [--count COLUMNS] Comma separated list of columns for count or * for all columns
453
455
  [--sum COLUMNS] Comma separated list of columns for sum or * for all numeric
454
456
  [--min COLUMNS] Comma separated list of columns for min or * for all numeric
@@ -45,6 +45,7 @@ third_party/ibis/ibis_addon/api.py
45
45
  third_party/ibis/ibis_addon/operations.py
46
46
  third_party/ibis/ibis_bigquery/__init__.py
47
47
  third_party/ibis/ibis_bigquery/api.py
48
+ third_party/ibis/ibis_bigquery/registry.py
48
49
  third_party/ibis/ibis_cloud_spanner/__init__.py
49
50
  third_party/ibis/ibis_cloud_spanner/api.py
50
51
  third_party/ibis/ibis_cloud_spanner/client.py
@@ -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.1.0"
21
+ version = "8.2.0"
22
22
  release_status = "Development Status :: 4 - Beta"
23
23
 
24
24
  with open("README.md", "r") as fh:
@@ -48,10 +48,7 @@ from ibis.backends.bigquery.client import (
48
48
  _LEGACY_TO_STANDARD as _BQ_LEGACY_TO_STANDARD,
49
49
  )
50
50
  from ibis.backends.bigquery.compiler import BigQueryExprTranslator
51
- from ibis.backends.bigquery.registry import (
52
- STRFTIME_FORMAT_FUNCTIONS as BQ_STRFTIME_FORMAT_FUNCTIONS,
53
- bigquery_cast,
54
- )
51
+ from ibis.backends.bigquery.registry import bigquery_cast
55
52
  from ibis.backends.impala.compiler import ImpalaExprTranslator
56
53
  from ibis.backends.mssql.compiler import MsSqlExprTranslator
57
54
  from ibis.backends.mysql.compiler import MySQLExprTranslator
@@ -62,6 +59,7 @@ from ibis.expr.types import BinaryValue, NumericValue, StringValue, TemporalValu
62
59
 
63
60
  # Do not remove these lines, they trigger patching of Ibis code.
64
61
  import third_party.ibis.ibis_bigquery.api # noqa
62
+ from third_party.ibis.ibis_bigquery import registry as bigquery_registry
65
63
  import third_party.ibis.ibis_mysql.compiler # noqa
66
64
  from third_party.ibis.ibis_mssql import registry as mssql_registry
67
65
  from third_party.ibis.ibis_postgres import registry as postgres_registry
@@ -101,7 +99,7 @@ NAT_INT64_MIN_IN_SECONDS = np.iinfo(np.int64).min // 1_000_000_000
101
99
 
102
100
 
103
101
  class BinaryLength(ops.Value):
104
- arg = rlz.one_of([rlz.value(dt.Binary)])
102
+ arg = rlz.one_of([rlz.value(dt.Binary), rlz.value(dt.String)])
105
103
  output_dtype = dt.int32
106
104
  output_shape = rlz.shape_like("arg")
107
105
 
@@ -154,24 +152,6 @@ def bigquery_cast_to_binary_generate(compiled_arg, from_, to):
154
152
  return f"FROM_HEX({compiled_arg})"
155
153
 
156
154
 
157
- def format_hash_bigquery(translator, op):
158
- arg = translator.translate(op.arg)
159
- if op.how == "farm_fingerprint":
160
- return f"FARM_FINGERPRINT({arg})"
161
- else:
162
- raise ValueError(f"unexpected value for 'how': {op.how}")
163
-
164
-
165
- def format_hashbytes_bigquery(translator, op):
166
- arg = translator.translate(op.arg)
167
- if op.how == "sha256":
168
- return f"TO_HEX(SHA256({arg}))"
169
- elif op.how == "farm_fingerprint":
170
- return f"FARM_FINGERPRINT({arg})"
171
- else:
172
- raise ValueError(f"unexpected value for 'how': {op.how}")
173
-
174
-
175
155
  def format_hashbytes_teradata(translator, op):
176
156
  arg = translator.translate(op.arg)
177
157
  if op.how == "sha256":
@@ -184,30 +164,6 @@ def format_hashbytes_teradata(translator, op):
184
164
  raise ValueError(f"unexpected value for 'how': {op.how}")
185
165
 
186
166
 
187
- def strftime_bigquery(translator, op):
188
- """Timestamp formatting."""
189
- arg = op.arg
190
- format_str = op.format_str
191
- arg_type = arg.output_dtype
192
- strftime_format_func_name = BQ_STRFTIME_FORMAT_FUNCTIONS[type(arg_type)]
193
- fmt_string = translator.translate(format_str)
194
- # Deal with issue 1181 due a GoogleSQL bug with dates before 1000 CE affects both date and timestamp types
195
- if format_str.value.startswith("%Y"):
196
- fmt_string = fmt_string.replace("%Y", "%E4Y", 1)
197
- arg_formatted = translator.translate(arg)
198
- if isinstance(arg_type, dt.Timestamp):
199
- return "FORMAT_{}({}, {}({}), {!r})".format(
200
- strftime_format_func_name,
201
- fmt_string,
202
- strftime_format_func_name,
203
- arg_formatted,
204
- arg_type.timezone if arg_type.timezone is not None else "UTC",
205
- )
206
- return "FORMAT_{}({}, {})".format(
207
- strftime_format_func_name, fmt_string, arg_formatted
208
- )
209
-
210
-
211
167
  def strftime_mysql(translator, op):
212
168
  arg = op.arg
213
169
  format_string = op.format_str
@@ -528,10 +484,10 @@ TemporalValue.to_char = compile_to_char
528
484
  # so we can piggy back Ibis code rather than writing metadata queries for all engines.
529
485
  BaseAlchemyBackend.dvt_list_tables = _dvt_list_tables
530
486
 
531
- BigQueryExprTranslator._registry[ops.HashBytes] = format_hashbytes_bigquery
487
+ BigQueryExprTranslator._registry[ops.HashBytes] = bigquery_registry.format_hashbytes
532
488
  BigQueryExprTranslator._registry[RawSQL] = format_raw_sql
533
- BigQueryExprTranslator._registry[ops.Strftime] = strftime_bigquery
534
- BigQueryExprTranslator._registry[BinaryLength] = sa_format_binary_length
489
+ BigQueryExprTranslator._registry[ops.Strftime] = bigquery_registry.strftime
490
+ BigQueryExprTranslator._registry[BinaryLength] = bigquery_registry.format_binary_length
535
491
 
536
492
  AlchemyExprTranslator._registry[RawSQL] = format_raw_sql
537
493
  AlchemyExprTranslator._registry[ops.HashBytes] = format_hashbytes_alchemy
@@ -580,6 +536,7 @@ MsSqlExprTranslator._registry[RawSQL] = sa_format_raw_sql
580
536
  MsSqlExprTranslator._registry[ops.IfNull] = sa_fixed_arity(sa.func.isnull, 2)
581
537
  MsSqlExprTranslator._registry[ops.StringJoin] = mssql_registry.sa_string_join
582
538
  MsSqlExprTranslator._registry[ops.RandomScalar] = mssql_registry.sa_format_new_id
539
+ MsSqlExprTranslator._registry[ops.StringLength] = mssql_registry.sa_format_string_length
583
540
  MsSqlExprTranslator._registry[ops.Strftime] = mssql_registry.strftime
584
541
  MsSqlExprTranslator._registry[ops.Cast] = mssql_registry.sa_cast_mssql
585
542
  MsSqlExprTranslator._registry[BinaryLength] = mssql_registry.sa_format_binary_length
@@ -614,7 +571,7 @@ if Db2ExprTranslator:
614
571
  ]
615
572
 
616
573
  SpannerExprTranslator._registry[RawSQL] = format_raw_sql
617
- SpannerExprTranslator._registry[ops.HashBytes] = format_hashbytes_bigquery
574
+ SpannerExprTranslator._registry[ops.HashBytes] = bigquery_registry.format_hashbytes
618
575
  SpannerExprTranslator._registry[BinaryLength] = sa_format_binary_length
619
576
 
620
577
  if TeradataExprTranslator:
@@ -0,0 +1,57 @@
1
+ # Copyright 2025 Google Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from ibis.backends.bigquery.registry import (
16
+ STRFTIME_FORMAT_FUNCTIONS as BQ_STRFTIME_FORMAT_FUNCTIONS,
17
+ )
18
+ import ibis.expr.datatypes as dt
19
+
20
+
21
+ def format_hashbytes(translator, op):
22
+ arg = translator.translate(op.arg)
23
+ if op.how == "sha256":
24
+ return f"TO_HEX(SHA256({arg}))"
25
+ elif op.how == "farm_fingerprint":
26
+ return f"FARM_FINGERPRINT({arg})"
27
+ else:
28
+ raise ValueError(f"unexpected value for 'how': {op.how}")
29
+
30
+
31
+ def strftime(translator, op):
32
+ """Timestamp formatting."""
33
+ arg = op.arg
34
+ format_str = op.format_str
35
+ arg_type = arg.output_dtype
36
+ strftime_format_func_name = BQ_STRFTIME_FORMAT_FUNCTIONS[type(arg_type)]
37
+ fmt_string = translator.translate(format_str)
38
+ # Deal with issue 1181 due a GoogleSQL bug with dates before 1000 CE affects both date and timestamp types
39
+ if format_str.value.startswith("%Y"):
40
+ fmt_string = fmt_string.replace("%Y", "%E4Y", 1)
41
+ arg_formatted = translator.translate(arg)
42
+ if isinstance(arg_type, dt.Timestamp):
43
+ return "FORMAT_{}({}, {}({}), {!r})".format(
44
+ strftime_format_func_name,
45
+ fmt_string,
46
+ strftime_format_func_name,
47
+ arg_formatted,
48
+ arg_type.timezone if arg_type.timezone is not None else "UTC",
49
+ )
50
+ return "FORMAT_{}({}, {})".format(
51
+ strftime_format_func_name, fmt_string, arg_formatted
52
+ )
53
+
54
+
55
+ def format_binary_length(translator, op):
56
+ arg = translator.translate(op.arg)
57
+ return f"LENGTH({arg})"
@@ -104,7 +104,7 @@ class Backend(BaseAlchemyBackend):
104
104
 
105
105
  def raw_column_metadata_not_implemented(
106
106
  self, database: str = None, table: str = None, query: str = None
107
- ) -> list[Tuple]:
107
+ ) -> list:
108
108
  """Define this method to allow DVT to test if backend specific transformations may be needed for comparison.
109
109
  Partner method to _metadata that retains raw data type information instead of converting
110
110
  to Ibis types. This works in the same way as _metadata by running a query over the DVT
@@ -117,7 +117,7 @@ class Backend(BaseAlchemyBackend):
117
117
  list: A list of tuples containing the standard 7 DB API fields:
118
118
  https://peps.python.org/pep-0249/#description
119
119
  """
120
- return ()
120
+ return []
121
121
 
122
122
  def is_char_type_padded(self, char_type: Tuple) -> bool:
123
123
  """Define this method if the backend supports character/string types that are padded and returns
@@ -84,9 +84,25 @@ def sa_epoch_seconds(translator, op):
84
84
  )
85
85
 
86
86
 
87
+ def sa_format_string_length(translator, op):
88
+ """Calculate SQL Server string length in characters, not bytes.
89
+
90
+ This uses len() rather than datalength() to give an output compatible with BigQuery.
91
+ We need to protect trailing spaces due to an odd quirk in SQL Server len()
92
+ behaviour that ignores them, so this is done by first replacing spaces with
93
+ underscores before passing the string to len().
94
+
95
+ The len expression is cast to BIGINT to prevent int overflow for large tables."""
96
+ arg = translator.translate(op.arg)
97
+ return sa.func.cast(sa.func.len(sa.func.replace(arg, " ", "_")), sa.BIGINT)
98
+
99
+
87
100
  def sa_format_binary_length(translator, op):
101
+ """Calculate SQL Server bytes/string length in bytes, not characters.
102
+
103
+ The len expression is cast to BIGINT to prevent int overflow for large tables."""
88
104
  arg = translator.translate(op.arg)
89
- return sa.func.datalength(arg)
105
+ return sa.func.cast(sa.func.datalength(arg), sa.BIGINT)
90
106
 
91
107
 
92
108
  def sa_format_hashbytes(translator, op):
@@ -130,6 +146,12 @@ def sa_cast_mssql(t, op):
130
146
  formatted_value = sa.func.format(sa_arg, format_string)
131
147
  # Replace trailing '.0' with ''
132
148
  return sa.func.replace(formatted_value, ".0", "")
149
+ elif arg_dtype.is_boolean() and typ.is_string():
150
+ return sa.case(
151
+ (sa_arg == 0, sa.literal_column("'false'")),
152
+ (sa_arg == 1, sa.literal_column("'true'")),
153
+ else_=sa.null(),
154
+ )
133
155
 
134
156
  # Follow the original Ibis code path.
135
157
  return sa_fixed_cast(t, op)
@@ -84,6 +84,7 @@ _type_mapping = {
84
84
  oracledb.DB_TYPE_BINARY_DOUBLE: dt.Float64,
85
85
  oracledb.DB_TYPE_INTERVAL_DS: dt.Interval,
86
86
  oracledb.DB_TYPE_INTERVAL_YM: dt.Interval,
87
+ oracledb.DB_TYPE_BOOLEAN: dt.Boolean,
87
88
  }
88
89
 
89
90
  # SQL Alchemy doesn't support LONG RAW which drops us into Ibis 5.1.0 method:
@@ -100,6 +101,9 @@ if "TIMESTAMP WITH LOCAL TIME ZONE" not in OracleDialect_oracledb.ischema_names:
100
101
  "TIMESTAMP WITH LOCAL TIME ZONE"
101
102
  ] = oracle.TIMESTAMP(timezone=True)
102
103
 
104
+ if "BOOLEAN" not in OracleDialect_oracledb.ischema_names:
105
+ OracleDialect_oracledb.ischema_names["BOOLEAN"] = sat.BOOLEAN
106
+
103
107
 
104
108
  @dt.dtype.register(OracleDialect_oracledb, sa.dialects.oracle.CLOB)
105
109
  def sa_oracle_CLOB(_, satype, nullable=True):
@@ -160,6 +160,8 @@ def _cast(t, op):
160
160
  (sa_arg.in_(["1", "Y"]), sa.literal(1)),
161
161
  else_=sa.literal_column("TO_NUMBER(NULL)"),
162
162
  )
163
+ elif arg_dtype.is_boolean() and typ.is_string():
164
+ return sa.func.lower(sa.func.to_char(sa_arg))
163
165
  elif (arg_dtype.is_timestamp() or arg_dtype.is_date()) and typ.is_date():
164
166
  # If we are casting to Date then simulate what all other engines
165
167
  # understand to be a Date, which is without the time element.
@@ -352,6 +352,8 @@ def _literal(t, op):
352
352
  dtype = op.output_dtype
353
353
  if dtype.is_string():
354
354
  return _string_literal_format(t, op)
355
+ elif dtype.is_time(): # The base backend does not have a renderer for TIME Literals
356
+ return f"TIME '{op.value.isoformat()}'"
355
357
  else:
356
358
  return ibis_literal(t, op)
357
359