Skip to content

Commit e85f437

Browse files
committed
feat: add support for table snapshots
1 parent 1246da8 commit e85f437

File tree

6 files changed

+175
-1
lines changed

6 files changed

+175
-1
lines changed

google/cloud/bigquery/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
from google.cloud.bigquery.job import ExtractJobConfig
6161
from google.cloud.bigquery.job import LoadJob
6262
from google.cloud.bigquery.job import LoadJobConfig
63+
from google.cloud.bigquery.job import OperationType
6364
from google.cloud.bigquery.job import QueryJob
6465
from google.cloud.bigquery.job import QueryJobConfig
6566
from google.cloud.bigquery.job import QueryPriority
@@ -86,6 +87,7 @@
8687
from google.cloud.bigquery.table import PartitionRange
8788
from google.cloud.bigquery.table import RangePartitioning
8889
from google.cloud.bigquery.table import Row
90+
from google.cloud.bigquery.table import SnapshotDefinition
8991
from google.cloud.bigquery.table import Table
9092
from google.cloud.bigquery.table import TableReference
9193
from google.cloud.bigquery.table import TimePartitioningType
@@ -114,6 +116,7 @@
114116
"PartitionRange",
115117
"RangePartitioning",
116118
"Row",
119+
"SnapshotDefinition",
117120
"TimePartitioning",
118121
"TimePartitioningType",
119122
# Jobs
@@ -153,6 +156,7 @@
153156
"ExternalSourceFormat",
154157
"Encoding",
155158
"KeyResultStatementKind",
159+
"OperationType",
156160
"QueryPriority",
157161
"SchemaUpdateOption",
158162
"SourceFormat",

google/cloud/bigquery/job/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from google.cloud.bigquery.job.base import UnknownJob
2626
from google.cloud.bigquery.job.copy_ import CopyJob
2727
from google.cloud.bigquery.job.copy_ import CopyJobConfig
28+
from google.cloud.bigquery.job.copy_ import OperationType
2829
from google.cloud.bigquery.job.extract import ExtractJob
2930
from google.cloud.bigquery.job.extract import ExtractJobConfig
3031
from google.cloud.bigquery.job.load import LoadJob
@@ -59,6 +60,7 @@
5960
"UnknownJob",
6061
"CopyJob",
6162
"CopyJobConfig",
63+
"OperationType",
6264
"ExtractJob",
6365
"ExtractJobConfig",
6466
"LoadJob",

google/cloud/bigquery/job/copy_.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414

1515
"""Classes for copy jobs."""
1616

17+
from typing import Optional
18+
1719
from google.cloud.bigquery.encryption_configuration import EncryptionConfiguration
1820
from google.cloud.bigquery import _helpers
1921
from google.cloud.bigquery.table import TableReference
@@ -23,6 +25,25 @@
2325
from google.cloud.bigquery.job.base import _JobReference
2426

2527

28+
class OperationType:
29+
"""Different operation types supported in table copy job.
30+
31+
https://cloud.google.com/bigquery/docs/reference/rest/v2/Job#operationtype
32+
"""
33+
34+
OPERATION_TYPE_UNSPECIFIED = "OPERATION_TYPE_UNSPECIFIED"
35+
"""Unspecified operation type."""
36+
37+
COPY = "COPY"
38+
"""The source and destination table have the same table type."""
39+
40+
SNAPSHOT = "SNAPSHOT"
41+
"""The source table type is TABLE and the destination table type is SNAPSHOT."""
42+
43+
RESTORE = "RESTORE"
44+
"""The source table type is SNAPSHOT and the destination table type is TABLE."""
45+
46+
2647
class CopyJobConfig(_JobConfig):
2748
"""Configuration options for copy jobs.
2849
@@ -85,6 +106,23 @@ def destination_encryption_configuration(self, value):
85106
api_repr = value.to_api_repr()
86107
self._set_sub_prop("destinationEncryptionConfiguration", api_repr)
87108

109+
@property
110+
def operation_type(self) -> str:
111+
"""The operation to perform with this copy job.
112+
113+
See
114+
https://cloud.google.com/bigquery/docs/reference/rest/v2/Job#JobConfigurationTableCopy.FIELDS.operation_type
115+
"""
116+
return self._get_sub_prop(
117+
"operationType", OperationType.OPERATION_TYPE_UNSPECIFIED
118+
)
119+
120+
@operation_type.setter
121+
def operation_type(self, value: Optional[str]):
122+
if value is None:
123+
value = OperationType.OPERATION_TYPE_UNSPECIFIED
124+
self._set_sub_prop("operationType", value)
125+
88126

89127
class CopyJob(_AsyncJob):
90128
"""Asynchronous job: copy data into a table from other tables.

google/cloud/bigquery/table.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,7 @@ class Table(object):
321321
"range_partitioning": "rangePartitioning",
322322
"time_partitioning": "timePartitioning",
323323
"schema": "schema",
324+
"snapshot_definition": "snapshotDefinition",
324325
"streaming_buffer": "streamingBuffer",
325326
"self_link": "selfLink",
326327
"table_id": ["tableReference", "tableId"],
@@ -910,6 +911,19 @@ def external_data_configuration(self, value):
910911
self._PROPERTY_TO_API_FIELD["external_data_configuration"]
911912
] = api_repr
912913

914+
@property
915+
def snapshot_definition(self) -> Optional["SnapshotDefinition"]:
916+
"""Information about the snapshot. This value is set via snapshot creation.
917+
918+
See: https://cloud.google.com/bigquery/docs/reference/rest/v2/tables#Table.FIELDS.snapshot_definition
919+
"""
920+
snapshot_info = self._properties.get(
921+
self._PROPERTY_TO_API_FIELD["snapshot_definition"]
922+
)
923+
if snapshot_info is not None:
924+
snapshot_info = SnapshotDefinition(snapshot_info)
925+
return snapshot_info
926+
913927
@classmethod
914928
def from_string(cls, full_table_id: str) -> "Table":
915929
"""Construct a table from fully-qualified table ID.
@@ -1274,6 +1288,30 @@ def __init__(self, resource):
12741288
)
12751289

12761290

1291+
class SnapshotDefinition:
1292+
"""Information about base table and snapshot time of the snapshot.
1293+
1294+
See https://cloud.google.com/bigquery/docs/reference/rest/v2/tables#snapshotdefinition
1295+
1296+
Args:
1297+
resource: Snapshot definition representation returned from the API.
1298+
"""
1299+
1300+
def __init__(self, resource: Dict[str, Any]):
1301+
self.base_table_reference = None
1302+
if "baseTableReference" in resource:
1303+
self.base_table_reference = TableReference.from_api_repr(
1304+
resource["baseTableReference"]
1305+
)
1306+
1307+
self.snapshot_time = None
1308+
if "snapshotTime" in resource:
1309+
dt = google.cloud._helpers._rfc3339_to_datetime(resource["snapshotTime"])
1310+
# The helper returns a timezone *aware* datetime object (with UTC tzinfo),
1311+
# we need to make it naive.
1312+
self.snapshot_time = dt.replace(tzinfo=None)
1313+
1314+
12771315
class Row(object):
12781316
"""A BigQuery row.
12791317

tests/unit/job/test_copy.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,16 +30,22 @@ def _get_target_class():
3030

3131
def test_ctor_w_properties(self):
3232
from google.cloud.bigquery.job import CreateDisposition
33+
from google.cloud.bigquery.job import OperationType
3334
from google.cloud.bigquery.job import WriteDisposition
3435

3536
create_disposition = CreateDisposition.CREATE_NEVER
3637
write_disposition = WriteDisposition.WRITE_TRUNCATE
38+
snapshot_operation = OperationType.SNAPSHOT
39+
3740
config = self._get_target_class()(
38-
create_disposition=create_disposition, write_disposition=write_disposition
41+
create_disposition=create_disposition,
42+
write_disposition=write_disposition,
43+
operation_type=snapshot_operation,
3944
)
4045

4146
self.assertEqual(config.create_disposition, create_disposition)
4247
self.assertEqual(config.write_disposition, write_disposition)
48+
self.assertEqual(config.operation_type, snapshot_operation)
4349

4450
def test_to_api_repr_with_encryption(self):
4551
from google.cloud.bigquery.encryption_configuration import (
@@ -70,6 +76,20 @@ def test_to_api_repr_with_encryption_none(self):
7076
resource, {"copy": {"destinationEncryptionConfiguration": None}}
7177
)
7278

79+
def test_operation_type_unspecified(self):
80+
from google.cloud.bigquery.job import OperationType
81+
82+
config = self._make_one()
83+
self.assertEqual(
84+
config.operation_type, OperationType.OPERATION_TYPE_UNSPECIFIED
85+
)
86+
87+
# Setting it to None is the same as setting it to OPERATION_TYPE_UNSPECIFIED.
88+
config.operation_type = None
89+
self.assertEqual(
90+
config.operation_type, OperationType.OPERATION_TYPE_UNSPECIFIED
91+
)
92+
7393

7494
class TestCopyJob(_Base):
7595
JOB_TYPE = "copy"

tests/unit/test_table.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -684,6 +684,39 @@ def test_props_set_by_server(self):
684684
self.assertEqual(table.full_table_id, TABLE_FULL_ID)
685685
self.assertEqual(table.table_type, "TABLE")
686686

687+
def test_snapshot_definition_not_set(self):
688+
dataset = DatasetReference(self.PROJECT, self.DS_ID)
689+
table_ref = dataset.table(self.TABLE_NAME)
690+
table = self._make_one(table_ref)
691+
692+
assert table.snapshot_definition is None
693+
694+
def test_snapshot_definition_set(self):
695+
from google.cloud.bigquery.table import SnapshotDefinition
696+
697+
dataset = DatasetReference(self.PROJECT, self.DS_ID)
698+
table_ref = dataset.table(self.TABLE_NAME)
699+
table = self._make_one(table_ref)
700+
701+
table._properties["snapshotDefinition"] = {
702+
"baseTableReference": {
703+
"projectId": "project_x",
704+
"datasetId": "dataset_y",
705+
"tableId": "table_z",
706+
},
707+
"snapshotTime": "2010-09-28T10:20:30.123Z",
708+
}
709+
710+
snapshot = table.snapshot_definition
711+
712+
assert isinstance(snapshot, SnapshotDefinition)
713+
assert snapshot.base_table_reference.path == (
714+
"/projects/project_x/datasets/dataset_y/tables/table_z"
715+
)
716+
assert snapshot.snapshot_time == datetime.datetime(
717+
2010, 9, 28, 10, 20, 30, 123000
718+
)
719+
687720
def test_description_setter_bad_value(self):
688721
dataset = DatasetReference(self.PROJECT, self.DS_ID)
689722
table_ref = dataset.table(self.TABLE_NAME)
@@ -1509,6 +1542,45 @@ def test_to_api_repr(self):
15091542
self.assertEqual(table.to_api_repr(), resource)
15101543

15111544

1545+
class TestSnapshotDefinition:
1546+
@staticmethod
1547+
def _get_target_class():
1548+
from google.cloud.bigquery.table import SnapshotDefinition
1549+
1550+
return SnapshotDefinition
1551+
1552+
@classmethod
1553+
def _make_one(cls, *args, **kwargs):
1554+
klass = cls._get_target_class()
1555+
return klass(*args, **kwargs)
1556+
1557+
def test_ctor_empty_resource(self):
1558+
instance = self._make_one(resource={})
1559+
assert instance.base_table_reference is None
1560+
assert instance.snapshot_time is None
1561+
1562+
def test_ctor_full_resource(self):
1563+
from google.cloud.bigquery.table import TableReference
1564+
1565+
resource = {
1566+
"baseTableReference": {
1567+
"projectId": "my-project",
1568+
"datasetId": "your-dataset",
1569+
"tableId": "our-table",
1570+
},
1571+
"snapshotTime": "2005-06-07T19:35:02.123Z",
1572+
}
1573+
instance = self._make_one(resource)
1574+
1575+
expected_table_ref = TableReference.from_string(
1576+
"my-project.your-dataset.our-table"
1577+
)
1578+
assert instance.base_table_reference == expected_table_ref
1579+
1580+
expected_time = datetime.datetime(2005, 6, 7, 19, 35, 2, 123000)
1581+
assert instance.snapshot_time == expected_time
1582+
1583+
15121584
class TestRow(unittest.TestCase):
15131585
def test_row(self):
15141586
from google.cloud.bigquery.table import Row

0 commit comments

Comments
 (0)