Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for updateDatabase in Cloud Spanner #914

Merged
merged 14 commits into from May 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
74 changes: 73 additions & 1 deletion google/cloud/spanner_v1/database.py
Expand Up @@ -29,6 +29,7 @@
from google.api_core import gapic_v1
from google.iam.v1 import iam_policy_pb2
from google.iam.v1 import options_pb2
from google.protobuf.field_mask_pb2 import FieldMask

from google.cloud.spanner_admin_database_v1 import CreateDatabaseRequest

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks

from google.cloud.spanner_admin_database_v1 import Database as DatabasePB
Expand Down Expand Up @@ -127,6 +128,9 @@ class Database(object):
(Optional) database dialect for the database
:type database_role: str or None
:param database_role: (Optional) user-assigned database_role for the session.
:type enable_drop_protection: boolean
:param enable_drop_protection: (Optional) Represents whether the database
has drop protection enabled or not.
"""

_spanner_api = None
Expand All @@ -141,6 +145,7 @@ def __init__(
encryption_config=None,
database_dialect=DatabaseDialect.DATABASE_DIALECT_UNSPECIFIED,
database_role=None,
enable_drop_protection=False,
):
self.database_id = database_id
self._instance = instance
Expand All @@ -159,6 +164,8 @@ def __init__(
self._database_dialect = database_dialect
self._database_role = database_role
self._route_to_leader_enabled = self._instance._client.route_to_leader_enabled
self._enable_drop_protection = enable_drop_protection
self._reconciling = False
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't see any place where this value is updated?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is updated at line 359 of this file, using the property setter.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am referring to reconciling


if pool is None:
pool = BurstyPool(database_role=database_role)
Expand Down Expand Up @@ -332,6 +339,29 @@ def database_role(self):
"""
return self._database_role

@property
def reconciling(self):
"""Whether the database is currently reconciling.

:rtype: boolean
:returns: a boolean representing whether the database is reconciling
"""
return self._reconciling

@property
def enable_drop_protection(self):
"""Whether the database has drop protection enabled.

:rtype: boolean
:returns: a boolean representing whether the database has drop
protection enabled
"""
return self._enable_drop_protection

@enable_drop_protection.setter
def enable_drop_protection(self, value):
self._enable_drop_protection = value

@property
def logger(self):
"""Logger used by the database.
Expand Down Expand Up @@ -461,14 +491,16 @@ def reload(self):
self._encryption_info = response.encryption_info
self._default_leader = response.default_leader
self._database_dialect = response.database_dialect
self._enable_drop_protection = response.enable_drop_protection
self._reconciling = response.reconciling

def update_ddl(self, ddl_statements, operation_id=""):
"""Update DDL for this database.

Apply any configured schema from :attr:`ddl_statements`.

See
https://cloud.google.com/spanner/reference/rpc/google.spanner.admin.database.v1#google.spanner.admin.database.v1.DatabaseAdmin.UpdateDatabase
https://cloud.google.com/spanner/reference/rpc/google.spanner.admin.database.v1#google.spanner.admin.database.v1.DatabaseAdmin.UpdateDatabaseDdl

:type ddl_statements: Sequence[str]
:param ddl_statements: a list of DDL statements to use on this database
Expand All @@ -492,6 +524,46 @@ def update_ddl(self, ddl_statements, operation_id=""):
future = api.update_database_ddl(request=request, metadata=metadata)
return future

def update(self, fields):
"""Update this database.

See
https://cloud.google.com/spanner/reference/rpc/google.spanner.admin.database.v1#google.spanner.admin.database.v1.DatabaseAdmin.UpdateDatabase

.. note::

Updates the specified fields of a Cloud Spanner database. Currently,
only the `enable_drop_protection` field supports updates. To change
this value before updating, set it via

.. code:: python

database.enable_drop_protection = True
asthamohta marked this conversation as resolved.
Show resolved Hide resolved

before calling :meth:`update`.

:type fields: Sequence[str]
:param fields: a list of fields to update

:rtype: :class:`google.api_core.operation.Operation`
:returns: an operation instance
:raises NotFound: if the database does not exist
"""
api = self._instance._client.database_admin_api
database_pb = DatabasePB(
name=self.name, enable_drop_protection=self._enable_drop_protection
)

# Only support updating drop protection for now.
field_mask = FieldMask(paths=fields)
metadata = _metadata_with_prefix(self.name)

future = api.update_database(
database=database_pb, update_mask=field_mask, metadata=metadata
)

return future

def drop(self):
"""Drop this database.

Expand Down
6 changes: 6 additions & 0 deletions google/cloud/spanner_v1/instance.py
Expand Up @@ -432,6 +432,7 @@ def database(
encryption_config=None,
database_dialect=DatabaseDialect.DATABASE_DIALECT_UNSPECIFIED,
database_role=None,
enable_drop_protection=False,
):
"""Factory to create a database within this instance.

Expand Down Expand Up @@ -467,6 +468,10 @@ def database(
:param database_dialect:
(Optional) database dialect for the database

:type enable_drop_protection: boolean
:param enable_drop_protection: (Optional) Represents whether the database
has drop protection enabled or not.

:rtype: :class:`~google.cloud.spanner_v1.database.Database`
:returns: a database owned by this instance.
"""
Expand All @@ -479,6 +484,7 @@ def database(
encryption_config=encryption_config,
database_dialect=database_dialect,
database_role=database_role,
enable_drop_protection=enable_drop_protection,
)

def list_databases(self, page_size=None):
Expand Down
21 changes: 21 additions & 0 deletions samples/samples/snippets.py
Expand Up @@ -196,6 +196,27 @@ def create_database(instance_id, database_id):
# [END spanner_create_database]


# [START spanner_update_database]
def update_database(instance_id, database_id):
"""Updates the drop protection setting for a database."""
spanner_client = spanner.Client()
instance = spanner_client.instance(instance_id)

db = instance.database(database_id)
db.enable_drop_protection = True

operation = db.update(["enable_drop_protection"])

print("Waiting for update operation for {} to complete...".format(
db.name))
operation.result(OPERATION_TIMEOUT_SECONDS)

print("Updated database {}.".format(db.name))


# [END spanner_update_database]


# [START spanner_create_database_with_encryption_key]
def create_database_with_encryption_key(instance_id, database_id, kms_key_name):
"""Creates a database with tables using a Customer Managed Encryption Key (CMEK)."""
Expand Down
13 changes: 13 additions & 0 deletions samples/samples/snippets_test.py
Expand Up @@ -154,6 +154,19 @@ def test_create_instance_with_processing_units(capsys, lci_instance_id):
retry_429(instance.delete)()


def test_update_database(capsys, instance_id, sample_database):
snippets.update_database(
instance_id, sample_database.database_id
)
out, _ = capsys.readouterr()
assert "Updated database {}.".format(sample_database.name) in out

# Cleanup
sample_database.enable_drop_protection = False
op = sample_database.update(["enable_drop_protection"])
op.result()


def test_create_database_with_encryption_config(
capsys, instance_id, cmek_database_id, kms_key_name
):
Expand Down
38 changes: 38 additions & 0 deletions tests/system/test_database_api.py
Expand Up @@ -562,3 +562,41 @@ def _unit_of_work(transaction, name):
rows = list(after.read(sd.COUNTERS_TABLE, sd.COUNTERS_COLUMNS, sd.ALL))

assert len(rows) == 2


def test_update_database_success(
not_emulator, shared_database, shared_instance, database_operation_timeout
):
old_protection = shared_database.enable_drop_protection
new_protection = True
shared_database.enable_drop_protection = new_protection
operation = shared_database.update(["enable_drop_protection"])

# We want to make sure the operation completes.
operation.result(database_operation_timeout) # raises on failure / timeout.

# Create a new database instance and reload it.
database_alt = shared_instance.database(shared_database.name.split("/")[-1])
assert database_alt.enable_drop_protection != new_protection

database_alt.reload()
assert database_alt.enable_drop_protection == new_protection

with pytest.raises(exceptions.FailedPrecondition):
database_alt.drop()

with pytest.raises(exceptions.FailedPrecondition):
shared_instance.delete()

# Make sure to put the database back the way it was for the
# other test cases.
shared_database.enable_drop_protection = old_protection
shared_database.update(["enable_drop_protection"])


def test_update_database_invalid(not_emulator, shared_database):
shared_database.enable_drop_protection = True

# Empty `fields` is not supported.
with pytest.raises(exceptions.InvalidArgument):
shared_database.update([])
32 changes: 32 additions & 0 deletions tests/unit/test_database.py
Expand Up @@ -17,8 +17,10 @@

import mock
from google.api_core import gapic_v1
from google.cloud.spanner_admin_database_v1 import Database as DatabasePB
from google.cloud.spanner_v1.param_types import INT64

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks

from google.api_core.retry import Retry
from google.protobuf.field_mask_pb2 import FieldMask

from google.cloud.spanner_v1 import RequestOptions

Expand Down Expand Up @@ -760,6 +762,8 @@ def test_reload_success(self):
encryption_config=encryption_config,
encryption_info=encryption_info,
default_leader=default_leader,
reconciling=True,
enable_drop_protection=True,
)
api.get_database.return_value = db_pb
instance = _Instance(self.INSTANCE_NAME, client=client)
Expand All @@ -776,6 +780,8 @@ def test_reload_success(self):
self.assertEqual(database._encryption_config, encryption_config)
self.assertEqual(database._encryption_info, encryption_info)
self.assertEqual(database._default_leader, default_leader)
self.assertEqual(database._reconciling, True)
self.assertEqual(database._enable_drop_protection, True)

api.get_database_ddl.assert_called_once_with(
database=self.DATABASE_NAME,
Expand Down Expand Up @@ -892,6 +898,32 @@ def test_update_ddl_w_operation_id(self):
metadata=[("google-cloud-resource-prefix", database.name)],
)

def test_update_success(self):
op_future = object()
client = _Client()
api = client.database_admin_api = self._make_database_admin_api()
api.update_database.return_value = op_future

instance = _Instance(self.INSTANCE_NAME, client=client)
pool = _Pool()
database = self._make_one(
self.DATABASE_ID, instance, enable_drop_protection=True, pool=pool
)

future = database.update(["enable_drop_protection"])

self.assertIs(future, op_future)

expected_database = DatabasePB(name=database.name, enable_drop_protection=True)

field_mask = FieldMask(paths=["enable_drop_protection"])

api.update_database.assert_called_once_with(
database=expected_database,
update_mask=field_mask,
metadata=[("google-cloud-resource-prefix", database.name)],
)

def test_drop_grpc_error(self):
from google.api_core.exceptions import Unknown

Expand Down