Skip to content

Commit 6c7ad29

Browse files
feat: add support for updateDatabase in Cloud Spanner (#914)
* feat: drop database protection Co-authored-by: Rajat Bhatta <[email protected]>
1 parent 2cff831 commit 6c7ad29

File tree

6 files changed

+183
-1
lines changed

6 files changed

+183
-1
lines changed

google/cloud/spanner_v1/database.py

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
from google.api_core import gapic_v1
3030
from google.iam.v1 import iam_policy_pb2
3131
from google.iam.v1 import options_pb2
32+
from google.protobuf.field_mask_pb2 import FieldMask
3233

3334
from google.cloud.spanner_admin_database_v1 import CreateDatabaseRequest
3435
from google.cloud.spanner_admin_database_v1 import Database as DatabasePB
@@ -127,6 +128,9 @@ class Database(object):
127128
(Optional) database dialect for the database
128129
:type database_role: str or None
129130
:param database_role: (Optional) user-assigned database_role for the session.
131+
:type enable_drop_protection: boolean
132+
:param enable_drop_protection: (Optional) Represents whether the database
133+
has drop protection enabled or not.
130134
"""
131135

132136
_spanner_api = None
@@ -141,6 +145,7 @@ def __init__(
141145
encryption_config=None,
142146
database_dialect=DatabaseDialect.DATABASE_DIALECT_UNSPECIFIED,
143147
database_role=None,
148+
enable_drop_protection=False,
144149
):
145150
self.database_id = database_id
146151
self._instance = instance
@@ -159,6 +164,8 @@ def __init__(
159164
self._database_dialect = database_dialect
160165
self._database_role = database_role
161166
self._route_to_leader_enabled = self._instance._client.route_to_leader_enabled
167+
self._enable_drop_protection = enable_drop_protection
168+
self._reconciling = False
162169

163170
if pool is None:
164171
pool = BurstyPool(database_role=database_role)
@@ -332,6 +339,29 @@ def database_role(self):
332339
"""
333340
return self._database_role
334341

342+
@property
343+
def reconciling(self):
344+
"""Whether the database is currently reconciling.
345+
346+
:rtype: boolean
347+
:returns: a boolean representing whether the database is reconciling
348+
"""
349+
return self._reconciling
350+
351+
@property
352+
def enable_drop_protection(self):
353+
"""Whether the database has drop protection enabled.
354+
355+
:rtype: boolean
356+
:returns: a boolean representing whether the database has drop
357+
protection enabled
358+
"""
359+
return self._enable_drop_protection
360+
361+
@enable_drop_protection.setter
362+
def enable_drop_protection(self, value):
363+
self._enable_drop_protection = value
364+
335365
@property
336366
def logger(self):
337367
"""Logger used by the database.
@@ -461,14 +491,16 @@ def reload(self):
461491
self._encryption_info = response.encryption_info
462492
self._default_leader = response.default_leader
463493
self._database_dialect = response.database_dialect
494+
self._enable_drop_protection = response.enable_drop_protection
495+
self._reconciling = response.reconciling
464496

465497
def update_ddl(self, ddl_statements, operation_id=""):
466498
"""Update DDL for this database.
467499
468500
Apply any configured schema from :attr:`ddl_statements`.
469501
470502
See
471-
https://cloud.google.com/spanner/reference/rpc/google.spanner.admin.database.v1#google.spanner.admin.database.v1.DatabaseAdmin.UpdateDatabase
503+
https://cloud.google.com/spanner/reference/rpc/google.spanner.admin.database.v1#google.spanner.admin.database.v1.DatabaseAdmin.UpdateDatabaseDdl
472504
473505
:type ddl_statements: Sequence[str]
474506
:param ddl_statements: a list of DDL statements to use on this database
@@ -492,6 +524,46 @@ def update_ddl(self, ddl_statements, operation_id=""):
492524
future = api.update_database_ddl(request=request, metadata=metadata)
493525
return future
494526

527+
def update(self, fields):
528+
"""Update this database.
529+
530+
See
531+
https://cloud.google.com/spanner/reference/rpc/google.spanner.admin.database.v1#google.spanner.admin.database.v1.DatabaseAdmin.UpdateDatabase
532+
533+
.. note::
534+
535+
Updates the specified fields of a Cloud Spanner database. Currently,
536+
only the `enable_drop_protection` field supports updates. To change
537+
this value before updating, set it via
538+
539+
.. code:: python
540+
541+
database.enable_drop_protection = True
542+
543+
before calling :meth:`update`.
544+
545+
:type fields: Sequence[str]
546+
:param fields: a list of fields to update
547+
548+
:rtype: :class:`google.api_core.operation.Operation`
549+
:returns: an operation instance
550+
:raises NotFound: if the database does not exist
551+
"""
552+
api = self._instance._client.database_admin_api
553+
database_pb = DatabasePB(
554+
name=self.name, enable_drop_protection=self._enable_drop_protection
555+
)
556+
557+
# Only support updating drop protection for now.
558+
field_mask = FieldMask(paths=fields)
559+
metadata = _metadata_with_prefix(self.name)
560+
561+
future = api.update_database(
562+
database=database_pb, update_mask=field_mask, metadata=metadata
563+
)
564+
565+
return future
566+
495567
def drop(self):
496568
"""Drop this database.
497569

google/cloud/spanner_v1/instance.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,7 @@ def database(
432432
encryption_config=None,
433433
database_dialect=DatabaseDialect.DATABASE_DIALECT_UNSPECIFIED,
434434
database_role=None,
435+
enable_drop_protection=False,
435436
):
436437
"""Factory to create a database within this instance.
437438
@@ -467,6 +468,10 @@ def database(
467468
:param database_dialect:
468469
(Optional) database dialect for the database
469470
471+
:type enable_drop_protection: boolean
472+
:param enable_drop_protection: (Optional) Represents whether the database
473+
has drop protection enabled or not.
474+
470475
:rtype: :class:`~google.cloud.spanner_v1.database.Database`
471476
:returns: a database owned by this instance.
472477
"""
@@ -479,6 +484,7 @@ def database(
479484
encryption_config=encryption_config,
480485
database_dialect=database_dialect,
481486
database_role=database_role,
487+
enable_drop_protection=enable_drop_protection,
482488
)
483489

484490
def list_databases(self, page_size=None):

samples/samples/snippets.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,27 @@ def create_database(instance_id, database_id):
196196
# [END spanner_create_database]
197197

198198

199+
# [START spanner_update_database]
200+
def update_database(instance_id, database_id):
201+
"""Updates the drop protection setting for a database."""
202+
spanner_client = spanner.Client()
203+
instance = spanner_client.instance(instance_id)
204+
205+
db = instance.database(database_id)
206+
db.enable_drop_protection = True
207+
208+
operation = db.update(["enable_drop_protection"])
209+
210+
print("Waiting for update operation for {} to complete...".format(
211+
db.name))
212+
operation.result(OPERATION_TIMEOUT_SECONDS)
213+
214+
print("Updated database {}.".format(db.name))
215+
216+
217+
# [END spanner_update_database]
218+
219+
199220
# [START spanner_create_database_with_encryption_key]
200221
def create_database_with_encryption_key(instance_id, database_id, kms_key_name):
201222
"""Creates a database with tables using a Customer Managed Encryption Key (CMEK)."""

samples/samples/snippets_test.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,19 @@ def test_create_instance_with_processing_units(capsys, lci_instance_id):
154154
retry_429(instance.delete)()
155155

156156

157+
def test_update_database(capsys, instance_id, sample_database):
158+
snippets.update_database(
159+
instance_id, sample_database.database_id
160+
)
161+
out, _ = capsys.readouterr()
162+
assert "Updated database {}.".format(sample_database.name) in out
163+
164+
# Cleanup
165+
sample_database.enable_drop_protection = False
166+
op = sample_database.update(["enable_drop_protection"])
167+
op.result()
168+
169+
157170
def test_create_database_with_encryption_config(
158171
capsys, instance_id, cmek_database_id, kms_key_name
159172
):

tests/system/test_database_api.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -562,3 +562,41 @@ def _unit_of_work(transaction, name):
562562
rows = list(after.read(sd.COUNTERS_TABLE, sd.COUNTERS_COLUMNS, sd.ALL))
563563

564564
assert len(rows) == 2
565+
566+
567+
def test_update_database_success(
568+
not_emulator, shared_database, shared_instance, database_operation_timeout
569+
):
570+
old_protection = shared_database.enable_drop_protection
571+
new_protection = True
572+
shared_database.enable_drop_protection = new_protection
573+
operation = shared_database.update(["enable_drop_protection"])
574+
575+
# We want to make sure the operation completes.
576+
operation.result(database_operation_timeout) # raises on failure / timeout.
577+
578+
# Create a new database instance and reload it.
579+
database_alt = shared_instance.database(shared_database.name.split("/")[-1])
580+
assert database_alt.enable_drop_protection != new_protection
581+
582+
database_alt.reload()
583+
assert database_alt.enable_drop_protection == new_protection
584+
585+
with pytest.raises(exceptions.FailedPrecondition):
586+
database_alt.drop()
587+
588+
with pytest.raises(exceptions.FailedPrecondition):
589+
shared_instance.delete()
590+
591+
# Make sure to put the database back the way it was for the
592+
# other test cases.
593+
shared_database.enable_drop_protection = old_protection
594+
shared_database.update(["enable_drop_protection"])
595+
596+
597+
def test_update_database_invalid(not_emulator, shared_database):
598+
shared_database.enable_drop_protection = True
599+
600+
# Empty `fields` is not supported.
601+
with pytest.raises(exceptions.InvalidArgument):
602+
shared_database.update([])

tests/unit/test_database.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@
1717

1818
import mock
1919
from google.api_core import gapic_v1
20+
from google.cloud.spanner_admin_database_v1 import Database as DatabasePB
2021
from google.cloud.spanner_v1.param_types import INT64
2122
from google.api_core.retry import Retry
23+
from google.protobuf.field_mask_pb2 import FieldMask
2224

2325
from google.cloud.spanner_v1 import RequestOptions
2426

@@ -760,6 +762,8 @@ def test_reload_success(self):
760762
encryption_config=encryption_config,
761763
encryption_info=encryption_info,
762764
default_leader=default_leader,
765+
reconciling=True,
766+
enable_drop_protection=True,
763767
)
764768
api.get_database.return_value = db_pb
765769
instance = _Instance(self.INSTANCE_NAME, client=client)
@@ -776,6 +780,8 @@ def test_reload_success(self):
776780
self.assertEqual(database._encryption_config, encryption_config)
777781
self.assertEqual(database._encryption_info, encryption_info)
778782
self.assertEqual(database._default_leader, default_leader)
783+
self.assertEqual(database._reconciling, True)
784+
self.assertEqual(database._enable_drop_protection, True)
779785

780786
api.get_database_ddl.assert_called_once_with(
781787
database=self.DATABASE_NAME,
@@ -892,6 +898,32 @@ def test_update_ddl_w_operation_id(self):
892898
metadata=[("google-cloud-resource-prefix", database.name)],
893899
)
894900

901+
def test_update_success(self):
902+
op_future = object()
903+
client = _Client()
904+
api = client.database_admin_api = self._make_database_admin_api()
905+
api.update_database.return_value = op_future
906+
907+
instance = _Instance(self.INSTANCE_NAME, client=client)
908+
pool = _Pool()
909+
database = self._make_one(
910+
self.DATABASE_ID, instance, enable_drop_protection=True, pool=pool
911+
)
912+
913+
future = database.update(["enable_drop_protection"])
914+
915+
self.assertIs(future, op_future)
916+
917+
expected_database = DatabasePB(name=database.name, enable_drop_protection=True)
918+
919+
field_mask = FieldMask(paths=["enable_drop_protection"])
920+
921+
api.update_database.assert_called_once_with(
922+
database=expected_database,
923+
update_mask=field_mask,
924+
metadata=[("google-cloud-resource-prefix", database.name)],
925+
)
926+
895927
def test_drop_grpc_error(self):
896928
from google.api_core.exceptions import Unknown
897929

0 commit comments

Comments
 (0)