Skip to content

Commit e5ed4b8

Browse files
committed
updates to foreign_type_info, tests and wiring
1 parent a0f823e commit e5ed4b8

File tree

5 files changed

+304
-110
lines changed

5 files changed

+304
-110
lines changed

google/cloud/bigquery/schema.py

Lines changed: 61 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
"""Schemas for BigQuery tables / queries."""
1616

1717
from __future__ import annotations
18-
import collections
1918
import enum
2019
import typing
2120
from typing import Any, cast, Dict, Iterable, Optional, Union
@@ -503,40 +502,78 @@ def _build_schema_resource(fields):
503502
Returns:
504503
Sequence[Dict]: Mappings describing the schema of the supplied fields.
505504
"""
506-
return [field.to_api_repr() for field in fields]
505+
if isinstance(fields, (list, tuple)):
506+
# Input is list-like: Process and return a list of SchemaFields
507+
return [field.to_api_repr() for field in fields]
508+
509+
elif isinstance(fields, dict):
510+
# Input is a dict: Update "fields" in place, if present
511+
512+
if "fields" not in fields:
513+
return fields # No fields to process, so nothing to convert
514+
515+
fields["fields"] = [field.to_api_repr() for field in fields["fields"]]
516+
return fields # Return the modified dictionary
517+
518+
else:
519+
raise TypeError("Schema must be a Sequence (list) or a Mapping (dict).")
507520

508521

509522
def _to_schema_fields(schema):
510-
"""Coerce `schema` to a list of schema field instances.
523+
"""Coerces schema to a list of SchemaField instances while
524+
preserving the original structure as much as possible.
511525
512526
Args:
513-
schema(Sequence[Union[ \
514-
:class:`~google.cloud.bigquery.schema.SchemaField`, \
515-
Mapping[str, Any] \
516-
]]):
517-
Table schema to convert. If some items are passed as mappings,
518-
their content must be compatible with
519-
:meth:`~google.cloud.bigquery.schema.SchemaField.from_api_repr`.
527+
schema (Union[ \
528+
dict, \
529+
Sequence[Union[ \
530+
:class:`~google.cloud.bigquery.schema.SchemaField`, \
531+
Mapping[str, Any] \
532+
]
533+
]
534+
]
535+
)::
536+
Table schema to convert. Can be a list of SchemaField
537+
objects or mappings, OR a dictionary with a "fields" key
538+
containing such a list and optionally a "foreign_type_info" key.
520539
521540
Returns:
522-
Sequence[:class:`~google.cloud.bigquery.schema.SchemaField`]
541+
A list of SchemaField objects if the input was a list.
542+
A dictionary with a list of Schemafield objects assigned to the 'fields'
543+
key, if the input was a dictionary. If a "foreign_type_info" key was present
544+
in the dictionary, it will be preserved.
523545
524546
Raises:
525-
Exception: If ``schema`` is not a sequence, or if any item in the
526-
sequence is not a :class:`~google.cloud.bigquery.schema.SchemaField`
527-
instance or a compatible mapping representation of the field.
547+
TypeError: If schema is not a list or dictionary.
548+
ValueError: If schema is a dictionary without a "fields" key, or
549+
if any element within the "fields" is invalid.
528550
"""
529-
for field in schema:
530-
if not isinstance(field, (SchemaField, collections.abc.Mapping)):
531-
raise ValueError(
532-
"Schema items must either be fields or compatible "
533-
"mapping representations."
534-
)
535551

536-
return [
537-
field if isinstance(field, SchemaField) else SchemaField.from_api_repr(field)
538-
for field in schema
539-
]
552+
if isinstance(schema, (list, tuple)):
553+
# Input is a list: Process and return a new list of SchemaFields
554+
return [
555+
field
556+
if isinstance(field, SchemaField)
557+
else SchemaField.from_api_repr(field)
558+
for field in schema
559+
]
560+
561+
elif isinstance(schema, dict):
562+
# Input is a dict: Update "fields" in place if present
563+
564+
if "fields" not in schema:
565+
return schema # No fields to process, so nothing to convert
566+
567+
schema["fields"] = [
568+
field
569+
if isinstance(field, SchemaField)
570+
else SchemaField.from_api_repr(field)
571+
for field in schema["fields"]
572+
]
573+
return schema # Return the modified dictionary
574+
575+
else:
576+
raise TypeError("Schema must be a Sequence (list) or a Mapping (dict).")
540577

541578

542579
class PolicyTagList(object):

google/cloud/bigquery/table.py

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -412,7 +412,7 @@ class Table(_TableBase):
412412
"max_staleness": "maxStaleness",
413413
"resource_tags": "resourceTags",
414414
"external_catalog_table_options": "externalCatalogTableOptions",
415-
"foreign_type_info": "foreignTypeInfo",
415+
"foreign_type_info": ["schema", "foreignTypeInfo"],
416416
}
417417

418418
def __init__(self, table_ref, schema=None) -> None:
@@ -1107,14 +1107,17 @@ def foreign_type_info(self) -> Optional[_schema.ForeignTypeInfo]:
11071107
Optional[schema.ForeignTypeInfo]:
11081108
Foreign type information, or :data:`None` if not set.
11091109
1110-
NOTE: foreign_type_info is only required if you are referencing an
1111-
external catalog such as a Hive table.
1112-
For details, see:
1113-
https://cloud.google.com/bigquery/docs/external-tables
1114-
https://cloud.google.com/bigquery/docs/datasets-intro#external_datasets
1110+
.. Note::
1111+
foreign_type_info is only required if you are referencing an
1112+
external catalog such as a Hive table.
1113+
For details, see:
1114+
https://cloud.google.com/bigquery/docs/external-tables
1115+
https://cloud.google.com/bigquery/docs/datasets-intro#external_datasets
11151116
"""
11161117

1117-
prop = self._properties.get(self._PROPERTY_TO_API_FIELD["foreign_type_info"])
1118+
prop = _helpers._get_sub_prop(
1119+
self._properties, self._PROPERTY_TO_API_FIELD["foreign_type_info"]
1120+
)
11181121
if prop is not None:
11191122
return _schema.ForeignTypeInfo.from_api_repr(prop)
11201123
return None
@@ -1127,11 +1130,10 @@ def foreign_type_info(self, value: Union[_schema.ForeignTypeInfo, dict, None]):
11271130
none_allowed=True,
11281131
)
11291132
if isinstance(value, _schema.ForeignTypeInfo):
1130-
self._properties[
1131-
self._PROPERTY_TO_API_FIELD["foreign_type_info"]
1132-
] = value.to_api_repr()
1133-
else:
1134-
self._properties[self._PROPERTY_TO_API_FIELD["foreign_type_info"]] = value
1133+
value = value.to_api_repr()
1134+
_helpers._set_sub_prop(
1135+
self._properties, self._PROPERTY_TO_API_FIELD["foreign_type_info"], value
1136+
)
11351137

11361138
@classmethod
11371139
def from_string(cls, full_table_id: str) -> "Table":

tests/unit/job/test_load.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,7 @@ def test_schema_setter_invalid_field(self):
272272

273273
config = LoadJobConfig()
274274
full_name = SchemaField("full_name", "STRING", mode="REQUIRED")
275-
with self.assertRaises(ValueError):
275+
with self.assertRaises(TypeError):
276276
config.schema = [full_name, object()]
277277

278278
def test_schema_setter(self):

0 commit comments

Comments
 (0)