Skip to content

Commit b42b84c

Browse files
committed
Add table_constraints field to Table model
1 parent 0d93073 commit b42b84c

File tree

2 files changed

+240
-0
lines changed

2 files changed

+240
-0
lines changed

google/cloud/bigquery/table.py

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,7 @@ class Table(_TableBase):
390390
"view_use_legacy_sql": "view",
391391
"view_query": "view",
392392
"require_partition_filter": "requirePartitionFilter",
393+
"table_constraints": "tableConstraints",
393394
}
394395

395396
def __init__(self, table_ref, schema=None) -> None:
@@ -973,6 +974,16 @@ def clone_definition(self) -> Optional["CloneDefinition"]:
973974
clone_info = CloneDefinition(clone_info)
974975
return clone_info
975976

977+
@property
978+
def table_constraints(self) -> Optional["TableConstraints"]:
979+
"""Tables Primary Key and Foreign Key information."""
980+
table_constraints = self._properties.get(
981+
self._PROPERTY_TO_API_FIELD["table_constraints"]
982+
)
983+
if table_constraints is not None:
984+
table_constraints = TableConstraints.from_api_repr(table_constraints)
985+
return table_constraints
986+
976987
@classmethod
977988
def from_string(cls, full_table_id: str) -> "Table":
978989
"""Construct a table from fully-qualified table ID.
@@ -2942,6 +2953,123 @@ def __repr__(self):
29422953
return "TimePartitioning({})".format(",".join(key_vals))
29432954

29442955

2956+
class PrimaryKey:
2957+
"""Represents the primary key constraint on a table's columns.
2958+
2959+
Args:
2960+
columns: The columns that are composed of the primary key constraint.
2961+
"""
2962+
2963+
def __init__(self, columns: List[str]):
2964+
self.columns = columns
2965+
2966+
def __eq__(self, other):
2967+
if not isinstance(other, PrimaryKey):
2968+
raise NotImplementedError
2969+
return self.columns == other.columns
2970+
2971+
2972+
class ColumnReference:
2973+
"""The pair of the foreign key column and primary key column.
2974+
2975+
Args:
2976+
referencing_column: The column that composes the foreign key.
2977+
referenced_column: The column in the primary key that are referenced by the referencingColumn.
2978+
"""
2979+
2980+
def __init__(self, referencing_column: str, referenced_column: str):
2981+
self.referencing_column = referencing_column
2982+
self.referenced_column = referenced_column
2983+
2984+
def __eq__(self, other):
2985+
if not isinstance(other, ColumnReference):
2986+
raise NotImplementedError
2987+
return (
2988+
self.referenced_column == other.referencing_column
2989+
and self.referenced_column == other.referenced_column
2990+
)
2991+
2992+
@classmethod
2993+
def from_api_repr(cls, api_repr: Dict[str, Any]) -> "ColumnReference":
2994+
"""Create an instance from API representation."""
2995+
return cls(api_repr["referencingColumn"], api_repr["referencedColumn"])
2996+
2997+
2998+
class ForeignKey:
2999+
"""Represents a foreign key constraint on a table's columns.
3000+
3001+
Args:
3002+
name: Set only if the foreign key constraint is named.
3003+
referenced_table: The table that holds the primary key and is referenced by this foreign key.
3004+
column_references: The columns that compose the foreign key.
3005+
"""
3006+
3007+
def __init__(
3008+
self,
3009+
name: str,
3010+
referenced_table: TableReference,
3011+
column_references: List[ColumnReference],
3012+
):
3013+
self.name = name
3014+
self.referenced_table = referenced_table
3015+
self.column_references = column_references
3016+
3017+
def __eq__(self, other):
3018+
if not isinstance(other, ForeignKey):
3019+
raise NotImplementedError
3020+
return (
3021+
self.name == other.name and self.referenced_table == other.referenced_table
3022+
)
3023+
3024+
@classmethod
3025+
def from_api_repr(cls, api_repr: Dict[str, Any]) -> "ForeignKey":
3026+
"""Create an instance from API representation."""
3027+
return cls(
3028+
name=api_repr["name"],
3029+
referenced_table=TableReference.from_api_repr(api_repr["referencedTable"]),
3030+
column_references=[
3031+
ColumnReference.from_api_repr(column_reference_resource)
3032+
for column_reference_resource in api_repr["columnReferences"]
3033+
],
3034+
)
3035+
3036+
3037+
class TableConstraints:
3038+
"""The TableConstraints defines the primary key and foreign key.
3039+
3040+
Args:
3041+
primary_key:
3042+
Represents a primary key constraint on a table's columns. Present only if the table
3043+
has a primary key. The primary key is not enforced.
3044+
foreign_keys:
3045+
Present only if the table has a foreign key. The foreign key is not enforced.
3046+
3047+
"""
3048+
3049+
def __init__(
3050+
self,
3051+
primary_key: Optional[PrimaryKey],
3052+
foreign_keys: Optional[List[ForeignKey]],
3053+
):
3054+
self.primary_key = primary_key
3055+
self.foreign_keys = foreign_keys
3056+
3057+
@classmethod
3058+
def from_api_repr(cls, resource: Dict[str, Any]) -> "TableConstraints":
3059+
"""Create an instance from API representation."""
3060+
primary_key = None
3061+
if "primaryKey" in resource:
3062+
primary_key = PrimaryKey(resource["primaryKey"]["columns"])
3063+
3064+
foreign_keys = None
3065+
if "foreignKeys" in resource:
3066+
foreign_keys = [
3067+
ForeignKey.from_api_repr(foreign_key_resource)
3068+
for foreign_key_resource in resource["foreignKeys"]
3069+
]
3070+
return cls(primary_key, foreign_keys)
3071+
3072+
29453073
def _item_to_row(iterator, resource):
29463074
"""Convert a JSON row to the native object.
29473075

tests/unit/test_table.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -603,6 +603,7 @@ def test_ctor(self):
603603
self.assertIsNone(table.encryption_configuration)
604604
self.assertIsNone(table.time_partitioning)
605605
self.assertIsNone(table.clustering_fields)
606+
self.assertIsNone(table.table_constraints)
606607

607608
def test_ctor_w_schema(self):
608609
from google.cloud.bigquery.schema import SchemaField
@@ -901,6 +902,21 @@ def test_clone_definition_set(self):
901902
2010, 9, 28, 10, 20, 30, 123000, tzinfo=UTC
902903
)
903904

905+
def test_table_constraints_property_getter(self):
906+
from google.cloud.bigquery.table import PrimaryKey, TableConstraints
907+
908+
dataset = DatasetReference(self.PROJECT, self.DS_ID)
909+
table_ref = dataset.table(self.TABLE_NAME)
910+
table = self._make_one(table_ref)
911+
table._properties["tableConstraints"] = {
912+
"primaryKey": {"columns": ["id"]},
913+
}
914+
915+
table_constraints = table.table_constraints
916+
917+
assert isinstance(table_constraints, TableConstraints)
918+
assert table_constraints.primary_key == PrimaryKey(columns=["id"])
919+
904920
def test_description_setter_bad_value(self):
905921
dataset = DatasetReference(self.PROJECT, self.DS_ID)
906922
table_ref = dataset.table(self.TABLE_NAME)
@@ -5385,6 +5401,102 @@ def test_set_expiration_w_none(self):
53855401
assert time_partitioning._properties["expirationMs"] is None
53865402

53875403

5404+
class TestTableConstraint(unittest.TestCase):
5405+
@staticmethod
5406+
def _get_target_class():
5407+
from google.cloud.bigquery.table import TableConstraints
5408+
5409+
return TableConstraints
5410+
5411+
@classmethod
5412+
def _make_one(cls, *args, **kwargs):
5413+
return cls._get_target_class()(*args, **kwargs)
5414+
5415+
def test_constructor_defaults(self):
5416+
instance = self._make_one(primary_key=None, foreign_keys=None)
5417+
self.assertIsNone(instance.primary_key)
5418+
self.assertIsNone(instance.foreign_keys)
5419+
5420+
def test_from_api_repr_full_resource(self):
5421+
from google.cloud.bigquery.table import (
5422+
ColumnReference,
5423+
ForeignKey,
5424+
TableReference,
5425+
)
5426+
5427+
resource = {
5428+
"primaryKey": {
5429+
"columns": ["id", "product_id"],
5430+
},
5431+
"foreignKeys": [
5432+
{
5433+
"name": "my_fk_name",
5434+
"referencedTable": {
5435+
"projectId": "my-project",
5436+
"datasetId": "your-dataset",
5437+
"tableId": "products",
5438+
},
5439+
"columnReferences": [
5440+
{"referencingColumn": "product_id", "referencedColumn": "id"},
5441+
],
5442+
}
5443+
],
5444+
}
5445+
instance = self._get_target_class().from_api_repr(resource)
5446+
5447+
self.assertIsNotNone(instance.primary_key)
5448+
self.assertEqual(instance.primary_key.columns, ["id", "product_id"])
5449+
self.assertEqual(
5450+
instance.foreign_keys,
5451+
[
5452+
ForeignKey(
5453+
name="my_fk_name",
5454+
referenced_table=TableReference.from_string(
5455+
"my-project.your-dataset.products"
5456+
),
5457+
column_references=[
5458+
ColumnReference(
5459+
referencing_column="product_id", referenced_column="id"
5460+
),
5461+
],
5462+
),
5463+
],
5464+
)
5465+
5466+
def test_from_api_repr_only_primary_key_resource(self):
5467+
resource = {
5468+
"primaryKey": {
5469+
"columns": ["id"],
5470+
},
5471+
}
5472+
instance = self._get_target_class().from_api_repr(resource)
5473+
5474+
self.assertIsNotNone(instance.primary_key)
5475+
self.assertEqual(instance.primary_key.columns, ["id"])
5476+
self.assertIsNone(instance.foreign_keys)
5477+
5478+
def test_from_api_repr_only_foreign_keys_resource(self):
5479+
resource = {
5480+
"foreignKeys": [
5481+
{
5482+
"name": "my_fk_name",
5483+
"referencedTable": {
5484+
"projectId": "my-project",
5485+
"datasetId": "your-dataset",
5486+
"tableId": "products",
5487+
},
5488+
"columnReferences": [
5489+
{"referencingColumn": "product_id", "referencedColumn": "id"},
5490+
],
5491+
}
5492+
]
5493+
}
5494+
instance = self._get_target_class().from_api_repr(resource)
5495+
5496+
self.assertIsNone(instance.primary_key)
5497+
self.assertIsNotNone(instance.foreign_keys)
5498+
5499+
53885500
@pytest.mark.skipif(
53895501
bigquery_storage is None, reason="Requires `google-cloud-bigquery-storage`"
53905502
)

0 commit comments

Comments
 (0)