This is an automated email from the ASF dual-hosted git repository.
jrmccluskey pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/beam.git
The following commit(s) were added to refs/heads/master by this push:
new f915665446e [Gemini] Add compatibility classes between apitools client
and modern bq client (#39998)
f915665446e is described below
commit f915665446e70f227fab99dd1739626615b0e387
Author: Jack McCluskey <[email protected]>
AuthorDate: Tue Sep 29 12:56:42 2026 -0400
[Gemini] Add compatibility classes between apitools client and modern bq
client (#39998)
* [Gemini] Add compatibility classes between apitools client and modern bq
classes
* fix docstring format
* Streamline compat classes
* manage funky apitools import
* Add explicit opt-in via environment variable
---
sdks/python/apache_beam/io/gcp/bigquery_compat.py | 1454 ++++++++++++++++++++
.../apache_beam/io/gcp/bigquery_compat_test.py | 796 +++++++++++
2 files changed, 2250 insertions(+)
diff --git a/sdks/python/apache_beam/io/gcp/bigquery_compat.py
b/sdks/python/apache_beam/io/gcp/bigquery_compat.py
new file mode 100644
index 00000000000..3ad76bbcc1d
--- /dev/null
+++ b/sdks/python/apache_beam/io/gcp/bigquery_compat.py
@@ -0,0 +1,1454 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+"""Compatibility shims and legacy client emulation for BigQuery.
+
+This module contains temporary compatibility models, monkey patches, and
+helpers designed to ease migration away from the deprecated apitools BigQuery
+client to modern ``google-cloud-bigquery``.
+
+.. warning::
+ Importing this module is disabled by default and requires explicit opt-in.
+ To enable these compatibility shims, set the environment variable
+ ``BEAM_USE_BIGQUERY_COMPAT_SHIMS=1``.
+
+.. note::
+ This module is intended to be removed in a future Beam release once the
+ apitools client dependency is completely removed.
+
+ **Future Removal Guidance**:
+
+ * Compatibility models (e.g. ``_TableReferenceCompat``,
+ ``_DatasetReferenceCompat``, ``_TableSchemaCompat``) and monkey-patches
+ will be dropped. Code using ``TableReference``, ``DatasetReference``,
+ etc. should import directly from ``google.cloud.bigquery``.
+ * Input normalization helpers (``_to_gcp_table_ref``,
+ ``_to_gcp_dataset_ref``, ``_to_gcp_schema``, ``_extract_dict_labels``,
+ ``_to_table_schema``) that convert string specs or dicts to modern
+ ``google.cloud.bigquery`` instances are actively used across pipeline
+ code paths and should be preserved in ``bigquery_tools.py`` when this
+ compat module is excised.
+"""
+
+# pytype: skip-file
+
+import logging
+import os
+import warnings
+
+from apache_beam.utils.annotations import BeamDeprecationWarning
+
+_LOGGER = logging.getLogger(__name__)
+
+BIGQUERY_COMPAT_ENV_VAR = "BEAM_USE_BIGQUERY_COMPAT_SHIMS"
+
+
+def _check_compat_opt_in():
+ """Checks whether BigQuery compatibility shims are enabled via environment
variable.
+
+ Raises:
+ ImportError: If the environment variable is not set to an enabled value.
+ """
+ val = os.environ.get(BIGQUERY_COMPAT_ENV_VAR, "").strip().lower()
+ if val not in ("1", "true", "yes", "enabled", "on"):
+ raise ImportError(
+ f"BigQuery compatibility shims in {__name__} are disabled by default "
+ f"and require explicit opt-in. Set the environment variable "
+ f"{BIGQUERY_COMPAT_ENV_VAR}=1 to enable them.")
+
+ warnings.warn(
+ f"BigQuery compatibility shims in {__name__} are deprecated and will be "
+ "removed in a future release of Apache Beam. Please migrate to using "
+ "google-cloud-bigquery directly.",
+ BeamDeprecationWarning,
+ stacklevel=2)
+
+
+_check_compat_opt_in()
+
+try:
+ from google.cloud import bigquery as gcp_bigquery
+ from google.cloud.bigquery import job as gcp_job
+except ImportError:
+ gcp_bigquery = None
+ gcp_job = None
+
+try:
+ from apache_beam.io.gcp.internal.clients import bigquery as apitools_bigquery
+except ImportError:
+ apitools_bigquery = None
+
+try:
+ from apitools.base.protorpclite import messages as _protorpclite_messages
+except ImportError:
+ _protorpclite_messages = None
+
+_LOGGER = logging.getLogger(__name__)
+
+# -----------------------------------------------------------------------------
+# Compatibility Models for TableReference, DatasetReference, Schema, and Jobs.
+#
+# These classes and monkey patches bridge between legacy apitools structures
+# and modern google.cloud.bigquery objects, providing camelCase attribute
access
+# (e.g. projectId, datasetId, tableId, tableReference) for backwards
+# compatibility across pipelines, transforms, and test suites.
+# -----------------------------------------------------------------------------
+
+
+class _DatasetReferenceCompat(object):
+ """Compatibility model for BigQuery DatasetReference when
google-cloud-bigquery is unavailable.
+
+ Supports both camelCase (projectId, datasetId) and snake_case (project,
dataset_id, project_id).
+ """
+ def __init__(
+ self,
+ project=None,
+ dataset_id=None,
+ projectId=None,
+ datasetId=None,
+ project_id=None):
+ p = (
+ projectId if projectId is not None else
+ (project_id if project_id is not None else project))
+ d = datasetId if datasetId is not None else dataset_id
+ self._project = p or ""
+ self._dataset_id = d or ""
+
+ @classmethod
+ def from_string(cls, dataset_ref, default_project=None):
+ last_sep = max(dataset_ref.rfind("."), dataset_ref.rfind(":"))
+ if last_sep != -1:
+ p = dataset_ref[:last_sep]
+ d = dataset_ref[last_sep + 1:]
+ else:
+ p = default_project or "default"
+ d = dataset_ref
+ return cls(project=p, dataset_id=d)
+
+ @property
+ def projectId(self):
+ return self._project
+
+ @projectId.setter
+ def projectId(self, val):
+ self._project = val
+
+ @property
+ def project(self):
+ return self._project
+
+ @project.setter
+ def project(self, val):
+ self._project = val
+
+ @property
+ def project_id(self):
+ return self._project
+
+ @project_id.setter
+ def project_id(self, val):
+ self._project = val
+
+ @property
+ def datasetId(self):
+ return self._dataset_id
+
+ @datasetId.setter
+ def datasetId(self, val):
+ self._dataset_id = val
+
+ @property
+ def dataset_id(self):
+ return self._dataset_id
+
+ @dataset_id.setter
+ def dataset_id(self, val):
+ self._dataset_id = val
+
+ def __repr__(self):
+ return f"DatasetReference('{self.project}', '{self.dataset_id}')"
+
+ def __eq__(self, other):
+ if other is None:
+ return False
+ if not hasattr(other, "project") and not hasattr(other, "projectId"):
+ return NotImplemented
+ other_p = getattr(other, "projectId", None) or getattr(
+ other, "project", None)
+ other_d = getattr(other, "datasetId", None) or getattr(
+ other, "dataset_id", None)
+ return (self.projectId, self.datasetId) == (other_p, other_d)
+
+ def __hash__(self):
+ return hash((self.projectId, self.datasetId))
+
+
+class _TableReferenceCompat(object):
+ """Compatibility model for BigQuery TableReference when
google-cloud-bigquery is unavailable.
+
+ Supports both camelCase (projectId, datasetId, tableId) and snake_case
+ (project, dataset_id, table_id, project_id).
+ """
+ def __init__(
+ self,
+ dataset_ref=None,
+ table_id=None,
+ projectId=None,
+ datasetId=None,
+ tableId=None,
+ project=None,
+ dataset_id=None,
+ project_id=None):
+ p = (
+ projectId if projectId is not None else
+ (project_id if project_id is not None else project))
+ d = datasetId if datasetId is not None else dataset_id
+ t = tableId if tableId is not None else table_id
+ if p is not None or d is not None or t is not None:
+ self._project = p
+ self._dataset_id = d
+ self._table_id = t
+ elif dataset_ref is not None:
+ self._project = getattr(dataset_ref, "projectId", None) or getattr(
+ dataset_ref, "project", None)
+ self._dataset_id = getattr(dataset_ref, "datasetId", None) or getattr(
+ dataset_ref, "dataset_id", None)
+ self._table_id = table_id or ""
+ else:
+ self._project = None
+ self._dataset_id = None
+ self._table_id = None
+
+ @classmethod
+ def from_string(cls, table_ref, default_project=None):
+ from apache_beam.io.gcp.bigquery_tools import parse_table_reference
+ parsed = parse_table_reference(table_ref, project=default_project)
+ return cls(
+ projectId=parsed.projectId or default_project,
+ datasetId=parsed.datasetId,
+ tableId=parsed.tableId)
+
+ @property
+ def projectId(self):
+ return self._project
+
+ @projectId.setter
+ def projectId(self, val):
+ self._project = val
+
+ @property
+ def project(self):
+ return self._project
+
+ @project.setter
+ def project(self, val):
+ self._project = val
+
+ @property
+ def project_id(self):
+ return self._project
+
+ @project_id.setter
+ def project_id(self, val):
+ self._project = val
+
+ @property
+ def datasetId(self):
+ return self._dataset_id
+
+ @datasetId.setter
+ def datasetId(self, val):
+ self._dataset_id = val
+
+ @property
+ def dataset_id(self):
+ return self._dataset_id
+
+ @dataset_id.setter
+ def dataset_id(self, val):
+ self._dataset_id = val
+
+ @property
+ def tableId(self):
+ return self._table_id
+
+ @tableId.setter
+ def tableId(self, val):
+ self._table_id = val
+
+ @property
+ def table_id(self):
+ return self._table_id
+
+ @table_id.setter
+ def table_id(self, val):
+ self._table_id = val
+
+ @property
+ def dataset_reference(self):
+ return _DatasetReferenceCompat(
+ projectId=self.projectId, datasetId=self.datasetId)
+
+ @property
+ def datasetReference(self):
+ return self.dataset_reference
+
+ def __repr__(self):
+ return (
+ f"TableReference(projectId='{self.projectId}', "
+ f"datasetId='{self.datasetId}', tableId='{self.tableId}')")
+
+ def __eq__(self, other):
+ if other is None:
+ return False
+ if not hasattr(other, "tableId") and not hasattr(other, "table_id"):
+ return NotImplemented
+ other_p = getattr(other, "projectId", None) or getattr(
+ other, "project", None)
+ other_d = getattr(other, "datasetId", None) or getattr(
+ other, "dataset_id", None)
+ other_t = getattr(other, "tableId", None) or getattr(
+ other, "table_id", None)
+ return (self.projectId, self.datasetId,
+ self.tableId) == (other_p, other_d, other_t)
+
+ def __hash__(self):
+ return hash((self.projectId, self.datasetId, self.tableId))
+
+
+class _TableFieldSchemaCompat(object):
+ def __init__(
+ self,
+ name="",
+ type="STRING",
+ mode="NULLABLE",
+ description=None,
+ fields=(),
+ field_type=None,
+ **kwargs):
+ ft = type or field_type or "STRING"
+ self.name = name
+ self.field_type = ft
+ self.mode = mode or "NULLABLE"
+ self.description = description
+ self.fields = list(fields) if fields else []
+
+ @property
+ def type(self):
+ return self.field_type
+
+ @type.setter
+ def type(self, val):
+ self.field_type = val
+
+
+class _TableSchemaCompat(list):
+ def __init__(self, fields=None):
+ if fields:
+ super().__init__(fields)
+ else:
+ super().__init__()
+
+ @property
+ def fields(self):
+ return self
+
+ @fields.setter
+ def fields(self, value):
+ self.clear()
+ if value:
+ self.extend(value)
+
+
+class _TableCellCompat(object):
+ def __init__(self, v=None):
+ self.v = v
+
+
+class _TableRowCompat(object):
+ def __init__(self, f=None):
+ self.f = f or []
+
+
+# Check if an underlying type is available because the internal client silently
+# passes on an import error if any dependencies (mainly apitools) are missing.
+if apitools_bigquery is not None and hasattr(apitools_bigquery,
+ "TableReference"):
+ TableReference = apitools_bigquery.TableReference
+ DatasetReference = apitools_bigquery.DatasetReference
+ TableFieldSchema = apitools_bigquery.TableFieldSchema
+ TableSchema = apitools_bigquery.TableSchema
+ TableRow = apitools_bigquery.TableRow
+ TableCell = apitools_bigquery.TableCell
+ Table = apitools_bigquery.Table
+ Dataset = apitools_bigquery.Dataset
+ Job = apitools_bigquery.Job
+ JobConfiguration = apitools_bigquery.JobConfiguration
+ JobConfigurationLoad = apitools_bigquery.JobConfigurationLoad
+ JobConfigurationQuery = apitools_bigquery.JobConfigurationQuery
+ JobConfigurationExtract = apitools_bigquery.JobConfigurationExtract
+ JobConfigurationTableCopy = apitools_bigquery.JobConfigurationTableCopy
+ JobStatistics = apitools_bigquery.JobStatistics
+ JobStatistics2 = apitools_bigquery.JobStatistics2
+ JobStatistics4 = apitools_bigquery.JobStatistics4
+ ErrorProto = apitools_bigquery.ErrorProto
+else:
+ TableReference = _TableReferenceCompat
+ DatasetReference = _DatasetReferenceCompat
+ TableFieldSchema = _TableFieldSchemaCompat
+ TableSchema = _TableSchemaCompat
+ TableRow = _TableRowCompat
+ TableCell = _TableCellCompat
+ Table = None
+ Dataset = None
+ Job = None
+ JobConfiguration = None
+ JobConfigurationLoad = None
+ JobConfigurationQuery = None
+ JobConfigurationExtract = None
+ JobConfigurationTableCopy = None
+ JobStatistics = None
+ JobStatistics2 = None
+ JobStatistics4 = None
+ ErrorProto = None
+
+
+class JobReference(object):
+ """Compatibility model for BigQuery JobReference.
+
+ Supports both camelCase (jobId, projectId) and snake_case (job_id, project,
project_id)
+ initialization and attribute access.
+ """
+ def __init__(
+ self,
+ jobId=None,
+ projectId=None,
+ location=None,
+ job_id=None,
+ project=None,
+ project_id=None):
+ self.jobId = jobId if jobId is not None else job_id
+ self.projectId = (
+ projectId if projectId is not None else
+ (project if project is not None else project_id))
+ self.location = location
+
+ @property
+ def job_id(self):
+ return self.jobId
+
+ @job_id.setter
+ def job_id(self, val):
+ self.jobId = val
+
+ @property
+ def project(self):
+ return self.projectId
+
+ @project.setter
+ def project(self, val):
+ self.projectId = val
+
+ @property
+ def project_id(self):
+ return self.projectId
+
+ @project_id.setter
+ def project_id(self, val):
+ self.projectId = val
+
+ def __eq__(self, other):
+ if other is None:
+ return False
+ if isinstance(other, JobReference):
+ return (
+ self.jobId == other.jobId and self.projectId == other.projectId and
+ self.location == other.location)
+ if apitools_bigquery and hasattr(apitools_bigquery,
+ "JobReference") and isinstance(
+ other,
apitools_bigquery.JobReference):
+ return (
+ self.jobId == getattr(other, "jobId", None) and
+ self.projectId == getattr(other, "projectId", None) and
+ self.location == getattr(other, "location", None))
+ return NotImplemented
+
+ def __hash__(self):
+ return hash((self.jobId, self.projectId, self.location))
+
+ def __repr__(self):
+ return (
+ f"JobReference(jobId={self.jobId!r}, "
+ f"projectId={self.projectId!r}, "
+ f"location={self.location!r})")
+
+
+_PATCHED_PROTORPCLITE = False
+
+
+def _patch_protorpclite_equality():
+ global _PATCHED_PROTORPCLITE
+ if _PATCHED_PROTORPCLITE:
+ return
+ if _protorpclite_messages is not None and hasattr(_protorpclite_messages,
+ "Message"):
+ _PATCHED_PROTORPCLITE = True
+ _orig_message_eq = _protorpclite_messages.Message.__eq__
+
+ def _message_compat_eq(self, other):
+ if isinstance(other, JobReference) and apitools_bigquery and hasattr(
+ apitools_bigquery, "JobReference") and isinstance(
+ self, apitools_bigquery.JobReference):
+ return (
+ getattr(self, "jobId", None) == other.jobId and
+ getattr(self, "projectId", None) == other.projectId and
+ getattr(self, "location", None) == other.location)
+ if isinstance(other, TableReference) and apitools_bigquery and hasattr(
+ apitools_bigquery, "TableReference") and isinstance(
+ self, apitools_bigquery.TableReference):
+ return (
+ getattr(self, "projectId", None) == other.projectId and
+ getattr(self, "datasetId", None) == other.datasetId and
+ getattr(self, "tableId", None) == other.tableId)
+ if isinstance(other, DatasetReference) and apitools_bigquery and hasattr(
+ apitools_bigquery, "DatasetReference") and isinstance(
+ self, apitools_bigquery.DatasetReference):
+ return (
+ getattr(self, "projectId", None) == other.projectId and
+ getattr(self, "datasetId", None) == other.datasetId)
+ return _orig_message_eq(self, other)
+
+ _protorpclite_messages.Message.__eq__ = _message_compat_eq
+
+
+def _set_table_ref_prop(ref, prop, val):
+ if hasattr(ref, "_properties") and isinstance(ref._properties, dict):
+ ref._properties[prop] = val
+ if prop == "projectId":
+ setattr(ref, "_project", val)
+ elif prop == "datasetId":
+ setattr(ref, "_dataset_id", val)
+ elif prop == "tableId":
+ setattr(ref, "_table_id", val)
+
+
+def _extract_field(obj, *field_names):
+ """Extracts the first matching non-None field from a dict or object."""
+ if obj is None:
+ return None
+ for name in field_names:
+ if isinstance(obj, dict):
+ if name in obj and obj[name] is not None:
+ return obj[name]
+ elif hasattr(obj, name):
+ val = getattr(obj, name)
+ if val is not None and not hasattr(val, "_mock_methods"):
+ return val
+ return None
+
+
+class _ClusteringCompat(dict):
+ """Compatibility model for Table clustering.
+
+ Supports dict access, json serialization, and attribute access to fields.
+ Note: Mutations to .fields in-place do not re-sync back to the parent table.
+ """
+ def __init__(self, fields=None):
+ fields_list = list(fields) if fields else []
+ super().__init__(fields=fields_list)
+
+ @property
+ def fields(self):
+ return self.get("fields", [])
+
+ @fields.setter
+ def fields(self, val):
+ self["fields"] = list(val) if val else []
+
+
+def _to_gcp_time_partitioning(tp):
+ """Converts a dict, apitools TimePartitioning, string, or
google.cloud.bigquery.TimePartitioning."""
+ if tp is None:
+ return None
+ if hasattr(tp, "_mock_methods") or hasattr(tp, "_mock_children"):
+ return tp
+ if gcp_bigquery is not None and isinstance(
+ tp, getattr(gcp_bigquery, "TimePartitioning", ())):
+ return tp
+ if isinstance(tp, str):
+ if gcp_bigquery is not None and hasattr(gcp_bigquery, "TimePartitioning"):
+ return gcp_bigquery.TimePartitioning(type_=tp)
+ return tp
+
+ tp_field = _extract_field(tp, "field")
+ tp_type = _extract_field(tp, "type", "type_")
+ tp_exp = _extract_field(tp, "expirationMs", "expiration_ms")
+ tp_req = _extract_field(
+ tp, "requirePartitionFilter", "require_partition_filter")
+ try:
+ exp_ms = int(tp_exp) if tp_exp is not None else None
+ except (ValueError, TypeError):
+ exp_ms = None
+ if gcp_bigquery is not None and hasattr(gcp_bigquery, "TimePartitioning"):
+ return gcp_bigquery.TimePartitioning(
+ type_=tp_type,
+ field=tp_field,
+ expiration_ms=exp_ms,
+ require_partition_filter=tp_req)
+ return tp
+
+
+def _to_gcp_range_partitioning(rp):
+ """Converts a dict, apitools RangePartitioning, or
google.cloud.bigquery.RangePartitioning."""
+ if rp is None:
+ return None
+ if hasattr(rp, "_mock_methods") or hasattr(rp, "_mock_children"):
+ return rp
+ if gcp_bigquery is not None and isinstance(
+ rp, getattr(gcp_bigquery, "RangePartitioning", ())):
+ return rp
+
+ rp_field = _extract_field(rp, "field")
+ rp_range = _extract_field(rp, "range", "range_")
+ if rp_range is not None and gcp_bigquery is not None and hasattr(
+ gcp_bigquery, "PartitionRange"):
+ if not isinstance(rp_range, getattr(gcp_bigquery, "PartitionRange", ())):
+ start = _extract_field(rp_range, "start")
+ end = _extract_field(rp_range, "end")
+ interval = _extract_field(rp_range, "interval")
+ try:
+ start = int(start) if start is not None else None
+ except (ValueError, TypeError):
+ start = None
+ try:
+ end = int(end) if end is not None else None
+ except (ValueError, TypeError):
+ end = None
+ try:
+ interval = int(interval) if interval is not None else None
+ except (ValueError, TypeError):
+ interval = None
+ rp_range = gcp_bigquery.PartitionRange(
+ start=start, end=end, interval=interval)
+ if gcp_bigquery is not None and hasattr(gcp_bigquery, "RangePartitioning"):
+ return gcp_bigquery.RangePartitioning(field=rp_field, range_=rp_range)
+ return rp
+
+
+def _to_gcp_clustering_fields(clustering):
+ """Extracts clustering field list from list, dict, or apitools Clustering."""
+ if clustering is None:
+ return None
+ if hasattr(clustering, "_mock_methods") or hasattr(clustering,
+ "_mock_children"):
+ return clustering
+ if isinstance(clustering, (list, tuple)):
+ return list(clustering)
+ fields = _extract_field(clustering, "fields")
+ if isinstance(fields, (list, tuple)):
+ return list(fields)
+ return None
+
+
+# -----------------------------------------------------------------------------
+# Compatibility Monkey-Patching for google.cloud.bigquery Classes
+# -----------------------------------------------------------------------------
+
+_PATCHED_GCP_BIGQUERY = False
+
+
+def _patch_gcp_bigquery():
+ global _PATCHED_GCP_BIGQUERY
+ if not gcp_bigquery or _PATCHED_GCP_BIGQUERY:
+ return
+ _PATCHED_GCP_BIGQUERY = True
+
+ if not hasattr(gcp_bigquery.TableReference, "projectId"):
+ gcp_bigquery.TableReference.projectId = property(
+ lambda self: self.project,
+ lambda self, val: _set_table_ref_prop(self, "projectId", val))
+ gcp_bigquery.TableReference.datasetId = property(
+ lambda self: self.dataset_id,
+ lambda self, val: _set_table_ref_prop(self, "datasetId", val))
+ gcp_bigquery.TableReference.tableId = property(
+ lambda self: self.table_id,
+ lambda self, val: _set_table_ref_prop(self, "tableId", val))
+
+ if not hasattr(gcp_bigquery.DatasetReference, "projectId"):
+ gcp_bigquery.DatasetReference.projectId = property(
+ lambda self: self.project,
+ lambda self, val: setattr(self, "_project", val))
+ gcp_bigquery.DatasetReference.datasetId = property(
+ lambda self: self.dataset_id,
+ lambda self, val: setattr(self, "_dataset_id", val))
+
+ if not hasattr(gcp_bigquery.SchemaField, "type"):
+ gcp_bigquery.SchemaField.type = property(
+ lambda self: self.field_type,
+ lambda self, val: setattr(self, "_field_type", val))
+
+ if not hasattr(gcp_bigquery.Table, "tableReference"):
+ gcp_bigquery.Table.tableReference = property(lambda self: self.reference)
+ gcp_bigquery.Table.numRows = property(lambda self: self.num_rows)
+ gcp_bigquery.Table.numBytes = property(lambda self: self.num_bytes)
+ gcp_bigquery.Table.timePartitioning = property(
+ lambda self: self.time_partitioning, lambda self, val: setattr(
+ self, "time_partitioning", _to_gcp_time_partitioning(val)))
+ gcp_bigquery.Table.rangePartitioning = property(
+ lambda self: self.range_partitioning, lambda self, val: setattr(
+ self, "range_partitioning", _to_gcp_range_partitioning(val)))
+
+ if hasattr(gcp_bigquery.Table, "time_partitioning"):
+ orig_tp_setter = gcp_bigquery.Table.time_partitioning.fset
+ if orig_tp_setter is not None:
+ gcp_bigquery.Table.time_partitioning = property(
+ gcp_bigquery.Table.time_partitioning.fget,
+ lambda self, val: orig_tp_setter(
+ self, _to_gcp_time_partitioning(val)))
+
+ if hasattr(gcp_bigquery.Table, "range_partitioning"):
+ orig_rp_setter = gcp_bigquery.Table.range_partitioning.fset
+ if orig_rp_setter is not None:
+ gcp_bigquery.Table.range_partitioning = property(
+ gcp_bigquery.Table.range_partitioning.fget,
+ lambda self, val: orig_rp_setter(
+ self, _to_gcp_range_partitioning(val)))
+
+ if not hasattr(gcp_bigquery.Table, "clustering"):
+ gcp_bigquery.Table.clustering = property(
+ lambda self: _ClusteringCompat(self.clustering_fields)
+ if self.clustering_fields else None, lambda self, val: setattr(
+ self, "clustering_fields", _to_gcp_clustering_fields(val)))
+
+ if hasattr(gcp_bigquery.Table, "clustering_fields"):
+ orig_cf_setter = gcp_bigquery.Table.clustering_fields.fset
+ if orig_cf_setter is not None:
+ gcp_bigquery.Table.clustering_fields = property(
+ gcp_bigquery.Table.clustering_fields.fget,
+ lambda self, val: orig_cf_setter(
+ self, _to_gcp_clustering_fields(val)))
+
+ if hasattr(gcp_bigquery, "TimePartitioning"):
+ if not hasattr(gcp_bigquery.TimePartitioning, "type"):
+ gcp_bigquery.TimePartitioning.type = property(
+ lambda self: self.type_,
+ lambda self, val: setattr(self, "type_", val))
+ if not hasattr(gcp_bigquery.TimePartitioning, "expirationMs"):
+ gcp_bigquery.TimePartitioning.expirationMs = property(
+ lambda self: self.expiration_ms,
+ lambda self, val: setattr(self, "expiration_ms", val))
+ if not hasattr(gcp_bigquery.TimePartitioning, "requirePartitionFilter"):
+ gcp_bigquery.TimePartitioning.requirePartitionFilter = property(
+ lambda self: self.require_partition_filter,
+ lambda self, val: setattr(self, "require_partition_filter", val))
+
+ if hasattr(gcp_bigquery, "RangePartitioning"):
+ if not hasattr(gcp_bigquery.RangePartitioning, "range"):
+ gcp_bigquery.RangePartitioning.range = property(
+ lambda self: self.range_,
+ lambda self, val: setattr(self, "range_", val))
+
+ if not hasattr(gcp_bigquery.Dataset, "datasetReference"):
+ gcp_bigquery.Dataset.datasetReference = property(
+ lambda self: self.reference)
+ gcp_bigquery.Dataset.defaultTableExpirationMs = property(
+ lambda self: self.default_table_expiration_ms,
+ lambda self, val: setattr(self, "default_table_expiration_ms", val))
+
+ if hasattr(gcp_bigquery, "LoadJobConfig"):
+ if not hasattr(gcp_bigquery.LoadJobConfig, "schemaUpdateOptions"):
+ gcp_bigquery.LoadJobConfig.schemaUpdateOptions = property(
+ lambda self: self.schema_update_options,
+ lambda self, val: setattr(self, "schema_update_options", val))
+ if not hasattr(gcp_bigquery.LoadJobConfig, "ignoreUnknownValues"):
+ gcp_bigquery.LoadJobConfig.ignoreUnknownValues = property(
+ lambda self: self.ignore_unknown_values,
+ lambda self, val: setattr(self, "ignore_unknown_values", val))
+ if not hasattr(gcp_bigquery.LoadJobConfig, "maxBadRecords"):
+ gcp_bigquery.LoadJobConfig.maxBadRecords = property(
+ lambda self: self.max_bad_records,
+ lambda self, val: setattr(self, "max_bad_records", val))
+ if not hasattr(gcp_bigquery.LoadJobConfig, "nullMarker"):
+ gcp_bigquery.LoadJobConfig.nullMarker = property(
+ lambda self: self.null_marker,
+ lambda self, val: setattr(self, "null_marker", val))
+ if not hasattr(gcp_bigquery.LoadJobConfig, "fieldDelimiter"):
+ gcp_bigquery.LoadJobConfig.fieldDelimiter = property(
+ lambda self: self.field_delimiter,
+ lambda self, val: setattr(self, "field_delimiter", val))
+ if not hasattr(gcp_bigquery.LoadJobConfig, "skipLeadingRows"):
+ gcp_bigquery.LoadJobConfig.skipLeadingRows = property(
+ lambda self: self.skip_leading_rows,
+ lambda self, val: setattr(self, "skip_leading_rows", val))
+ if not hasattr(gcp_bigquery.LoadJobConfig, "allowJaggedRows"):
+ gcp_bigquery.LoadJobConfig.allowJaggedRows = property(
+ lambda self: self.allow_jagged_rows,
+ lambda self, val: setattr(self, "allow_jagged_rows", val))
+ if not hasattr(gcp_bigquery.LoadJobConfig, "allowQuotedNewlines"):
+ gcp_bigquery.LoadJobConfig.allowQuotedNewlines = property(
+ lambda self: self.allow_quoted_newlines,
+ lambda self, val: setattr(self, "allow_quoted_newlines", val))
+ if not hasattr(gcp_bigquery.LoadJobConfig, "decimalTargetTypes"):
+ gcp_bigquery.LoadJobConfig.decimalTargetTypes = property(
+ lambda self: self.decimal_target_types,
+ lambda self, val: setattr(self, "decimal_target_types", val))
+ if not hasattr(gcp_bigquery.LoadJobConfig, "useAvroLogicalTypes"):
+ gcp_bigquery.LoadJobConfig.useAvroLogicalTypes = property(
+ lambda self: self.use_avro_logical_types,
+ lambda self, val: setattr(self, "use_avro_logical_types", val))
+
+ if hasattr(gcp_bigquery, "QueryJobConfig"):
+ if not hasattr(gcp_bigquery.QueryJobConfig, "schemaUpdateOptions"):
+ gcp_bigquery.QueryJobConfig.schemaUpdateOptions = property(
+ lambda self: self.schema_update_options,
+ lambda self, val: setattr(self, "schema_update_options", val))
+ if not hasattr(gcp_bigquery.QueryJobConfig, "useLegacySql"):
+ gcp_bigquery.QueryJobConfig.useLegacySql = property(
+ lambda self: self.use_legacy_sql,
+ lambda self, val: setattr(self, "use_legacy_sql", val))
+ if not hasattr(gcp_bigquery.QueryJobConfig, "flattenResults"):
+ gcp_bigquery.QueryJobConfig.flattenResults = property(
+ lambda self: self.flatten_results,
+ lambda self, val: setattr(self, "flatten_results", val))
+ if not hasattr(gcp_bigquery.QueryJobConfig, "allowLargeResults"):
+ gcp_bigquery.QueryJobConfig.allowLargeResults = property(
+ lambda self: self.allow_large_results,
+ lambda self, val: setattr(self, "allow_large_results", val))
+ if not hasattr(gcp_bigquery.QueryJobConfig, "maximumBytesBilled"):
+ gcp_bigquery.QueryJobConfig.maximumBytesBilled = property(
+ lambda self: self.maximum_bytes_billed,
+ lambda self, val: setattr(self, "maximum_bytes_billed", val))
+
+ if hasattr(gcp_bigquery, "Table") and hasattr(gcp_bigquery.Table, "labels"):
+ _orig_tbl_labels_setter = gcp_bigquery.Table.labels.fset
+ if _orig_tbl_labels_setter:
+
+ def _safe_tbl_labels_setter(self, value):
+ if value is None:
+ value = {}
+ elif not isinstance(value, dict) and hasattr(value,
+ "additionalProperties"):
+ from apitools.base.py import encoding
+ value = encoding.MessageToDict(value)
+ _orig_tbl_labels_setter(self, value)
+
+ gcp_bigquery.Table.labels = gcp_bigquery.Table.labels.setter(
+ _safe_tbl_labels_setter)
+
+ if hasattr(gcp_bigquery, "Dataset") and hasattr(gcp_bigquery.Dataset,
+ "labels"):
+ _orig_ds_labels_setter = gcp_bigquery.Dataset.labels.fset
+ if _orig_ds_labels_setter:
+
+ def _safe_ds_labels_setter(self, value):
+ if value is None:
+ value = {}
+ elif not isinstance(value, dict) and hasattr(value,
+ "additionalProperties"):
+ from apitools.base.py import encoding
+ value = encoding.MessageToDict(value)
+ _orig_ds_labels_setter(self, value)
+
+ gcp_bigquery.Dataset.labels = gcp_bigquery.Dataset.labels.setter(
+ _safe_ds_labels_setter)
+
+ try:
+ from google.cloud.bigquery.job.base import _JobConfig as _GcpJobConfig
+ if hasattr(_GcpJobConfig, "labels") and hasattr(_GcpJobConfig.labels,
+ "fset"):
+ _orig_job_labels_setter = _GcpJobConfig.labels.fset
+ if _orig_job_labels_setter:
+
+ def _safe_job_labels_setter(self, value):
+ if value is None:
+ value = {}
+ elif not isinstance(value, dict) and hasattr(value,
+ "additionalProperties"):
+ from apitools.base.py import encoding
+ value = encoding.MessageToDict(value)
+ _orig_job_labels_setter(self, value)
+
+ _GcpJobConfig.labels = _GcpJobConfig.labels.setter(
+ _safe_job_labels_setter)
+ except ImportError:
+ pass
+
+ if hasattr(gcp_job,
+ "_AsyncJob") and not hasattr(gcp_job._AsyncJob, "jobReference"):
+ gcp_job._AsyncJob.jobReference = property(
+ lambda self: JobReference(
+ job_id=self.job_id, project=self.project, location=self.location))
+ gcp_job._AsyncJob.status = property(lambda self: _JobStatusCompat(self))
+ gcp_job._AsyncJob.statistics = property(lambda self: _JobStatsCompat(self))
+
+ if not hasattr(gcp_bigquery.Client, "tables"):
+ gcp_bigquery.Client.tables = property(
+ lambda self: _ClientTablesCompat(self))
+ if not hasattr(gcp_bigquery.Client, "datasets"):
+ gcp_bigquery.Client.datasets = property(
+ lambda self: _ClientDatasetsCompat(self))
+ if not hasattr(gcp_bigquery.Client, "jobs"):
+ gcp_bigquery.Client.jobs = property(lambda self: _ClientJobsCompat(self))
+
+
+# -----------------------------------------------------------------------------
+# Compatibility Helpers
+# -----------------------------------------------------------------------------
+
+
+def _to_json_compatible(obj):
+ """Converts an object or nested structure to JSON/API-compatible
dicts/types."""
+ if obj is None:
+ return None
+ if isinstance(obj, (str, int, float, bool)):
+ return obj
+ if isinstance(obj, (list, tuple, set)):
+ return [_to_json_compatible(item) for item in obj]
+ if isinstance(obj, dict):
+ return {k: _to_json_compatible(v) for k, v in obj.items()}
+ if hasattr(obj, "to_api_repr") and callable(obj.to_api_repr):
+ return obj.to_api_repr()
+ if _protorpclite_messages is not None and hasattr(
+ _protorpclite_messages, "Message") and isinstance(
+ obj, _protorpclite_messages.Message):
+ try:
+ from apitools.base.py import encoding
+ return encoding.MessageToDict(obj)
+ except Exception:
+ pass
+ return obj
+
+
+def _extract_dict_labels(labels):
+ """Converts labels to a non-empty dictionary or returns None."""
+ if not labels:
+ return None
+ labels = _to_json_compatible(labels)
+ if isinstance(labels, dict) and labels:
+ return labels
+ return None
+
+
+def _to_gcp_table_ref(table_ref, default_project=None):
+ """Converts a TableReference or string into a
google.cloud.bigquery.TableReference."""
+ if table_ref is None:
+ return None
+ if gcp_bigquery is not None and isinstance(
+ table_ref, getattr(gcp_bigquery, "TableReference", ())):
+ return table_ref
+ if isinstance(table_ref, str):
+ from apache_beam.io.gcp.bigquery_tools import parse_table_reference
+ table_ref = parse_table_reference(table_ref, project=default_project)
+ proj = getattr(table_ref, "projectId", None) or getattr(
+ table_ref, "project", None) or getattr(
+ table_ref, "project_id", None) or default_project or "default"
+ dataset_id = getattr(table_ref, "datasetId", None) or getattr(
+ table_ref, "dataset_id", None) or getattr(table_ref, "dataset", None)
+ table_id = getattr(table_ref, "tableId", None) or getattr(
+ table_ref, "table_id", None) or getattr(table_ref, "table", None)
+ if dataset_id and table_id:
+ if gcp_bigquery is not None and hasattr(
+ gcp_bigquery, "TableReference") and hasattr(gcp_bigquery,
+ "DatasetReference"):
+ return gcp_bigquery.TableReference(
+ gcp_bigquery.DatasetReference(proj, dataset_id), table_id)
+ return _TableReferenceCompat(
+ projectId=proj, datasetId=dataset_id, tableId=table_id)
+ return table_ref
+
+
+def _to_gcp_dataset_ref(dataset_ref, project=None):
+ """Converts a DatasetReference or string into a
google.cloud.bigquery.DatasetReference."""
+ if dataset_ref is None:
+ return None
+ if gcp_bigquery is not None and isinstance(
+ dataset_ref, getattr(gcp_bigquery, "DatasetReference", ())):
+ return dataset_ref
+ if isinstance(dataset_ref, str):
+ last_sep = max(dataset_ref.rfind("."), dataset_ref.rfind(":"))
+ if last_sep != -1:
+ proj = dataset_ref[:last_sep]
+ ds_id = dataset_ref[last_sep + 1:]
+ if gcp_bigquery is not None and hasattr(gcp_bigquery,
"DatasetReference"):
+ return gcp_bigquery.DatasetReference(proj, ds_id)
+ return _DatasetReferenceCompat(projectId=proj, datasetId=ds_id)
+ proj = project or "default"
+ if gcp_bigquery is not None and hasattr(gcp_bigquery, "DatasetReference"):
+ return gcp_bigquery.DatasetReference(proj, dataset_ref)
+ return _DatasetReferenceCompat(projectId=proj, datasetId=dataset_ref)
+ if hasattr(dataset_ref, "projectId") or hasattr(dataset_ref, "project"):
+ proj = getattr(dataset_ref, "projectId", None) or getattr(
+ dataset_ref, "project", None) or getattr(
+ dataset_ref, "project_id", None) or project or "default"
+ ds_id = getattr(dataset_ref, "datasetId", None) or getattr(
+ dataset_ref, "dataset_id", None)
+ if gcp_bigquery is not None and hasattr(gcp_bigquery, "DatasetReference"):
+ return gcp_bigquery.DatasetReference(proj, ds_id)
+ return _DatasetReferenceCompat(projectId=proj, datasetId=ds_id)
+ return dataset_ref
+
+
+def _table_field_to_dict(field):
+ """Create a dictionary representation of a table field for serialization."""
+ if isinstance(field, dict):
+ return field
+ result = {}
+ result['name'] = getattr(field, 'name', '')
+ result['type'] = getattr(field, 'type', None) or getattr(
+ field, 'field_type', None) or 'STRING'
+ result['mode'] = getattr(field, 'mode', 'NULLABLE') or 'NULLABLE'
+ if hasattr(field, 'description') and field.description is not None:
+ result['description'] = field.description
+ if hasattr(field, 'fields') and field.fields:
+ result['fields'] = [_table_field_to_dict(f) for f in field.fields]
+ return result
+
+
+def _to_gcp_schema(schema):
+ """Converts a TableSchema, list of fields, dict, or string into a list of
google.cloud.bigquery.SchemaField."""
+ if schema is None:
+ return None
+ if isinstance(schema, (list, tuple)):
+ fields = []
+ for f in schema:
+ if gcp_bigquery is not None and isinstance(
+ f, getattr(gcp_bigquery, "SchemaField", ())):
+ fields.append(f)
+ elif isinstance(f, dict) and gcp_bigquery is not None:
+ fields.append(gcp_bigquery.SchemaField.from_api_repr(f))
+ elif hasattr(f, "name") and gcp_bigquery is not None:
+ dict_field = _table_field_to_dict(f)
+ if isinstance(dict_field, dict):
+ fields.append(gcp_bigquery.SchemaField.from_api_repr(dict_field))
+ else:
+ fields.append(f)
+ else:
+ fields.append(f)
+ return fields
+ if isinstance(schema, TableSchema) or hasattr(schema, "fields"):
+ from apache_beam.io.gcp.bigquery_tools import get_dict_table_schema
+ dict_schema = get_dict_table_schema(schema)
+ if isinstance(dict_schema, dict) and gcp_bigquery is not None:
+ return [
+ gcp_bigquery.SchemaField.from_api_repr(f)
+ for f in dict_schema.get("fields", [])
+ ]
+ if hasattr(schema, "fields") and schema.fields is not None:
+ return list(schema.fields)
+ if isinstance(schema, dict):
+ if gcp_bigquery is not None:
+ return [
+ gcp_bigquery.SchemaField.from_api_repr(f)
+ for f in schema.get("fields", [])
+ ]
+ return schema.get("fields", [])
+ if isinstance(schema, str):
+ from apache_beam.io.gcp.bigquery_tools import get_dict_table_schema
+ return _to_gcp_schema(get_dict_table_schema(schema))
+ return schema
+
+
+def _to_table_schema(schema):
+ """Converts a list of google.cloud.bigquery.SchemaField, dict, or
TableSchema into a TableSchema."""
+ if schema is None:
+ return TableSchema()
+ if isinstance(schema, TableSchema):
+ return schema
+ if isinstance(schema, dict):
+ return _to_table_schema(schema.get("fields", []))
+ if hasattr(schema, "fields") and not isinstance(schema, (list, tuple)):
+ return _to_table_schema(schema.fields)
+
+ def _to_field_schema(f):
+ if isinstance(f, TableFieldSchema):
+ return f
+ if isinstance(f, dict):
+ f_dict = f
+ elif hasattr(f, "to_api_repr"):
+ f_dict = f.to_api_repr()
+ else:
+ f_dict = None
+
+ if f_dict is not None:
+ name = f_dict.get("name", "")
+ field_type = f_dict.get("type") or f_dict.get("type_") or "STRING"
+ mode = f_dict.get("mode", "NULLABLE")
+ description = f_dict.get("description", None)
+ sub_fields = [_to_field_schema(sf) for sf in f_dict.get("fields", [])]
+ return TableFieldSchema(
+ name=name,
+ type=field_type,
+ mode=mode,
+ description=description,
+ fields=sub_fields)
+
+ name = getattr(f, "name", "")
+ field_type = getattr(f, "field_type", None) or getattr(
+ f, "type", None) or "STRING"
+ mode = getattr(f, "mode", "NULLABLE")
+ description = getattr(f, "description", None)
+ sub = getattr(f, "fields", ())
+ sub_fields = [_to_field_schema(sf) for sf in sub] if sub else ()
+ return TableFieldSchema(
+ name=name,
+ type=field_type,
+ mode=mode,
+ description=description,
+ fields=sub_fields)
+
+ if isinstance(schema, (list, tuple)):
+ return TableSchema(fields=[_to_field_schema(f) for f in schema])
+ return TableSchema()
+
+
+# -----------------------------------------------------------------------------
+# Emulated Client Compatibility Interfaces
+# -----------------------------------------------------------------------------
+
+
+class _JobStatusCompat:
+ def __init__(self, job):
+ self._job = job
+
+ @property
+ def state(self):
+ return self._job.state
+
+ @property
+ def errorResult(self):
+ return self._job.error_result
+
+ @property
+ def errors(self):
+ return self._job.errors
+
+
+class _JobStatsCompat:
+ def __init__(self, job):
+ self._job = job
+
+ @property
+ def query(self):
+ return self
+
+ @property
+ def totalBytesBilled(self):
+ return getattr(self._job, "total_bytes_billed", None)
+
+ @property
+ def totalBytesProcessed(self):
+ return getattr(self._job, "total_bytes_processed", None)
+
+ @property
+ def referencedTables(self):
+ tables = getattr(self._job, "referenced_tables", None)
+ if tables is not None:
+ return [
+ TableReference(
+ projectId=t.project, datasetId=t.dataset_id, tableId=t.table_id)
+ for t in tables
+ ]
+ return None
+
+
+class _ClientTablesCompat:
+ def __init__(self, client):
+ self._client = client
+
+ def Get(self, request):
+ proj = getattr(request, "projectId", None)
+ ds_id = getattr(request, "datasetId", None)
+ tbl_id = getattr(request, "tableId", None)
+ if ds_id and tbl_id:
+ table_ref = gcp_bigquery.TableReference(
+ gcp_bigquery.DatasetReference(
+ proj or getattr(self._client, "project", None) or "default",
+ ds_id),
+ tbl_id)
+ else:
+ t_ref = getattr(request, "tableReference", None) or getattr(
+ request, "tableId", None) or request
+ table_ref = _to_gcp_table_ref(
+ t_ref, default_project=proj or getattr(self._client, "project",
None))
+ return self._client.get_table(table_ref)
+
+ def Insert(self, request):
+ table = getattr(request, "table", None)
+ if table is not None:
+ t_ref = getattr(table, "tableReference", None)
+ proj = getattr(t_ref, "projectId", None) or getattr(
+ request, "projectId", None)
+ ds_id = getattr(t_ref, "datasetId", None) or getattr(
+ request, "datasetId", None)
+ tbl_id = getattr(t_ref, "tableId", None)
+ schema = getattr(table, "schema", None)
+ else:
+ proj = getattr(request, "projectId", None)
+ ds_id = getattr(request, "datasetId", None)
+ tbl_id = getattr(request, "tableId", None)
+ schema = getattr(request, "schema", None)
+ gcp_tbl_ref = gcp_bigquery.TableReference(
+ gcp_bigquery.DatasetReference(
+ proj or getattr(self._client, "project", None) or "default",
ds_id),
+ tbl_id)
+ gcp_table = gcp_bigquery.Table(gcp_tbl_ref, schema=_to_gcp_schema(schema))
+ if table is not None:
+ tp = _extract_field(table, "timePartitioning", "time_partitioning")
+ if tp is not None:
+ gcp_table.time_partitioning = _to_gcp_time_partitioning(tp)
+ rp = _extract_field(table, "rangePartitioning", "range_partitioning")
+ if rp is not None:
+ gcp_table.range_partitioning = _to_gcp_range_partitioning(rp)
+ clustering = _extract_field(table, "clustering", "clustering_fields")
+ if clustering is not None:
+ cl_fields = _to_gcp_clustering_fields(clustering)
+ if cl_fields:
+ gcp_table.clustering_fields = cl_fields
+ desc = _extract_field(table, "description")
+ if desc is not None:
+ gcp_table.description = desc
+ fname = _extract_field(table, "friendlyName", "friendly_name")
+ if fname is not None:
+ gcp_table.friendly_name = fname
+ dict_labels = _extract_dict_labels(getattr(table, "labels", None))
+ if dict_labels:
+ gcp_table.labels = dict_labels
+ enc = _extract_field(
+ table, "encryptionConfiguration", "encryption_configuration")
+ kms = _extract_field(
+ enc, "kmsKeyName", "kms_key_name") if enc is not None else None
+ if kms:
+ gcp_table.encryption_configuration = (
+ gcp_bigquery.EncryptionConfiguration(kms_key_name=kms))
+ return self._client.create_table(gcp_table, exists_ok=True)
+
+ def Delete(self, request):
+ t_ref = getattr(request, "tableReference", None)
+ proj = getattr(t_ref, "projectId", None) or getattr(
+ request, "projectId", None)
+ ds_id = getattr(t_ref, "datasetId", None) or getattr(
+ request, "datasetId", None)
+ tbl_id = getattr(t_ref, "tableId", None) or getattr(
+ request, "tableId", None)
+ gcp_tbl_ref = gcp_bigquery.TableReference(
+ gcp_bigquery.DatasetReference(
+ proj or getattr(self._client, "project", None) or "default",
ds_id),
+ tbl_id)
+ return self._client.delete_table(gcp_tbl_ref, not_found_ok=True)
+
+ def List(self, request):
+ proj = getattr(request, "projectId", None)
+ ds_id = getattr(request, "datasetId", None)
+ ds_ref = gcp_bigquery.DatasetReference(
+ proj or getattr(self._client, "project", None) or "default", ds_id)
+ return self._client.list_tables(ds_ref)
+
+ def Patch(self, request):
+ table = getattr(request, "table", None)
+ proj = getattr(request, "projectId", None)
+ ds_id = getattr(request, "datasetId", None)
+ tbl_id = getattr(request, "tableId", None)
+ gcp_tbl_ref = gcp_bigquery.TableReference(
+ gcp_bigquery.DatasetReference(
+ proj or getattr(self._client, "project", None) or "default",
ds_id),
+ tbl_id)
+ gcp_table = gcp_bigquery.Table(gcp_tbl_ref)
+ if table and getattr(table, "schema", None):
+ gcp_table.schema = _to_gcp_schema(table.schema)
+ return self._client.update_table(gcp_table, ["schema"])
+
+ def Update(self, request):
+ return self.Patch(request)
+
+
+class _ClientDatasetsCompat:
+ def __init__(self, client):
+ self._client = client
+
+ def Get(self, request):
+ proj = getattr(request, "projectId", None)
+ ds_id = getattr(request, "datasetId", None)
+ ds_ref = gcp_bigquery.DatasetReference(
+ proj or getattr(self._client, "project", None) or "default", ds_id)
+ return self._client.get_dataset(ds_ref)
+
+ def Insert(self, request):
+ dataset = getattr(request, "dataset", None)
+ ds_ref_raw = getattr(dataset, "datasetReference", None) if dataset else
None
+ proj = getattr(ds_ref_raw, "projectId", None) or getattr(
+ request, "projectId", None)
+ ds_id = getattr(ds_ref_raw, "datasetId", None) or getattr(
+ request, "datasetId", None)
+ ds_ref = gcp_bigquery.DatasetReference(
+ proj or getattr(self._client, "project", None) or "default", ds_id)
+ gcp_ds = gcp_bigquery.Dataset(ds_ref)
+ if dataset:
+ if getattr(dataset, "location", None):
+ gcp_ds.location = dataset.location
+ if getattr(dataset, "defaultTableExpirationMs", None):
+ gcp_ds.default_table_expiration_ms = dataset.defaultTableExpirationMs
+ return self._client.create_dataset(gcp_ds, exists_ok=True)
+
+ def Delete(self, request):
+ proj = getattr(request, "projectId", None)
+ ds_id = getattr(request, "datasetId", None)
+ delete_contents = getattr(request, "deleteContents", True)
+ ds_ref = gcp_bigquery.DatasetReference(
+ proj or getattr(self._client, "project", None) or "default", ds_id)
+ return self._client.delete_dataset(
+ ds_ref, delete_contents=delete_contents, not_found_ok=True)
+
+ def List(self, request):
+ proj = getattr(request, "projectId", None) or getattr(
+ self._client, "project", None)
+ return self._client.list_datasets(project=proj)
+
+ def Patch(self, request):
+ dataset = getattr(request, "dataset", None)
+ proj = getattr(request, "projectId", None)
+ ds_id = getattr(request, "datasetId", None)
+ ds_ref = gcp_bigquery.DatasetReference(
+ proj or getattr(self._client, "project", None) or "default", ds_id)
+ gcp_ds = gcp_bigquery.Dataset(ds_ref)
+ fields_to_update = []
+ if dataset:
+ if getattr(dataset, "defaultTableExpirationMs", None):
+ gcp_ds.default_table_expiration_ms = dataset.defaultTableExpirationMs
+ fields_to_update.append("default_table_expiration_ms")
+ return self._client.update_dataset(gcp_ds, fields_to_update)
+
+ def Update(self, request):
+ return self.Patch(request)
+
+
+class _ClientJobsCompat:
+ def __init__(self, client):
+ self._client = client
+
+ def Get(self, request):
+ proj = getattr(request, "projectId", None)
+ job_id = getattr(request, "jobId", None)
+ loc = getattr(request, "location", None)
+ return self._client.get_job(job_id, project=proj, location=loc)
+
+ def GetQueryResults(self, request):
+ proj = getattr(request, "projectId", None)
+ job_id = getattr(request, "jobId", None)
+ loc = getattr(request, "location", None)
+ page_token = getattr(request, "pageToken", None)
+ max_results = getattr(request, "maxResults", None)
+ job = self._client.get_job(job_id, project=proj, location=loc)
+ if page_token is not None:
+ return self._client.list_rows(
+ job, page_token=page_token, max_results=max_results)
+ return job.result(max_results=max_results)
+
+ def Insert(self, request, upload=None):
+ job_obj = getattr(request, "job", None)
+ job_ref = (
+ getattr(job_obj, "jobReference", None) if job_obj else getattr(
+ request, "jobReference", None))
+ job_id = getattr(job_ref, "jobId", None) or getattr(job_ref, "job_id",
None)
+ proj = (
+ getattr(request, "projectId", None) or
+ getattr(job_ref, "projectId", None) or
+ getattr(job_ref, "project", None))
+ config = getattr(job_obj, "configuration", None) if job_obj else None
+ dict_labels = _extract_dict_labels(
+ getattr(config, "labels", None)) if config else None
+ if config and getattr(config, "query", None):
+ q = config.query
+ dest = None
+ if getattr(q, "destinationTable", None):
+ dest = _to_gcp_table_ref(q.destinationTable, default_project=proj)
+ job_config = gcp_bigquery.QueryJobConfig(
+ dry_run=getattr(q, "dryRun", False),
+ use_legacy_sql=getattr(q, "useLegacySql", False) if getattr(
+ q, "useLegacySql", None) is not None else False,
+ flatten_results=getattr(q, "flattenResults", None),
+ priority=getattr(q, "priority", "INTERACTIVE"),
+ destination=dest,
+ )
+ if dict_labels:
+ job_config.labels = dict_labels
+ kms = getattr(
+ getattr(q, "destinationEncryptionConfiguration", None),
+ "kmsKeyName",
+ None)
+ if kms:
+ job_config.destination_encryption_configuration = (
+ gcp_bigquery.EncryptionConfiguration(kms_key_name=kms))
+ return self._client.query(
+ q.query,
+ job_config=job_config,
+ job_id=job_id,
+ project=proj,
+ job_retry=None,
+ )
+ elif config and getattr(config, "load", None):
+ ld = config.load
+ dest = _to_gcp_table_ref(
+ getattr(ld, "destinationTable", None), default_project=proj)
+ uris = list(getattr(ld, "sourceUris", []))
+ if uris:
+ job_config = gcp_bigquery.LoadJobConfig()
+ if dict_labels:
+ job_config.labels = dict_labels
+ if getattr(ld, "schema", None):
+ job_config.schema = _to_gcp_schema(ld.schema)
+ if getattr(ld, "writeDisposition", None):
+ job_config.write_disposition = ld.writeDisposition
+ if getattr(ld, "createDisposition", None):
+ job_config.create_disposition = ld.createDisposition
+ return self._client.load_table_from_uri(
+ uris, dest, job_config=job_config, job_id=job_id, project=proj)
+ elif config and getattr(config, "copy", None):
+ cp = config.copy
+ sources = [
+ _to_gcp_table_ref(s, default_project=proj)
+ for s in getattr(cp, "sourceTables", [])
+ ]
+ dest = _to_gcp_table_ref(
+ getattr(cp, "destinationTable", None), default_project=proj)
+ job_config = gcp_bigquery.CopyJobConfig()
+ if dict_labels:
+ job_config.labels = dict_labels
+ if getattr(cp, "writeDisposition", None):
+ job_config.write_disposition = cp.writeDisposition
+ if getattr(cp, "createDisposition", None):
+ job_config.create_disposition = cp.createDisposition
+ return self._client.copy_table(
+ sources, dest, job_config=job_config, job_id=job_id, project=proj)
+ elif config and getattr(config, "extract", None):
+ ex = config.extract
+ src = _to_gcp_table_ref(
+ getattr(ex, "sourceTable", None), default_project=proj)
+ uris = list(getattr(ex, "destinationUris", []))
+ job_config = gcp_bigquery.ExtractJobConfig()
+ if dict_labels:
+ job_config.labels = dict_labels
+ if getattr(ex, "destinationFormat", None):
+ job_config.destination_format = ex.destinationFormat
+ return self._client.extract_table(
+ src, uris, job_config=job_config, job_id=job_id, project=proj)
+
+ return self._client.get_job(job_id, project=proj)
+
+
+# Automatically execute compatibility setup and monkey-patching upon import
+_patch_protorpclite_equality()
+_patch_gcp_bigquery()
diff --git a/sdks/python/apache_beam/io/gcp/bigquery_compat_test.py
b/sdks/python/apache_beam/io/gcp/bigquery_compat_test.py
new file mode 100644
index 00000000000..ca77abc8dcb
--- /dev/null
+++ b/sdks/python/apache_beam/io/gcp/bigquery_compat_test.py
@@ -0,0 +1,796 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+"""Unit tests for BigQuery compatibility shims and legacy client emulation.
+
+These tests verify the compatibility models, monkey-patches, and emulated
+interfaces in ``apache_beam.io.gcp.bigquery_compat``.
+
+.. note::
+ This test suite is intended to be completely removed along with
+ ``bigquery_compat.py`` in a future Beam release when apitools is removed.
+"""
+
+# pytype: skip-file
+
+import importlib
+import logging
+import os
+import pickle
+import unittest
+import warnings
+from unittest import mock
+
+import apache_beam as beam
+from apache_beam.utils.annotations import BeamDeprecationWarning
+
+# Opt in to BigQuery compatibility shims so the compatibility test suite can
run.
+os.environ["BEAM_USE_BIGQUERY_COMPAT_SHIMS"] = "1"
+
+try:
+ from apache_beam.io.gcp import bigquery_compat
+ from apache_beam.io.gcp.internal.clients import bigquery as apitools_bigquery
+except ImportError:
+ bigquery_compat = None
+ apitools_bigquery = None
+
+try:
+ from apitools.base.py.exceptions import HttpError
+except ImportError:
+ HttpError = None
+
+try:
+ from google.cloud import bigquery as gcp_bigquery
+except ImportError:
+ gcp_bigquery = None
+
+
[email protected](bigquery_compat is None, 'GCP dependencies are not installed')
+class TestBigQueryCompatOptIn(unittest.TestCase):
+ """Tests for explicit opt-in environment variable check and warnings."""
+ def test_check_compat_opt_in_disabled(self):
+ # Disabled when unset or empty
+ with mock.patch.dict(os.environ, {"BEAM_USE_BIGQUERY_COMPAT_SHIMS": ""}):
+ with self.assertRaises(ImportError) as ctx:
+ bigquery_compat._check_compat_opt_in()
+ self.assertIn("BEAM_USE_BIGQUERY_COMPAT_SHIMS", str(ctx.exception))
+ self.assertIn("disabled by default", str(ctx.exception))
+
+ # Disabled when 0 or false or no
+ for disabled_val in ("0", "false", "False", "no", "NO"):
+ with mock.patch.dict(os.environ,
+ {"BEAM_USE_BIGQUERY_COMPAT_SHIMS": disabled_val}):
+ with self.assertRaises(ImportError):
+ bigquery_compat._check_compat_opt_in()
+
+ def test_check_compat_opt_in_enabled_warning(self):
+ for enabled_val in ("1", "true", "True", "yes", "enabled", "on"):
+ with mock.patch.dict(os.environ,
+ {"BEAM_USE_BIGQUERY_COMPAT_SHIMS": enabled_val}):
+ with warnings.catch_warnings(record=True) as recorded:
+ warnings.simplefilter("always")
+ bigquery_compat._check_compat_opt_in()
+ self.assertTrue(
+ any(
+ issubclass(w.category, BeamDeprecationWarning)
+ for w in recorded),
+ f"Expected BeamDeprecationWarning for {enabled_val}")
+ self.assertTrue(
+ any("deprecated" in str(w.message).lower() for w in recorded),
+ f"Expected deprecation message for {enabled_val}")
+
+ def test_reload_without_env_var_raises_import_error(self):
+ try:
+ with mock.patch.dict(os.environ, {"BEAM_USE_BIGQUERY_COMPAT_SHIMS": ""}):
+ with self.assertRaises(ImportError):
+ importlib.reload(bigquery_compat)
+ finally:
+ with mock.patch.dict(os.environ, {"BEAM_USE_BIGQUERY_COMPAT_SHIMS":
"1"}):
+ importlib.reload(bigquery_compat)
+
+
[email protected](bigquery_compat is None, 'GCP dependencies are not installed')
+class TestJobReferenceCompatibility(unittest.TestCase):
+ def test_init_camel_case(self):
+ ref = bigquery_compat.JobReference(
+ jobId="test-job", projectId="test-proj", location="US")
+ self.assertEqual(ref.jobId, "test-job")
+ self.assertEqual(ref.job_id, "test-job")
+ self.assertEqual(ref.projectId, "test-proj")
+ self.assertEqual(ref.project, "test-proj")
+ self.assertEqual(ref.project_id, "test-proj")
+ self.assertEqual(ref.location, "US")
+
+ def test_init_snake_case(self):
+ ref = bigquery_compat.JobReference(
+ job_id="test-job", project="test-proj", location="EU")
+ self.assertEqual(ref.jobId, "test-job")
+ self.assertEqual(ref.job_id, "test-job")
+ self.assertEqual(ref.projectId, "test-proj")
+ self.assertEqual(ref.project, "test-proj")
+ self.assertEqual(ref.project_id, "test-proj")
+ self.assertEqual(ref.location, "EU")
+
+ def test_init_with_project_id(self):
+ ref = bigquery_compat.JobReference(
+ job_id="test-job", project_id="test-proj", location="EU")
+ self.assertEqual(ref.jobId, "test-job")
+ self.assertEqual(ref.job_id, "test-job")
+ self.assertEqual(ref.projectId, "test-proj")
+ self.assertEqual(ref.project, "test-proj")
+ self.assertEqual(ref.project_id, "test-proj")
+
+ def test_attribute_setters(self):
+ ref = bigquery_compat.JobReference()
+ ref.job_id = "j1"
+ ref.project = "p1"
+ self.assertEqual(ref.jobId, "j1")
+ self.assertEqual(ref.job_id, "j1")
+ self.assertEqual(ref.projectId, "p1")
+ self.assertEqual(ref.project, "p1")
+ self.assertEqual(ref.project_id, "p1")
+ ref.project_id = "p2"
+ self.assertEqual(ref.projectId, "p2")
+ self.assertEqual(ref.project, "p2")
+ self.assertEqual(ref.project_id, "p2")
+
+ def test_equality_with_custom_and_apitools(self):
+ ref1 = bigquery_compat.JobReference(
+ job_id="j1", project="p1", location="US")
+ ref2 = bigquery_compat.JobReference(
+ jobId="j1", projectId="p1", location="US")
+ ref3 = bigquery_compat.JobReference(
+ jobId="j2", projectId="p1", location="US")
+ self.assertEqual(ref1, ref2)
+ self.assertNotEqual(ref1, ref3)
+ self.assertEqual(hash(ref1), hash(ref2))
+
+ if apitools_bigquery is not None and hasattr(apitools_bigquery,
+ "JobReference"):
+ ap_ref = apitools_bigquery.JobReference(
+ jobId="j1", projectId="p1", location="US")
+ self.assertEqual(ref1, ap_ref)
+ self.assertEqual(ap_ref, ref1)
+
+ def test_equality_type_safety(self):
+ empty_ref = bigquery_compat.JobReference()
+ self.assertNotEqual(empty_ref, None)
+ self.assertNotEqual(empty_ref, 123)
+ self.assertNotEqual(empty_ref, "")
+ self.assertNotEqual(empty_ref, {})
+
+ ref = bigquery_compat.JobReference(project="p1")
+ table_ref = bigquery_compat.TableReference(
+ projectId="p1", datasetId="d1", tableId="t1")
+ self.assertNotEqual(ref, table_ref)
+ self.assertNotEqual(empty_ref, table_ref)
+
+ def test_pickle_and_coder_roundtrip(self):
+ ref = bigquery_compat.JobReference(job_id="j1", project="p1",
location="US")
+ pickled = pickle.dumps(ref)
+ unpickled = pickle.loads(pickled)
+ self.assertEqual(ref, unpickled)
+ self.assertEqual(unpickled.jobId, "j1")
+ self.assertEqual(unpickled.projectId, "p1")
+ self.assertEqual(unpickled.location, "US")
+
+ coder = beam.coders.FastPrimitivesCoder()
+ encoded = coder.encode(ref)
+ decoded = coder.decode(encoded)
+ self.assertEqual(ref, decoded)
+
+
[email protected](
+ HttpError is None or gcp_bigquery is None,
+ 'GCP dependencies are not installed')
+class TestTableAndDatasetReferenceCompatibility(unittest.TestCase):
+ @unittest.skipIf(gcp_bigquery is None, "google-cloud-bigquery not installed")
+ def test_table_reference_property_mutability(self):
+ ds = gcp_bigquery.DatasetReference("p1", "d1")
+ table = gcp_bigquery.TableReference(ds, "t1")
+ table.tableId = "t2"
+ self.assertEqual(table.tableId, "t2")
+ self.assertEqual(table.table_id, "t2")
+ table.datasetId = "d2"
+ self.assertEqual(table.datasetId, "d2")
+ self.assertEqual(table.dataset_id, "d2")
+ table.projectId = "p2"
+ self.assertEqual(table.projectId, "p2")
+ self.assertEqual(table.project, "p2")
+
+ def test_table_reference_compat_model(self):
+ table = bigquery_compat._TableReferenceCompat(
+ projectId="p1", datasetId="d1", tableId="t1")
+ self.assertEqual(table.projectId, "p1")
+ self.assertEqual(table.project, "p1")
+ self.assertEqual(table.project_id, "p1")
+ self.assertEqual(table.datasetId, "d1")
+ self.assertEqual(table.dataset_id, "d1")
+ self.assertEqual(table.tableId, "t1")
+ self.assertEqual(table.table_id, "t1")
+ table.tableId = "t2"
+ self.assertEqual(table.tableId, "t2")
+ self.assertEqual(table.table_id, "t2")
+ table.datasetId = "d2"
+ self.assertEqual(table.datasetId, "d2")
+ self.assertEqual(table.dataset_id, "d2")
+ table.projectId = "p2"
+ self.assertEqual(table.projectId, "p2")
+ self.assertEqual(table.project, "p2")
+ self.assertEqual(table.project_id, "p2")
+
+ def test_dataset_reference_compat_model(self):
+ ds = bigquery_compat._DatasetReferenceCompat(projectId="p1",
datasetId="d1")
+ self.assertEqual(ds.projectId, "p1")
+ self.assertEqual(ds.project, "p1")
+ self.assertEqual(ds.project_id, "p1")
+ self.assertEqual(ds.datasetId, "d1")
+ self.assertEqual(ds.dataset_id, "d1")
+ ds.projectId = "p2"
+ self.assertEqual(ds.projectId, "p2")
+ self.assertEqual(ds.project, "p2")
+ ds.datasetId = "d2"
+ self.assertEqual(ds.datasetId, "d2")
+ self.assertEqual(ds.dataset_id, "d2")
+
+ def test_to_gcp_dataset_ref_colon_format(self):
+ ds_ref = bigquery_compat._to_gcp_dataset_ref("my-project:my_dataset")
+ self.assertEqual(ds_ref.project, "my-project")
+ self.assertEqual(ds_ref.dataset_id, "my_dataset")
+ self.assertEqual(ds_ref.projectId, "my-project")
+ self.assertEqual(ds_ref.datasetId, "my_dataset")
+
+ def test_to_gcp_dataset_ref_domain_scoped(self):
+ ds_ref1 = bigquery_compat._to_gcp_dataset_ref(
+ "google.com:clouddfe:my_dataset")
+ self.assertEqual(ds_ref1.project, "google.com:clouddfe")
+ self.assertEqual(ds_ref1.dataset_id, "my_dataset")
+
+ ds_ref2 = bigquery_compat._to_gcp_dataset_ref(
+ "google.com:clouddfe.my_dataset")
+ self.assertEqual(ds_ref2.project, "google.com:clouddfe")
+ self.assertEqual(ds_ref2.dataset_id, "my_dataset")
+
+ def test_table_reference_from_string_default_project(self):
+ t_ref = bigquery_compat._TableReferenceCompat.from_string(
+ "my_ds.my_tbl", default_project="default-proj")
+ self.assertEqual(t_ref.projectId, "default-proj")
+ self.assertEqual(t_ref.datasetId, "my_ds")
+ self.assertEqual(t_ref.tableId, "my_tbl")
+
+ def test_dataset_reference_from_string_domain_scoped(self):
+ ds_ref = bigquery_compat._DatasetReferenceCompat.from_string(
+ "google.com:clouddfe:my_dataset")
+ self.assertEqual(ds_ref.projectId, "google.com:clouddfe")
+ self.assertEqual(ds_ref.datasetId, "my_dataset")
+
+ def test_table_field_schema_compat(self):
+ f = bigquery_compat._TableFieldSchemaCompat(
+ name="age", type="INTEGER", mode="REQUIRED")
+ self.assertEqual(f.name, "age")
+ self.assertEqual(f.type, "INTEGER")
+ self.assertEqual(f.mode, "REQUIRED")
+
+ def test_table_schema_compat(self):
+ f1 = bigquery_compat._TableFieldSchemaCompat(name="id", type="INTEGER")
+ f2 = bigquery_compat._TableFieldSchemaCompat(name="val", type="STRING")
+ s = bigquery_compat._TableSchemaCompat([f1, f2])
+ self.assertEqual(len(s.fields), 2)
+ self.assertEqual(s.fields[0].name, "id")
+ self.assertEqual(s.fields[1].name, "val")
+
+
[email protected](
+ HttpError is None or gcp_bigquery is None,
+ 'GCP dependencies are not installed')
+class TestJobConfigCompatibility(unittest.TestCase):
+ def test_load_job_config_camel_case_properties(self):
+ if gcp_bigquery is None:
+ raise unittest.SkipTest("google-cloud-bigquery is not installed")
+
+ config = gcp_bigquery.LoadJobConfig(
+ schemaUpdateOptions=["ALLOW_FIELD_ADDITION", "ALLOW_FIELD_RELAXATION"],
+ ignoreUnknownValues=True,
+ maxBadRecords=10,
+ nullMarker="\\N",
+ fieldDelimiter="\t",
+ skipLeadingRows=1,
+ allowJaggedRows=True,
+ allowQuotedNewlines=True,
+ decimalTargetTypes=["NUMERIC"],
+ useAvroLogicalTypes=True,
+ )
+ self.assertEqual(
+ config.schemaUpdateOptions,
+ ["ALLOW_FIELD_ADDITION", "ALLOW_FIELD_RELAXATION"])
+ self.assertEqual(
+ config.schema_update_options,
+ ["ALLOW_FIELD_ADDITION", "ALLOW_FIELD_RELAXATION"])
+ self.assertTrue(config.ignoreUnknownValues)
+ self.assertTrue(config.ignore_unknown_values)
+ self.assertEqual(config.maxBadRecords, 10)
+ self.assertEqual(config.max_bad_records, 10)
+ self.assertEqual(config.nullMarker, "\\N")
+ self.assertEqual(config.null_marker, "\\N")
+ self.assertEqual(config.fieldDelimiter, "\t")
+ self.assertEqual(config.field_delimiter, "\t")
+ self.assertEqual(config.skipLeadingRows, 1)
+ self.assertEqual(config.skip_leading_rows, 1)
+ self.assertTrue(config.allowJaggedRows)
+ self.assertTrue(config.allow_jagged_rows)
+ self.assertTrue(config.allowQuotedNewlines)
+ self.assertTrue(config.allow_quoted_newlines)
+ self.assertEqual(set(config.decimalTargetTypes), {"NUMERIC"})
+ self.assertEqual(set(config.decimal_target_types), {"NUMERIC"})
+ self.assertTrue(config.useAvroLogicalTypes)
+ self.assertTrue(config.use_avro_logical_types)
+
+ def test_query_job_config_camel_case_properties(self):
+ if gcp_bigquery is None:
+ raise unittest.SkipTest("google-cloud-bigquery is not installed")
+
+ config = gcp_bigquery.QueryJobConfig(
+ schemaUpdateOptions=["ALLOW_FIELD_ADDITION"],
+ useLegacySql=True,
+ flattenResults=False,
+ allowLargeResults=True,
+ maximumBytesBilled=1000000,
+ )
+ self.assertEqual(config.schemaUpdateOptions, ["ALLOW_FIELD_ADDITION"])
+ self.assertEqual(config.schema_update_options, ["ALLOW_FIELD_ADDITION"])
+ self.assertTrue(config.useLegacySql)
+ self.assertTrue(config.use_legacy_sql)
+ self.assertFalse(config.flattenResults)
+ self.assertFalse(config.flatten_results)
+ self.assertTrue(config.allowLargeResults)
+ self.assertTrue(config.allow_large_results)
+ self.assertEqual(config.maximumBytesBilled, 1000000)
+ self.assertEqual(config.maximum_bytes_billed, 1000000)
+
+ def test_insert_load_job_with_none_labels(self):
+ if gcp_bigquery is None:
+ raise unittest.SkipTest("google-cloud-bigquery is not installed")
+
+ client = mock.MagicMock(spec=gcp_bigquery.Client)
+ mock_job = mock.MagicMock()
+ mock_job.job_id = "load_job_id"
+ mock_job.project = "test-project"
+ mock_job.location = "US"
+ client.load_table_from_uri.return_value = mock_job
+
+ jobs_compat = bigquery_compat._ClientJobsCompat(client)
+ request = mock.MagicMock()
+ request.projectId = "test-project"
+ request.job = mock.MagicMock()
+ request.job.jobReference =
bigquery_compat.JobReference(jobId="load_job_id")
+ request.job.configuration = mock.MagicMock(spec=["load", "labels"])
+ request.job.configuration.load.destinationTable = (
+ bigquery_compat.TableReference(
+ projectId="test-project",
+ datasetId="test_dataset",
+ tableId="test_table"))
+ request.job.configuration.load.sourceUris = ["gs://test-bucket/test.csv"]
+ request.job.configuration.load.schema = None
+ request.job.configuration.load.writeDisposition = None
+ request.job.configuration.load.createDisposition = None
+ request.job.configuration.labels = None
+
+ jobs_compat.Insert(request)
+ client.load_table_from_uri.assert_called_once()
+ called_config = client.load_table_from_uri.call_args.kwargs["job_config"]
+ self.assertEqual(called_config.labels, {})
+
+ def test_insert_copy_job_with_none_labels(self):
+ if gcp_bigquery is None:
+ raise unittest.SkipTest("google-cloud-bigquery is not installed")
+
+ client = mock.MagicMock(spec=gcp_bigquery.Client)
+ mock_job = mock.MagicMock()
+ mock_job.job_id = "copy_job_id"
+ mock_job.project = "test-project"
+ mock_job.location = "US"
+ client.copy_table.return_value = mock_job
+
+ jobs_compat = bigquery_compat._ClientJobsCompat(client)
+ request = mock.MagicMock()
+ request.projectId = "test-project"
+ request.job = mock.MagicMock()
+ request.job.jobReference =
bigquery_compat.JobReference(jobId="copy_job_id")
+ request.job.configuration = mock.MagicMock(spec=["copy", "labels"])
+ request.job.configuration.copy.sourceTables = [
+ bigquery_compat.TableReference(
+ projectId="test-project",
+ datasetId="test_dataset",
+ tableId="src_table")
+ ]
+ request.job.configuration.copy.destinationTable = (
+ bigquery_compat.TableReference(
+ projectId="test-project",
+ datasetId="test_dataset",
+ tableId="dst_table"))
+ request.job.configuration.copy.writeDisposition = None
+ request.job.configuration.copy.createDisposition = None
+ request.job.configuration.labels = None
+
+ jobs_compat.Insert(request)
+ client.copy_table.assert_called_once()
+ called_config = client.copy_table.call_args.kwargs["job_config"]
+ self.assertEqual(called_config.labels, {})
+
+ def test_perform_extract_job_with_none_labels(self):
+ if gcp_bigquery is None:
+ raise unittest.SkipTest("google-cloud-bigquery is not installed")
+
+ client = mock.MagicMock(spec=gcp_bigquery.Client)
+ mock_job = mock.MagicMock()
+ mock_job.job_id = "extract_job_id"
+ mock_job.project = "test-project"
+ mock_job.location = "US"
+ client.extract_table.return_value = mock_job
+
+ jobs_compat = bigquery_compat._ClientJobsCompat(client)
+ request = mock.MagicMock()
+ request.projectId = "test-project"
+ request.job = mock.MagicMock()
+ request.job.jobReference = bigquery_compat.JobReference(
+ jobId="extract_job_id")
+ request.job.configuration = mock.MagicMock(spec=["extract", "labels"])
+ request.job.configuration.extract.sourceTable = (
+ bigquery_compat.TableReference(
+ projectId="test-project",
+ datasetId="test_dataset",
+ tableId="src_table"))
+ request.job.configuration.extract.destinationUris = [
+ "gs://test-bucket/output.csv"
+ ]
+ request.job.configuration.extract.destinationFormat = "CSV"
+ request.job.configuration.labels = None
+
+ jobs_compat.Insert(request)
+ client.extract_table.assert_called_once()
+ called_config = client.extract_table.call_args.kwargs["job_config"]
+ self.assertEqual(called_config.labels, {})
+
+ def test_labels_setter_clears_on_none(self):
+ if gcp_bigquery is None:
+ raise unittest.SkipTest("google-cloud-bigquery is not installed")
+
+ # Table labels
+ table = gcp_bigquery.Table("test-project.dataset.table")
+ table.labels = {"initial": "label"}
+ self.assertEqual(table.labels, {"initial": "label"})
+ table.labels = None
+ self.assertEqual(table.labels, {})
+
+ # Dataset labels
+ ds = gcp_bigquery.Dataset("test-project.dataset")
+ ds.labels = {"initial": "label"}
+ self.assertEqual(ds.labels, {"initial": "label"})
+ ds.labels = None
+ self.assertEqual(ds.labels, {})
+
+ # Job configs labels
+ for config_cls in (
+ gcp_bigquery.QueryJobConfig,
+ gcp_bigquery.LoadJobConfig,
+ gcp_bigquery.CopyJobConfig,
+ gcp_bigquery.ExtractJobConfig,
+ ):
+ cfg = config_cls(labels={"initial": "label"})
+ self.assertEqual(cfg.labels, {"initial": "label"})
+ cfg.labels = None
+ self.assertEqual(cfg.labels, {})
+
+ def test_table_partitioning_property_setters(self):
+ if gcp_bigquery is None:
+ raise unittest.SkipTest("google-cloud-bigquery is not installed")
+
+ table = gcp_bigquery.Table("test-project.dataset.table")
+ tp = gcp_bigquery.TimePartitioning(type_="DAY")
+ table.timePartitioning = tp
+ self.assertEqual(table.timePartitioning, tp)
+ self.assertEqual(table.time_partitioning, tp)
+
+ rp = gcp_bigquery.RangePartitioning(field="id")
+ table.rangePartitioning = rp
+ self.assertEqual(table.rangePartitioning, rp)
+ self.assertEqual(table.range_partitioning, rp)
+
+
[email protected](
+ HttpError is None or gcp_bigquery is None,
+ 'GCP dependencies are not installed')
+class TestSchemaConversionCompatibility(unittest.TestCase):
+ def test_to_table_schema_nested_records(self):
+ if gcp_bigquery is None:
+ raise unittest.SkipTest("google-cloud-bigquery is not installed")
+
+ schema = [
+ gcp_bigquery.SchemaField(
+ "person",
+ "RECORD",
+ mode="NULLABLE",
+ fields=[
+ gcp_bigquery.SchemaField("name", "STRING", mode="REQUIRED"),
+ gcp_bigquery.SchemaField("age", "INTEGER", mode="NULLABLE"),
+ gcp_bigquery.SchemaField(
+ "address",
+ "RECORD",
+ fields=[
+ gcp_bigquery.SchemaField(
+ "city", "STRING", mode="NULLABLE"),
+ ]),
+ ]),
+ ]
+ table_schema = bigquery_compat._to_table_schema(schema)
+ self.assertEqual(len(table_schema.fields), 1)
+ person = table_schema.fields[0]
+ self.assertEqual(person.name, "person")
+ self.assertEqual(person.type, "RECORD")
+ self.assertEqual(len(person.fields), 3)
+ self.assertEqual(person.fields[0].name, "name")
+ self.assertEqual(person.fields[0].type, "STRING")
+ self.assertEqual(person.fields[1].name, "age")
+ self.assertEqual(person.fields[1].type, "INTEGER")
+ self.assertEqual(person.fields[2].name, "address")
+ self.assertEqual(person.fields[2].type, "RECORD")
+ self.assertEqual(len(person.fields[2].fields), 1)
+ self.assertEqual(person.fields[2].fields[0].name, "city")
+ self.assertEqual(person.fields[2].fields[0].type, "STRING")
+
+ def test_to_table_schema_dict(self):
+ dict_schema = {
+ "fields": [
+ {
+ "name": "id", "type": "INTEGER", "mode": "REQUIRED"
+ },
+ {
+ "name": "val", "type": "STRING", "mode": "NULLABLE"
+ },
+ ]
+ }
+ table_schema = bigquery_compat._to_table_schema(dict_schema)
+ self.assertEqual(len(table_schema.fields), 2)
+ self.assertEqual(table_schema.fields[0].name, "id")
+ self.assertEqual(table_schema.fields[0].type, "INTEGER")
+ self.assertEqual(table_schema.fields[1].name, "val")
+ self.assertEqual(table_schema.fields[1].type, "STRING")
+
+
[email protected](
+ HttpError is None or gcp_bigquery is None,
+ 'GCP dependencies are not installed')
+class TestClientCompatibility(unittest.TestCase):
+ def test_job_stats_referenced_tables(self):
+ if gcp_bigquery is None:
+ raise unittest.SkipTest("google-cloud-bigquery is not installed")
+
+ mock_job = mock.MagicMock(spec=gcp_bigquery.QueryJob)
+ mock_job.job_id = "query_job_id"
+ mock_job.project = "test-project"
+ mock_job.location = "US"
+ t1 = gcp_bigquery.TableReference.from_string("test-project.dataset.table1")
+ t2 = gcp_bigquery.TableReference.from_string("test-project.dataset.table2")
+ mock_job.referenced_tables = [t1, t2]
+
+ stats = bigquery_compat._JobStatsCompat(mock_job)
+ ref_tables = stats.referencedTables
+ self.assertEqual(len(ref_tables), 2)
+ self.assertEqual(ref_tables[0].projectId, "test-project")
+ self.assertEqual(ref_tables[0].datasetId, "dataset")
+ self.assertEqual(ref_tables[0].tableId, "table1")
+ self.assertEqual(ref_tables[1].projectId, "test-project")
+ self.assertEqual(ref_tables[1].datasetId, "dataset")
+ self.assertEqual(ref_tables[1].tableId, "table2")
+
+ def test_client_tables_compat_insert_labels_and_metadata(self):
+ if gcp_bigquery is None:
+ raise unittest.SkipTest("google-cloud-bigquery is not installed")
+
+ client = mock.MagicMock(spec=gcp_bigquery.Client)
+ client.project = "test-project"
+ created_table_mock = mock.MagicMock(spec=gcp_bigquery.Table)
+ client.create_table.return_value = created_table_mock
+
+ tables_compat = bigquery_compat._ClientTablesCompat(client)
+ request = mock.MagicMock()
+ request.projectId = "test-project"
+ request.datasetId = "test_dataset"
+ request.table = mock.MagicMock()
+ request.table.tableReference = mock.MagicMock()
+ request.table.tableReference.projectId = "test-project"
+ request.table.tableReference.datasetId = "test_dataset"
+ request.table.tableReference.tableId = "test_table"
+ request.table.schema = None
+ request.table.labels = {"env": "test", "tier": "frontend"}
+ request.table.friendlyName = "My Test Table"
+ request.table.description = "A test table description"
+ request.table.timePartitioning = None
+ request.table.rangePartitioning = None
+ request.table.clustering = None
+ request.table.encryptionConfiguration = None
+
+ tables_compat.Insert(request)
+ client.create_table.assert_called_once()
+ passed_table = client.create_table.call_args.args[0]
+ self.assertEqual(passed_table.labels, {"env": "test", "tier": "frontend"})
+ self.assertEqual(passed_table.friendly_name, "My Test Table")
+ self.assertEqual(passed_table.description, "A test table description")
+
+
[email protected](
+ HttpError is None or gcp_bigquery is None,
+ 'GCP dependencies are not installed')
+class TestTablePartitioningAndClusteringCompatibility(unittest.TestCase):
+ def test_time_partitioning_from_dict_and_camel_case(self):
+ if gcp_bigquery is None:
+ raise unittest.SkipTest("google-cloud-bigquery is not installed")
+
+ table = gcp_bigquery.Table("my-proj.my_ds.my_tbl")
+ table.timePartitioning = {"type": "DAY"}
+ self.assertIsNotNone(table.time_partitioning)
+ self.assertEqual(table.time_partitioning.type_, "DAY")
+ self.assertEqual(table.timePartitioning.type, "DAY")
+
+ table2 = gcp_bigquery.Table("my-proj.my_ds.my_tbl")
+ table2.time_partitioning = {
+ "type": "HOUR", "field": "ts", "expirationMs": 86400000
+ }
+ self.assertEqual(table2.time_partitioning.type_, "HOUR")
+ self.assertEqual(table2.time_partitioning.field, "ts")
+ self.assertEqual(table2.time_partitioning.expiration_ms, 86400000)
+
+ def test_range_partitioning_from_dict_and_camel_case(self):
+ if gcp_bigquery is None:
+ raise unittest.SkipTest("google-cloud-bigquery is not installed")
+
+ table = gcp_bigquery.Table("my-proj.my_ds.my_tbl")
+ table.rangePartitioning = {
+ "field": "id", "range": {
+ "start": 0, "end": 100, "interval": 10
+ }
+ }
+ self.assertIsNotNone(table.range_partitioning)
+ self.assertEqual(table.range_partitioning.field, "id")
+ self.assertEqual(table.range_partitioning.range_.start, 0)
+ self.assertEqual(table.range_partitioning.range_.end, 100)
+ self.assertEqual(table.range_partitioning.range_.interval, 10)
+
+ def test_clustering_from_dict_and_list(self):
+ if gcp_bigquery is None:
+ raise unittest.SkipTest("google-cloud-bigquery is not installed")
+
+ table = gcp_bigquery.Table("my-proj.my_ds.my_tbl")
+ table.clustering = {"fields": ["language", "country"]}
+ self.assertEqual(table.clustering_fields, ["language", "country"])
+ self.assertEqual(table.clustering.fields, ["language", "country"])
+ self.assertEqual(table.clustering["fields"], ["language", "country"])
+ self.assertEqual(table.clustering.get("fields"), ["language", "country"])
+
+ table2 = gcp_bigquery.Table("my-proj.my_ds.my_tbl")
+ table2.clustering = ["language"]
+ self.assertEqual(table2.clustering_fields, ["language"])
+ self.assertEqual(table2.clustering.fields, ["language"])
+
+ def test_client_tables_compat_insert_with_partitioning_and_clustering(self):
+ if gcp_bigquery is None:
+ raise unittest.SkipTest("google-cloud-bigquery is not installed")
+
+ client = mock.Mock()
+ tables_compat = bigquery_compat._ClientTablesCompat(client)
+
+ request = mock.Mock()
+ request.projectId = "my-proj"
+ request.datasetId = "my-ds"
+ request.table = mock.Mock()
+ request.table.tableReference = mock.Mock()
+ request.table.tableReference.projectId = "my-proj"
+ request.table.tableReference.datasetId = "my-ds"
+ request.table.tableReference.tableId = "my-tbl"
+ request.table.schema = None
+ request.table.timePartitioning = {"type": "DAY"}
+ request.table.rangePartitioning = None
+ request.table.clustering = {"fields": ["language"]}
+ request.table.description = None
+ request.table.friendlyName = None
+ request.table.labels = None
+ request.table.encryptionConfiguration = None
+
+ tables_compat.Insert(request)
+ client.create_table.assert_called_once()
+ passed_table = client.create_table.call_args.args[0]
+ self.assertEqual(passed_table.time_partitioning.type_, "DAY")
+ self.assertEqual(passed_table.clustering_fields, ["language"])
+
+ def test_range_partitioning_from_apitools_with_zero_start(self):
+ if gcp_bigquery is None:
+ raise unittest.SkipTest("google-cloud-bigquery is not installed")
+
+ # If apitools RangePartitioning model is available, test with it directly
+ if apitools_bigquery is not None and hasattr(apitools_bigquery,
+ "RangePartitioning"):
+ rp = apitools_bigquery.RangePartitioning(
+ field="id",
+ range=apitools_bigquery.RangePartitioning.RangeValue(
+ start=0, end=100, interval=10))
+ gcp_rp = bigquery_compat._to_gcp_range_partitioning(rp)
+ self.assertEqual(gcp_rp.field, "id")
+ self.assertEqual(gcp_rp.range_.start, 0)
+ self.assertEqual(gcp_rp.range_.end, 100)
+ self.assertEqual(gcp_rp.range_.interval, 10)
+
+ # Also test with object having start=0
+ class RangeObj:
+ def __init__(self):
+ self.start = 0
+ self.end = 50
+ self.interval = 5
+
+ class RPObj:
+ def __init__(self):
+ self.field = "num"
+ self.range = RangeObj()
+
+ gcp_rp2 = bigquery_compat._to_gcp_range_partitioning(RPObj())
+ self.assertEqual(gcp_rp2.field, "num")
+ self.assertEqual(gcp_rp2.range_.start, 0)
+ self.assertEqual(gcp_rp2.range_.end, 50)
+ self.assertEqual(gcp_rp2.range_.interval, 5)
+
+ def test_time_partitioning_require_filter_false_and_str(self):
+ if gcp_bigquery is None:
+ raise unittest.SkipTest("google-cloud-bigquery is not installed")
+
+ # String input
+ table = gcp_bigquery.Table("my-proj.my_ds.my_tbl")
+ table.timePartitioning = "DAY"
+ self.assertEqual(table.time_partitioning.type_, "DAY")
+
+ # requirePartitionFilter = False and expirationMs = 0 explicitly preserved
+ table2 = gcp_bigquery.Table("my-proj.my_ds.my_tbl2")
+ table2.timePartitioning = {
+ "type": "HOUR",
+ "requirePartitionFilter": False,
+ "expirationMs": 0,
+ }
+ self.assertEqual(table2.time_partitioning.type_, "HOUR")
+ self.assertIs(table2.time_partitioning.require_partition_filter, False)
+ self.assertEqual(table2.time_partitioning.expiration_ms, 0)
+
+ def test_client_tables_compat_insert_with_bare_mock(self):
+ if gcp_bigquery is None:
+ raise unittest.SkipTest("google-cloud-bigquery is not installed")
+
+ client = mock.Mock()
+ tables_compat = bigquery_compat._ClientTablesCompat(client)
+
+ # Bare mock where request.table has unconfigured attributes returning Mocks
+ req = mock.Mock()
+ req.table = mock.Mock()
+ req.table.tableReference = mock.Mock()
+ req.table.tableReference.projectId = "my-proj"
+ req.table.tableReference.datasetId = "my-ds"
+ req.table.tableReference.tableId = "my-tbl"
+ req.table.schema = None
+
+ tables_compat.Insert(req)
+ client.create_table.assert_called_once()
+
+
+if __name__ == "__main__":
+ logging.getLogger().setLevel(logging.INFO)
+ unittest.main()