From d4a5a7b307a2c5f25ee44f4dd6901dd5ce9dd2f2 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Tue, 30 Jan 2024 17:23:12 +0100 Subject: [PATCH 01/17] chore(deps): update all dependencies (#1066) Co-authored-by: Sri Harsha CH <57220027+harshachinta@users.noreply.github.com> --- .devcontainer/requirements.txt | 12 ++++++------ samples/samples/requirements-test.txt | 4 ++-- samples/samples/requirements.txt | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.devcontainer/requirements.txt b/.devcontainer/requirements.txt index 3053bad715..3796c72c55 100644 --- a/.devcontainer/requirements.txt +++ b/.devcontainer/requirements.txt @@ -4,13 +4,13 @@ # # pip-compile --generate-hashes requirements.in # -argcomplete==3.2.1 \ - --hash=sha256:30891d87f3c1abe091f2142613c9d33cac84a5e15404489f033b20399b691fec \ - --hash=sha256:437f67fb9b058da5a090df505ef9be0297c4883993f3f56cb186ff087778cfb4 +argcomplete==3.2.2 \ + --hash=sha256:e44f4e7985883ab3e73a103ef0acd27299dbfe2dfed00142c35d4ddd3005901d \ + --hash=sha256:f3e49e8ea59b4026ee29548e24488af46e30c9de57d48638e24f54a1ea1000a2 # via nox -colorlog==6.8.0 \ - --hash=sha256:4ed23b05a1154294ac99f511fabe8c1d6d4364ec1f7fc989c7fb515ccc29d375 \ - --hash=sha256:fbb6fdf9d5685f2517f388fb29bb27d54e8654dd31f58bc2a3b217e967a95ca6 +colorlog==6.8.2 \ + --hash=sha256:3e3e079a41feb5a1b64f978b5ea4f46040a94f11f0e8bbb8261e3dbbeca64d44 \ + --hash=sha256:4dcbb62368e2800cb3c5abd348da7e53f6c362dda502ec27c560b2e58a66bd33 # via nox distlib==0.3.8 \ --hash=sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784 \ diff --git a/samples/samples/requirements-test.txt b/samples/samples/requirements-test.txt index bf07e9eaad..915735b7fd 100644 --- a/samples/samples/requirements-test.txt +++ b/samples/samples/requirements-test.txt @@ -1,4 +1,4 @@ -pytest==7.4.3 -pytest-dependency==0.5.1 +pytest==8.0.0 +pytest-dependency==0.6.0 mock==5.1.0 google-cloud-testutils==1.4.0 diff --git a/samples/samples/requirements.txt b/samples/samples/requirements.txt index 7747037537..36cf07c89a 100644 --- a/samples/samples/requirements.txt +++ b/samples/samples/requirements.txt @@ -1,2 +1,2 @@ -google-cloud-spanner==3.40.1 +google-cloud-spanner==3.41.0 futures==3.4.0; python_version < "3" From 122ab3679184276eb15e9629ca09956229f4b53a Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Tue, 30 Jan 2024 18:53:09 +0100 Subject: [PATCH 02/17] chore(deps): update dependency google-cloud-spanner to v3.42.0 (#1089) --- samples/samples/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/samples/requirements.txt b/samples/samples/requirements.txt index 36cf07c89a..88fb99e49b 100644 --- a/samples/samples/requirements.txt +++ b/samples/samples/requirements.txt @@ -1,2 +1,2 @@ -google-cloud-spanner==3.41.0 +google-cloud-spanner==3.42.0 futures==3.4.0; python_version < "3" From d5acc263d86fcbde7d5f972930255119e2f60e76 Mon Sep 17 00:00:00 2001 From: nginsberg-google <131713109+nginsberg-google@users.noreply.github.com> Date: Sun, 4 Feb 2024 20:17:21 -0800 Subject: [PATCH 03/17] feat: Add support for max commit delay (#1050) * proto generation * max commit delay * Fix some errors * Unit tests * regenerate proto changes * Fix unit tests * Finish test_transaction.py * Finish test_batch.py * Formatting * Cleanup * Fix merge conflict * Add optional=True * Remove optional=True, try calling HasField. * Update HasField to be called on the protobuf. * Update to timedelta.duration instead of an int. * Cleanup * Changes from Sri to pipe value to top-level funcitons and to add integration tests. Thanks Sri * Run nox -s blacken * feat(spanner): remove unused imports and add line * feat(spanner): add empty line in python docs * Update comment with valid values. * Update comment with valid values. * feat(spanner): fix lint * feat(spanner): rever nox file changes --------- Co-authored-by: Sri Harsha CH <57220027+harshachinta@users.noreply.github.com> Co-authored-by: Sri Harsha CH --- google/cloud/spanner_v1/batch.py | 10 ++++- google/cloud/spanner_v1/database.py | 25 ++++++++++-- google/cloud/spanner_v1/session.py | 4 ++ google/cloud/spanner_v1/transaction.py | 11 +++++- tests/system/test_database_api.py | 40 +++++++++++++++++++ tests/unit/test_batch.py | 54 +++++++++++++++++++++----- tests/unit/test_transaction.py | 34 ++++++++++++++-- 7 files changed, 159 insertions(+), 19 deletions(-) diff --git a/google/cloud/spanner_v1/batch.py b/google/cloud/spanner_v1/batch.py index da74bf35f0..9cb2afbc2c 100644 --- a/google/cloud/spanner_v1/batch.py +++ b/google/cloud/spanner_v1/batch.py @@ -146,7 +146,9 @@ def _check_state(self): if self.committed is not None: raise ValueError("Batch already committed") - def commit(self, return_commit_stats=False, request_options=None): + def commit( + self, return_commit_stats=False, request_options=None, max_commit_delay=None + ): """Commit mutations to the database. :type return_commit_stats: bool @@ -160,6 +162,11 @@ def commit(self, return_commit_stats=False, request_options=None): If a dict is provided, it must be of the same form as the protobuf message :class:`~google.cloud.spanner_v1.types.RequestOptions`. + :type max_commit_delay: :class:`datetime.timedelta` + :param max_commit_delay: + (Optional) The amount of latency this request is willing to incur + in order to improve throughput. + :rtype: datetime :returns: timestamp of the committed changes. """ @@ -188,6 +195,7 @@ def commit(self, return_commit_stats=False, request_options=None): mutations=self._mutations, single_use_transaction=txn_options, return_commit_stats=return_commit_stats, + max_commit_delay=max_commit_delay, request_options=request_options, ) with trace_call("CloudSpanner.Commit", self._session, trace_attributes): diff --git a/google/cloud/spanner_v1/database.py b/google/cloud/spanner_v1/database.py index 1a651a66f5..b23db95284 100644 --- a/google/cloud/spanner_v1/database.py +++ b/google/cloud/spanner_v1/database.py @@ -721,7 +721,7 @@ def snapshot(self, **kw): """ return SnapshotCheckout(self, **kw) - def batch(self, request_options=None): + def batch(self, request_options=None, max_commit_delay=None): """Return an object which wraps a batch. The wrapper *must* be used as a context manager, with the batch @@ -734,10 +734,16 @@ def batch(self, request_options=None): If a dict is provided, it must be of the same form as the protobuf message :class:`~google.cloud.spanner_v1.types.RequestOptions`. + :type max_commit_delay: :class:`datetime.timedelta` + :param max_commit_delay: + (Optional) The amount of latency this request is willing to incur + in order to improve throughput. Value must be between 0ms and + 500ms. + :rtype: :class:`~google.cloud.spanner_v1.database.BatchCheckout` :returns: new wrapper """ - return BatchCheckout(self, request_options) + return BatchCheckout(self, request_options, max_commit_delay) def mutation_groups(self): """Return an object which wraps a mutation_group. @@ -796,9 +802,13 @@ def run_in_transaction(self, func, *args, **kw): :type kw: dict :param kw: (Optional) keyword arguments to be passed to ``func``. - If passed, "timeout_secs" will be removed and used to + If passed, + "timeout_secs" will be removed and used to override the default retry timeout which defines maximum timestamp to continue retrying the transaction. + "max_commit_delay" will be removed and used to set the + max_commit_delay for the request. Value must be between + 0ms and 500ms. :rtype: Any :returns: The return value of ``func``. @@ -1035,9 +1045,14 @@ class BatchCheckout(object): (Optional) Common options for the commit request. If a dict is provided, it must be of the same form as the protobuf message :class:`~google.cloud.spanner_v1.types.RequestOptions`. + + :type max_commit_delay: :class:`datetime.timedelta` + :param max_commit_delay: + (Optional) The amount of latency this request is willing to incur + in order to improve throughput. """ - def __init__(self, database, request_options=None): + def __init__(self, database, request_options=None, max_commit_delay=None): self._database = database self._session = self._batch = None if request_options is None: @@ -1046,6 +1061,7 @@ def __init__(self, database, request_options=None): self._request_options = RequestOptions(request_options) else: self._request_options = request_options + self._max_commit_delay = max_commit_delay def __enter__(self): """Begin ``with`` block.""" @@ -1062,6 +1078,7 @@ def __exit__(self, exc_type, exc_val, exc_tb): self._batch.commit( return_commit_stats=self._database.log_commit_stats, request_options=self._request_options, + max_commit_delay=self._max_commit_delay, ) finally: if self._database.log_commit_stats and self._batch.commit_stats: diff --git a/google/cloud/spanner_v1/session.py b/google/cloud/spanner_v1/session.py index b25af53805..d0a44f6856 100644 --- a/google/cloud/spanner_v1/session.py +++ b/google/cloud/spanner_v1/session.py @@ -363,6 +363,8 @@ def run_in_transaction(self, func, *args, **kw): to continue retrying the transaction. "commit_request_options" will be removed and used to set the request options for the commit request. + "max_commit_delay" will be removed and used to set the max commit delay for the request. + "transaction_tag" will be removed and used to set the transaction tag for the request. :rtype: Any :returns: The return value of ``func``. @@ -372,6 +374,7 @@ def run_in_transaction(self, func, *args, **kw): """ deadline = time.time() + kw.pop("timeout_secs", DEFAULT_RETRY_TIMEOUT_SECS) commit_request_options = kw.pop("commit_request_options", None) + max_commit_delay = kw.pop("max_commit_delay", None) transaction_tag = kw.pop("transaction_tag", None) attempts = 0 @@ -400,6 +403,7 @@ def run_in_transaction(self, func, *args, **kw): txn.commit( return_commit_stats=self._database.log_commit_stats, request_options=commit_request_options, + max_commit_delay=max_commit_delay, ) except Aborted as exc: del self._transaction diff --git a/google/cloud/spanner_v1/transaction.py b/google/cloud/spanner_v1/transaction.py index d564d0d488..3c950401ac 100644 --- a/google/cloud/spanner_v1/transaction.py +++ b/google/cloud/spanner_v1/transaction.py @@ -180,7 +180,9 @@ def rollback(self): self.rolled_back = True del self._session._transaction - def commit(self, return_commit_stats=False, request_options=None): + def commit( + self, return_commit_stats=False, request_options=None, max_commit_delay=None + ): """Commit mutations to the database. :type return_commit_stats: bool @@ -194,6 +196,12 @@ def commit(self, return_commit_stats=False, request_options=None): If a dict is provided, it must be of the same form as the protobuf message :class:`~google.cloud.spanner_v1.types.RequestOptions`. + :type max_commit_delay: :class:`datetime.timedelta` + :param max_commit_delay: + (Optional) The amount of latency this request is willing to incur + in order to improve throughput. + :class:`~google.cloud.spanner_v1.types.MaxCommitDelay`. + :rtype: datetime :returns: timestamp of the committed changes. :raises ValueError: if there are no mutations to commit. @@ -228,6 +236,7 @@ def commit(self, return_commit_stats=False, request_options=None): mutations=self._mutations, transaction_id=self._transaction_id, return_commit_stats=return_commit_stats, + max_commit_delay=max_commit_delay, request_options=request_options, ) with trace_call("CloudSpanner.Commit", self._session, trace_attributes): diff --git a/tests/system/test_database_api.py b/tests/system/test_database_api.py index 052e628188..fbaee7476d 100644 --- a/tests/system/test_database_api.py +++ b/tests/system/test_database_api.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import datetime import time import uuid @@ -819,3 +820,42 @@ def _transaction_read(transaction): with pytest.raises(exceptions.InvalidArgument): shared_database.run_in_transaction(_transaction_read) + + +def test_db_batch_insert_w_max_commit_delay(shared_database): + _helpers.retry_has_all_dll(shared_database.reload)() + sd = _sample_data + + with shared_database.batch( + max_commit_delay=datetime.timedelta(milliseconds=100) + ) as batch: + batch.delete(sd.TABLE, sd.ALL) + batch.insert(sd.TABLE, sd.COLUMNS, sd.ROW_DATA) + + with shared_database.snapshot(read_timestamp=batch.committed) as snapshot: + from_snap = list(snapshot.read(sd.TABLE, sd.COLUMNS, sd.ALL)) + + sd._check_rows_data(from_snap) + + +def test_db_run_in_transaction_w_max_commit_delay(shared_database): + _helpers.retry_has_all_dll(shared_database.reload)() + sd = _sample_data + + with shared_database.batch() as batch: + batch.delete(sd.TABLE, sd.ALL) + + def _unit_of_work(transaction, test): + rows = list(transaction.read(test.TABLE, test.COLUMNS, sd.ALL)) + assert rows == [] + + transaction.insert_or_update(test.TABLE, test.COLUMNS, test.ROW_DATA) + + shared_database.run_in_transaction( + _unit_of_work, test=sd, max_commit_delay=datetime.timedelta(milliseconds=100) + ) + + with shared_database.snapshot() as after: + rows = list(after.execute_sql(sd.SQL)) + + sd._check_rows_data(rows) diff --git a/tests/unit/test_batch.py b/tests/unit/test_batch.py index 203c8a0cb5..1c02e93f1d 100644 --- a/tests/unit/test_batch.py +++ b/tests/unit/test_batch.py @@ -233,7 +233,14 @@ def test_commit_ok(self): self.assertEqual(committed, now) self.assertEqual(batch.committed, committed) - (session, mutations, single_use_txn, request_options, metadata) = api._committed + ( + session, + mutations, + single_use_txn, + request_options, + max_commit_delay, + metadata, + ) = api._committed self.assertEqual(session, self.SESSION_NAME) self.assertEqual(mutations, batch._mutations) self.assertIsInstance(single_use_txn, TransactionOptions) @@ -246,12 +253,13 @@ def test_commit_ok(self): ], ) self.assertEqual(request_options, RequestOptions()) + self.assertEqual(max_commit_delay, None) self.assertSpanAttributes( "CloudSpanner.Commit", attributes=dict(BASE_ATTRIBUTES, num_mutations=1) ) - def _test_commit_with_request_options(self, request_options=None): + def _test_commit_with_options(self, request_options=None, max_commit_delay_in=None): import datetime from google.cloud.spanner_v1 import CommitResponse from google.cloud.spanner_v1 import TransactionOptions @@ -267,7 +275,9 @@ def _test_commit_with_request_options(self, request_options=None): batch = self._make_one(session) batch.transaction_tag = self.TRANSACTION_TAG batch.insert(TABLE_NAME, COLUMNS, VALUES) - committed = batch.commit(request_options=request_options) + committed = batch.commit( + request_options=request_options, max_commit_delay=max_commit_delay_in + ) self.assertEqual(committed, now) self.assertEqual(batch.committed, committed) @@ -284,6 +294,7 @@ def _test_commit_with_request_options(self, request_options=None): mutations, single_use_txn, actual_request_options, + max_commit_delay, metadata, ) = api._committed self.assertEqual(session, self.SESSION_NAME) @@ -303,33 +314,46 @@ def _test_commit_with_request_options(self, request_options=None): "CloudSpanner.Commit", attributes=dict(BASE_ATTRIBUTES, num_mutations=1) ) + self.assertEqual(max_commit_delay_in, max_commit_delay) + def test_commit_w_request_tag_success(self): request_options = RequestOptions( request_tag="tag-1", ) - self._test_commit_with_request_options(request_options=request_options) + self._test_commit_with_options(request_options=request_options) def test_commit_w_transaction_tag_success(self): request_options = RequestOptions( transaction_tag="tag-1-1", ) - self._test_commit_with_request_options(request_options=request_options) + self._test_commit_with_options(request_options=request_options) def test_commit_w_request_and_transaction_tag_success(self): request_options = RequestOptions( request_tag="tag-1", transaction_tag="tag-1-1", ) - self._test_commit_with_request_options(request_options=request_options) + self._test_commit_with_options(request_options=request_options) def test_commit_w_request_and_transaction_tag_dictionary_success(self): request_options = {"request_tag": "tag-1", "transaction_tag": "tag-1-1"} - self._test_commit_with_request_options(request_options=request_options) + self._test_commit_with_options(request_options=request_options) def test_commit_w_incorrect_tag_dictionary_error(self): request_options = {"incorrect_tag": "tag-1-1"} with self.assertRaises(ValueError): - self._test_commit_with_request_options(request_options=request_options) + self._test_commit_with_options(request_options=request_options) + + def test_commit_w_max_commit_delay(self): + import datetime + + request_options = RequestOptions( + request_tag="tag-1", + ) + self._test_commit_with_options( + request_options=request_options, + max_commit_delay_in=datetime.timedelta(milliseconds=100), + ) def test_context_mgr_already_committed(self): import datetime @@ -368,7 +392,14 @@ def test_context_mgr_success(self): self.assertEqual(batch.committed, now) - (session, mutations, single_use_txn, request_options, metadata) = api._committed + ( + session, + mutations, + single_use_txn, + request_options, + _, + metadata, + ) = api._committed self.assertEqual(session, self.SESSION_NAME) self.assertEqual(mutations, batch._mutations) self.assertIsInstance(single_use_txn, TransactionOptions) @@ -565,12 +596,17 @@ def commit( ): from google.api_core.exceptions import Unknown + max_commit_delay = None + if type(request).pb(request).HasField("max_commit_delay"): + max_commit_delay = request.max_commit_delay + assert request.transaction_id == b"" self._committed = ( request.session, request.mutations, request.single_use_transaction, request.request_options, + max_commit_delay, metadata, ) if self._rpc_error: diff --git a/tests/unit/test_transaction.py b/tests/unit/test_transaction.py index 2d2f208424..d391fe4c13 100644 --- a/tests/unit/test_transaction.py +++ b/tests/unit/test_transaction.py @@ -346,9 +346,14 @@ def test_commit_w_other_error(self): ) def _commit_helper( - self, mutate=True, return_commit_stats=False, request_options=None + self, + mutate=True, + return_commit_stats=False, + request_options=None, + max_commit_delay_in=None, ): import datetime + from google.cloud.spanner_v1 import CommitResponse from google.cloud.spanner_v1.keyset import KeySet from google.cloud._helpers import UTC @@ -370,13 +375,22 @@ def _commit_helper( transaction.delete(TABLE_NAME, keyset) transaction.commit( - return_commit_stats=return_commit_stats, request_options=request_options + return_commit_stats=return_commit_stats, + request_options=request_options, + max_commit_delay=max_commit_delay_in, ) self.assertEqual(transaction.committed, now) self.assertIsNone(session._transaction) - session_id, mutations, txn_id, actual_request_options, metadata = api._committed + ( + session_id, + mutations, + txn_id, + actual_request_options, + max_commit_delay, + metadata, + ) = api._committed if request_options is None: expected_request_options = RequestOptions( @@ -391,6 +405,7 @@ def _commit_helper( expected_request_options.transaction_tag = self.TRANSACTION_TAG expected_request_options.request_tag = None + self.assertEqual(max_commit_delay_in, max_commit_delay) self.assertEqual(session_id, session.name) self.assertEqual(txn_id, self.TRANSACTION_ID) self.assertEqual(mutations, transaction._mutations) @@ -423,6 +438,11 @@ def test_commit_w_mutations(self): def test_commit_w_return_commit_stats(self): self._commit_helper(return_commit_stats=True) + def test_commit_w_max_commit_delay(self): + import datetime + + self._commit_helper(max_commit_delay_in=datetime.timedelta(milliseconds=100)) + def test_commit_w_request_tag_success(self): request_options = RequestOptions( request_tag="tag-1", @@ -851,7 +871,7 @@ def test_context_mgr_success(self): self.assertEqual(transaction.committed, now) - session_id, mutations, txn_id, _, metadata = api._committed + session_id, mutations, txn_id, _, _, metadata = api._committed self.assertEqual(session_id, self.SESSION_NAME) self.assertEqual(txn_id, self.TRANSACTION_ID) self.assertEqual(mutations, transaction._mutations) @@ -938,11 +958,17 @@ def commit( metadata=None, ): assert not request.single_use_transaction + + max_commit_delay = None + if type(request).pb(request).HasField("max_commit_delay"): + max_commit_delay = request.max_commit_delay + self._committed = ( request.session, request.mutations, request.transaction_id, request.request_options, + max_commit_delay, metadata, ) return self._commit_response From 9299212fb8aa6ed27ca40367e8d5aaeeba80c675 Mon Sep 17 00:00:00 2001 From: Ankit Agarwal <146331865+ankiaga@users.noreply.github.com> Date: Mon, 12 Feb 2024 11:50:15 +0530 Subject: [PATCH 04/17] feat: Exposing Spanner client in dbapi connection (#1100) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Exposing Spanner client in dbapi connection * Update comment Co-authored-by: Knut Olav Løite --------- Co-authored-by: Knut Olav Løite --- google/cloud/spanner_dbapi/connection.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/google/cloud/spanner_dbapi/connection.py b/google/cloud/spanner_dbapi/connection.py index 27983b8bd5..02a450b20e 100644 --- a/google/cloud/spanner_dbapi/connection.py +++ b/google/cloud/spanner_dbapi/connection.py @@ -117,6 +117,13 @@ def __init__(self, instance, database=None, read_only=False): self._batch_dml_executor: BatchDmlExecutor = None self._transaction_helper = TransactionRetryHelper(self) + @property + def spanner_client(self): + """Client for interacting with Cloud Spanner API. This property exposes + the spanner client so that underlying methods can be accessed. + """ + return self._instance._client + @property def autocommit(self): """Autocommit mode flag for this connection. From 2bf031913a620591f93f7f2ee429e93a8c3224a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Mon, 12 Feb 2024 08:27:22 +0100 Subject: [PATCH 05/17] chore: support named schemas (#1073) * chore: support named schemas * chore: import type and typecode * fix: use magic string instead of method reference as default value * fix: dialect property now also reloads the database * Comment addressed * Fix test --------- Co-authored-by: Ankit Agarwal <146331865+ankiaga@users.noreply.github.com> Co-authored-by: ankiaga --- google/cloud/spanner_dbapi/_helpers.py | 4 +- google/cloud/spanner_dbapi/cursor.py | 17 +++++-- google/cloud/spanner_v1/database.py | 47 ++++++++++++++--- google/cloud/spanner_v1/table.py | 68 ++++++++++++++++++++----- tests/system/test_table_api.py | 2 +- tests/unit/spanner_dbapi/test_cursor.py | 14 +++-- tests/unit/test_database.py | 13 ++++- tests/unit/test_table.py | 19 ++++--- 8 files changed, 147 insertions(+), 37 deletions(-) diff --git a/google/cloud/spanner_dbapi/_helpers.py b/google/cloud/spanner_dbapi/_helpers.py index c7f9e59afb..e9a71d9ae9 100644 --- a/google/cloud/spanner_dbapi/_helpers.py +++ b/google/cloud/spanner_dbapi/_helpers.py @@ -18,13 +18,13 @@ SQL_LIST_TABLES = """ SELECT table_name FROM information_schema.tables -WHERE table_catalog = '' AND table_schema = '' +WHERE table_catalog = '' AND table_schema = @table_schema """ SQL_GET_TABLE_COLUMN_SCHEMA = """ SELECT COLUMN_NAME, IS_NULLABLE, SPANNER_TYPE FROM INFORMATION_SCHEMA.COLUMNS -WHERE TABLE_SCHEMA = '' AND TABLE_NAME = @table_name +WHERE TABLE_SCHEMA = @table_schema AND TABLE_NAME = @table_name """ # This table maps spanner_types to Spanner's data type sizes as per diff --git a/google/cloud/spanner_dbapi/cursor.py b/google/cloud/spanner_dbapi/cursor.py index c8cb450394..2bd46ab643 100644 --- a/google/cloud/spanner_dbapi/cursor.py +++ b/google/cloud/spanner_dbapi/cursor.py @@ -510,13 +510,17 @@ def __iter__(self): raise ProgrammingError("no results to return") return self._itr - def list_tables(self): + def list_tables(self, schema_name=""): """List the tables of the linked Database. :rtype: list :returns: The list of tables within the Database. """ - return self.run_sql_in_snapshot(_helpers.SQL_LIST_TABLES) + return self.run_sql_in_snapshot( + sql=_helpers.SQL_LIST_TABLES, + params={"table_schema": schema_name}, + param_types={"table_schema": spanner.param_types.STRING}, + ) def run_sql_in_snapshot(self, sql, params=None, param_types=None): # Some SQL e.g. for INFORMATION_SCHEMA cannot be run in read-write transactions @@ -528,11 +532,14 @@ def run_sql_in_snapshot(self, sql, params=None, param_types=None): with self.connection.database.snapshot() as snapshot: return list(snapshot.execute_sql(sql, params, param_types)) - def get_table_column_schema(self, table_name): + def get_table_column_schema(self, table_name, schema_name=""): rows = self.run_sql_in_snapshot( sql=_helpers.SQL_GET_TABLE_COLUMN_SCHEMA, - params={"table_name": table_name}, - param_types={"table_name": spanner.param_types.STRING}, + params={"schema_name": schema_name, "table_name": table_name}, + param_types={ + "schema_name": spanner.param_types.STRING, + "table_name": spanner.param_types.STRING, + }, ) column_details = {} diff --git a/google/cloud/spanner_v1/database.py b/google/cloud/spanner_v1/database.py index b23db95284..1ef2754a6e 100644 --- a/google/cloud/spanner_v1/database.py +++ b/google/cloud/spanner_v1/database.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""User friendly container for Cloud Spanner Database.""" +"""User-friendly container for Cloud Spanner Database.""" import copy import functools @@ -42,6 +42,8 @@ from google.cloud.spanner_admin_database_v1.types import DatabaseDialect from google.cloud.spanner_dbapi.partition_helper import BatchTransactionId from google.cloud.spanner_v1 import ExecuteSqlRequest +from google.cloud.spanner_v1 import Type +from google.cloud.spanner_v1 import TypeCode from google.cloud.spanner_v1 import TransactionSelector from google.cloud.spanner_v1 import TransactionOptions from google.cloud.spanner_v1 import RequestOptions @@ -334,8 +336,21 @@ def database_dialect(self): :rtype: :class:`google.cloud.spanner_admin_database_v1.types.DatabaseDialect` :returns: the dialect of the database """ + if self._database_dialect == DatabaseDialect.DATABASE_DIALECT_UNSPECIFIED: + self.reload() return self._database_dialect + @property + def default_schema_name(self): + """Default schema name for this database. + + :rtype: str + :returns: "" for GoogleSQL and "public" for PostgreSQL + """ + if self.database_dialect == DatabaseDialect.POSTGRESQL: + return "public" + return "" + @property def database_role(self): """User-assigned database_role for sessions created by the pool. @@ -961,20 +976,40 @@ def table(self, table_id): """ return Table(table_id, self) - def list_tables(self): + def list_tables(self, schema="_default"): """List tables within the database. + :type schema: str + :param schema: The schema to search for tables, or None for all schemas. Use the special string "_default" to + search for tables in the default schema of the database. + :type: Iterable :returns: Iterable of :class:`~google.cloud.spanner_v1.table.Table` resources within the current database. """ + if "_default" == schema: + schema = self.default_schema_name + with self.snapshot() as snapshot: - if self._database_dialect == DatabaseDialect.POSTGRESQL: - where_clause = "WHERE TABLE_SCHEMA = 'public'" + if schema is None: + results = snapshot.execute_sql( + sql=_LIST_TABLES_QUERY.format(""), + ) else: - where_clause = "WHERE SPANNER_STATE = 'COMMITTED'" - results = snapshot.execute_sql(_LIST_TABLES_QUERY.format(where_clause)) + if self._database_dialect == DatabaseDialect.POSTGRESQL: + where_clause = "WHERE TABLE_SCHEMA = $1" + param_name = "p1" + else: + where_clause = ( + "WHERE TABLE_SCHEMA = @schema AND SPANNER_STATE = 'COMMITTED'" + ) + param_name = "schema" + results = snapshot.execute_sql( + sql=_LIST_TABLES_QUERY.format(where_clause), + params={param_name: schema}, + param_types={param_name: Type(code=TypeCode.STRING)}, + ) for row in results: yield self.table(row[0]) diff --git a/google/cloud/spanner_v1/table.py b/google/cloud/spanner_v1/table.py index 38ca798db8..c072775f43 100644 --- a/google/cloud/spanner_v1/table.py +++ b/google/cloud/spanner_v1/table.py @@ -43,13 +43,26 @@ class Table(object): :param database: The database that owns the table. """ - def __init__(self, table_id, database): + def __init__(self, table_id, database, schema_name=None): + if schema_name is None: + self._schema_name = database.default_schema_name + else: + self._schema_name = schema_name self._table_id = table_id self._database = database # Calculated properties. self._schema = None + @property + def schema_name(self): + """The schema name of the table used in SQL. + + :rtype: str + :returns: The table schema name. + """ + return self._schema_name + @property def table_id(self): """The ID of the table used in SQL. @@ -59,6 +72,30 @@ def table_id(self): """ return self._table_id + @property + def qualified_table_name(self): + """The qualified name of the table used in SQL. + + :rtype: str + :returns: The qualified table name. + """ + if self.schema_name == self._database.default_schema_name: + return self._quote_identifier(self.table_id) + return "{}.{}".format( + self._quote_identifier(self.schema_name), + self._quote_identifier(self.table_id), + ) + + def _quote_identifier(self, identifier): + """Quotes the given identifier using the rules of the dialect of the database of this table. + + :rtype: str + :returns: The quoted identifier. + """ + if self._database.database_dialect == DatabaseDialect.POSTGRESQL: + return '"{}"'.format(identifier) + return "`{}`".format(identifier) + def exists(self): """Test whether this table exists. @@ -77,22 +114,27 @@ def _exists(self, snapshot): :rtype: bool :returns: True if the table exists, else false. """ - if ( - self._database.database_dialect - == DatabaseDialect.DATABASE_DIALECT_UNSPECIFIED - ): - self._database.reload() if self._database.database_dialect == DatabaseDialect.POSTGRESQL: results = snapshot.execute_sql( - _EXISTS_TEMPLATE.format("WHERE TABLE_NAME = $1"), - params={"p1": self.table_id}, - param_types={"p1": Type(code=TypeCode.STRING)}, + sql=_EXISTS_TEMPLATE.format( + "WHERE TABLE_SCHEMA=$1 AND TABLE_NAME = $2" + ), + params={"p1": self.schema_name, "p2": self.table_id}, + param_types={ + "p1": Type(code=TypeCode.STRING), + "p2": Type(code=TypeCode.STRING), + }, ) else: results = snapshot.execute_sql( - _EXISTS_TEMPLATE.format("WHERE TABLE_NAME = @table_id"), - params={"table_id": self.table_id}, - param_types={"table_id": Type(code=TypeCode.STRING)}, + sql=_EXISTS_TEMPLATE.format( + "WHERE TABLE_SCHEMA = @schema_name AND TABLE_NAME = @table_id" + ), + params={"schema_name": self.schema_name, "table_id": self.table_id}, + param_types={ + "schema_name": Type(code=TypeCode.STRING), + "table_id": Type(code=TypeCode.STRING), + }, ) return next(iter(results))[0] @@ -117,7 +159,7 @@ def _get_schema(self, snapshot): :rtype: list of :class:`~google.cloud.spanner_v1.types.StructType.Field` :returns: The table schema. """ - query = _GET_SCHEMA_TEMPLATE.format(self.table_id) + query = _GET_SCHEMA_TEMPLATE.format(self.qualified_table_name) results = snapshot.execute_sql(query) # Start iterating to force the schema to download. try: diff --git a/tests/system/test_table_api.py b/tests/system/test_table_api.py index 7d4da2b363..80dbc1ccfc 100644 --- a/tests/system/test_table_api.py +++ b/tests/system/test_table_api.py @@ -33,7 +33,7 @@ def test_table_exists_reload_database_dialect( shared_instance, shared_database, not_emulator ): database = shared_instance.database(shared_database.database_id) - assert database.database_dialect == DatabaseDialect.DATABASE_DIALECT_UNSPECIFIED + assert database.database_dialect != DatabaseDialect.DATABASE_DIALECT_UNSPECIFIED table = database.table("all_types") assert table.exists() assert database.database_dialect != DatabaseDialect.DATABASE_DIALECT_UNSPECIFIED diff --git a/tests/unit/spanner_dbapi/test_cursor.py b/tests/unit/spanner_dbapi/test_cursor.py index 9735185a5c..1fcdb03a96 100644 --- a/tests/unit/spanner_dbapi/test_cursor.py +++ b/tests/unit/spanner_dbapi/test_cursor.py @@ -936,6 +936,7 @@ def test_iter(self): def test_list_tables(self): from google.cloud.spanner_dbapi import _helpers + from google.cloud.spanner_v1 import param_types connection = self._make_connection(self.INSTANCE, self.DATABASE) cursor = self._make_one(connection) @@ -946,7 +947,11 @@ def test_list_tables(self): return_value=table_list, ) as mock_run_sql: cursor.list_tables() - mock_run_sql.assert_called_once_with(_helpers.SQL_LIST_TABLES) + mock_run_sql.assert_called_once_with( + sql=_helpers.SQL_LIST_TABLES, + params={"table_schema": ""}, + param_types={"table_schema": param_types.STRING}, + ) def test_run_sql_in_snapshot(self): connection = self._make_connection(self.INSTANCE, mock.MagicMock()) @@ -987,8 +992,11 @@ def test_get_table_column_schema(self): result = cursor.get_table_column_schema(table_name=table_name) mock_run_sql.assert_called_once_with( sql=_helpers.SQL_GET_TABLE_COLUMN_SCHEMA, - params={"table_name": table_name}, - param_types={"table_name": param_types.STRING}, + params={"schema_name": "", "table_name": table_name}, + param_types={ + "schema_name": param_types.STRING, + "table_name": param_types.STRING, + }, ) self.assertEqual(result, expected) diff --git a/tests/unit/test_database.py b/tests/unit/test_database.py index 88e7bf8f66..00c57797ef 100644 --- a/tests/unit/test_database.py +++ b/tests/unit/test_database.py @@ -17,7 +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_admin_database_v1 import ( + Database as DatabasePB, + DatabaseDialect, +) from google.cloud.spanner_v1.param_types import INT64 from google.api_core.retry import Retry from google.protobuf.field_mask_pb2 import FieldMask @@ -1680,6 +1683,7 @@ def test_table_factory_defaults(self): instance = _Instance(self.INSTANCE_NAME, client=client) pool = _Pool() database = self._make_one(self.DATABASE_ID, instance, pool=pool) + database._database_dialect = DatabaseDialect.GOOGLE_STANDARD_SQL my_table = database.table("my_table") self.assertIsInstance(my_table, Table) self.assertIs(my_table._database, database) @@ -3011,6 +3015,12 @@ def _make_instance_api(): return mock.create_autospec(InstanceAdminClient) +def _make_database_admin_api(): + from google.cloud.spanner_admin_database_v1 import DatabaseAdminClient + + return mock.create_autospec(DatabaseAdminClient) + + class _Client(object): def __init__( self, @@ -3023,6 +3033,7 @@ def __init__( self.project = project self.project_name = "projects/" + self.project self._endpoint_cache = {} + self.database_admin_api = _make_database_admin_api() self.instance_admin_api = _make_instance_api() self._client_info = mock.Mock() self._client_options = mock.Mock() diff --git a/tests/unit/test_table.py b/tests/unit/test_table.py index 7ab30ea139..3b0cb949aa 100644 --- a/tests/unit/test_table.py +++ b/tests/unit/test_table.py @@ -26,6 +26,7 @@ class _BaseTest(unittest.TestCase): TABLE_ID = "test_table" + TABLE_SCHEMA = "" def _make_one(self, *args, **kwargs): return self._get_target_class()(*args, **kwargs) @@ -55,13 +56,18 @@ def test_exists_executes_query(self): db.snapshot.return_value = checkout checkout.__enter__.return_value = snapshot snapshot.execute_sql.return_value = [[False]] - table = self._make_one(self.TABLE_ID, db) + table = self._make_one(self.TABLE_ID, db, schema_name=self.TABLE_SCHEMA) exists = table.exists() self.assertFalse(exists) snapshot.execute_sql.assert_called_with( - _EXISTS_TEMPLATE.format("WHERE TABLE_NAME = @table_id"), - params={"table_id": self.TABLE_ID}, - param_types={"table_id": Type(code=TypeCode.STRING)}, + _EXISTS_TEMPLATE.format( + "WHERE TABLE_SCHEMA = @schema_name AND TABLE_NAME = @table_id" + ), + params={"schema_name": self.TABLE_SCHEMA, "table_id": self.TABLE_ID}, + param_types={ + "schema_name": Type(code=TypeCode.STRING), + "table_id": Type(code=TypeCode.STRING), + }, ) def test_schema_executes_query(self): @@ -70,14 +76,15 @@ def test_schema_executes_query(self): from google.cloud.spanner_v1.table import _GET_SCHEMA_TEMPLATE db = mock.create_autospec(Database, instance=True) + db.default_schema_name = "" checkout = mock.create_autospec(SnapshotCheckout, instance=True) snapshot = mock.create_autospec(Snapshot, instance=True) db.snapshot.return_value = checkout checkout.__enter__.return_value = snapshot - table = self._make_one(self.TABLE_ID, db) + table = self._make_one(self.TABLE_ID, db, schema_name=self.TABLE_SCHEMA) schema = table.schema self.assertIsInstance(schema, list) - expected_query = _GET_SCHEMA_TEMPLATE.format(self.TABLE_ID) + expected_query = _GET_SCHEMA_TEMPLATE.format("`{}`".format(self.TABLE_ID)) snapshot.execute_sql.assert_called_with(expected_query) def test_schema_returns_cache(self): From 1750328bbc7f8a1125f8e0c38024ced8e195a1b9 Mon Sep 17 00:00:00 2001 From: Astha Mohta <35952883+asthamohta@users.noreply.github.com> Date: Tue, 13 Feb 2024 17:15:47 +0530 Subject: [PATCH 06/17] feat: Untyped param (#1001) * changes * change * tests * tests * changes * change * lint * lint --------- Co-authored-by: surbhigarg92 --- google/cloud/spanner_v1/database.py | 2 -- google/cloud/spanner_v1/snapshot.py | 4 --- google/cloud/spanner_v1/transaction.py | 5 --- tests/system/test_session_api.py | 50 ++++++++++++++++++++++++-- tests/unit/test_database.py | 4 --- tests/unit/test_snapshot.py | 22 ------------ tests/unit/test_transaction.py | 24 ------------- 7 files changed, 48 insertions(+), 63 deletions(-) diff --git a/google/cloud/spanner_v1/database.py b/google/cloud/spanner_v1/database.py index 1ef2754a6e..650b4fda4c 100644 --- a/google/cloud/spanner_v1/database.py +++ b/google/cloud/spanner_v1/database.py @@ -648,8 +648,6 @@ def execute_partitioned_dml( if params is not None: from google.cloud.spanner_v1.transaction import Transaction - if param_types is None: - raise ValueError("Specify 'param_types' when passing 'params'.") params_pb = Transaction._make_params_pb(params, param_types) else: params_pb = {} diff --git a/google/cloud/spanner_v1/snapshot.py b/google/cloud/spanner_v1/snapshot.py index 491ff37d4a..2b6e1ce924 100644 --- a/google/cloud/spanner_v1/snapshot.py +++ b/google/cloud/spanner_v1/snapshot.py @@ -410,8 +410,6 @@ def execute_sql( raise ValueError("Transaction ID pending.") if params is not None: - if param_types is None: - raise ValueError("Specify 'param_types' when passing 'params'.") params_pb = Struct( fields={key: _make_value_pb(value) for key, value in params.items()} ) @@ -646,8 +644,6 @@ def partition_query( raise ValueError("Transaction not started.") if params is not None: - if param_types is None: - raise ValueError("Specify 'param_types' when passing 'params'.") params_pb = Struct( fields={key: _make_value_pb(value) for (key, value) in params.items()} ) diff --git a/google/cloud/spanner_v1/transaction.py b/google/cloud/spanner_v1/transaction.py index 3c950401ac..1f5ff1098a 100644 --- a/google/cloud/spanner_v1/transaction.py +++ b/google/cloud/spanner_v1/transaction.py @@ -276,14 +276,9 @@ def _make_params_pb(params, param_types): If ``params`` is None but ``param_types`` is not None. """ if params is not None: - if param_types is None: - raise ValueError("Specify 'param_types' when passing 'params'.") return Struct( fields={key: _make_value_pb(value) for key, value in params.items()} ) - else: - if param_types is not None: - raise ValueError("Specify 'params' when passing 'param_types'.") return {} diff --git a/tests/system/test_session_api.py b/tests/system/test_session_api.py index 9ea66b65ec..29d196b011 100644 --- a/tests/system/test_session_api.py +++ b/tests/system/test_session_api.py @@ -90,6 +90,8 @@ "jsonb_array", ) +QUERY_ALL_TYPES_COLUMNS = LIVE_ALL_TYPES_COLUMNS[1:17:2] + AllTypesRowData = collections.namedtuple("AllTypesRowData", LIVE_ALL_TYPES_COLUMNS) AllTypesRowData.__new__.__defaults__ = tuple([None for colum in LIVE_ALL_TYPES_COLUMNS]) EmulatorAllTypesRowData = collections.namedtuple( @@ -211,6 +213,17 @@ PostGresAllTypesRowData(pkey=309, jsonb_array=[JSON_1, JSON_2, None]), ) +QUERY_ALL_TYPES_DATA = ( + 123, + False, + BYTES_1, + SOME_DATE, + 1.4142136, + "VALUE", + SOME_TIME, + NUMERIC_1, +) + if _helpers.USE_EMULATOR: ALL_TYPES_COLUMNS = EMULATOR_ALL_TYPES_COLUMNS ALL_TYPES_ROWDATA = EMULATOR_ALL_TYPES_ROWDATA @@ -475,6 +488,39 @@ def test_batch_insert_or_update_then_query(sessions_database): sd._check_rows_data(rows) +def test_batch_insert_then_read_wo_param_types( + sessions_database, database_dialect, not_emulator +): + sd = _sample_data + + with sessions_database.batch() as batch: + batch.delete(ALL_TYPES_TABLE, sd.ALL) + batch.insert(ALL_TYPES_TABLE, ALL_TYPES_COLUMNS, ALL_TYPES_ROWDATA) + + with sessions_database.snapshot(multi_use=True) as snapshot: + for column_type, value in list( + zip(QUERY_ALL_TYPES_COLUMNS, QUERY_ALL_TYPES_DATA) + ): + placeholder = ( + "$1" if database_dialect == DatabaseDialect.POSTGRESQL else "@value" + ) + sql = ( + "SELECT * FROM " + + ALL_TYPES_TABLE + + " WHERE " + + column_type + + " = " + + placeholder + ) + param = ( + {"p1": value} + if database_dialect == DatabaseDialect.POSTGRESQL + else {"value": value} + ) + rows = list(snapshot.execute_sql(sql, params=param)) + assert len(rows) == 1 + + def test_batch_insert_w_commit_timestamp(sessions_database, not_postgres): table = "users_history" columns = ["id", "commit_ts", "name", "email", "deleted"] @@ -1930,8 +1976,8 @@ def _check_sql_results( database, sql, params, - param_types, - expected, + param_types=None, + expected=None, order=True, recurse_into_lists=True, ): diff --git a/tests/unit/test_database.py b/tests/unit/test_database.py index 00c57797ef..6bcacd379b 100644 --- a/tests/unit/test_database.py +++ b/tests/unit/test_database.py @@ -1136,10 +1136,6 @@ def _execute_partitioned_dml_helper( def test_execute_partitioned_dml_wo_params(self): self._execute_partitioned_dml_helper(dml=DML_WO_PARAM) - def test_execute_partitioned_dml_w_params_wo_param_types(self): - with self.assertRaises(ValueError): - self._execute_partitioned_dml_helper(dml=DML_W_PARAM, params=PARAMS) - def test_execute_partitioned_dml_w_params_and_param_types(self): self._execute_partitioned_dml_helper( dml=DML_W_PARAM, params=PARAMS, param_types=PARAM_TYPES diff --git a/tests/unit/test_snapshot.py b/tests/unit/test_snapshot.py index aec20c2f54..bf5563dcfd 100644 --- a/tests/unit/test_snapshot.py +++ b/tests/unit/test_snapshot.py @@ -868,16 +868,6 @@ def test_execute_sql_other_error(self): attributes=dict(BASE_ATTRIBUTES, **{"db.statement": SQL_QUERY}), ) - def test_execute_sql_w_params_wo_param_types(self): - database = _Database() - session = _Session(database) - derived = self._makeDerived(session) - - with self.assertRaises(ValueError): - derived.execute_sql(SQL_QUERY_WITH_PARAM, PARAMS) - - self.assertNoSpans() - def _execute_sql_helper( self, multi_use, @@ -1397,18 +1387,6 @@ def test_partition_query_other_error(self): attributes=dict(BASE_ATTRIBUTES, **{"db.statement": SQL_QUERY}), ) - def test_partition_query_w_params_wo_param_types(self): - database = _Database() - session = _Session(database) - derived = self._makeDerived(session) - derived._multi_use = True - derived._transaction_id = TXN_ID - - with self.assertRaises(ValueError): - list(derived.partition_query(SQL_QUERY_WITH_PARAM, PARAMS)) - - self.assertNoSpans() - def test_partition_query_single_use_raises(self): with self.assertRaises(ValueError): self._partition_query_helper(multi_use=False, w_txn=True) diff --git a/tests/unit/test_transaction.py b/tests/unit/test_transaction.py index d391fe4c13..a673eabb83 100644 --- a/tests/unit/test_transaction.py +++ b/tests/unit/test_transaction.py @@ -471,20 +471,6 @@ def test_commit_w_incorrect_tag_dictionary_error(self): with self.assertRaises(ValueError): self._commit_helper(request_options=request_options) - def test__make_params_pb_w_params_wo_param_types(self): - session = _Session() - transaction = self._make_one(session) - - with self.assertRaises(ValueError): - transaction._make_params_pb(PARAMS, None) - - def test__make_params_pb_wo_params_w_param_types(self): - session = _Session() - transaction = self._make_one(session) - - with self.assertRaises(ValueError): - transaction._make_params_pb(None, PARAM_TYPES) - def test__make_params_pb_w_params_w_param_types(self): from google.protobuf.struct_pb2 import Struct from google.cloud.spanner_v1._helpers import _make_value_pb @@ -510,16 +496,6 @@ def test_execute_update_other_error(self): with self.assertRaises(RuntimeError): transaction.execute_update(DML_QUERY) - def test_execute_update_w_params_wo_param_types(self): - database = _Database() - database.spanner_api = self._make_spanner_api() - session = _Session(database) - transaction = self._make_one(session) - transaction._transaction_id = self.TRANSACTION_ID - - with self.assertRaises(ValueError): - transaction.execute_update(DML_QUERY_WITH_PARAM, PARAMS) - def _execute_update_helper( self, count=0, From 689fa2e016e2b05724615cfe3b21fa19d921b333 Mon Sep 17 00:00:00 2001 From: Ankit Agarwal <146331865+ankiaga@users.noreply.github.com> Date: Wed, 14 Feb 2024 14:04:27 +0530 Subject: [PATCH 07/17] chore: Adding schema name property in dbapi connection (#1101) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: Adding schema name property in dbapi connection * small fix * More changes * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * Comments incorporated * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * Comments incorporated * lint issues fixed * comment incorporated --------- Co-authored-by: Owl Bot --- google/cloud/spanner_dbapi/connection.py | 19 ++++++++-- tests/unit/spanner_dbapi/test_connection.py | 40 ++++++++++++++++++--- 2 files changed, 51 insertions(+), 8 deletions(-) diff --git a/google/cloud/spanner_dbapi/connection.py b/google/cloud/spanner_dbapi/connection.py index 02a450b20e..70b0f2cfbc 100644 --- a/google/cloud/spanner_dbapi/connection.py +++ b/google/cloud/spanner_dbapi/connection.py @@ -124,6 +124,18 @@ def spanner_client(self): """ return self._instance._client + @property + def current_schema(self): + """schema name for this connection. + + :rtype: str + :returns: the current default schema of this connection. Currently, this + is always "" for GoogleSQL and "public" for PostgreSQL databases. + """ + if self.database is None: + raise ValueError("database property not set on the connection") + return self.database.default_schema_name + @property def autocommit(self): """Autocommit mode flag for this connection. @@ -664,9 +676,10 @@ def connect( raise ValueError("project in url does not match client object project") instance = client.instance(instance_id) - conn = Connection( - instance, instance.database(database_id, pool=pool) if database_id else None - ) + database = None + if database_id: + database = instance.database(database_id, pool=pool) + conn = Connection(instance, database) if pool is not None: conn._own_pool = False diff --git a/tests/unit/spanner_dbapi/test_connection.py b/tests/unit/spanner_dbapi/test_connection.py index dec32285d4..c62d226a29 100644 --- a/tests/unit/spanner_dbapi/test_connection.py +++ b/tests/unit/spanner_dbapi/test_connection.py @@ -20,6 +20,7 @@ import warnings import pytest +from google.cloud.spanner_admin_database_v1 import DatabaseDialect from google.cloud.spanner_dbapi.batch_dml_executor import BatchMode from google.cloud.spanner_dbapi.exceptions import ( InterfaceError, @@ -58,14 +59,16 @@ def _get_client_info(self): return ClientInfo(user_agent=USER_AGENT) - def _make_connection(self, **kwargs): + def _make_connection( + self, database_dialect=DatabaseDialect.DATABASE_DIALECT_UNSPECIFIED, **kwargs + ): from google.cloud.spanner_v1.instance import Instance from google.cloud.spanner_v1.client import Client # We don't need a real Client object to test the constructor client = Client() instance = Instance(INSTANCE, client=client) - database = instance.database(DATABASE) + database = instance.database(DATABASE, database_dialect=database_dialect) return Connection(instance, database, **kwargs) @mock.patch("google.cloud.spanner_dbapi.connection.Connection.commit") @@ -105,6 +108,22 @@ def test_property_instance(self): self.assertIsInstance(connection.instance, Instance) self.assertEqual(connection.instance, connection._instance) + def test_property_current_schema_google_sql_dialect(self): + from google.cloud.spanner_v1.database import Database + + connection = self._make_connection( + database_dialect=DatabaseDialect.GOOGLE_STANDARD_SQL + ) + self.assertIsInstance(connection.database, Database) + self.assertEqual(connection.current_schema, "") + + def test_property_current_schema_postgres_sql_dialect(self): + from google.cloud.spanner_v1.database import Database + + connection = self._make_connection(database_dialect=DatabaseDialect.POSTGRESQL) + self.assertIsInstance(connection.database, Database) + self.assertEqual(connection.current_schema, "public") + def test_read_only_connection(self): connection = self._make_connection(read_only=True) self.assertTrue(connection.read_only) @@ -745,11 +764,22 @@ def __init__(self, name="instance_id", client=None): self.name = name self._client = client - def database(self, database_id="database_id", pool=None): - return _Database(database_id, pool) + def database( + self, + database_id="database_id", + pool=None, + database_dialect=DatabaseDialect.GOOGLE_STANDARD_SQL, + ): + return _Database(database_id, pool, database_dialect) class _Database(object): - def __init__(self, database_id="database_id", pool=None): + def __init__( + self, + database_id="database_id", + pool=None, + database_dialect=DatabaseDialect.GOOGLE_STANDARD_SQL, + ): self.name = database_id self.pool = pool + self.database_dialect = database_dialect From b0a31e99ba8dbe21a859f1ae2086ffb41ad46a92 Mon Sep 17 00:00:00 2001 From: rahul2393 Date: Wed, 14 Feb 2024 17:53:55 +0530 Subject: [PATCH 08/17] chore: add a new directory for archived samples of admin APIs. (#1102) --- samples/samples/archived/.gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 samples/samples/archived/.gitkeep diff --git a/samples/samples/archived/.gitkeep b/samples/samples/archived/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 From 3669303fb50b4207975b380f356227aceaa1189a Mon Sep 17 00:00:00 2001 From: Scott Nam Date: Sat, 17 Feb 2024 03:03:31 -0800 Subject: [PATCH 09/17] feat: Include RENAME in DDL regex (#1075) Co-authored-by: Sri Harsha CH <57220027+harshachinta@users.noreply.github.com> --- google/cloud/spanner_dbapi/parse_utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/google/cloud/spanner_dbapi/parse_utils.py b/google/cloud/spanner_dbapi/parse_utils.py index b642daf084..3f8f61af08 100644 --- a/google/cloud/spanner_dbapi/parse_utils.py +++ b/google/cloud/spanner_dbapi/parse_utils.py @@ -154,7 +154,9 @@ # DDL statements follow # https://cloud.google.com/spanner/docs/data-definition-language -RE_DDL = re.compile(r"^\s*(CREATE|ALTER|DROP|GRANT|REVOKE)", re.IGNORECASE | re.DOTALL) +RE_DDL = re.compile( + r"^\s*(CREATE|ALTER|DROP|GRANT|REVOKE|RENAME)", re.IGNORECASE | re.DOTALL +) RE_IS_INSERT = re.compile(r"^\s*(INSERT)", re.IGNORECASE | re.DOTALL) From 3aab0ed5ed3cd078835812dae183a333fe1d3a20 Mon Sep 17 00:00:00 2001 From: Ankit Agarwal <146331865+ankiaga@users.noreply.github.com> Date: Tue, 20 Feb 2024 19:31:27 +0530 Subject: [PATCH 10/17] feat: support partitioned dml in dbapi (#1103) * feat: Implementation to support executing partitioned dml query at dbapi * Small fix * Comments incorporated * Comments incorporated --- .../client_side_statement_executor.py | 2 + .../client_side_statement_parser.py | 7 +++ google/cloud/spanner_dbapi/connection.py | 50 ++++++++++++++++ google/cloud/spanner_dbapi/cursor.py | 12 ++++ .../cloud/spanner_dbapi/parsed_statement.py | 6 ++ tests/system/test_dbapi.py | 22 +++++++ tests/unit/spanner_dbapi/test_connection.py | 58 +++++++++++++++++++ tests/unit/spanner_dbapi/test_parse_utils.py | 14 +++++ 8 files changed, 171 insertions(+) diff --git a/google/cloud/spanner_dbapi/client_side_statement_executor.py b/google/cloud/spanner_dbapi/client_side_statement_executor.py index dfbf33c1e8..b1ed2873ae 100644 --- a/google/cloud/spanner_dbapi/client_side_statement_executor.py +++ b/google/cloud/spanner_dbapi/client_side_statement_executor.py @@ -105,6 +105,8 @@ def execute(cursor: "Cursor", parsed_statement: ParsedStatement): ) if statement_type == ClientSideStatementType.RUN_PARTITIONED_QUERY: return connection.run_partitioned_query(parsed_statement) + if statement_type == ClientSideStatementType.SET_AUTOCOMMIT_DML_MODE: + return connection._set_autocommit_dml_mode(parsed_statement) def _get_streamed_result_set(column_name, type_code, column_values): diff --git a/google/cloud/spanner_dbapi/client_side_statement_parser.py b/google/cloud/spanner_dbapi/client_side_statement_parser.py index 63188a032a..002779adb4 100644 --- a/google/cloud/spanner_dbapi/client_side_statement_parser.py +++ b/google/cloud/spanner_dbapi/client_side_statement_parser.py @@ -38,6 +38,9 @@ RE_RUN_PARTITIONED_QUERY = re.compile( r"^\s*(RUN)\s+(PARTITIONED)\s+(QUERY)\s+(.+)", re.IGNORECASE ) +RE_SET_AUTOCOMMIT_DML_MODE = re.compile( + r"^\s*(SET)\s+(AUTOCOMMIT_DML_MODE)\s+(=)\s+(.+)", re.IGNORECASE +) def parse_stmt(query): @@ -82,6 +85,10 @@ def parse_stmt(query): match = re.search(RE_RUN_PARTITION, query) client_side_statement_params.append(match.group(3)) client_side_statement_type = ClientSideStatementType.RUN_PARTITION + elif RE_SET_AUTOCOMMIT_DML_MODE.match(query): + match = re.search(RE_SET_AUTOCOMMIT_DML_MODE, query) + client_side_statement_params.append(match.group(4)) + client_side_statement_type = ClientSideStatementType.SET_AUTOCOMMIT_DML_MODE if client_side_statement_type is not None: return ParsedStatement( StatementType.CLIENT_SIDE, diff --git a/google/cloud/spanner_dbapi/connection.py b/google/cloud/spanner_dbapi/connection.py index 70b0f2cfbc..3dec2bd028 100644 --- a/google/cloud/spanner_dbapi/connection.py +++ b/google/cloud/spanner_dbapi/connection.py @@ -23,6 +23,7 @@ from google.cloud.spanner_dbapi.parse_utils import _get_statement_type from google.cloud.spanner_dbapi.parsed_statement import ( StatementType, + AutocommitDmlMode, ) from google.cloud.spanner_dbapi.partition_helper import PartitionId from google.cloud.spanner_dbapi.parsed_statement import ParsedStatement, Statement @@ -116,6 +117,7 @@ def __init__(self, instance, database=None, read_only=False): self._batch_mode = BatchMode.NONE self._batch_dml_executor: BatchDmlExecutor = None self._transaction_helper = TransactionRetryHelper(self) + self._autocommit_dml_mode: AutocommitDmlMode = AutocommitDmlMode.TRANSACTIONAL @property def spanner_client(self): @@ -167,6 +169,23 @@ def database(self): """ return self._database + @property + def autocommit_dml_mode(self): + """Modes for executing DML statements in autocommit mode for this connection. + + The DML autocommit modes are: + 1) TRANSACTIONAL - DML statements are executed as single read-write transaction. + After successful execution, the DML statement is guaranteed to have been applied + exactly once to the database. + + 2) PARTITIONED_NON_ATOMIC - DML statements are executed as partitioned DML transactions. + If an error occurs during the execution of the DML statement, it is possible that the + statement has been applied to some but not all of the rows specified in the statement. + + :rtype: :class:`~google.cloud.spanner_dbapi.parsed_statement.AutocommitDmlMode` + """ + return self._autocommit_dml_mode + @property @deprecated( reason="This method is deprecated. Use _spanner_transaction_started field" @@ -577,6 +596,37 @@ def run_partitioned_query( partitioned_query, statement.params, statement.param_types ) + @check_not_closed + def _set_autocommit_dml_mode( + self, + parsed_statement: ParsedStatement, + ): + autocommit_dml_mode_str = parsed_statement.client_side_statement_params[0] + autocommit_dml_mode = AutocommitDmlMode[autocommit_dml_mode_str.upper()] + self.set_autocommit_dml_mode(autocommit_dml_mode) + + def set_autocommit_dml_mode( + self, + autocommit_dml_mode, + ): + """ + Sets the mode for executing DML statements in autocommit mode for this connection. + This mode is only used when the connection is in autocommit mode, and may only + be set while the transaction is in autocommit mode and not in a temporary transaction. + """ + + if self._client_transaction_started is True: + raise ProgrammingError( + "Cannot set autocommit DML mode while not in autocommit mode or while a transaction is active." + ) + if self.read_only is True: + raise ProgrammingError( + "Cannot set autocommit DML mode for a read-only connection." + ) + if self._batch_mode is not BatchMode.NONE: + raise ProgrammingError("Cannot set autocommit DML mode while in a batch.") + self._autocommit_dml_mode = autocommit_dml_mode + def _partitioned_query_validation(self, partitioned_query, statement): if _get_statement_type(Statement(partitioned_query)) is not StatementType.QUERY: raise ProgrammingError( diff --git a/google/cloud/spanner_dbapi/cursor.py b/google/cloud/spanner_dbapi/cursor.py index 2bd46ab643..bd2ad974f9 100644 --- a/google/cloud/spanner_dbapi/cursor.py +++ b/google/cloud/spanner_dbapi/cursor.py @@ -45,6 +45,7 @@ StatementType, Statement, ParsedStatement, + AutocommitDmlMode, ) from google.cloud.spanner_dbapi.transaction_helper import CursorStatementType from google.cloud.spanner_dbapi.utils import PeekIterator @@ -272,6 +273,17 @@ def _execute(self, sql, args=None, call_from_execute_many=False): self._batch_DDLs(sql) if not self.connection._client_transaction_started: self.connection.run_prior_DDL_statements() + elif ( + self.connection.autocommit_dml_mode + is AutocommitDmlMode.PARTITIONED_NON_ATOMIC + ): + self._row_count = self.connection.database.execute_partitioned_dml( + sql, + params=args, + param_types=self._parsed_statement.statement.param_types, + request_options=self.connection.request_options, + ) + self._result_set = None else: self._execute_in_rw_transaction() diff --git a/google/cloud/spanner_dbapi/parsed_statement.py b/google/cloud/spanner_dbapi/parsed_statement.py index 1bb0ed25f4..f89d6ea19e 100644 --- a/google/cloud/spanner_dbapi/parsed_statement.py +++ b/google/cloud/spanner_dbapi/parsed_statement.py @@ -36,6 +36,12 @@ class ClientSideStatementType(Enum): PARTITION_QUERY = 9 RUN_PARTITION = 10 RUN_PARTITIONED_QUERY = 11 + SET_AUTOCOMMIT_DML_MODE = 12 + + +class AutocommitDmlMode(Enum): + TRANSACTIONAL = 1 + PARTITIONED_NON_ATOMIC = 2 @dataclass diff --git a/tests/system/test_dbapi.py b/tests/system/test_dbapi.py index 52a80d5714..67854eeeac 100644 --- a/tests/system/test_dbapi.py +++ b/tests/system/test_dbapi.py @@ -26,6 +26,7 @@ OperationalError, RetryAborted, ) +from google.cloud.spanner_dbapi.parsed_statement import AutocommitDmlMode from google.cloud.spanner_v1 import JsonObject from google.cloud.spanner_v1 import gapic_version as package_version from google.api_core.datetime_helpers import DatetimeWithNanoseconds @@ -669,6 +670,27 @@ def test_run_partitioned_query(self): assert len(rows) == 10 self._conn.commit() + def test_partitioned_dml_query(self): + """Test partitioned_dml query works in autocommit mode.""" + self._cursor.execute("start batch dml") + for i in range(1, 11): + self._insert_row(i) + self._cursor.execute("run batch") + self._conn.commit() + + self._conn.autocommit = True + self._cursor.execute("set autocommit_dml_mode = PARTITIONED_NON_ATOMIC") + self._cursor.execute("DELETE FROM contacts WHERE contact_id > 3") + assert self._cursor.rowcount == 7 + + self._cursor.execute("set autocommit_dml_mode = TRANSACTIONAL") + assert self._conn.autocommit_dml_mode == AutocommitDmlMode.TRANSACTIONAL + + self._conn.autocommit = False + # Test changing autocommit_dml_mode is not allowed when connection is in autocommit mode + with pytest.raises(ProgrammingError): + self._cursor.execute("set autocommit_dml_mode = PARTITIONED_NON_ATOMIC") + def _insert_row(self, i): self._cursor.execute( f""" diff --git a/tests/unit/spanner_dbapi/test_connection.py b/tests/unit/spanner_dbapi/test_connection.py index c62d226a29..d0fa521f8f 100644 --- a/tests/unit/spanner_dbapi/test_connection.py +++ b/tests/unit/spanner_dbapi/test_connection.py @@ -33,6 +33,8 @@ ParsedStatement, StatementType, Statement, + ClientSideStatementType, + AutocommitDmlMode, ) PROJECT = "test-project" @@ -433,6 +435,62 @@ def test_abort_dml_batch(self, mock_batch_dml_executor): self.assertEqual(self._under_test._batch_mode, BatchMode.NONE) self.assertEqual(self._under_test._batch_dml_executor, None) + def test_set_autocommit_dml_mode_with_autocommit_false(self): + self._under_test.autocommit = False + parsed_statement = ParsedStatement( + StatementType.CLIENT_SIDE, + Statement("sql"), + ClientSideStatementType.SET_AUTOCOMMIT_DML_MODE, + ["PARTITIONED_NON_ATOMIC"], + ) + + with self.assertRaises(ProgrammingError): + self._under_test._set_autocommit_dml_mode(parsed_statement) + + def test_set_autocommit_dml_mode_with_readonly(self): + self._under_test.autocommit = True + self._under_test.read_only = True + parsed_statement = ParsedStatement( + StatementType.CLIENT_SIDE, + Statement("sql"), + ClientSideStatementType.SET_AUTOCOMMIT_DML_MODE, + ["PARTITIONED_NON_ATOMIC"], + ) + + with self.assertRaises(ProgrammingError): + self._under_test._set_autocommit_dml_mode(parsed_statement) + + def test_set_autocommit_dml_mode_with_batch_mode(self): + self._under_test.autocommit = True + parsed_statement = ParsedStatement( + StatementType.CLIENT_SIDE, + Statement("sql"), + ClientSideStatementType.SET_AUTOCOMMIT_DML_MODE, + ["PARTITIONED_NON_ATOMIC"], + ) + + self._under_test._set_autocommit_dml_mode(parsed_statement) + + assert ( + self._under_test.autocommit_dml_mode + == AutocommitDmlMode.PARTITIONED_NON_ATOMIC + ) + + def test_set_autocommit_dml_mode(self): + self._under_test.autocommit = True + parsed_statement = ParsedStatement( + StatementType.CLIENT_SIDE, + Statement("sql"), + ClientSideStatementType.SET_AUTOCOMMIT_DML_MODE, + ["PARTITIONED_NON_ATOMIC"], + ) + + self._under_test._set_autocommit_dml_mode(parsed_statement) + assert ( + self._under_test.autocommit_dml_mode + == AutocommitDmlMode.PARTITIONED_NON_ATOMIC + ) + @mock.patch("google.cloud.spanner_v1.database.Database", autospec=True) def test_run_prior_DDL_statements(self, mock_database): from google.cloud.spanner_dbapi import Connection, InterfaceError diff --git a/tests/unit/spanner_dbapi/test_parse_utils.py b/tests/unit/spanner_dbapi/test_parse_utils.py index 239fc9d6b3..3a325014fa 100644 --- a/tests/unit/spanner_dbapi/test_parse_utils.py +++ b/tests/unit/spanner_dbapi/test_parse_utils.py @@ -115,6 +115,20 @@ def test_run_partitioned_query_classify_stmt(self): ), ) + def test_set_autocommit_dml_mode_stmt(self): + parsed_statement = classify_statement( + " set autocommit_dml_mode = PARTITIONED_NON_ATOMIC " + ) + self.assertEqual( + parsed_statement, + ParsedStatement( + StatementType.CLIENT_SIDE, + Statement("set autocommit_dml_mode = PARTITIONED_NON_ATOMIC"), + ClientSideStatementType.SET_AUTOCOMMIT_DML_MODE, + ["PARTITIONED_NON_ATOMIC"], + ), + ) + @unittest.skipIf(skip_condition, skip_message) def test_sql_pyformat_args_to_spanner(self): from google.cloud.spanner_dbapi.parse_utils import sql_pyformat_args_to_spanner From 5410c32febbef48d4623d8023a6eb9f07a65c2f5 Mon Sep 17 00:00:00 2001 From: rahul2393 Date: Mon, 26 Feb 2024 10:17:24 +0530 Subject: [PATCH 11/17] docs: samples and tests for admin backup APIs (#1105) * docs: samples and tests for admin backup APIs * fix test * fix tests * incorporate suggestions --------- Co-authored-by: Sri Harsha CH <57220027+harshachinta@users.noreply.github.com> --- samples/samples/admin/backup_snippet.py | 575 +++++++++++++++++++ samples/samples/admin/backup_snippet_test.py | 192 +++++++ samples/samples/admin/samples.py | 2 +- samples/samples/admin/samples_test.py | 2 +- 4 files changed, 769 insertions(+), 2 deletions(-) create mode 100644 samples/samples/admin/backup_snippet.py create mode 100644 samples/samples/admin/backup_snippet_test.py diff --git a/samples/samples/admin/backup_snippet.py b/samples/samples/admin/backup_snippet.py new file mode 100644 index 0000000000..0a7260d115 --- /dev/null +++ b/samples/samples/admin/backup_snippet.py @@ -0,0 +1,575 @@ +# Copyright 2024 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""This application demonstrates how to create and restore from backups +using Cloud Spanner. +For more information, see the README.rst under /spanner. +""" + +import time +from datetime import datetime, timedelta + +from google.api_core import protobuf_helpers +from google.cloud import spanner +from google.cloud.exceptions import NotFound + + +# [START spanner_create_backup] +def create_backup(instance_id, database_id, backup_id, version_time): + """Creates a backup for a database.""" + + from google.cloud.spanner_admin_database_v1.types import backup as backup_pb + + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + database = instance.database(database_id) + + # Create a backup + expire_time = datetime.utcnow() + timedelta(days=14) + + request = backup_pb.CreateBackupRequest( + parent=instance.name, + backup_id=backup_id, + backup=backup_pb.Backup( + database=database.name, + expire_time=expire_time, + version_time=version_time, + ), + ) + + operation = spanner_client.database_admin_api.create_backup(request) + + # Wait for backup operation to complete. + backup = operation.result(2100) + + # Verify that the backup is ready. + assert backup.state == backup_pb.Backup.State.READY + + print( + "Backup {} of size {} bytes was created at {} for version of database at {}".format( + backup.name, backup.size_bytes, backup.create_time, backup.version_time + ) + ) + + +# [END spanner_create_backup] + + +# [START spanner_create_backup_with_encryption_key] +def create_backup_with_encryption_key( + instance_id, database_id, backup_id, kms_key_name +): + """Creates a backup for a database using a Customer Managed Encryption Key (CMEK).""" + + from google.cloud.spanner_admin_database_v1.types import backup as backup_pb + + from google.cloud.spanner_admin_database_v1 import CreateBackupEncryptionConfig + + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + database = instance.database(database_id) + + # Create a backup + expire_time = datetime.utcnow() + timedelta(days=14) + encryption_config = { + "encryption_type": CreateBackupEncryptionConfig.EncryptionType.CUSTOMER_MANAGED_ENCRYPTION, + "kms_key_name": kms_key_name, + } + request = backup_pb.CreateBackupRequest( + parent=instance.name, + backup_id=backup_id, + backup=backup_pb.Backup( + database=database.name, + expire_time=expire_time, + ), + encryption_config=encryption_config, + ) + operation = spanner_client.database_admin_api.create_backup(request) + + # Wait for backup operation to complete. + backup = operation.result(2100) + + # Verify that the backup is ready. + assert backup.state == backup_pb.Backup.State.READY + + # Get the name, create time, backup size and encryption key. + print( + "Backup {} of size {} bytes was created at {} using encryption key {}".format( + backup.name, backup.size_bytes, backup.create_time, kms_key_name + ) + ) + + +# [END spanner_create_backup_with_encryption_key] + + +# [START spanner_restore_backup] +def restore_database(instance_id, new_database_id, backup_id): + """Restores a database from a backup.""" + from google.cloud.spanner_admin_database_v1 import RestoreDatabaseRequest + + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + + # Start restoring an existing backup to a new database. + request = RestoreDatabaseRequest( + parent=instance.name, + database_id=new_database_id, + backup="{}/backups/{}".format(instance.name, backup_id), + ) + operation = spanner_client.database_admin_api.restore_database(request) + + # Wait for restore operation to complete. + db = operation.result(1600) + + # Newly created database has restore information. + restore_info = db.restore_info + print( + "Database {} restored to {} from backup {} with version time {}.".format( + restore_info.backup_info.source_database, + new_database_id, + restore_info.backup_info.backup, + restore_info.backup_info.version_time, + ) + ) + + +# [END spanner_restore_backup] + + +# [START spanner_restore_backup_with_encryption_key] +def restore_database_with_encryption_key( + instance_id, new_database_id, backup_id, kms_key_name +): + """Restores a database from a backup using a Customer Managed Encryption Key (CMEK).""" + from google.cloud.spanner_admin_database_v1 import ( + RestoreDatabaseEncryptionConfig, + RestoreDatabaseRequest, + ) + + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + + # Start restoring an existing backup to a new database. + encryption_config = { + "encryption_type": RestoreDatabaseEncryptionConfig.EncryptionType.CUSTOMER_MANAGED_ENCRYPTION, + "kms_key_name": kms_key_name, + } + + request = RestoreDatabaseRequest( + parent=instance.name, + database_id=new_database_id, + backup="{}/backups/{}".format(instance.name, backup_id), + encryption_config=encryption_config, + ) + operation = spanner_client.database_admin_api.restore_database(request) + + # Wait for restore operation to complete. + db = operation.result(1600) + + # Newly created database has restore information. + restore_info = db.restore_info + print( + "Database {} restored to {} from backup {} with using encryption key {}.".format( + restore_info.backup_info.source_database, + new_database_id, + restore_info.backup_info.backup, + db.encryption_config.kms_key_name, + ) + ) + + +# [END spanner_restore_backup_with_encryption_key] + + +# [START spanner_cancel_backup_create] +def cancel_backup(instance_id, database_id, backup_id): + from google.cloud.spanner_admin_database_v1.types import backup as backup_pb + + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + database = instance.database(database_id) + + expire_time = datetime.utcnow() + timedelta(days=30) + + # Create a backup. + request = backup_pb.CreateBackupRequest( + parent=instance.name, + backup_id=backup_id, + backup=backup_pb.Backup( + database=database.name, + expire_time=expire_time, + ), + ) + + operation = spanner_client.database_admin_api.create_backup(request) + # Cancel backup creation. + operation.cancel() + + # Cancel operations are best effort so either it will complete or + # be cancelled. + while not operation.done(): + time.sleep(300) # 5 mins + + try: + spanner_client.database_admin_api.get_backup( + backup_pb.GetBackupRequest( + name="{}/backups/{}".format(instance.name, backup_id) + ) + ) + except NotFound: + print("Backup creation was successfully cancelled.") + return + print("Backup was created before the cancel completed.") + spanner_client.database_admin_api.delete_backup( + backup_pb.DeleteBackupRequest( + name="{}/backups/{}".format(instance.name, backup_id) + ) + ) + print("Backup deleted.") + + +# [END spanner_cancel_backup_create] + + +# [START spanner_list_backup_operations] +def list_backup_operations(instance_id, database_id, backup_id): + from google.cloud.spanner_admin_database_v1.types import backup as backup_pb + + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + + # List the CreateBackup operations. + filter_ = ( + "(metadata.@type:type.googleapis.com/" + "google.spanner.admin.database.v1.CreateBackupMetadata) " + "AND (metadata.database:{})" + ).format(database_id) + request = backup_pb.ListBackupOperationsRequest( + parent=instance.name, filter=filter_ + ) + operations = spanner_client.database_admin_api.list_backup_operations(request) + for op in operations: + metadata = protobuf_helpers.from_any_pb( + backup_pb.CreateBackupMetadata, op.metadata + ) + print( + "Backup {} on database {}: {}% complete.".format( + metadata.name, metadata.database, metadata.progress.progress_percent + ) + ) + + # List the CopyBackup operations. + filter_ = ( + "(metadata.@type:type.googleapis.com/google.spanner.admin.database.v1.CopyBackupMetadata) " + "AND (metadata.source_backup:{})" + ).format(backup_id) + request = backup_pb.ListBackupOperationsRequest( + parent=instance.name, filter=filter_ + ) + operations = spanner_client.database_admin_api.list_backup_operations(request) + for op in operations: + metadata = protobuf_helpers.from_any_pb( + backup_pb.CopyBackupMetadata, op.metadata + ) + print( + "Backup {} on source backup {}: {}% complete.".format( + metadata.name, + metadata.source_backup, + metadata.progress.progress_percent, + ) + ) + + +# [END spanner_list_backup_operations] + + +# [START spanner_list_database_operations] +def list_database_operations(instance_id): + from google.cloud.spanner_admin_database_v1.types import spanner_database_admin + + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + + # List the progress of restore. + filter_ = ( + "(metadata.@type:type.googleapis.com/" + "google.spanner.admin.database.v1.OptimizeRestoredDatabaseMetadata)" + ) + request = spanner_database_admin.ListDatabaseOperationsRequest( + parent=instance.name, filter=filter_ + ) + operations = spanner_client.database_admin_api.list_database_operations(request) + for op in operations: + metadata = protobuf_helpers.from_any_pb( + spanner_database_admin.OptimizeRestoredDatabaseMetadata, op.metadata + ) + print( + "Database {} restored from backup is {}% optimized.".format( + metadata.name, metadata.progress.progress_percent + ) + ) + + +# [END spanner_list_database_operations] + + +# [START spanner_list_backups] +def list_backups(instance_id, database_id, backup_id): + from google.cloud.spanner_admin_database_v1.types import backup as backup_pb + + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + + # List all backups. + print("All backups:") + request = backup_pb.ListBackupsRequest(parent=instance.name, filter="") + operations = spanner_client.database_admin_api.list_backups(request) + for backup in operations: + print(backup.name) + + # List all backups that contain a name. + print('All backups with backup name containing "{}":'.format(backup_id)) + request = backup_pb.ListBackupsRequest( + parent=instance.name, filter="name:{}".format(backup_id) + ) + operations = spanner_client.database_admin_api.list_backups(request) + for backup in operations: + print(backup.name) + + # List all backups for a database that contains a name. + print('All backups with database name containing "{}":'.format(database_id)) + request = backup_pb.ListBackupsRequest( + parent=instance.name, filter="database:{}".format(database_id) + ) + operations = spanner_client.database_admin_api.list_backups(request) + for backup in operations: + print(backup.name) + + # List all backups that expire before a timestamp. + expire_time = datetime.utcnow().replace(microsecond=0) + timedelta(days=30) + print( + 'All backups with expire_time before "{}-{}-{}T{}:{}:{}Z":'.format( + *expire_time.timetuple() + ) + ) + request = backup_pb.ListBackupsRequest( + parent=instance.name, + filter='expire_time < "{}-{}-{}T{}:{}:{}Z"'.format(*expire_time.timetuple()), + ) + operations = spanner_client.database_admin_api.list_backups(request) + for backup in operations: + print(backup.name) + + # List all backups with a size greater than some bytes. + print("All backups with backup size more than 100 bytes:") + request = backup_pb.ListBackupsRequest( + parent=instance.name, filter="size_bytes > 100" + ) + operations = spanner_client.database_admin_api.list_backups(request) + for backup in operations: + print(backup.name) + + # List backups that were created after a timestamp that are also ready. + create_time = datetime.utcnow().replace(microsecond=0) - timedelta(days=1) + print( + 'All backups created after "{}-{}-{}T{}:{}:{}Z" and are READY:'.format( + *create_time.timetuple() + ) + ) + request = backup_pb.ListBackupsRequest( + parent=instance.name, + filter='create_time >= "{}-{}-{}T{}:{}:{}Z" AND state:READY'.format( + *create_time.timetuple() + ), + ) + operations = spanner_client.database_admin_api.list_backups(request) + for backup in operations: + print(backup.name) + + print("All backups with pagination") + # If there are multiple pages, additional ``ListBackup`` + # requests will be made as needed while iterating. + paged_backups = set() + request = backup_pb.ListBackupsRequest(parent=instance.name, page_size=2) + operations = spanner_client.database_admin_api.list_backups(request) + for backup in operations: + paged_backups.add(backup.name) + for backup in paged_backups: + print(backup) + + +# [END spanner_list_backups] + + +# [START spanner_delete_backup] +def delete_backup(instance_id, backup_id): + from google.cloud.spanner_admin_database_v1.types import backup as backup_pb + + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + backup = spanner_client.database_admin_api.get_backup( + backup_pb.GetBackupRequest( + name="{}/backups/{}".format(instance.name, backup_id) + ) + ) + + # Wait for databases that reference this backup to finish optimizing. + while backup.referencing_databases: + time.sleep(30) + backup = spanner_client.database_admin_api.get_backup( + backup_pb.GetBackupRequest( + name="{}/backups/{}".format(instance.name, backup_id) + ) + ) + + # Delete the backup. + spanner_client.database_admin_api.delete_backup( + backup_pb.DeleteBackupRequest(name=backup.name) + ) + + # Verify that the backup is deleted. + try: + backup = spanner_client.database_admin_api.get_backup( + backup_pb.GetBackupRequest(name=backup.name) + ) + except NotFound: + print("Backup {} has been deleted.".format(backup.name)) + return + + +# [END spanner_delete_backup] + + +# [START spanner_update_backup] +def update_backup(instance_id, backup_id): + from google.cloud.spanner_admin_database_v1.types import backup as backup_pb + + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + + backup = spanner_client.database_admin_api.get_backup( + backup_pb.GetBackupRequest( + name="{}/backups/{}".format(instance.name, backup_id) + ) + ) + + # Expire time must be within 366 days of the create time of the backup. + old_expire_time = backup.expire_time + # New expire time should be less than the max expire time + new_expire_time = min(backup.max_expire_time, old_expire_time + timedelta(days=30)) + spanner_client.database_admin_api.update_backup( + backup_pb.UpdateBackupRequest( + backup=backup_pb.Backup(name=backup.name, expire_time=new_expire_time), + update_mask={"paths": ["expire_time"]}, + ) + ) + print( + "Backup {} expire time was updated from {} to {}.".format( + backup.name, old_expire_time, new_expire_time + ) + ) + + +# [END spanner_update_backup] + + +# [START spanner_create_database_with_version_retention_period] +def create_database_with_version_retention_period( + instance_id, database_id, retention_period +): + """Creates a database with a version retention period.""" + + from google.cloud.spanner_admin_database_v1.types import spanner_database_admin + + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + ddl_statements = [ + "CREATE TABLE Singers (" + + " SingerId INT64 NOT NULL," + + " FirstName STRING(1024)," + + " LastName STRING(1024)," + + " SingerInfo BYTES(MAX)" + + ") PRIMARY KEY (SingerId)", + "CREATE TABLE Albums (" + + " SingerId INT64 NOT NULL," + + " AlbumId INT64 NOT NULL," + + " AlbumTitle STRING(MAX)" + + ") PRIMARY KEY (SingerId, AlbumId)," + + " INTERLEAVE IN PARENT Singers ON DELETE CASCADE", + "ALTER DATABASE `{}`" + " SET OPTIONS (version_retention_period = '{}')".format( + database_id, retention_period + ), + ] + operation = spanner_client.database_admin_api.create_database( + request=spanner_database_admin.CreateDatabaseRequest( + parent=instance.name, + create_statement="CREATE DATABASE `{}`".format(database_id), + extra_statements=ddl_statements, + ) + ) + + db = operation.result(30) + print( + "Database {} created with version retention period {} and earliest version time {}".format( + db.name, db.version_retention_period, db.earliest_version_time + ) + ) + + spanner_client.database_admin_api.drop_database( + spanner_database_admin.DropDatabaseRequest(database=db.name) + ) + + +# [END spanner_create_database_with_version_retention_period] + + +# [START spanner_copy_backup] +def copy_backup(instance_id, backup_id, source_backup_path): + """Copies a backup.""" + + from google.cloud.spanner_admin_database_v1.types import backup as backup_pb + + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + + # Create a backup object and wait for copy backup operation to complete. + expire_time = datetime.utcnow() + timedelta(days=14) + request = backup_pb.CopyBackupRequest( + parent=instance.name, + backup_id=backup_id, + source_backup=source_backup_path, + expire_time=expire_time, + ) + + operation = spanner_client.database_admin_api.copy_backup(request) + + # Wait for backup operation to complete. + copy_backup = operation.result(2100) + + # Verify that the copy backup is ready. + assert copy_backup.state == backup_pb.Backup.State.READY + + print( + "Backup {} of size {} bytes was created at {} with version time {}".format( + copy_backup.name, + copy_backup.size_bytes, + copy_backup.create_time, + copy_backup.version_time, + ) + ) + + +# [END spanner_copy_backup] diff --git a/samples/samples/admin/backup_snippet_test.py b/samples/samples/admin/backup_snippet_test.py new file mode 100644 index 0000000000..8fc29b9425 --- /dev/null +++ b/samples/samples/admin/backup_snippet_test.py @@ -0,0 +1,192 @@ +# Copyright 2024 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import uuid + +import backup_snippet +import pytest +from google.api_core.exceptions import DeadlineExceeded +from test_utils.retry import RetryErrors + + +@pytest.fixture(scope="module") +def sample_name(): + return "backup" + + +def unique_database_id(): + """Creates a unique id for the database.""" + return f"test-db-{uuid.uuid4().hex[:10]}" + + +def unique_backup_id(): + """Creates a unique id for the backup.""" + return f"test-backup-{uuid.uuid4().hex[:10]}" + + +RESTORE_DB_ID = unique_database_id() +BACKUP_ID = unique_backup_id() +CMEK_RESTORE_DB_ID = unique_database_id() +CMEK_BACKUP_ID = unique_backup_id() +RETENTION_DATABASE_ID = unique_database_id() +RETENTION_PERIOD = "7d" +COPY_BACKUP_ID = unique_backup_id() + + +@pytest.mark.dependency(name="create_backup") +def test_create_backup(capsys, instance_id, sample_database): + with sample_database.snapshot() as snapshot: + results = snapshot.execute_sql("SELECT CURRENT_TIMESTAMP()") + version_time = list(results)[0][0] + + backup_snippet.create_backup( + instance_id, + sample_database.database_id, + BACKUP_ID, + version_time, + ) + out, _ = capsys.readouterr() + assert BACKUP_ID in out + + +@pytest.mark.dependency(name="copy_backup", depends=["create_backup"]) +def test_copy_backup(capsys, instance_id, spanner_client): + source_backp_path = ( + spanner_client.project_name + + "/instances/" + + instance_id + + "/backups/" + + BACKUP_ID + ) + backup_snippet.copy_backup(instance_id, COPY_BACKUP_ID, source_backp_path) + out, _ = capsys.readouterr() + assert COPY_BACKUP_ID in out + + +@pytest.mark.dependency(name="create_backup_with_encryption_key") +def test_create_backup_with_encryption_key( + capsys, + instance_id, + sample_database, + kms_key_name, +): + backup_snippet.create_backup_with_encryption_key( + instance_id, + sample_database.database_id, + CMEK_BACKUP_ID, + kms_key_name, + ) + out, _ = capsys.readouterr() + assert CMEK_BACKUP_ID in out + assert kms_key_name in out + + +@pytest.mark.dependency(depends=["create_backup"]) +@RetryErrors(exception=DeadlineExceeded, max_tries=2) +def test_restore_database(capsys, instance_id, sample_database): + backup_snippet.restore_database(instance_id, RESTORE_DB_ID, BACKUP_ID) + out, _ = capsys.readouterr() + assert (sample_database.database_id + " restored to ") in out + assert (RESTORE_DB_ID + " from backup ") in out + assert BACKUP_ID in out + + +@pytest.mark.dependency(depends=["create_backup_with_encryption_key"]) +@RetryErrors(exception=DeadlineExceeded, max_tries=2) +def test_restore_database_with_encryption_key( + capsys, + instance_id, + sample_database, + kms_key_name, +): + backup_snippet.restore_database_with_encryption_key( + instance_id, CMEK_RESTORE_DB_ID, CMEK_BACKUP_ID, kms_key_name + ) + out, _ = capsys.readouterr() + assert (sample_database.database_id + " restored to ") in out + assert (CMEK_RESTORE_DB_ID + " from backup ") in out + assert CMEK_BACKUP_ID in out + assert kms_key_name in out + + +@pytest.mark.dependency(depends=["create_backup", "copy_backup"]) +def test_list_backup_operations(capsys, instance_id, sample_database): + backup_snippet.list_backup_operations( + instance_id, sample_database.database_id, BACKUP_ID + ) + out, _ = capsys.readouterr() + assert BACKUP_ID in out + assert sample_database.database_id in out + assert COPY_BACKUP_ID in out + print(out) + + +@pytest.mark.dependency(name="list_backup", depends=["create_backup", "copy_backup"]) +def test_list_backups( + capsys, + instance_id, + sample_database, +): + backup_snippet.list_backups( + instance_id, + sample_database.database_id, + BACKUP_ID, + ) + out, _ = capsys.readouterr() + id_count = out.count(BACKUP_ID) + assert id_count == 7 + + +@pytest.mark.dependency(depends=["create_backup"]) +def test_update_backup(capsys, instance_id): + backup_snippet.update_backup(instance_id, BACKUP_ID) + out, _ = capsys.readouterr() + assert BACKUP_ID in out + + +@pytest.mark.dependency(depends=["create_backup", "copy_backup", "list_backup"]) +def test_delete_backup(capsys, instance_id): + backup_snippet.delete_backup(instance_id, BACKUP_ID) + out, _ = capsys.readouterr() + assert BACKUP_ID in out + backup_snippet.delete_backup(instance_id, COPY_BACKUP_ID) + out, _ = capsys.readouterr() + assert "has been deleted." in out + assert COPY_BACKUP_ID in out + + +@pytest.mark.dependency(depends=["create_backup"]) +def test_cancel_backup(capsys, instance_id, sample_database): + backup_snippet.cancel_backup( + instance_id, + sample_database.database_id, + BACKUP_ID, + ) + out, _ = capsys.readouterr() + cancel_success = "Backup creation was successfully cancelled." in out + cancel_failure = ("Backup was created before the cancel completed." in out) and ( + "Backup deleted." in out + ) + assert cancel_success or cancel_failure + + +@RetryErrors(exception=DeadlineExceeded, max_tries=2) +def test_create_database_with_retention_period(capsys, sample_instance): + backup_snippet.create_database_with_version_retention_period( + sample_instance.instance_id, + RETENTION_DATABASE_ID, + RETENTION_PERIOD, + ) + out, _ = capsys.readouterr() + assert (RETENTION_DATABASE_ID + " created with ") in out + assert ("retention period " + RETENTION_PERIOD) in out diff --git a/samples/samples/admin/samples.py b/samples/samples/admin/samples.py index 7a7afac93c..09d6bfae33 100644 --- a/samples/samples/admin/samples.py +++ b/samples/samples/admin/samples.py @@ -22,8 +22,8 @@ import time from google.cloud import spanner -from google.cloud.spanner_admin_instance_v1.types import spanner_instance_admin from google.cloud.spanner_admin_database_v1.types import spanner_database_admin +from google.cloud.spanner_admin_instance_v1.types import spanner_instance_admin OPERATION_TIMEOUT_SECONDS = 240 diff --git a/samples/samples/admin/samples_test.py b/samples/samples/admin/samples_test.py index 1fe8e0bd17..a83c42f8d9 100644 --- a/samples/samples/admin/samples_test.py +++ b/samples/samples/admin/samples_test.py @@ -21,9 +21,9 @@ import uuid +import pytest from google.api_core import exceptions from google.cloud.spanner_admin_database_v1.types.common import DatabaseDialect -import pytest from test_utils.retry import RetryErrors import samples From c25376c8513af293c9db752ffc1970dbfca1c5b8 Mon Sep 17 00:00:00 2001 From: rahul2393 Date: Mon, 26 Feb 2024 12:26:48 +0530 Subject: [PATCH 12/17] docs: samples and tests for admin database APIs (#1099) * docs: samples and tests for admin database APIs * rebase branch with main * remove backup samples * add more database samples * remove unused import * add more samples * incorporate suggestions * fix tests * incorporate suggestions * fix tests * remove parallel run --------- Co-authored-by: Sri Harsha CH <57220027+harshachinta@users.noreply.github.com> --- samples/samples/admin/pg_samples.py | 351 ++++++++++ samples/samples/admin/pg_samples_test.py | 178 +++++ samples/samples/admin/samples.py | 818 ++++++++++++++++++++++- samples/samples/admin/samples_test.py | 212 ++++++ 4 files changed, 1552 insertions(+), 7 deletions(-) create mode 100644 samples/samples/admin/pg_samples.py create mode 100644 samples/samples/admin/pg_samples_test.py diff --git a/samples/samples/admin/pg_samples.py b/samples/samples/admin/pg_samples.py new file mode 100644 index 0000000000..4da2cafc33 --- /dev/null +++ b/samples/samples/admin/pg_samples.py @@ -0,0 +1,351 @@ +#!/usr/bin/env python + +# Copyright 2024 Google, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""This application demonstrates how to do basic operations using Cloud +Spanner PostgreSql dialect. +For more information, see the README.rst under /spanner. +""" +from google.cloud import spanner +from google.cloud.spanner_admin_database_v1.types.common import DatabaseDialect + +OPERATION_TIMEOUT_SECONDS = 240 + + +# [START spanner_postgresql_create_database] +def create_database(instance_id, database_id): + """Creates a PostgreSql database and tables for sample data.""" + + from google.cloud.spanner_admin_database_v1.types import spanner_database_admin + + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + + request = spanner_database_admin.CreateDatabaseRequest( + parent=instance.name, + create_statement=f'CREATE DATABASE "{database_id}"', + database_dialect=DatabaseDialect.POSTGRESQL, + ) + + operation = spanner_client.database_admin_api.create_database(request=request) + + print("Waiting for operation to complete...") + database = operation.result(OPERATION_TIMEOUT_SECONDS) + + create_table_using_ddl(database.name) + print("Created database {} on instance {}".format(database_id, instance_id)) + + +def create_table_using_ddl(database_name): + from google.cloud.spanner_admin_database_v1.types import spanner_database_admin + + spanner_client = spanner.Client() + request = spanner_database_admin.UpdateDatabaseDdlRequest( + database=database_name, + statements=[ + """CREATE TABLE Singers ( + SingerId bigint NOT NULL, + FirstName character varying(1024), + LastName character varying(1024), + SingerInfo bytea, + FullName character varying(2048) + GENERATED ALWAYS AS (FirstName || ' ' || LastName) STORED, + PRIMARY KEY (SingerId) + )""", + """CREATE TABLE Albums ( + SingerId bigint NOT NULL, + AlbumId bigint NOT NULL, + AlbumTitle character varying(1024), + PRIMARY KEY (SingerId, AlbumId) + ) INTERLEAVE IN PARENT Singers ON DELETE CASCADE""", + ], + ) + operation = spanner_client.database_admin_api.update_database_ddl(request) + operation.result(OPERATION_TIMEOUT_SECONDS) + + +# [END spanner_postgresql_create_database] + + +def create_table_with_datatypes(instance_id, database_id): + """Creates a table with supported datatypes.""" + # [START spanner_postgresql_create_table_with_datatypes] + # instance_id = "your-spanner-instance" + # database_id = "your-spanner-db-id" + + from google.cloud.spanner_admin_database_v1.types import spanner_database_admin + + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + database = instance.database(database_id) + + request = spanner_database_admin.UpdateDatabaseDdlRequest( + database=database.name, + statements=[ + """CREATE TABLE Venues ( + VenueId BIGINT NOT NULL, + VenueName character varying(100), + VenueInfo BYTEA, + Capacity BIGINT, + OutdoorVenue BOOL, + PopularityScore FLOAT8, + Revenue NUMERIC, + LastUpdateTime SPANNER.COMMIT_TIMESTAMP NOT NULL, + PRIMARY KEY (VenueId))""" + ], + ) + operation = spanner_client.database_admin_api.update_database_ddl(request) + + print("Waiting for operation to complete...") + operation.result(OPERATION_TIMEOUT_SECONDS) + + print( + "Created Venues table on database {} on instance {}".format( + database_id, instance_id + ) + ) + # [END spanner_postgresql_create_table_with_datatypes] + + +# [START spanner_postgresql_add_column] +def add_column(instance_id, database_id): + """Adds a new column to the Albums table in the example database.""" + + from google.cloud.spanner_admin_database_v1.types import spanner_database_admin + + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + database = instance.database(database_id) + + request = spanner_database_admin.UpdateDatabaseDdlRequest( + database=database.name, + statements=["ALTER TABLE Albums ADD COLUMN MarketingBudget BIGINT"], + ) + operation = spanner_client.database_admin_api.update_database_ddl(request) + + print("Waiting for operation to complete...") + operation.result(OPERATION_TIMEOUT_SECONDS) + + print("Added the MarketingBudget column.") + + +# [END spanner_postgresql_add_column] + + +# [START spanner_postgresql_jsonb_add_column] +def add_jsonb_column(instance_id, database_id): + """ + Alters Venues tables in the database adding a JSONB column. + You can create the table by running the `create_table_with_datatypes` + sample or by running this DDL statement against your database: + CREATE TABLE Venues ( + VenueId BIGINT NOT NULL, + VenueName character varying(100), + VenueInfo BYTEA, + Capacity BIGINT, + OutdoorVenue BOOL, + PopularityScore FLOAT8, + Revenue NUMERIC, + LastUpdateTime SPANNER.COMMIT_TIMESTAMP NOT NULL, + PRIMARY KEY (VenueId)) + """ + # instance_id = "your-spanner-instance" + # database_id = "your-spanner-db-id" + + from google.cloud.spanner_admin_database_v1.types import spanner_database_admin + + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + database = instance.database(database_id) + + request = spanner_database_admin.UpdateDatabaseDdlRequest( + database=database.name, + statements=["ALTER TABLE Venues ADD COLUMN VenueDetails JSONB"], + ) + + operation = spanner_client.database_admin_api.update_database_ddl(request) + + print("Waiting for operation to complete...") + operation.result(OPERATION_TIMEOUT_SECONDS) + + print( + 'Altered table "Venues" on database {} on instance {}.'.format( + database_id, instance_id + ) + ) + + +# [END spanner_postgresql_jsonb_add_column] + + +# [START spanner_postgresql_create_storing_index] +def add_storing_index(instance_id, database_id): + """Adds an storing index to the example database.""" + + from google.cloud.spanner_admin_database_v1.types import spanner_database_admin + + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + database = instance.database(database_id) + + request = spanner_database_admin.UpdateDatabaseDdlRequest( + database=database.name, + statements=[ + "CREATE INDEX AlbumsByAlbumTitle2 ON Albums(AlbumTitle)" + "INCLUDE (MarketingBudget)" + ], + ) + + operation = spanner_client.database_admin_api.update_database_ddl(request) + + print("Waiting for operation to complete...") + operation.result(OPERATION_TIMEOUT_SECONDS) + + print("Added the AlbumsByAlbumTitle2 index.") + + +# [END spanner_postgresql_create_storing_index] + + +# [START spanner_postgresql_create_sequence] +def create_sequence(instance_id, database_id): + """Creates the Sequence and insert data""" + + from google.cloud.spanner_admin_database_v1.types import spanner_database_admin + + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + database = instance.database(database_id) + + request = spanner_database_admin.UpdateDatabaseDdlRequest( + database=database.name, + statements=[ + "CREATE SEQUENCE Seq BIT_REVERSED_POSITIVE", + """CREATE TABLE Customers ( + CustomerId BIGINT DEFAULT nextval('Seq'), + CustomerName character varying(1024), + PRIMARY KEY (CustomerId) + )""", + ], + ) + operation = spanner_client.database_admin_api.update_database_ddl(request) + print("Waiting for operation to complete...") + operation.result(OPERATION_TIMEOUT_SECONDS) + + print( + "Created Seq sequence and Customers table, where the key column CustomerId uses the sequence as a default value on database {} on instance {}".format( + database_id, instance_id + ) + ) + + def insert_customers(transaction): + results = transaction.execute_sql( + "INSERT INTO Customers (CustomerName) VALUES " + "('Alice'), " + "('David'), " + "('Marc') " + "RETURNING CustomerId" + ) + for result in results: + print("Inserted customer record with Customer Id: {}".format(*result)) + print( + "Number of customer records inserted is {}".format( + results.stats.row_count_exact + ) + ) + + database.run_in_transaction(insert_customers) + + +# [END spanner_postgresql_create_sequence] + + +# [START spanner_postgresql_alter_sequence] +def alter_sequence(instance_id, database_id): + """Alters the Sequence and insert data""" + + from google.cloud.spanner_admin_database_v1.types import spanner_database_admin + + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + database = instance.database(database_id) + + request = spanner_database_admin.UpdateDatabaseDdlRequest( + database=database.name, + statements=["ALTER SEQUENCE Seq SKIP RANGE 1000 5000000"], + ) + operation = spanner_client.database_admin_api.update_database_ddl(request) + + print("Waiting for operation to complete...") + operation.result(OPERATION_TIMEOUT_SECONDS) + + print( + "Altered Seq sequence to skip an inclusive range between 1000 and 5000000 on database {} on instance {}".format( + database_id, instance_id + ) + ) + + def insert_customers(transaction): + results = transaction.execute_sql( + "INSERT INTO Customers (CustomerName) VALUES " + "('Lea'), " + "('Cataline'), " + "('Smith') " + "RETURNING CustomerId" + ) + for result in results: + print("Inserted customer record with Customer Id: {}".format(*result)) + print( + "Number of customer records inserted is {}".format( + results.stats.row_count_exact + ) + ) + + database.run_in_transaction(insert_customers) + + +# [END spanner_postgresql_alter_sequence] + + +# [START spanner_postgresql_drop_sequence] +def drop_sequence(instance_id, database_id): + """Drops the Sequence""" + + from google.cloud.spanner_admin_database_v1.types import spanner_database_admin + + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + database = instance.database(database_id) + + request = spanner_database_admin.UpdateDatabaseDdlRequest( + database=database.name, + statements=[ + "ALTER TABLE Customers ALTER COLUMN CustomerId DROP DEFAULT", + "DROP SEQUENCE Seq", + ], + ) + operation = spanner_client.database_admin_api.update_database_ddl(request) + + print("Waiting for operation to complete...") + operation.result(OPERATION_TIMEOUT_SECONDS) + + print( + "Altered Customers table to drop DEFAULT from CustomerId column and dropped the Seq sequence on database {} on instance {}".format( + database_id, instance_id + ) + ) + + +# [END spanner_postgresql_drop_sequence] diff --git a/samples/samples/admin/pg_samples_test.py b/samples/samples/admin/pg_samples_test.py new file mode 100644 index 0000000000..3863f5aa56 --- /dev/null +++ b/samples/samples/admin/pg_samples_test.py @@ -0,0 +1,178 @@ +# Copyright 2024 Google, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import uuid + +import pg_samples as samples +import pytest +from google.api_core import exceptions +from google.cloud.spanner_admin_database_v1.types.common import DatabaseDialect +from test_utils.retry import RetryErrors + +CREATE_TABLE_SINGERS = """\ +CREATE TABLE Singers ( + SingerId BIGINT NOT NULL, + FirstName CHARACTER VARYING(1024), + LastName CHARACTER VARYING(1024), + SingerInfo BYTEA, + FullName CHARACTER VARYING(2048) + GENERATED ALWAYS AS (FirstName || ' ' || LastName) STORED, + PRIMARY KEY (SingerId) +) +""" + +CREATE_TABLE_ALBUMS = """\ +CREATE TABLE Albums ( + SingerId BIGINT NOT NULL, + AlbumId BIGINT NOT NULL, + AlbumTitle CHARACTER VARYING(1024), + PRIMARY KEY (SingerId, AlbumId) + ) INTERLEAVE IN PARENT Singers ON DELETE CASCADE +""" + +retry_429 = RetryErrors(exceptions.ResourceExhausted, delay=15) + + +@pytest.fixture(scope="module") +def sample_name(): + return "pg_snippets" + + +@pytest.fixture(scope="module") +def database_dialect(): + """Spanner dialect to be used for this sample. + The dialect is used to initialize the dialect for the database. + It can either be GoogleStandardSql or PostgreSql. + """ + return DatabaseDialect.POSTGRESQL + + +@pytest.fixture(scope="module") +def create_instance_id(): + """Id for the low-cost instance.""" + return f"create-instance-{uuid.uuid4().hex[:10]}" + + +@pytest.fixture(scope="module") +def lci_instance_id(): + """Id for the low-cost instance.""" + return f"lci-instance-{uuid.uuid4().hex[:10]}" + + +@pytest.fixture(scope="module") +def database_id(): + return f"test-db-{uuid.uuid4().hex[:10]}" + + +@pytest.fixture(scope="module") +def create_database_id(): + return f"create-db-{uuid.uuid4().hex[:10]}" + + +@pytest.fixture(scope="module") +def cmek_database_id(): + return f"cmek-db-{uuid.uuid4().hex[:10]}" + + +@pytest.fixture(scope="module") +def default_leader_database_id(): + return f"leader_db_{uuid.uuid4().hex[:10]}" + + +@pytest.fixture(scope="module") +def database_ddl(): + """Sequence of DDL statements used to set up the database. + Sample testcase modules can override as needed. + """ + return [CREATE_TABLE_SINGERS, CREATE_TABLE_ALBUMS] + + +@pytest.fixture(scope="module") +def default_leader(): + """Default leader for multi-region instances.""" + return "us-east4" + + +@pytest.mark.dependency(name="create_database") +def test_create_database_explicit(sample_instance, create_database_id): + # Rather than re-use 'sample_database', we create a new database, to + # ensure that the 'create_database' snippet is tested. + samples.create_database(sample_instance.instance_id, create_database_id) + database = sample_instance.database(create_database_id) + database.drop() + + +@pytest.mark.dependency(name="create_table_with_datatypes") +def test_create_table_with_datatypes(capsys, instance_id, sample_database): + samples.create_table_with_datatypes(instance_id, sample_database.database_id) + out, _ = capsys.readouterr() + assert "Created Venues table on database" in out + + +@pytest.mark.dependency(name="add_column", depends=["create_database"]) +def test_add_column(capsys, instance_id, sample_database): + samples.add_column(instance_id, sample_database.database_id) + out, _ = capsys.readouterr() + assert "Added the MarketingBudget column." in out + + +@pytest.mark.dependency(name="add_storing_index", depends=["create_database"]) +def test_add_storing_index(capsys, instance_id, sample_database): + samples.add_storing_index(instance_id, sample_database.database_id) + out, _ = capsys.readouterr() + assert "Added the AlbumsByAlbumTitle2 index." in out + + +@pytest.mark.dependency( + name="add_jsonb_column", depends=["create_table_with_datatypes"] +) +def test_add_jsonb_column(capsys, instance_id, sample_database): + samples.add_jsonb_column(instance_id, sample_database.database_id) + out, _ = capsys.readouterr() + assert "Waiting for operation to complete..." in out + assert 'Altered table "Venues" on database ' in out + + +@pytest.mark.dependency(name="create_sequence") +def test_create_sequence(capsys, instance_id, bit_reverse_sequence_database): + samples.create_sequence(instance_id, bit_reverse_sequence_database.database_id) + out, _ = capsys.readouterr() + assert ( + "Created Seq sequence and Customers table, where the key column CustomerId uses the sequence as a default value on database" + in out + ) + assert "Number of customer records inserted is 3" in out + assert "Inserted customer record with Customer Id:" in out + + +@pytest.mark.dependency(name="alter_sequence", depends=["create_sequence"]) +def test_alter_sequence(capsys, instance_id, bit_reverse_sequence_database): + samples.alter_sequence(instance_id, bit_reverse_sequence_database.database_id) + out, _ = capsys.readouterr() + assert ( + "Altered Seq sequence to skip an inclusive range between 1000 and 5000000 on database" + in out + ) + assert "Number of customer records inserted is 3" in out + assert "Inserted customer record with Customer Id:" in out + + +@pytest.mark.dependency(depends=["alter_sequence"]) +def test_drop_sequence(capsys, instance_id, bit_reverse_sequence_database): + samples.drop_sequence(instance_id, bit_reverse_sequence_database.database_id) + out, _ = capsys.readouterr() + assert ( + "Altered Customers table to drop DEFAULT from CustomerId column and dropped the Seq sequence on database" + in out + ) diff --git a/samples/samples/admin/samples.py b/samples/samples/admin/samples.py index 09d6bfae33..a4119f602f 100644 --- a/samples/samples/admin/samples.py +++ b/samples/samples/admin/samples.py @@ -22,8 +22,6 @@ import time from google.cloud import spanner -from google.cloud.spanner_admin_database_v1.types import spanner_database_admin -from google.cloud.spanner_admin_instance_v1.types import spanner_instance_admin OPERATION_TIMEOUT_SECONDS = 240 @@ -31,6 +29,8 @@ # [START spanner_create_instance] def create_instance(instance_id): """Creates an instance.""" + from google.cloud.spanner_admin_instance_v1.types import spanner_instance_admin + spanner_client = spanner.Client() config_name = "{}/instanceConfigs/regional-us-central1".format( @@ -38,7 +38,7 @@ def create_instance(instance_id): ) operation = spanner_client.instance_admin_api.create_instance( - parent="projects/{}".format(spanner_client.project), + parent=spanner_client.project_name, instance_id=instance_id, instance=spanner_instance_admin.Instance( config=config_name, @@ -61,16 +61,128 @@ def create_instance(instance_id): # [END spanner_create_instance] +# [START spanner_create_instance_with_processing_units] +def create_instance_with_processing_units(instance_id, processing_units): + """Creates an instance.""" + from google.cloud.spanner_admin_instance_v1.types import spanner_instance_admin + + spanner_client = spanner.Client() + + config_name = "{}/instanceConfigs/regional-us-central1".format( + spanner_client.project_name + ) + + request = spanner_instance_admin.CreateInstanceRequest( + parent=spanner_client.project_name, + instance_id=instance_id, + instance=spanner_instance_admin.Instance( + config=config_name, + display_name="This is a display name.", + processing_units=processing_units, + labels={ + "cloud_spanner_samples": "true", + "sample_name": "snippets-create_instance_with_processing_units", + "created": str(int(time.time())), + }, + ), + ) + + operation = spanner_client.instance_admin_api.create_instance(request=request) + + print("Waiting for operation to complete...") + instance = operation.result(OPERATION_TIMEOUT_SECONDS) + + print( + "Created instance {} with {} processing units".format( + instance_id, instance.processing_units + ) + ) + + +# [END spanner_create_instance_with_processing_units] + + +# [START spanner_create_database] +def create_database(instance_id, database_id): + """Creates a database and tables for sample data.""" + from google.cloud.spanner_admin_database_v1.types import spanner_database_admin + + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + + request = spanner_database_admin.CreateDatabaseRequest( + parent=instance.name, + create_statement=f"CREATE DATABASE `{database_id}`", + extra_statements=[ + """CREATE TABLE Singers ( + SingerId INT64 NOT NULL, + FirstName STRING(1024), + LastName STRING(1024), + SingerInfo BYTES(MAX), + FullName STRING(2048) AS ( + ARRAY_TO_STRING([FirstName, LastName], " ") + ) STORED + ) PRIMARY KEY (SingerId)""", + """CREATE TABLE Albums ( + SingerId INT64 NOT NULL, + AlbumId INT64 NOT NULL, + AlbumTitle STRING(MAX) + ) PRIMARY KEY (SingerId, AlbumId), + INTERLEAVE IN PARENT Singers ON DELETE CASCADE""", + ], + ) + + operation = spanner_client.database_admin_api.create_database(request=request) + + print("Waiting for operation to complete...") + database = operation.result(OPERATION_TIMEOUT_SECONDS) + + print("Created database {} on instance {}".format(database.name, instance.name)) + + +# [START spanner_update_database] +def update_database(instance_id, database_id): + """Updates the drop protection setting for a database.""" + from google.cloud.spanner_admin_database_v1.types import spanner_database_admin + + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + + request = spanner_database_admin.UpdateDatabaseRequest( + database=spanner_database_admin.Database( + name="{}/databases/{}".format(instance.name, database_id), + enable_drop_protection=True, + ), + update_mask={"paths": ["enable_drop_protection"]}, + ) + operation = spanner_client.database_admin_api.update_database(request=request) + print( + "Waiting for update operation for {}/databases/{} to complete...".format( + instance.name, database_id + ) + ) + operation.result(OPERATION_TIMEOUT_SECONDS) + + print("Updated database {}/databases/{}.".format(instance.name, database_id)) + + +# [END spanner_update_database] + +# [END spanner_create_database] + + # [START spanner_create_database_with_default_leader] def create_database_with_default_leader(instance_id, database_id, default_leader): """Creates a database with tables with a default leader.""" + from google.cloud.spanner_admin_database_v1.types import spanner_database_admin + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + operation = spanner_client.database_admin_api.create_database( request=spanner_database_admin.CreateDatabaseRequest( - parent="projects/{}/instances/{}".format( - spanner_client.project, instance_id - ), - create_statement="CREATE DATABASE {}".format(database_id), + parent=instance.name, + create_statement=f"CREATE DATABASE `{database_id}`", extra_statements=[ """CREATE TABLE Singers ( SingerId INT64 NOT NULL, @@ -103,3 +215,695 @@ def create_database_with_default_leader(instance_id, database_id, default_leader # [END spanner_create_database_with_default_leader] + + +# [START spanner_update_database_with_default_leader] +def update_database_with_default_leader(instance_id, database_id, default_leader): + """Updates a database with tables with a default leader.""" + from google.cloud.spanner_admin_database_v1.types import spanner_database_admin + + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + database = instance.database(database_id) + + request = spanner_database_admin.UpdateDatabaseDdlRequest( + database=database.name, + statements=[ + "ALTER DATABASE {}" + " SET OPTIONS (default_leader = '{}')".format(database_id, default_leader) + ], + ) + operation = spanner_client.database_admin_api.update_database_ddl(request) + + operation.result(OPERATION_TIMEOUT_SECONDS) + + print( + "Database {} updated with default leader {}".format(database_id, default_leader) + ) + + +# [END spanner_update_database_with_default_leader] + + +# [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).""" + from google.cloud.spanner_admin_database_v1.types import spanner_database_admin + from google.cloud.spanner_admin_database_v1 import EncryptionConfig + + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + + request = spanner_database_admin.CreateDatabaseRequest( + parent=instance.name, + create_statement=f"CREATE DATABASE `{database_id}`", + extra_statements=[ + """CREATE TABLE Singers ( + SingerId INT64 NOT NULL, + FirstName STRING(1024), + LastName STRING(1024), + SingerInfo BYTES(MAX) + ) PRIMARY KEY (SingerId)""", + """CREATE TABLE Albums ( + SingerId INT64 NOT NULL, + AlbumId INT64 NOT NULL, + AlbumTitle STRING(MAX) + ) PRIMARY KEY (SingerId, AlbumId), + INTERLEAVE IN PARENT Singers ON DELETE CASCADE""", + ], + encryption_config=EncryptionConfig(kms_key_name=kms_key_name), + ) + + operation = spanner_client.database_admin_api.create_database(request=request) + + print("Waiting for operation to complete...") + database = operation.result(OPERATION_TIMEOUT_SECONDS) + + print( + "Database {} created with encryption key {}".format( + database.name, database.encryption_config.kms_key_name + ) + ) + + +# [END spanner_create_database_with_encryption_key] + + +def add_and_drop_database_roles(instance_id, database_id): + """Showcases how to manage a user defined database role.""" + # [START spanner_add_and_drop_database_role] + # instance_id = "your-spanner-instance" + # database_id = "your-spanner-db-id" + + from google.cloud.spanner_admin_database_v1.types import spanner_database_admin + + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + database = instance.database(database_id) + + role_parent = "new_parent" + role_child = "new_child" + + request = spanner_database_admin.UpdateDatabaseDdlRequest( + database=database.name, + statements=[ + "CREATE ROLE {}".format(role_parent), + "GRANT SELECT ON TABLE Singers TO ROLE {}".format(role_parent), + "CREATE ROLE {}".format(role_child), + "GRANT ROLE {} TO ROLE {}".format(role_parent, role_child), + ], + ) + operation = spanner_client.database_admin_api.update_database_ddl(request) + + operation.result(OPERATION_TIMEOUT_SECONDS) + print( + "Created roles {} and {} and granted privileges".format(role_parent, role_child) + ) + + request = spanner_database_admin.UpdateDatabaseDdlRequest( + database=database.name, + statements=[ + "REVOKE ROLE {} FROM ROLE {}".format(role_parent, role_child), + "DROP ROLE {}".format(role_child), + ], + ) + operation = spanner_client.database_admin_api.update_database_ddl(request) + + operation.result(OPERATION_TIMEOUT_SECONDS) + print("Revoked privileges and dropped role {}".format(role_child)) + + # [END spanner_add_and_drop_database_role] + + +def create_table_with_datatypes(instance_id, database_id): + """Creates a table with supported datatypes.""" + # [START spanner_create_table_with_datatypes] + # instance_id = "your-spanner-instance" + # database_id = "your-spanner-db-id" + + from google.cloud.spanner_admin_database_v1.types import spanner_database_admin + + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + database = instance.database(database_id) + + request = spanner_database_admin.UpdateDatabaseDdlRequest( + database=database.name, + statements=[ + """CREATE TABLE Venues ( + VenueId INT64 NOT NULL, + VenueName STRING(100), + VenueInfo BYTES(MAX), + Capacity INT64, + AvailableDates ARRAY, + LastContactDate DATE, + OutdoorVenue BOOL, + PopularityScore FLOAT64, + LastUpdateTime TIMESTAMP NOT NULL + OPTIONS(allow_commit_timestamp=true) + ) PRIMARY KEY (VenueId)""" + ], + ) + operation = spanner_client.database_admin_api.update_database_ddl(request) + + print("Waiting for operation to complete...") + operation.result(OPERATION_TIMEOUT_SECONDS) + + print( + "Created Venues table on database {} on instance {}".format( + database_id, instance_id + ) + ) + # [END spanner_create_table_with_datatypes] + + +# [START spanner_add_json_column] +def add_json_column(instance_id, database_id): + """Adds a new JSON column to the Venues table in the example database.""" + # instance_id = "your-spanner-instance" + # database_id = "your-spanner-db-id" + + from google.cloud.spanner_admin_database_v1.types import spanner_database_admin + + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + database = instance.database(database_id) + + request = spanner_database_admin.UpdateDatabaseDdlRequest( + database=database.name, + statements=["ALTER TABLE Venues ADD COLUMN VenueDetails JSON"], + ) + + operation = spanner_client.database_admin_api.update_database_ddl(request) + + print("Waiting for operation to complete...") + operation.result(OPERATION_TIMEOUT_SECONDS) + + print( + 'Altered table "Venues" on database {} on instance {}.'.format( + database_id, instance_id + ) + ) + + +# [END spanner_add_json_column] + + +# [START spanner_add_numeric_column] +def add_numeric_column(instance_id, database_id): + """Adds a new NUMERIC column to the Venues table in the example database.""" + + from google.cloud.spanner_admin_database_v1.types import spanner_database_admin + + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + database = instance.database(database_id) + + request = spanner_database_admin.UpdateDatabaseDdlRequest( + database=database.name, + statements=["ALTER TABLE Venues ADD COLUMN Revenue NUMERIC"], + ) + + operation = spanner_client.database_admin_api.update_database_ddl(request) + + print("Waiting for operation to complete...") + operation.result(OPERATION_TIMEOUT_SECONDS) + + print( + 'Altered table "Venues" on database {} on instance {}.'.format( + database_id, instance_id + ) + ) + + +# [END spanner_add_numeric_column] + + +# [START spanner_create_table_with_timestamp_column] +def create_table_with_timestamp(instance_id, database_id): + """Creates a table with a COMMIT_TIMESTAMP column.""" + + from google.cloud.spanner_admin_database_v1.types import spanner_database_admin + + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + database = instance.database(database_id) + + request = spanner_database_admin.UpdateDatabaseDdlRequest( + database=database.name, + statements=[ + """CREATE TABLE Performances ( + SingerId INT64 NOT NULL, + VenueId INT64 NOT NULL, + EventDate Date, + Revenue INT64, + LastUpdateTime TIMESTAMP NOT NULL + OPTIONS(allow_commit_timestamp=true) + ) PRIMARY KEY (SingerId, VenueId, EventDate), + INTERLEAVE IN PARENT Singers ON DELETE CASCADE""" + ], + ) + + operation = spanner_client.database_admin_api.update_database_ddl(request) + + print("Waiting for operation to complete...") + operation.result(OPERATION_TIMEOUT_SECONDS) + + print( + "Created Performances table on database {} on instance {}".format( + database_id, instance_id + ) + ) + + +# [END spanner_create_table_with_timestamp_column] + + +# [START spanner_create_table_with_foreign_key_delete_cascade] +def create_table_with_foreign_key_delete_cascade(instance_id, database_id): + """Creates a table with foreign key delete cascade action""" + + from google.cloud.spanner_admin_database_v1.types import spanner_database_admin + + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + database = instance.database(database_id) + + request = spanner_database_admin.UpdateDatabaseDdlRequest( + database=database.name, + statements=[ + """CREATE TABLE Customers ( + CustomerId INT64 NOT NULL, + CustomerName STRING(62) NOT NULL, + ) PRIMARY KEY (CustomerId) + """, + """ + CREATE TABLE ShoppingCarts ( + CartId INT64 NOT NULL, + CustomerId INT64 NOT NULL, + CustomerName STRING(62) NOT NULL, + CONSTRAINT FKShoppingCartsCustomerId FOREIGN KEY (CustomerId) + REFERENCES Customers (CustomerId) ON DELETE CASCADE + ) PRIMARY KEY (CartId) + """, + ], + ) + + operation = spanner_client.database_admin_api.update_database_ddl(request) + + print("Waiting for operation to complete...") + operation.result(OPERATION_TIMEOUT_SECONDS) + + print( + """Created Customers and ShoppingCarts table with FKShoppingCartsCustomerId + foreign key constraint on database {} on instance {}""".format( + database_id, instance_id + ) + ) + + +# [END spanner_create_table_with_foreign_key_delete_cascade] + + +# [START spanner_alter_table_with_foreign_key_delete_cascade] +def alter_table_with_foreign_key_delete_cascade(instance_id, database_id): + """Alters a table with foreign key delete cascade action""" + + from google.cloud.spanner_admin_database_v1.types import spanner_database_admin + + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + database = instance.database(database_id) + + request = spanner_database_admin.UpdateDatabaseDdlRequest( + database=database.name, + statements=[ + """ALTER TABLE ShoppingCarts + ADD CONSTRAINT FKShoppingCartsCustomerName + FOREIGN KEY (CustomerName) + REFERENCES Customers(CustomerName) + ON DELETE CASCADE""" + ], + ) + + operation = spanner_client.database_admin_api.update_database_ddl(request) + + print("Waiting for operation to complete...") + operation.result(OPERATION_TIMEOUT_SECONDS) + + print( + """Altered ShoppingCarts table with FKShoppingCartsCustomerName + foreign key constraint on database {} on instance {}""".format( + database_id, instance_id + ) + ) + + +# [END spanner_alter_table_with_foreign_key_delete_cascade] + + +# [START spanner_drop_foreign_key_constraint_delete_cascade] +def drop_foreign_key_constraint_delete_cascade(instance_id, database_id): + """Alter table to drop foreign key delete cascade action""" + + from google.cloud.spanner_admin_database_v1.types import spanner_database_admin + + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + database = instance.database(database_id) + + request = spanner_database_admin.UpdateDatabaseDdlRequest( + database=database.name, + statements=[ + """ALTER TABLE ShoppingCarts + DROP CONSTRAINT FKShoppingCartsCustomerName""" + ], + ) + + operation = spanner_client.database_admin_api.update_database_ddl(request) + + print("Waiting for operation to complete...") + operation.result(OPERATION_TIMEOUT_SECONDS) + + print( + """Altered ShoppingCarts table to drop FKShoppingCartsCustomerName + foreign key constraint on database {} on instance {}""".format( + database_id, instance_id + ) + ) + + +# [END spanner_drop_foreign_key_constraint_delete_cascade] + + +# [START spanner_create_sequence] +def create_sequence(instance_id, database_id): + """Creates the Sequence and insert data""" + + from google.cloud.spanner_admin_database_v1.types import spanner_database_admin + + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + database = instance.database(database_id) + + request = spanner_database_admin.UpdateDatabaseDdlRequest( + database=database.name, + statements=[ + "CREATE SEQUENCE Seq OPTIONS (sequence_kind = 'bit_reversed_positive')", + """CREATE TABLE Customers ( + CustomerId INT64 DEFAULT (GET_NEXT_SEQUENCE_VALUE(Sequence Seq)), + CustomerName STRING(1024) + ) PRIMARY KEY (CustomerId)""", + ], + ) + + operation = spanner_client.database_admin_api.update_database_ddl(request) + + print("Waiting for operation to complete...") + operation.result(OPERATION_TIMEOUT_SECONDS) + + print( + "Created Seq sequence and Customers table, where the key column CustomerId uses the sequence as a default value on database {} on instance {}".format( + database_id, instance_id + ) + ) + + def insert_customers(transaction): + results = transaction.execute_sql( + "INSERT INTO Customers (CustomerName) VALUES " + "('Alice'), " + "('David'), " + "('Marc') " + "THEN RETURN CustomerId" + ) + for result in results: + print("Inserted customer record with Customer Id: {}".format(*result)) + print( + "Number of customer records inserted is {}".format( + results.stats.row_count_exact + ) + ) + + database.run_in_transaction(insert_customers) + + +# [END spanner_create_sequence] + + +# [START spanner_alter_sequence] +def alter_sequence(instance_id, database_id): + """Alters the Sequence and insert data""" + + from google.cloud.spanner_admin_database_v1.types import spanner_database_admin + + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + database = instance.database(database_id) + + request = spanner_database_admin.UpdateDatabaseDdlRequest( + database=database.name, + statements=[ + "ALTER SEQUENCE Seq SET OPTIONS (skip_range_min = 1000, skip_range_max = 5000000)", + ], + ) + + operation = spanner_client.database_admin_api.update_database_ddl(request) + + print("Waiting for operation to complete...") + operation.result(OPERATION_TIMEOUT_SECONDS) + + print( + "Altered Seq sequence to skip an inclusive range between 1000 and 5000000 on database {} on instance {}".format( + database_id, instance_id + ) + ) + + def insert_customers(transaction): + results = transaction.execute_sql( + "INSERT INTO Customers (CustomerName) VALUES " + "('Lea'), " + "('Cataline'), " + "('Smith') " + "THEN RETURN CustomerId" + ) + for result in results: + print("Inserted customer record with Customer Id: {}".format(*result)) + print( + "Number of customer records inserted is {}".format( + results.stats.row_count_exact + ) + ) + + database.run_in_transaction(insert_customers) + + +# [END spanner_alter_sequence] + + +# [START spanner_drop_sequence] +def drop_sequence(instance_id, database_id): + """Drops the Sequence""" + + from google.cloud.spanner_admin_database_v1.types import spanner_database_admin + + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + database = instance.database(database_id) + + request = spanner_database_admin.UpdateDatabaseDdlRequest( + database=database.name, + statements=[ + "ALTER TABLE Customers ALTER COLUMN CustomerId DROP DEFAULT", + "DROP SEQUENCE Seq", + ], + ) + + operation = spanner_client.database_admin_api.update_database_ddl(request) + + print("Waiting for operation to complete...") + operation.result(OPERATION_TIMEOUT_SECONDS) + + print( + "Altered Customers table to drop DEFAULT from CustomerId column and dropped the Seq sequence on database {} on instance {}".format( + database_id, instance_id + ) + ) + + +# [END spanner_drop_sequence] + + +# [START spanner_add_column] +def add_column(instance_id, database_id): + """Adds a new column to the Albums table in the example database.""" + + from google.cloud.spanner_admin_database_v1.types import spanner_database_admin + + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + database = instance.database(database_id) + + request = spanner_database_admin.UpdateDatabaseDdlRequest( + database=database.name, + statements=[ + "ALTER TABLE Albums ADD COLUMN MarketingBudget INT64", + ], + ) + + operation = spanner_client.database_admin_api.update_database_ddl(request) + + print("Waiting for operation to complete...") + operation.result(OPERATION_TIMEOUT_SECONDS) + print("Added the MarketingBudget column.") + + +# [END spanner_add_column] + + +# [START spanner_add_timestamp_column] +def add_timestamp_column(instance_id, database_id): + """Adds a new TIMESTAMP column to the Albums table in the example database.""" + + from google.cloud.spanner_admin_database_v1.types import spanner_database_admin + + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + + database = instance.database(database_id) + + request = spanner_database_admin.UpdateDatabaseDdlRequest( + database=database.name, + statements=[ + "ALTER TABLE Albums ADD COLUMN LastUpdateTime TIMESTAMP " + "OPTIONS(allow_commit_timestamp=true)" + ], + ) + + operation = spanner_client.database_admin_api.update_database_ddl(request) + + print("Waiting for operation to complete...") + operation.result(OPERATION_TIMEOUT_SECONDS) + + print( + 'Altered table "Albums" on database {} on instance {}.'.format( + database_id, instance_id + ) + ) + + +# [END spanner_add_timestamp_column] + + +# [START spanner_create_index] +def add_index(instance_id, database_id): + """Adds a simple index to the example database.""" + + from google.cloud.spanner_admin_database_v1.types import spanner_database_admin + + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + database = instance.database(database_id) + + request = spanner_database_admin.UpdateDatabaseDdlRequest( + database=database.name, + statements=["CREATE INDEX AlbumsByAlbumTitle ON Albums(AlbumTitle)"], + ) + + operation = spanner_client.database_admin_api.update_database_ddl(request) + + print("Waiting for operation to complete...") + operation.result(OPERATION_TIMEOUT_SECONDS) + + print("Added the AlbumsByAlbumTitle index.") + + +# [END spanner_create_index] + + +# [START spanner_create_storing_index] +def add_storing_index(instance_id, database_id): + """Adds an storing index to the example database.""" + + from google.cloud.spanner_admin_database_v1.types import spanner_database_admin + + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + database = instance.database(database_id) + + request = spanner_database_admin.UpdateDatabaseDdlRequest( + database=database.name, + statements=[ + "CREATE INDEX AlbumsByAlbumTitle2 ON Albums(AlbumTitle)" + "STORING (MarketingBudget)" + ], + ) + + operation = spanner_client.database_admin_api.update_database_ddl(request) + + print("Waiting for operation to complete...") + operation.result(OPERATION_TIMEOUT_SECONDS) + + print("Added the AlbumsByAlbumTitle2 index.") + + +# [END spanner_create_storing_index] + + +def enable_fine_grained_access( + instance_id, + database_id, + iam_member="user:alice@example.com", + database_role="new_parent", + title="condition title", +): + """Showcases how to enable fine grained access control.""" + # [START spanner_enable_fine_grained_access] + # instance_id = "your-spanner-instance" + # database_id = "your-spanner-db-id" + # iam_member = "user:alice@example.com" + # database_role = "new_parent" + # title = "condition title" + + from google.type import expr_pb2 + from google.iam.v1 import iam_policy_pb2 + from google.iam.v1 import options_pb2 + from google.iam.v1 import policy_pb2 + + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + database = instance.database(database_id) + + # The policy in the response from getDatabaseIAMPolicy might use the policy version + # that you specified, or it might use a lower policy version. For example, if you + # specify version 3, but the policy has no conditional role bindings, the response + # uses version 1. Valid values are 0, 1, and 3. + request = iam_policy_pb2.GetIamPolicyRequest( + resource=database.name, + options=options_pb2.GetPolicyOptions(requested_policy_version=3), + ) + policy = spanner_client.database_admin_api.get_iam_policy(request=request) + if policy.version < 3: + policy.version = 3 + + new_binding = policy_pb2.Binding( + role="roles/spanner.fineGrainedAccessUser", + members=[iam_member], + condition=expr_pb2.Expr( + title=title, + expression=f'resource.name.endsWith("/databaseRoles/{database_role}")', + ), + ) + + policy.version = 3 + policy.bindings.append(new_binding) + set_request = iam_policy_pb2.SetIamPolicyRequest( + resource=database.name, + policy=policy, + ) + spanner_client.database_admin_api.set_iam_policy(set_request) + + new_policy = spanner_client.database_admin_api.get_iam_policy(request=request) + print( + f"Enabled fine-grained access in IAM. New policy has version {new_policy.version}" + ) + # [END spanner_enable_fine_grained_access] diff --git a/samples/samples/admin/samples_test.py b/samples/samples/admin/samples_test.py index a83c42f8d9..959c2f48fc 100644 --- a/samples/samples/admin/samples_test.py +++ b/samples/samples/admin/samples_test.py @@ -23,6 +23,7 @@ import pytest from google.api_core import exceptions +from google.cloud import spanner from google.cloud.spanner_admin_database_v1.types.common import DatabaseDialect from test_utils.retry import RetryErrors @@ -127,6 +128,40 @@ def test_create_instance_explicit(spanner_client, create_instance_id): retry_429(instance.delete)() +def test_create_instance_with_processing_units(capsys, lci_instance_id): + processing_units = 500 + retry_429(samples.create_instance_with_processing_units)( + lci_instance_id, + processing_units, + ) + out, _ = capsys.readouterr() + assert lci_instance_id in out + assert "{} processing units".format(processing_units) in out + spanner_client = spanner.Client() + instance = spanner_client.instance(lci_instance_id) + retry_429(instance.delete)() + + +def test_create_database_explicit(sample_instance, create_database_id): + # Rather than re-use 'sample_database', we create a new database, to + # ensure that the 'create_database' snippet is tested. + samples.create_database(sample_instance.instance_id, create_database_id) + database = sample_instance.database(create_database_id) + database.drop() + + +def test_create_database_with_encryption_config( + capsys, instance_id, cmek_database_id, kms_key_name +): + samples.create_database_with_encryption_key( + instance_id, cmek_database_id, kms_key_name + ) + out, _ = capsys.readouterr() + assert cmek_database_id in out + assert kms_key_name in out + + +@pytest.mark.dependency(name="create_database_with_default_leader") def test_create_database_with_default_leader( capsys, multi_region_instance, @@ -141,3 +176,180 @@ def test_create_database_with_default_leader( out, _ = capsys.readouterr() assert default_leader_database_id in out assert default_leader in out + + +@pytest.mark.dependency(depends=["create_database_with_default_leader"]) +def test_update_database_with_default_leader( + capsys, + multi_region_instance, + multi_region_instance_id, + default_leader_database_id, + default_leader, +): + retry_429 = RetryErrors(exceptions.ResourceExhausted, delay=15) + retry_429(samples.update_database_with_default_leader)( + multi_region_instance_id, default_leader_database_id, default_leader + ) + out, _ = capsys.readouterr() + assert default_leader_database_id in out + assert default_leader in out + + +def test_update_database(capsys, instance_id, sample_database): + samples.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() + + +@pytest.mark.dependency( + name="add_and_drop_database_roles", depends=["create_table_with_datatypes"] +) +def test_add_and_drop_database_roles(capsys, instance_id, sample_database): + samples.add_and_drop_database_roles(instance_id, sample_database.database_id) + out, _ = capsys.readouterr() + assert "Created roles new_parent and new_child and granted privileges" in out + assert "Revoked privileges and dropped role new_child" in out + + +@pytest.mark.dependency(name="create_table_with_datatypes") +def test_create_table_with_datatypes(capsys, instance_id, sample_database): + samples.create_table_with_datatypes(instance_id, sample_database.database_id) + out, _ = capsys.readouterr() + assert "Created Venues table on database" in out + + +@pytest.mark.dependency(name="create_table_with_timestamp") +def test_create_table_with_timestamp(capsys, instance_id, sample_database): + samples.create_table_with_timestamp(instance_id, sample_database.database_id) + out, _ = capsys.readouterr() + assert "Created Performances table on database" in out + + +@pytest.mark.dependency( + name="add_json_column", + depends=["create_table_with_datatypes"], +) +def test_add_json_column(capsys, instance_id, sample_database): + samples.add_json_column(instance_id, sample_database.database_id) + out, _ = capsys.readouterr() + assert 'Altered table "Venues" on database ' in out + + +@pytest.mark.dependency( + name="add_numeric_column", + depends=["create_table_with_datatypes"], +) +def test_add_numeric_column(capsys, instance_id, sample_database): + samples.add_numeric_column(instance_id, sample_database.database_id) + out, _ = capsys.readouterr() + assert 'Altered table "Venues" on database ' in out + + +@pytest.mark.dependency(name="create_table_with_foreign_key_delete_cascade") +def test_create_table_with_foreign_key_delete_cascade( + capsys, instance_id, sample_database +): + samples.create_table_with_foreign_key_delete_cascade( + instance_id, sample_database.database_id + ) + out, _ = capsys.readouterr() + assert ( + "Created Customers and ShoppingCarts table with FKShoppingCartsCustomerId" + in out + ) + + +@pytest.mark.dependency( + name="alter_table_with_foreign_key_delete_cascade", + depends=["create_table_with_foreign_key_delete_cascade"], +) +def test_alter_table_with_foreign_key_delete_cascade( + capsys, instance_id, sample_database +): + samples.alter_table_with_foreign_key_delete_cascade( + instance_id, sample_database.database_id + ) + out, _ = capsys.readouterr() + assert "Altered ShoppingCarts table with FKShoppingCartsCustomerName" in out + + +@pytest.mark.dependency(depends=["alter_table_with_foreign_key_delete_cascade"]) +def test_drop_foreign_key_contraint_delete_cascade( + capsys, instance_id, sample_database +): + samples.drop_foreign_key_constraint_delete_cascade( + instance_id, sample_database.database_id + ) + out, _ = capsys.readouterr() + assert "Altered ShoppingCarts table to drop FKShoppingCartsCustomerName" in out + + +@pytest.mark.dependency(name="create_sequence") +def test_create_sequence(capsys, instance_id, bit_reverse_sequence_database): + samples.create_sequence(instance_id, bit_reverse_sequence_database.database_id) + out, _ = capsys.readouterr() + assert ( + "Created Seq sequence and Customers table, where the key column CustomerId uses the sequence as a default value on database" + in out + ) + assert "Number of customer records inserted is 3" in out + assert "Inserted customer record with Customer Id:" in out + + +@pytest.mark.dependency(depends=["create_sequence"]) +def test_alter_sequence(capsys, instance_id, bit_reverse_sequence_database): + samples.alter_sequence(instance_id, bit_reverse_sequence_database.database_id) + out, _ = capsys.readouterr() + assert ( + "Altered Seq sequence to skip an inclusive range between 1000 and 5000000 on database" + in out + ) + assert "Number of customer records inserted is 3" in out + assert "Inserted customer record with Customer Id:" in out + + +@pytest.mark.dependency(depends=["alter_sequence"]) +def test_drop_sequence(capsys, instance_id, bit_reverse_sequence_database): + samples.drop_sequence(instance_id, bit_reverse_sequence_database.database_id) + out, _ = capsys.readouterr() + assert ( + "Altered Customers table to drop DEFAULT from CustomerId column and dropped the Seq sequence on database" + in out + ) + + +@pytest.mark.dependency(name="add_column", depends=["create_table_with_datatypes"]) +def test_add_column(capsys, instance_id, sample_database): + samples.add_column(instance_id, sample_database.database_id) + out, _ = capsys.readouterr() + assert "Added the MarketingBudget column." in out + + +@pytest.mark.dependency( + name="add_timestamp_column", depends=["create_table_with_datatypes"] +) +def test_add_timestamp_column(capsys, instance_id, sample_database): + samples.add_timestamp_column(instance_id, sample_database.database_id) + out, _ = capsys.readouterr() + assert 'Altered table "Albums" on database ' in out + + +@pytest.mark.dependency(name="add_index", depends=["create_table_with_datatypes"]) +def test_add_index(capsys, instance_id, sample_database): + samples.add_index(instance_id, sample_database.database_id) + out, _ = capsys.readouterr() + assert "Added the AlbumsByAlbumTitle index" in out + + +@pytest.mark.dependency( + name="add_storing_index", depends=["create_table_with_datatypes"] +) +def test_add_storing_index(capsys, instance_id, sample_database): + samples.add_storing_index(instance_id, sample_database.database_id) + out, _ = capsys.readouterr() + assert "Added the AlbumsByAlbumTitle2 index." in out From d683a14ccc574e49cefd4e2b2f8b6d9bfd3663ec Mon Sep 17 00:00:00 2001 From: rahul2393 Date: Mon, 4 Mar 2024 12:24:03 +0530 Subject: [PATCH 13/17] docs: update all public documents to use auto-generated admin clients. (#1109) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: update all public documents to use auto-generated admin clients. * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * fix lint issue * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * lint fixes * add missing samples --------- Co-authored-by: Owl Bot --- .../{admin => archived}/backup_snippet.py | 557 ++++------ .../backup_snippet_test.py | 0 .../samples/{admin => archived}/pg_samples.py | 298 +++--- .../{admin => archived}/pg_samples_test.py | 0 .../samples/{admin => archived}/samples.py | 977 ++++++++---------- .../{admin => archived}/samples_test.py | 19 + samples/samples/autocommit_test.py | 2 +- samples/samples/backup_sample.py | 303 ++++-- samples/samples/backup_sample_test.py | 2 +- samples/samples/conftest.py | 8 +- samples/samples/pg_snippets.py | 89 +- samples/samples/pg_snippets_test.py | 2 +- samples/samples/quickstart.py | 1 - samples/samples/snippets.py | 447 +++++--- samples/samples/snippets_test.py | 2 +- 15 files changed, 1419 insertions(+), 1288 deletions(-) rename samples/samples/{admin => archived}/backup_snippet.py (58%) rename samples/samples/{admin => archived}/backup_snippet_test.py (100%) rename samples/samples/{admin => archived}/pg_samples.py (78%) rename samples/samples/{admin => archived}/pg_samples_test.py (100%) rename samples/samples/{admin => archived}/samples.py (66%) rename samples/samples/{admin => archived}/samples_test.py (95%) diff --git a/samples/samples/admin/backup_snippet.py b/samples/samples/archived/backup_snippet.py similarity index 58% rename from samples/samples/admin/backup_snippet.py rename to samples/samples/archived/backup_snippet.py index 0a7260d115..f31cbc1f2c 100644 --- a/samples/samples/admin/backup_snippet.py +++ b/samples/samples/archived/backup_snippet.py @@ -14,48 +14,104 @@ """This application demonstrates how to create and restore from backups using Cloud Spanner. + For more information, see the README.rst under /spanner. """ import time from datetime import datetime, timedelta -from google.api_core import protobuf_helpers from google.cloud import spanner -from google.cloud.exceptions import NotFound + + +# [START spanner_cancel_backup_create] +def cancel_backup(instance_id, database_id, backup_id): + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + database = instance.database(database_id) + + expire_time = datetime.utcnow() + timedelta(days=30) + + # Create a backup. + backup = instance.backup(backup_id, database=database, expire_time=expire_time) + operation = backup.create() + + # Cancel backup creation. + operation.cancel() + + # Cancel operations are best effort so either it will complete or + # be cancelled. + while not operation.done(): + time.sleep(300) # 5 mins + + # Deal with resource if the operation succeeded. + if backup.exists(): + print("Backup was created before the cancel completed.") + backup.delete() + print("Backup deleted.") + else: + print("Backup creation was successfully cancelled.") + + +# [END spanner_cancel_backup_create] + + +# [START spanner_copy_backup] +def copy_backup(instance_id, backup_id, source_backup_path): + """Copies a backup.""" + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + + # Create a backup object and wait for copy backup operation to complete. + expire_time = datetime.utcnow() + timedelta(days=14) + copy_backup = instance.copy_backup( + backup_id=backup_id, source_backup=source_backup_path, expire_time=expire_time + ) + operation = copy_backup.create() + + # Wait for copy backup operation to complete. + operation.result(2100) + + # Verify that the copy backup is ready. + copy_backup.reload() + assert copy_backup.is_ready() is True + + print( + "Backup {} of size {} bytes was created at {} with version time {}".format( + copy_backup.name, + copy_backup.size_bytes, + copy_backup.create_time, + copy_backup.version_time, + ) + ) + + +# [END spanner_copy_backup] # [START spanner_create_backup] def create_backup(instance_id, database_id, backup_id, version_time): """Creates a backup for a database.""" - - from google.cloud.spanner_admin_database_v1.types import backup as backup_pb - spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) database = instance.database(database_id) # Create a backup expire_time = datetime.utcnow() + timedelta(days=14) - - request = backup_pb.CreateBackupRequest( - parent=instance.name, - backup_id=backup_id, - backup=backup_pb.Backup( - database=database.name, - expire_time=expire_time, - version_time=version_time, - ), + backup = instance.backup( + backup_id, database=database, expire_time=expire_time, version_time=version_time ) - - operation = spanner_client.database_admin_api.create_backup(request) + operation = backup.create() # Wait for backup operation to complete. - backup = operation.result(2100) + operation.result(2100) # Verify that the backup is ready. - assert backup.state == backup_pb.Backup.State.READY + backup.reload() + assert backup.is_ready() is True + # Get the name, create time and backup size. + backup.reload() print( "Backup {} of size {} bytes was created at {} for version of database at {}".format( backup.name, backup.size_bytes, backup.create_time, backup.version_time @@ -71,10 +127,8 @@ def create_backup_with_encryption_key( instance_id, database_id, backup_id, kms_key_name ): """Creates a backup for a database using a Customer Managed Encryption Key (CMEK).""" - - from google.cloud.spanner_admin_database_v1.types import backup as backup_pb - - from google.cloud.spanner_admin_database_v1 import CreateBackupEncryptionConfig + from google.cloud.spanner_admin_database_v1 import \ + CreateBackupEncryptionConfig spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) @@ -86,24 +140,23 @@ def create_backup_with_encryption_key( "encryption_type": CreateBackupEncryptionConfig.EncryptionType.CUSTOMER_MANAGED_ENCRYPTION, "kms_key_name": kms_key_name, } - request = backup_pb.CreateBackupRequest( - parent=instance.name, - backup_id=backup_id, - backup=backup_pb.Backup( - database=database.name, - expire_time=expire_time, - ), + backup = instance.backup( + backup_id, + database=database, + expire_time=expire_time, encryption_config=encryption_config, ) - operation = spanner_client.database_admin_api.create_backup(request) + operation = backup.create() # Wait for backup operation to complete. - backup = operation.result(2100) + operation.result(2100) # Verify that the backup is ready. - assert backup.state == backup_pb.Backup.State.READY + backup.reload() + assert backup.is_ready() is True # Get the name, create time, backup size and encryption key. + backup.reload() print( "Backup {} of size {} bytes was created at {} using encryption key {}".format( backup.name, backup.size_bytes, backup.create_time, kms_key_name @@ -114,139 +167,75 @@ def create_backup_with_encryption_key( # [END spanner_create_backup_with_encryption_key] -# [START spanner_restore_backup] -def restore_database(instance_id, new_database_id, backup_id): - """Restores a database from a backup.""" - from google.cloud.spanner_admin_database_v1 import RestoreDatabaseRequest - - spanner_client = spanner.Client() - instance = spanner_client.instance(instance_id) - - # Start restoring an existing backup to a new database. - request = RestoreDatabaseRequest( - parent=instance.name, - database_id=new_database_id, - backup="{}/backups/{}".format(instance.name, backup_id), - ) - operation = spanner_client.database_admin_api.restore_database(request) - - # Wait for restore operation to complete. - db = operation.result(1600) - - # Newly created database has restore information. - restore_info = db.restore_info - print( - "Database {} restored to {} from backup {} with version time {}.".format( - restore_info.backup_info.source_database, - new_database_id, - restore_info.backup_info.backup, - restore_info.backup_info.version_time, - ) - ) - - -# [END spanner_restore_backup] - - -# [START spanner_restore_backup_with_encryption_key] -def restore_database_with_encryption_key( - instance_id, new_database_id, backup_id, kms_key_name +# [START spanner_create_database_with_version_retention_period] +def create_database_with_version_retention_period( + instance_id, database_id, retention_period ): - """Restores a database from a backup using a Customer Managed Encryption Key (CMEK).""" - from google.cloud.spanner_admin_database_v1 import ( - RestoreDatabaseEncryptionConfig, - RestoreDatabaseRequest, - ) - + """Creates a database with a version retention period.""" spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) + ddl_statements = [ + "CREATE TABLE Singers (" + + " SingerId INT64 NOT NULL," + + " FirstName STRING(1024)," + + " LastName STRING(1024)," + + " SingerInfo BYTES(MAX)" + + ") PRIMARY KEY (SingerId)", + "CREATE TABLE Albums (" + + " SingerId INT64 NOT NULL," + + " AlbumId INT64 NOT NULL," + + " AlbumTitle STRING(MAX)" + + ") PRIMARY KEY (SingerId, AlbumId)," + + " INTERLEAVE IN PARENT Singers ON DELETE CASCADE", + "ALTER DATABASE `{}`" + " SET OPTIONS (version_retention_period = '{}')".format( + database_id, retention_period + ), + ] + db = instance.database(database_id, ddl_statements) + operation = db.create() - # Start restoring an existing backup to a new database. - encryption_config = { - "encryption_type": RestoreDatabaseEncryptionConfig.EncryptionType.CUSTOMER_MANAGED_ENCRYPTION, - "kms_key_name": kms_key_name, - } - - request = RestoreDatabaseRequest( - parent=instance.name, - database_id=new_database_id, - backup="{}/backups/{}".format(instance.name, backup_id), - encryption_config=encryption_config, - ) - operation = spanner_client.database_admin_api.restore_database(request) + operation.result(30) - # Wait for restore operation to complete. - db = operation.result(1600) + db.reload() - # Newly created database has restore information. - restore_info = db.restore_info print( - "Database {} restored to {} from backup {} with using encryption key {}.".format( - restore_info.backup_info.source_database, - new_database_id, - restore_info.backup_info.backup, - db.encryption_config.kms_key_name, + "Database {} created with version retention period {} and earliest version time {}".format( + db.database_id, db.version_retention_period, db.earliest_version_time ) ) + db.drop() -# [END spanner_restore_backup_with_encryption_key] +# [END spanner_create_database_with_version_retention_period] -# [START spanner_cancel_backup_create] -def cancel_backup(instance_id, database_id, backup_id): - from google.cloud.spanner_admin_database_v1.types import backup as backup_pb +# [START spanner_delete_backup] +def delete_backup(instance_id, backup_id): spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) - database = instance.database(database_id) - - expire_time = datetime.utcnow() + timedelta(days=30) + backup = instance.backup(backup_id) + backup.reload() - # Create a backup. - request = backup_pb.CreateBackupRequest( - parent=instance.name, - backup_id=backup_id, - backup=backup_pb.Backup( - database=database.name, - expire_time=expire_time, - ), - ) - - operation = spanner_client.database_admin_api.create_backup(request) - # Cancel backup creation. - operation.cancel() + # Wait for databases that reference this backup to finish optimizing. + while backup.referencing_databases: + time.sleep(30) + backup.reload() - # Cancel operations are best effort so either it will complete or - # be cancelled. - while not operation.done(): - time.sleep(300) # 5 mins + # Delete the backup. + backup.delete() - try: - spanner_client.database_admin_api.get_backup( - backup_pb.GetBackupRequest( - name="{}/backups/{}".format(instance.name, backup_id) - ) - ) - except NotFound: - print("Backup creation was successfully cancelled.") - return - print("Backup was created before the cancel completed.") - spanner_client.database_admin_api.delete_backup( - backup_pb.DeleteBackupRequest( - name="{}/backups/{}".format(instance.name, backup_id) - ) - ) - print("Backup deleted.") + # Verify that the backup is deleted. + assert backup.exists() is False + print("Backup {} has been deleted.".format(backup.name)) -# [END spanner_cancel_backup_create] +# [END spanner_delete_backup] # [START spanner_list_backup_operations] def list_backup_operations(instance_id, database_id, backup_id): - from google.cloud.spanner_admin_database_v1.types import backup as backup_pb - spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) @@ -256,14 +245,9 @@ def list_backup_operations(instance_id, database_id, backup_id): "google.spanner.admin.database.v1.CreateBackupMetadata) " "AND (metadata.database:{})" ).format(database_id) - request = backup_pb.ListBackupOperationsRequest( - parent=instance.name, filter=filter_ - ) - operations = spanner_client.database_admin_api.list_backup_operations(request) + operations = instance.list_backup_operations(filter_=filter_) for op in operations: - metadata = protobuf_helpers.from_any_pb( - backup_pb.CreateBackupMetadata, op.metadata - ) + metadata = op.metadata print( "Backup {} on database {}: {}% complete.".format( metadata.name, metadata.database, metadata.progress.progress_percent @@ -275,14 +259,9 @@ def list_backup_operations(instance_id, database_id, backup_id): "(metadata.@type:type.googleapis.com/google.spanner.admin.database.v1.CopyBackupMetadata) " "AND (metadata.source_backup:{})" ).format(backup_id) - request = backup_pb.ListBackupOperationsRequest( - parent=instance.name, filter=filter_ - ) - operations = spanner_client.database_admin_api.list_backup_operations(request) + operations = instance.list_backup_operations(filter_=filter_) for op in operations: - metadata = protobuf_helpers.from_any_pb( - backup_pb.CopyBackupMetadata, op.metadata - ) + metadata = op.metadata print( "Backup {} on source backup {}: {}% complete.".format( metadata.name, @@ -295,66 +274,24 @@ def list_backup_operations(instance_id, database_id, backup_id): # [END spanner_list_backup_operations] -# [START spanner_list_database_operations] -def list_database_operations(instance_id): - from google.cloud.spanner_admin_database_v1.types import spanner_database_admin - - spanner_client = spanner.Client() - instance = spanner_client.instance(instance_id) - - # List the progress of restore. - filter_ = ( - "(metadata.@type:type.googleapis.com/" - "google.spanner.admin.database.v1.OptimizeRestoredDatabaseMetadata)" - ) - request = spanner_database_admin.ListDatabaseOperationsRequest( - parent=instance.name, filter=filter_ - ) - operations = spanner_client.database_admin_api.list_database_operations(request) - for op in operations: - metadata = protobuf_helpers.from_any_pb( - spanner_database_admin.OptimizeRestoredDatabaseMetadata, op.metadata - ) - print( - "Database {} restored from backup is {}% optimized.".format( - metadata.name, metadata.progress.progress_percent - ) - ) - - -# [END spanner_list_database_operations] - - # [START spanner_list_backups] def list_backups(instance_id, database_id, backup_id): - from google.cloud.spanner_admin_database_v1.types import backup as backup_pb - spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) # List all backups. print("All backups:") - request = backup_pb.ListBackupsRequest(parent=instance.name, filter="") - operations = spanner_client.database_admin_api.list_backups(request) - for backup in operations: + for backup in instance.list_backups(): print(backup.name) # List all backups that contain a name. print('All backups with backup name containing "{}":'.format(backup_id)) - request = backup_pb.ListBackupsRequest( - parent=instance.name, filter="name:{}".format(backup_id) - ) - operations = spanner_client.database_admin_api.list_backups(request) - for backup in operations: + for backup in instance.list_backups(filter_="name:{}".format(backup_id)): print(backup.name) # List all backups for a database that contains a name. print('All backups with database name containing "{}":'.format(database_id)) - request = backup_pb.ListBackupsRequest( - parent=instance.name, filter="database:{}".format(database_id) - ) - operations = spanner_client.database_admin_api.list_backups(request) - for backup in operations: + for backup in instance.list_backups(filter_="database:{}".format(database_id)): print(backup.name) # List all backups that expire before a timestamp. @@ -364,21 +301,14 @@ def list_backups(instance_id, database_id, backup_id): *expire_time.timetuple() ) ) - request = backup_pb.ListBackupsRequest( - parent=instance.name, - filter='expire_time < "{}-{}-{}T{}:{}:{}Z"'.format(*expire_time.timetuple()), - ) - operations = spanner_client.database_admin_api.list_backups(request) - for backup in operations: + for backup in instance.list_backups( + filter_='expire_time < "{}-{}-{}T{}:{}:{}Z"'.format(*expire_time.timetuple()) + ): print(backup.name) # List all backups with a size greater than some bytes. print("All backups with backup size more than 100 bytes:") - request = backup_pb.ListBackupsRequest( - parent=instance.name, filter="size_bytes > 100" - ) - operations = spanner_client.database_admin_api.list_backups(request) - for backup in operations: + for backup in instance.list_backups(filter_="size_bytes > 100"): print(backup.name) # List backups that were created after a timestamp that are also ready. @@ -388,23 +318,18 @@ def list_backups(instance_id, database_id, backup_id): *create_time.timetuple() ) ) - request = backup_pb.ListBackupsRequest( - parent=instance.name, - filter='create_time >= "{}-{}-{}T{}:{}:{}Z" AND state:READY'.format( + for backup in instance.list_backups( + filter_='create_time >= "{}-{}-{}T{}:{}:{}Z" AND state:READY'.format( *create_time.timetuple() - ), - ) - operations = spanner_client.database_admin_api.list_backups(request) - for backup in operations: + ) + ): print(backup.name) print("All backups with pagination") # If there are multiple pages, additional ``ListBackup`` # requests will be made as needed while iterating. paged_backups = set() - request = backup_pb.ListBackupsRequest(parent=instance.name, page_size=2) - operations = spanner_client.database_admin_api.list_backups(request) - for backup in operations: + for backup in instance.list_backups(page_size=2): paged_backups.add(backup.name) for backup in paged_backups: print(backup) @@ -413,163 +338,117 @@ def list_backups(instance_id, database_id, backup_id): # [END spanner_list_backups] -# [START spanner_delete_backup] -def delete_backup(instance_id, backup_id): - from google.cloud.spanner_admin_database_v1.types import backup as backup_pb - +# [START spanner_list_database_operations] +def list_database_operations(instance_id): spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) - backup = spanner_client.database_admin_api.get_backup( - backup_pb.GetBackupRequest( - name="{}/backups/{}".format(instance.name, backup_id) - ) - ) - # Wait for databases that reference this backup to finish optimizing. - while backup.referencing_databases: - time.sleep(30) - backup = spanner_client.database_admin_api.get_backup( - backup_pb.GetBackupRequest( - name="{}/backups/{}".format(instance.name, backup_id) - ) - ) - - # Delete the backup. - spanner_client.database_admin_api.delete_backup( - backup_pb.DeleteBackupRequest(name=backup.name) + # List the progress of restore. + filter_ = ( + "(metadata.@type:type.googleapis.com/" + "google.spanner.admin.database.v1.OptimizeRestoredDatabaseMetadata)" ) - - # Verify that the backup is deleted. - try: - backup = spanner_client.database_admin_api.get_backup( - backup_pb.GetBackupRequest(name=backup.name) + operations = instance.list_database_operations(filter_=filter_) + for op in operations: + print( + "Database {} restored from backup is {}% optimized.".format( + op.metadata.name, op.metadata.progress.progress_percent + ) ) - except NotFound: - print("Backup {} has been deleted.".format(backup.name)) - return - -# [END spanner_delete_backup] +# [END spanner_list_database_operations] -# [START spanner_update_backup] -def update_backup(instance_id, backup_id): - from google.cloud.spanner_admin_database_v1.types import backup as backup_pb +# [START spanner_restore_backup] +def restore_database(instance_id, new_database_id, backup_id): + """Restores a database from a backup.""" spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) + # Create a backup on database_id. - backup = spanner_client.database_admin_api.get_backup( - backup_pb.GetBackupRequest( - name="{}/backups/{}".format(instance.name, backup_id) - ) - ) + # Start restoring an existing backup to a new database. + backup = instance.backup(backup_id) + new_database = instance.database(new_database_id) + operation = new_database.restore(backup) - # Expire time must be within 366 days of the create time of the backup. - old_expire_time = backup.expire_time - # New expire time should be less than the max expire time - new_expire_time = min(backup.max_expire_time, old_expire_time + timedelta(days=30)) - spanner_client.database_admin_api.update_backup( - backup_pb.UpdateBackupRequest( - backup=backup_pb.Backup(name=backup.name, expire_time=new_expire_time), - update_mask={"paths": ["expire_time"]}, - ) - ) + # Wait for restore operation to complete. + operation.result(1600) + + # Newly created database has restore information. + new_database.reload() + restore_info = new_database.restore_info print( - "Backup {} expire time was updated from {} to {}.".format( - backup.name, old_expire_time, new_expire_time + "Database {} restored to {} from backup {} with version time {}.".format( + restore_info.backup_info.source_database, + new_database_id, + restore_info.backup_info.backup, + restore_info.backup_info.version_time, ) ) -# [END spanner_update_backup] +# [END spanner_restore_backup] -# [START spanner_create_database_with_version_retention_period] -def create_database_with_version_retention_period( - instance_id, database_id, retention_period +# [START spanner_restore_backup_with_encryption_key] +def restore_database_with_encryption_key( + instance_id, new_database_id, backup_id, kms_key_name ): - """Creates a database with a version retention period.""" - - from google.cloud.spanner_admin_database_v1.types import spanner_database_admin + """Restores a database from a backup using a Customer Managed Encryption Key (CMEK).""" + from google.cloud.spanner_admin_database_v1 import \ + RestoreDatabaseEncryptionConfig spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) - ddl_statements = [ - "CREATE TABLE Singers (" - + " SingerId INT64 NOT NULL," - + " FirstName STRING(1024)," - + " LastName STRING(1024)," - + " SingerInfo BYTES(MAX)" - + ") PRIMARY KEY (SingerId)", - "CREATE TABLE Albums (" - + " SingerId INT64 NOT NULL," - + " AlbumId INT64 NOT NULL," - + " AlbumTitle STRING(MAX)" - + ") PRIMARY KEY (SingerId, AlbumId)," - + " INTERLEAVE IN PARENT Singers ON DELETE CASCADE", - "ALTER DATABASE `{}`" - " SET OPTIONS (version_retention_period = '{}')".format( - database_id, retention_period - ), - ] - operation = spanner_client.database_admin_api.create_database( - request=spanner_database_admin.CreateDatabaseRequest( - parent=instance.name, - create_statement="CREATE DATABASE `{}`".format(database_id), - extra_statements=ddl_statements, - ) + + # Start restoring an existing backup to a new database. + backup = instance.backup(backup_id) + encryption_config = { + "encryption_type": RestoreDatabaseEncryptionConfig.EncryptionType.CUSTOMER_MANAGED_ENCRYPTION, + "kms_key_name": kms_key_name, + } + new_database = instance.database( + new_database_id, encryption_config=encryption_config ) + operation = new_database.restore(backup) + + # Wait for restore operation to complete. + operation.result(1600) - db = operation.result(30) + # Newly created database has restore information. + new_database.reload() + restore_info = new_database.restore_info print( - "Database {} created with version retention period {} and earliest version time {}".format( - db.name, db.version_retention_period, db.earliest_version_time + "Database {} restored to {} from backup {} with using encryption key {}.".format( + restore_info.backup_info.source_database, + new_database_id, + restore_info.backup_info.backup, + new_database.encryption_config.kms_key_name, ) ) - spanner_client.database_admin_api.drop_database( - spanner_database_admin.DropDatabaseRequest(database=db.name) - ) - - -# [END spanner_create_database_with_version_retention_period] - -# [START spanner_copy_backup] -def copy_backup(instance_id, backup_id, source_backup_path): - """Copies a backup.""" +# [END spanner_restore_backup_with_encryption_key] - from google.cloud.spanner_admin_database_v1.types import backup as backup_pb +# [START spanner_update_backup] +def update_backup(instance_id, backup_id): spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) + backup = instance.backup(backup_id) + backup.reload() - # Create a backup object and wait for copy backup operation to complete. - expire_time = datetime.utcnow() + timedelta(days=14) - request = backup_pb.CopyBackupRequest( - parent=instance.name, - backup_id=backup_id, - source_backup=source_backup_path, - expire_time=expire_time, - ) - - operation = spanner_client.database_admin_api.copy_backup(request) - - # Wait for backup operation to complete. - copy_backup = operation.result(2100) - - # Verify that the copy backup is ready. - assert copy_backup.state == backup_pb.Backup.State.READY - + # Expire time must be within 366 days of the create time of the backup. + old_expire_time = backup.expire_time + # New expire time should be less than the max expire time + new_expire_time = min(backup.max_expire_time, old_expire_time + timedelta(days=30)) + backup.update_expire_time(new_expire_time) print( - "Backup {} of size {} bytes was created at {} with version time {}".format( - copy_backup.name, - copy_backup.size_bytes, - copy_backup.create_time, - copy_backup.version_time, + "Backup {} expire time was updated from {} to {}.".format( + backup.name, old_expire_time, new_expire_time ) ) -# [END spanner_copy_backup] +# [END spanner_update_backup] diff --git a/samples/samples/admin/backup_snippet_test.py b/samples/samples/archived/backup_snippet_test.py similarity index 100% rename from samples/samples/admin/backup_snippet_test.py rename to samples/samples/archived/backup_snippet_test.py diff --git a/samples/samples/admin/pg_samples.py b/samples/samples/archived/pg_samples.py similarity index 78% rename from samples/samples/admin/pg_samples.py rename to samples/samples/archived/pg_samples.py index 4da2cafc33..2d0dd0e5a9 100644 --- a/samples/samples/admin/pg_samples.py +++ b/samples/samples/archived/pg_samples.py @@ -18,122 +18,22 @@ Spanner PostgreSql dialect. For more information, see the README.rst under /spanner. """ -from google.cloud import spanner +from google.cloud import spanner, spanner_admin_database_v1 from google.cloud.spanner_admin_database_v1.types.common import DatabaseDialect OPERATION_TIMEOUT_SECONDS = 240 -# [START spanner_postgresql_create_database] -def create_database(instance_id, database_id): - """Creates a PostgreSql database and tables for sample data.""" - - from google.cloud.spanner_admin_database_v1.types import spanner_database_admin - - spanner_client = spanner.Client() - instance = spanner_client.instance(instance_id) - - request = spanner_database_admin.CreateDatabaseRequest( - parent=instance.name, - create_statement=f'CREATE DATABASE "{database_id}"', - database_dialect=DatabaseDialect.POSTGRESQL, - ) - - operation = spanner_client.database_admin_api.create_database(request=request) - - print("Waiting for operation to complete...") - database = operation.result(OPERATION_TIMEOUT_SECONDS) - - create_table_using_ddl(database.name) - print("Created database {} on instance {}".format(database_id, instance_id)) - - -def create_table_using_ddl(database_name): - from google.cloud.spanner_admin_database_v1.types import spanner_database_admin - - spanner_client = spanner.Client() - request = spanner_database_admin.UpdateDatabaseDdlRequest( - database=database_name, - statements=[ - """CREATE TABLE Singers ( - SingerId bigint NOT NULL, - FirstName character varying(1024), - LastName character varying(1024), - SingerInfo bytea, - FullName character varying(2048) - GENERATED ALWAYS AS (FirstName || ' ' || LastName) STORED, - PRIMARY KEY (SingerId) - )""", - """CREATE TABLE Albums ( - SingerId bigint NOT NULL, - AlbumId bigint NOT NULL, - AlbumTitle character varying(1024), - PRIMARY KEY (SingerId, AlbumId) - ) INTERLEAVE IN PARENT Singers ON DELETE CASCADE""", - ], - ) - operation = spanner_client.database_admin_api.update_database_ddl(request) - operation.result(OPERATION_TIMEOUT_SECONDS) - - -# [END spanner_postgresql_create_database] - - -def create_table_with_datatypes(instance_id, database_id): - """Creates a table with supported datatypes.""" - # [START spanner_postgresql_create_table_with_datatypes] - # instance_id = "your-spanner-instance" - # database_id = "your-spanner-db-id" - - from google.cloud.spanner_admin_database_v1.types import spanner_database_admin - - spanner_client = spanner.Client() - instance = spanner_client.instance(instance_id) - database = instance.database(database_id) - - request = spanner_database_admin.UpdateDatabaseDdlRequest( - database=database.name, - statements=[ - """CREATE TABLE Venues ( - VenueId BIGINT NOT NULL, - VenueName character varying(100), - VenueInfo BYTEA, - Capacity BIGINT, - OutdoorVenue BOOL, - PopularityScore FLOAT8, - Revenue NUMERIC, - LastUpdateTime SPANNER.COMMIT_TIMESTAMP NOT NULL, - PRIMARY KEY (VenueId))""" - ], - ) - operation = spanner_client.database_admin_api.update_database_ddl(request) - - print("Waiting for operation to complete...") - operation.result(OPERATION_TIMEOUT_SECONDS) - - print( - "Created Venues table on database {} on instance {}".format( - database_id, instance_id - ) - ) - # [END spanner_postgresql_create_table_with_datatypes] - - # [START spanner_postgresql_add_column] def add_column(instance_id, database_id): """Adds a new column to the Albums table in the example database.""" - - from google.cloud.spanner_admin_database_v1.types import spanner_database_admin - spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) database = instance.database(database_id) - request = spanner_database_admin.UpdateDatabaseDdlRequest( - database=database.name, - statements=["ALTER TABLE Albums ADD COLUMN MarketingBudget BIGINT"], + operation = database.update_ddl( + ["ALTER TABLE Albums ADD COLUMN MarketingBudget BIGINT"] ) - operation = spanner_client.database_admin_api.update_database_ddl(request) print("Waiting for operation to complete...") operation.result(OPERATION_TIMEOUT_SECONDS) @@ -164,19 +64,14 @@ def add_jsonb_column(instance_id, database_id): # instance_id = "your-spanner-instance" # database_id = "your-spanner-db-id" - from google.cloud.spanner_admin_database_v1.types import spanner_database_admin - spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) database = instance.database(database_id) - request = spanner_database_admin.UpdateDatabaseDdlRequest( - database=database.name, - statements=["ALTER TABLE Venues ADD COLUMN VenueDetails JSONB"], + operation = database.update_ddl( + ["ALTER TABLE Venues ADD COLUMN VenueDetails JSONB"] ) - operation = spanner_client.database_admin_api.update_database_ddl(request) - print("Waiting for operation to complete...") operation.result(OPERATION_TIMEOUT_SECONDS) @@ -190,46 +85,103 @@ def add_jsonb_column(instance_id, database_id): # [END spanner_postgresql_jsonb_add_column] -# [START spanner_postgresql_create_storing_index] -def add_storing_index(instance_id, database_id): - """Adds an storing index to the example database.""" +# [START spanner_postgresql_alter_sequence] +def alter_sequence(instance_id, database_id): + """Alters the Sequence and insert data""" + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + database = instance.database(database_id) + + operation = database.update_ddl(["ALTER SEQUENCE Seq SKIP RANGE 1000 5000000"]) + + print("Waiting for operation to complete...") + operation.result(OPERATION_TIMEOUT_SECONDS) + + print( + "Altered Seq sequence to skip an inclusive range between 1000 and 5000000 on database {} on instance {}".format( + database_id, instance_id + ) + ) + + def insert_customers(transaction): + results = transaction.execute_sql( + "INSERT INTO Customers (CustomerName) VALUES " + "('Lea'), " + "('Cataline'), " + "('Smith') " + "RETURNING CustomerId" + ) + for result in results: + print("Inserted customer record with Customer Id: {}".format(*result)) + print( + "Number of customer records inserted is {}".format( + results.stats.row_count_exact + ) + ) + + database.run_in_transaction(insert_customers) + + +# [END spanner_postgresql_alter_sequence] - from google.cloud.spanner_admin_database_v1.types import spanner_database_admin +# [START spanner_postgresql_create_database] +def create_database(instance_id, database_id): + """Creates a PostgreSql database and tables for sample data.""" spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) - database = instance.database(database_id) - request = spanner_database_admin.UpdateDatabaseDdlRequest( - database=database.name, - statements=[ - "CREATE INDEX AlbumsByAlbumTitle2 ON Albums(AlbumTitle)" - "INCLUDE (MarketingBudget)" - ], + database = instance.database( + database_id, + database_dialect=DatabaseDialect.POSTGRESQL, ) - operation = spanner_client.database_admin_api.update_database_ddl(request) + operation = database.create() print("Waiting for operation to complete...") operation.result(OPERATION_TIMEOUT_SECONDS) - print("Added the AlbumsByAlbumTitle2 index.") + create_table_using_ddl(database.name) + print("Created database {} on instance {}".format(database_id, instance_id)) -# [END spanner_postgresql_create_storing_index] +def create_table_using_ddl(database_name): + spanner_client = spanner.Client() + request = spanner_admin_database_v1.UpdateDatabaseDdlRequest( + database=database_name, + statements=[ + """CREATE TABLE Singers ( + SingerId bigint NOT NULL, + FirstName character varying(1024), + LastName character varying(1024), + SingerInfo bytea, + FullName character varying(2048) + GENERATED ALWAYS AS (FirstName || ' ' || LastName) STORED, + PRIMARY KEY (SingerId) + )""", + """CREATE TABLE Albums ( + SingerId bigint NOT NULL, + AlbumId bigint NOT NULL, + AlbumTitle character varying(1024), + PRIMARY KEY (SingerId, AlbumId) + ) INTERLEAVE IN PARENT Singers ON DELETE CASCADE""", + ], + ) + operation = spanner_client.database_admin_api.update_database_ddl(request) + operation.result(OPERATION_TIMEOUT_SECONDS) + + +# [END spanner_postgresql_create_database] # [START spanner_postgresql_create_sequence] def create_sequence(instance_id, database_id): """Creates the Sequence and insert data""" - - from google.cloud.spanner_admin_database_v1.types import spanner_database_admin - spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) database = instance.database(database_id) - request = spanner_database_admin.UpdateDatabaseDdlRequest( + request = spanner_admin_database_v1.UpdateDatabaseDdlRequest( database=database.name, statements=[ "CREATE SEQUENCE Seq BIT_REVERSED_POSITIVE", @@ -272,68 +224,78 @@ def insert_customers(transaction): # [END spanner_postgresql_create_sequence] -# [START spanner_postgresql_alter_sequence] -def alter_sequence(instance_id, database_id): - """Alters the Sequence and insert data""" +# [START spanner_postgresql_create_storing_index] +def add_storing_index(instance_id, database_id): + """Adds an storing index to the example database.""" + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + database = instance.database(database_id) + + operation = database.update_ddl( + [ + "CREATE INDEX AlbumsByAlbumTitle2 ON Albums(AlbumTitle)" + "INCLUDE (MarketingBudget)" + ] + ) - from google.cloud.spanner_admin_database_v1.types import spanner_database_admin + print("Waiting for operation to complete...") + operation.result(OPERATION_TIMEOUT_SECONDS) + print("Added the AlbumsByAlbumTitle2 index.") + + +# [END spanner_postgresql_create_storing_index] + + +# [START spanner_postgresql_drop_sequence] +def drop_sequence(instance_id, database_id): + """Drops the Sequence""" spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) database = instance.database(database_id) - request = spanner_database_admin.UpdateDatabaseDdlRequest( - database=database.name, - statements=["ALTER SEQUENCE Seq SKIP RANGE 1000 5000000"], + operation = database.update_ddl( + [ + "ALTER TABLE Customers ALTER COLUMN CustomerId DROP DEFAULT", + "DROP SEQUENCE Seq", + ] ) - operation = spanner_client.database_admin_api.update_database_ddl(request) print("Waiting for operation to complete...") operation.result(OPERATION_TIMEOUT_SECONDS) print( - "Altered Seq sequence to skip an inclusive range between 1000 and 5000000 on database {} on instance {}".format( + "Altered Customers table to drop DEFAULT from CustomerId column and dropped the Seq sequence on database {} on instance {}".format( database_id, instance_id ) ) - def insert_customers(transaction): - results = transaction.execute_sql( - "INSERT INTO Customers (CustomerName) VALUES " - "('Lea'), " - "('Cataline'), " - "('Smith') " - "RETURNING CustomerId" - ) - for result in results: - print("Inserted customer record with Customer Id: {}".format(*result)) - print( - "Number of customer records inserted is {}".format( - results.stats.row_count_exact - ) - ) - - database.run_in_transaction(insert_customers) +# [END spanner_postgresql_drop_sequence] -# [END spanner_postgresql_alter_sequence] - - -# [START spanner_postgresql_drop_sequence] -def drop_sequence(instance_id, database_id): - """Drops the Sequence""" - - from google.cloud.spanner_admin_database_v1.types import spanner_database_admin +def create_table_with_datatypes(instance_id, database_id): + """Creates a table with supported datatypes.""" + # [START spanner_postgresql_create_table_with_datatypes] + # instance_id = "your-spanner-instance" + # database_id = "your-spanner-db-id" spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) database = instance.database(database_id) - request = spanner_database_admin.UpdateDatabaseDdlRequest( + request = spanner_admin_database_v1.UpdateDatabaseDdlRequest( database=database.name, statements=[ - "ALTER TABLE Customers ALTER COLUMN CustomerId DROP DEFAULT", - "DROP SEQUENCE Seq", + """CREATE TABLE Venues ( + VenueId BIGINT NOT NULL, + VenueName character varying(100), + VenueInfo BYTEA, + Capacity BIGINT, + OutdoorVenue BOOL, + PopularityScore FLOAT8, + Revenue NUMERIC, + LastUpdateTime SPANNER.COMMIT_TIMESTAMP NOT NULL, + PRIMARY KEY (VenueId))""" ], ) operation = spanner_client.database_admin_api.update_database_ddl(request) @@ -342,10 +304,8 @@ def drop_sequence(instance_id, database_id): operation.result(OPERATION_TIMEOUT_SECONDS) print( - "Altered Customers table to drop DEFAULT from CustomerId column and dropped the Seq sequence on database {} on instance {}".format( + "Created Venues table on database {} on instance {}".format( database_id, instance_id ) ) - - -# [END spanner_postgresql_drop_sequence] + # [END spanner_postgresql_create_table_with_datatypes] diff --git a/samples/samples/admin/pg_samples_test.py b/samples/samples/archived/pg_samples_test.py similarity index 100% rename from samples/samples/admin/pg_samples_test.py rename to samples/samples/archived/pg_samples_test.py diff --git a/samples/samples/admin/samples.py b/samples/samples/archived/samples.py similarity index 66% rename from samples/samples/admin/samples.py rename to samples/samples/archived/samples.py index a4119f602f..0f930d4a35 100644 --- a/samples/samples/admin/samples.py +++ b/samples/samples/archived/samples.py @@ -16,609 +16,449 @@ """This application demonstrates how to do basic operations using Cloud Spanner. + For more information, see the README.rst under /spanner. """ import time from google.cloud import spanner +from google.iam.v1 import policy_pb2 +from google.type import expr_pb2 OPERATION_TIMEOUT_SECONDS = 240 -# [START spanner_create_instance] -def create_instance(instance_id): - """Creates an instance.""" - from google.cloud.spanner_admin_instance_v1.types import spanner_instance_admin - +def add_and_drop_database_roles(instance_id, database_id): + """Showcases how to manage a user defined database role.""" + # [START spanner_add_and_drop_database_role] + # instance_id = "your-spanner-instance" + # database_id = "your-spanner-db-id" spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + database = instance.database(database_id) + role_parent = "new_parent" + role_child = "new_child" - config_name = "{}/instanceConfigs/regional-us-central1".format( - spanner_client.project_name - ) - - operation = spanner_client.instance_admin_api.create_instance( - parent=spanner_client.project_name, - instance_id=instance_id, - instance=spanner_instance_admin.Instance( - config=config_name, - display_name="This is a display name.", - node_count=1, - labels={ - "cloud_spanner_samples": "true", - "sample_name": "snippets-create_instance-explicit", - "created": str(int(time.time())), - }, - ), + operation = database.update_ddl( + [ + "CREATE ROLE {}".format(role_parent), + "GRANT SELECT ON TABLE Singers TO ROLE {}".format(role_parent), + "CREATE ROLE {}".format(role_child), + "GRANT ROLE {} TO ROLE {}".format(role_parent, role_child), + ] ) - - print("Waiting for operation to complete...") operation.result(OPERATION_TIMEOUT_SECONDS) - - print("Created instance {}".format(instance_id)) - - -# [END spanner_create_instance] - - -# [START spanner_create_instance_with_processing_units] -def create_instance_with_processing_units(instance_id, processing_units): - """Creates an instance.""" - from google.cloud.spanner_admin_instance_v1.types import spanner_instance_admin - - spanner_client = spanner.Client() - - config_name = "{}/instanceConfigs/regional-us-central1".format( - spanner_client.project_name - ) - - request = spanner_instance_admin.CreateInstanceRequest( - parent=spanner_client.project_name, - instance_id=instance_id, - instance=spanner_instance_admin.Instance( - config=config_name, - display_name="This is a display name.", - processing_units=processing_units, - labels={ - "cloud_spanner_samples": "true", - "sample_name": "snippets-create_instance_with_processing_units", - "created": str(int(time.time())), - }, - ), - ) - - operation = spanner_client.instance_admin_api.create_instance(request=request) - - print("Waiting for operation to complete...") - instance = operation.result(OPERATION_TIMEOUT_SECONDS) - print( - "Created instance {} with {} processing units".format( - instance_id, instance.processing_units - ) + "Created roles {} and {} and granted privileges".format(role_parent, role_child) ) + operation = database.update_ddl( + [ + "REVOKE ROLE {} FROM ROLE {}".format(role_parent, role_child), + "DROP ROLE {}".format(role_child), + ] + ) + operation.result(OPERATION_TIMEOUT_SECONDS) + print("Revoked privileges and dropped role {}".format(role_child)) -# [END spanner_create_instance_with_processing_units] - + # [END spanner_add_and_drop_database_role] -# [START spanner_create_database] -def create_database(instance_id, database_id): - """Creates a database and tables for sample data.""" - from google.cloud.spanner_admin_database_v1.types import spanner_database_admin +# [START spanner_add_column] +def add_column(instance_id, database_id): + """Adds a new column to the Albums table in the example database.""" spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) + database = instance.database(database_id) - request = spanner_database_admin.CreateDatabaseRequest( - parent=instance.name, - create_statement=f"CREATE DATABASE `{database_id}`", - extra_statements=[ - """CREATE TABLE Singers ( - SingerId INT64 NOT NULL, - FirstName STRING(1024), - LastName STRING(1024), - SingerInfo BYTES(MAX), - FullName STRING(2048) AS ( - ARRAY_TO_STRING([FirstName, LastName], " ") - ) STORED - ) PRIMARY KEY (SingerId)""", - """CREATE TABLE Albums ( - SingerId INT64 NOT NULL, - AlbumId INT64 NOT NULL, - AlbumTitle STRING(MAX) - ) PRIMARY KEY (SingerId, AlbumId), - INTERLEAVE IN PARENT Singers ON DELETE CASCADE""", - ], + operation = database.update_ddl( + ["ALTER TABLE Albums ADD COLUMN MarketingBudget INT64"] ) - operation = spanner_client.database_admin_api.create_database(request=request) - print("Waiting for operation to complete...") - database = operation.result(OPERATION_TIMEOUT_SECONDS) - - print("Created database {} on instance {}".format(database.name, instance.name)) - - -# [START spanner_update_database] -def update_database(instance_id, database_id): - """Updates the drop protection setting for a database.""" - from google.cloud.spanner_admin_database_v1.types import spanner_database_admin - - spanner_client = spanner.Client() - instance = spanner_client.instance(instance_id) - - request = spanner_database_admin.UpdateDatabaseRequest( - database=spanner_database_admin.Database( - name="{}/databases/{}".format(instance.name, database_id), - enable_drop_protection=True, - ), - update_mask={"paths": ["enable_drop_protection"]}, - ) - operation = spanner_client.database_admin_api.update_database(request=request) - print( - "Waiting for update operation for {}/databases/{} to complete...".format( - instance.name, database_id - ) - ) operation.result(OPERATION_TIMEOUT_SECONDS) - print("Updated database {}/databases/{}.".format(instance.name, database_id)) - + print("Added the MarketingBudget column.") -# [END spanner_update_database] -# [END spanner_create_database] +# [END spanner_add_column] -# [START spanner_create_database_with_default_leader] -def create_database_with_default_leader(instance_id, database_id, default_leader): - """Creates a database with tables with a default leader.""" - from google.cloud.spanner_admin_database_v1.types import spanner_database_admin - +# [START spanner_add_json_column] +def add_json_column(instance_id, database_id): + """Adds a new JSON column to the Venues table in the example database.""" spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) - operation = spanner_client.database_admin_api.create_database( - request=spanner_database_admin.CreateDatabaseRequest( - parent=instance.name, - create_statement=f"CREATE DATABASE `{database_id}`", - extra_statements=[ - """CREATE TABLE Singers ( - SingerId INT64 NOT NULL, - FirstName STRING(1024), - LastName STRING(1024), - SingerInfo BYTES(MAX) - ) PRIMARY KEY (SingerId)""", - """CREATE TABLE Albums ( - SingerId INT64 NOT NULL, - AlbumId INT64 NOT NULL, - AlbumTitle STRING(MAX) - ) PRIMARY KEY (SingerId, AlbumId), - INTERLEAVE IN PARENT Singers ON DELETE CASCADE""", - "ALTER DATABASE {}" - " SET OPTIONS (default_leader = '{}')".format( - database_id, default_leader - ), - ], - ) - ) + database = instance.database(database_id) + + operation = database.update_ddl(["ALTER TABLE Venues ADD COLUMN VenueDetails JSON"]) print("Waiting for operation to complete...") - database = operation.result(OPERATION_TIMEOUT_SECONDS) + operation.result(OPERATION_TIMEOUT_SECONDS) print( - "Database {} created with default leader {}".format( - database.name, database.default_leader + 'Altered table "Venues" on database {} on instance {}.'.format( + database_id, instance_id ) ) -# [END spanner_create_database_with_default_leader] +# [END spanner_add_json_column] -# [START spanner_update_database_with_default_leader] -def update_database_with_default_leader(instance_id, database_id, default_leader): - """Updates a database with tables with a default leader.""" - from google.cloud.spanner_admin_database_v1.types import spanner_database_admin - +# [START spanner_add_numeric_column] +def add_numeric_column(instance_id, database_id): + """Adds a new NUMERIC column to the Venues table in the example database.""" spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) + database = instance.database(database_id) - request = spanner_database_admin.UpdateDatabaseDdlRequest( - database=database.name, - statements=[ - "ALTER DATABASE {}" - " SET OPTIONS (default_leader = '{}')".format(database_id, default_leader) - ], - ) - operation = spanner_client.database_admin_api.update_database_ddl(request) + operation = database.update_ddl(["ALTER TABLE Venues ADD COLUMN Revenue NUMERIC"]) + print("Waiting for operation to complete...") operation.result(OPERATION_TIMEOUT_SECONDS) print( - "Database {} updated with default leader {}".format(database_id, default_leader) + 'Altered table "Venues" on database {} on instance {}.'.format( + database_id, instance_id + ) ) -# [END spanner_update_database_with_default_leader] - +# [END spanner_add_numeric_column] -# [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).""" - from google.cloud.spanner_admin_database_v1.types import spanner_database_admin - from google.cloud.spanner_admin_database_v1 import EncryptionConfig +# [START spanner_add_timestamp_column] +def add_timestamp_column(instance_id, database_id): + """Adds a new TIMESTAMP column to the Albums table in the example database.""" spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) - request = spanner_database_admin.CreateDatabaseRequest( - parent=instance.name, - create_statement=f"CREATE DATABASE `{database_id}`", - extra_statements=[ - """CREATE TABLE Singers ( - SingerId INT64 NOT NULL, - FirstName STRING(1024), - LastName STRING(1024), - SingerInfo BYTES(MAX) - ) PRIMARY KEY (SingerId)""", - """CREATE TABLE Albums ( - SingerId INT64 NOT NULL, - AlbumId INT64 NOT NULL, - AlbumTitle STRING(MAX) - ) PRIMARY KEY (SingerId, AlbumId), - INTERLEAVE IN PARENT Singers ON DELETE CASCADE""", - ], - encryption_config=EncryptionConfig(kms_key_name=kms_key_name), - ) + database = instance.database(database_id) - operation = spanner_client.database_admin_api.create_database(request=request) + operation = database.update_ddl( + [ + "ALTER TABLE Albums ADD COLUMN LastUpdateTime TIMESTAMP " + "OPTIONS(allow_commit_timestamp=true)" + ] + ) print("Waiting for operation to complete...") - database = operation.result(OPERATION_TIMEOUT_SECONDS) + operation.result(OPERATION_TIMEOUT_SECONDS) print( - "Database {} created with encryption key {}".format( - database.name, database.encryption_config.kms_key_name + 'Altered table "Albums" on database {} on instance {}.'.format( + database_id, instance_id ) ) -# [END spanner_create_database_with_encryption_key] - - -def add_and_drop_database_roles(instance_id, database_id): - """Showcases how to manage a user defined database role.""" - # [START spanner_add_and_drop_database_role] - # instance_id = "your-spanner-instance" - # database_id = "your-spanner-db-id" +# [END spanner_add_timestamp_column] - from google.cloud.spanner_admin_database_v1.types import spanner_database_admin +# [START spanner_alter_sequence] +def alter_sequence(instance_id, database_id): + """Alters the Sequence and insert data""" spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) database = instance.database(database_id) - role_parent = "new_parent" - role_child = "new_child" - - request = spanner_database_admin.UpdateDatabaseDdlRequest( - database=database.name, - statements=[ - "CREATE ROLE {}".format(role_parent), - "GRANT SELECT ON TABLE Singers TO ROLE {}".format(role_parent), - "CREATE ROLE {}".format(role_child), - "GRANT ROLE {} TO ROLE {}".format(role_parent, role_child), - ], + operation = database.update_ddl( + [ + "ALTER SEQUENCE Seq SET OPTIONS (skip_range_min = 1000, skip_range_max = 5000000)" + ] ) - operation = spanner_client.database_admin_api.update_database_ddl(request) + print("Waiting for operation to complete...") operation.result(OPERATION_TIMEOUT_SECONDS) - print( - "Created roles {} and {} and granted privileges".format(role_parent, role_child) - ) - request = spanner_database_admin.UpdateDatabaseDdlRequest( - database=database.name, - statements=[ - "REVOKE ROLE {} FROM ROLE {}".format(role_parent, role_child), - "DROP ROLE {}".format(role_child), - ], + print( + "Altered Seq sequence to skip an inclusive range between 1000 and 5000000 on database {} on instance {}".format( + database_id, instance_id + ) ) - operation = spanner_client.database_admin_api.update_database_ddl(request) - operation.result(OPERATION_TIMEOUT_SECONDS) - print("Revoked privileges and dropped role {}".format(role_child)) + def insert_customers(transaction): + results = transaction.execute_sql( + "INSERT INTO Customers (CustomerName) VALUES " + "('Lea'), " + "('Cataline'), " + "('Smith') " + "THEN RETURN CustomerId" + ) + for result in results: + print("Inserted customer record with Customer Id: {}".format(*result)) + print( + "Number of customer records inserted is {}".format( + results.stats.row_count_exact + ) + ) - # [END spanner_add_and_drop_database_role] + database.run_in_transaction(insert_customers) -def create_table_with_datatypes(instance_id, database_id): - """Creates a table with supported datatypes.""" - # [START spanner_create_table_with_datatypes] - # instance_id = "your-spanner-instance" - # database_id = "your-spanner-db-id" +# [END spanner_alter_sequence] - from google.cloud.spanner_admin_database_v1.types import spanner_database_admin +# [START spanner_alter_table_with_foreign_key_delete_cascade] +def alter_table_with_foreign_key_delete_cascade(instance_id, database_id): + """Alters a table with foreign key delete cascade action""" spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) database = instance.database(database_id) - request = spanner_database_admin.UpdateDatabaseDdlRequest( - database=database.name, - statements=[ - """CREATE TABLE Venues ( - VenueId INT64 NOT NULL, - VenueName STRING(100), - VenueInfo BYTES(MAX), - Capacity INT64, - AvailableDates ARRAY, - LastContactDate DATE, - OutdoorVenue BOOL, - PopularityScore FLOAT64, - LastUpdateTime TIMESTAMP NOT NULL - OPTIONS(allow_commit_timestamp=true) - ) PRIMARY KEY (VenueId)""" - ], + operation = database.update_ddl( + [ + """ALTER TABLE ShoppingCarts + ADD CONSTRAINT FKShoppingCartsCustomerName + FOREIGN KEY (CustomerName) + REFERENCES Customers(CustomerName) + ON DELETE CASCADE""" + ] ) - operation = spanner_client.database_admin_api.update_database_ddl(request) print("Waiting for operation to complete...") operation.result(OPERATION_TIMEOUT_SECONDS) print( - "Created Venues table on database {} on instance {}".format( + """Altered ShoppingCarts table with FKShoppingCartsCustomerName + foreign key constraint on database {} on instance {}""".format( database_id, instance_id ) ) - # [END spanner_create_table_with_datatypes] -# [START spanner_add_json_column] -def add_json_column(instance_id, database_id): - """Adds a new JSON column to the Venues table in the example database.""" - # instance_id = "your-spanner-instance" - # database_id = "your-spanner-db-id" +# [END spanner_alter_table_with_foreign_key_delete_cascade] - from google.cloud.spanner_admin_database_v1.types import spanner_database_admin +# [START spanner_create_database] +def create_database(instance_id, database_id): + """Creates a database and tables for sample data.""" spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) - database = instance.database(database_id) - request = spanner_database_admin.UpdateDatabaseDdlRequest( - database=database.name, - statements=["ALTER TABLE Venues ADD COLUMN VenueDetails JSON"], + database = instance.database( + database_id, + ddl_statements=[ + """CREATE TABLE Singers ( + SingerId INT64 NOT NULL, + FirstName STRING(1024), + LastName STRING(1024), + SingerInfo BYTES(MAX), + FullName STRING(2048) AS ( + ARRAY_TO_STRING([FirstName, LastName], " ") + ) STORED + ) PRIMARY KEY (SingerId)""", + """CREATE TABLE Albums ( + SingerId INT64 NOT NULL, + AlbumId INT64 NOT NULL, + AlbumTitle STRING(MAX) + ) PRIMARY KEY (SingerId, AlbumId), + INTERLEAVE IN PARENT Singers ON DELETE CASCADE""", + ], ) - operation = spanner_client.database_admin_api.update_database_ddl(request) + operation = database.create() print("Waiting for operation to complete...") operation.result(OPERATION_TIMEOUT_SECONDS) - print( - 'Altered table "Venues" on database {} on instance {}.'.format( - database_id, instance_id - ) - ) - - -# [END spanner_add_json_column] + print("Created database {} on instance {}".format(database_id, instance_id)) -# [START spanner_add_numeric_column] -def add_numeric_column(instance_id, database_id): - """Adds a new NUMERIC column to the Venues table in the example database.""" +# [END spanner_create_database] - from google.cloud.spanner_admin_database_v1.types import spanner_database_admin +# [START spanner_create_database_with_default_leader] +def create_database_with_default_leader(instance_id, database_id, default_leader): + """Creates a database with tables with a default leader.""" spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) - database = instance.database(database_id) - request = spanner_database_admin.UpdateDatabaseDdlRequest( - database=database.name, - statements=["ALTER TABLE Venues ADD COLUMN Revenue NUMERIC"], + database = instance.database( + database_id, + ddl_statements=[ + """CREATE TABLE Singers ( + SingerId INT64 NOT NULL, + FirstName STRING(1024), + LastName STRING(1024), + SingerInfo BYTES(MAX) + ) PRIMARY KEY (SingerId)""", + """CREATE TABLE Albums ( + SingerId INT64 NOT NULL, + AlbumId INT64 NOT NULL, + AlbumTitle STRING(MAX) + ) PRIMARY KEY (SingerId, AlbumId), + INTERLEAVE IN PARENT Singers ON DELETE CASCADE""", + "ALTER DATABASE {}" + " SET OPTIONS (default_leader = '{}')".format(database_id, default_leader), + ], ) - - operation = spanner_client.database_admin_api.update_database_ddl(request) + operation = database.create() print("Waiting for operation to complete...") operation.result(OPERATION_TIMEOUT_SECONDS) + database.reload() + print( - 'Altered table "Venues" on database {} on instance {}.'.format( - database_id, instance_id + "Database {} created with default leader {}".format( + database.name, database.default_leader ) ) -# [END spanner_add_numeric_column] - - -# [START spanner_create_table_with_timestamp_column] -def create_table_with_timestamp(instance_id, database_id): - """Creates a table with a COMMIT_TIMESTAMP column.""" +# [END spanner_create_database_with_default_leader] - from google.cloud.spanner_admin_database_v1.types import spanner_database_admin +# [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).""" spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) - database = instance.database(database_id) - request = spanner_database_admin.UpdateDatabaseDdlRequest( - database=database.name, - statements=[ - """CREATE TABLE Performances ( + database = instance.database( + database_id, + ddl_statements=[ + """CREATE TABLE Singers ( SingerId INT64 NOT NULL, - VenueId INT64 NOT NULL, - EventDate Date, - Revenue INT64, - LastUpdateTime TIMESTAMP NOT NULL - OPTIONS(allow_commit_timestamp=true) - ) PRIMARY KEY (SingerId, VenueId, EventDate), - INTERLEAVE IN PARENT Singers ON DELETE CASCADE""" + FirstName STRING(1024), + LastName STRING(1024), + SingerInfo BYTES(MAX) + ) PRIMARY KEY (SingerId)""", + """CREATE TABLE Albums ( + SingerId INT64 NOT NULL, + AlbumId INT64 NOT NULL, + AlbumTitle STRING(MAX) + ) PRIMARY KEY (SingerId, AlbumId), + INTERLEAVE IN PARENT Singers ON DELETE CASCADE""", ], + encryption_config={"kms_key_name": kms_key_name}, ) - operation = spanner_client.database_admin_api.update_database_ddl(request) + operation = database.create() print("Waiting for operation to complete...") operation.result(OPERATION_TIMEOUT_SECONDS) print( - "Created Performances table on database {} on instance {}".format( - database_id, instance_id + "Database {} created with encryption key {}".format( + database.name, database.encryption_config.kms_key_name ) ) -# [END spanner_create_table_with_timestamp_column] - - -# [START spanner_create_table_with_foreign_key_delete_cascade] -def create_table_with_foreign_key_delete_cascade(instance_id, database_id): - """Creates a table with foreign key delete cascade action""" +# [END spanner_create_database_with_encryption_key] - from google.cloud.spanner_admin_database_v1.types import spanner_database_admin +# [START spanner_create_index] +def add_index(instance_id, database_id): + """Adds a simple index to the example database.""" spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) database = instance.database(database_id) - request = spanner_database_admin.UpdateDatabaseDdlRequest( - database=database.name, - statements=[ - """CREATE TABLE Customers ( - CustomerId INT64 NOT NULL, - CustomerName STRING(62) NOT NULL, - ) PRIMARY KEY (CustomerId) - """, - """ - CREATE TABLE ShoppingCarts ( - CartId INT64 NOT NULL, - CustomerId INT64 NOT NULL, - CustomerName STRING(62) NOT NULL, - CONSTRAINT FKShoppingCartsCustomerId FOREIGN KEY (CustomerId) - REFERENCES Customers (CustomerId) ON DELETE CASCADE - ) PRIMARY KEY (CartId) - """, - ], + operation = database.update_ddl( + ["CREATE INDEX AlbumsByAlbumTitle ON Albums(AlbumTitle)"] ) - operation = spanner_client.database_admin_api.update_database_ddl(request) - print("Waiting for operation to complete...") operation.result(OPERATION_TIMEOUT_SECONDS) - print( - """Created Customers and ShoppingCarts table with FKShoppingCartsCustomerId - foreign key constraint on database {} on instance {}""".format( - database_id, instance_id - ) - ) - - -# [END spanner_create_table_with_foreign_key_delete_cascade] + print("Added the AlbumsByAlbumTitle index.") -# [START spanner_alter_table_with_foreign_key_delete_cascade] -def alter_table_with_foreign_key_delete_cascade(instance_id, database_id): - """Alters a table with foreign key delete cascade action""" +# [END spanner_create_index] - from google.cloud.spanner_admin_database_v1.types import spanner_database_admin +# [START spanner_create_instance] +def create_instance(instance_id): + """Creates an instance.""" spanner_client = spanner.Client() - instance = spanner_client.instance(instance_id) - database = instance.database(database_id) - request = spanner_database_admin.UpdateDatabaseDdlRequest( - database=database.name, - statements=[ - """ALTER TABLE ShoppingCarts - ADD CONSTRAINT FKShoppingCartsCustomerName - FOREIGN KEY (CustomerName) - REFERENCES Customers(CustomerName) - ON DELETE CASCADE""" - ], + config_name = "{}/instanceConfigs/regional-us-central1".format( + spanner_client.project_name ) - operation = spanner_client.database_admin_api.update_database_ddl(request) + instance = spanner_client.instance( + instance_id, + configuration_name=config_name, + display_name="This is a display name.", + node_count=1, + labels={ + "cloud_spanner_samples": "true", + "sample_name": "snippets-create_instance-explicit", + "created": str(int(time.time())), + }, + ) + + operation = instance.create() print("Waiting for operation to complete...") operation.result(OPERATION_TIMEOUT_SECONDS) - print( - """Altered ShoppingCarts table with FKShoppingCartsCustomerName - foreign key constraint on database {} on instance {}""".format( - database_id, instance_id - ) - ) - - -# [END spanner_alter_table_with_foreign_key_delete_cascade] + print("Created instance {}".format(instance_id)) -# [START spanner_drop_foreign_key_constraint_delete_cascade] -def drop_foreign_key_constraint_delete_cascade(instance_id, database_id): - """Alter table to drop foreign key delete cascade action""" +# [END spanner_create_instance] - from google.cloud.spanner_admin_database_v1.types import spanner_database_admin - spanner_client = spanner.Client() - instance = spanner_client.instance(instance_id) - database = instance.database(database_id) +# [START spanner_create_instance_with_processing_units] +def create_instance_with_processing_units(instance_id, processing_units): + """Creates an instance.""" + spanner_client = spanner.Client() - request = spanner_database_admin.UpdateDatabaseDdlRequest( - database=database.name, - statements=[ - """ALTER TABLE ShoppingCarts - DROP CONSTRAINT FKShoppingCartsCustomerName""" - ], + config_name = "{}/instanceConfigs/regional-us-central1".format( + spanner_client.project_name + ) + + instance = spanner_client.instance( + instance_id, + configuration_name=config_name, + display_name="This is a display name.", + processing_units=processing_units, + labels={ + "cloud_spanner_samples": "true", + "sample_name": "snippets-create_instance_with_processing_units", + "created": str(int(time.time())), + }, ) - operation = spanner_client.database_admin_api.update_database_ddl(request) + operation = instance.create() print("Waiting for operation to complete...") operation.result(OPERATION_TIMEOUT_SECONDS) print( - """Altered ShoppingCarts table to drop FKShoppingCartsCustomerName - foreign key constraint on database {} on instance {}""".format( - database_id, instance_id + "Created instance {} with {} processing units".format( + instance_id, instance.processing_units ) ) -# [END spanner_drop_foreign_key_constraint_delete_cascade] +# [END spanner_create_instance_with_processing_units] # [START spanner_create_sequence] def create_sequence(instance_id, database_id): """Creates the Sequence and insert data""" - - from google.cloud.spanner_admin_database_v1.types import spanner_database_admin - spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) database = instance.database(database_id) - request = spanner_database_admin.UpdateDatabaseDdlRequest( - database=database.name, - statements=[ + operation = database.update_ddl( + [ "CREATE SEQUENCE Seq OPTIONS (sequence_kind = 'bit_reversed_positive')", """CREATE TABLE Customers ( CustomerId INT64 DEFAULT (GET_NEXT_SEQUENCE_VALUE(Sequence Seq)), CustomerName STRING(1024) ) PRIMARY KEY (CustomerId)""", - ], + ] ) - operation = spanner_client.database_admin_api.update_database_ddl(request) - print("Waiting for operation to complete...") operation.result(OPERATION_TIMEOUT_SECONDS) @@ -650,203 +490,194 @@ def insert_customers(transaction): # [END spanner_create_sequence] -# [START spanner_alter_sequence] -def alter_sequence(instance_id, database_id): - """Alters the Sequence and insert data""" - - from google.cloud.spanner_admin_database_v1.types import spanner_database_admin - +# [START spanner_create_storing_index] +def add_storing_index(instance_id, database_id): + """Adds an storing index to the example database.""" spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) database = instance.database(database_id) - request = spanner_database_admin.UpdateDatabaseDdlRequest( - database=database.name, - statements=[ - "ALTER SEQUENCE Seq SET OPTIONS (skip_range_min = 1000, skip_range_max = 5000000)", - ], + operation = database.update_ddl( + [ + "CREATE INDEX AlbumsByAlbumTitle2 ON Albums(AlbumTitle)" + "STORING (MarketingBudget)" + ] ) - operation = spanner_client.database_admin_api.update_database_ddl(request) - print("Waiting for operation to complete...") operation.result(OPERATION_TIMEOUT_SECONDS) - print( - "Altered Seq sequence to skip an inclusive range between 1000 and 5000000 on database {} on instance {}".format( - database_id, instance_id - ) - ) - - def insert_customers(transaction): - results = transaction.execute_sql( - "INSERT INTO Customers (CustomerName) VALUES " - "('Lea'), " - "('Cataline'), " - "('Smith') " - "THEN RETURN CustomerId" - ) - for result in results: - print("Inserted customer record with Customer Id: {}".format(*result)) - print( - "Number of customer records inserted is {}".format( - results.stats.row_count_exact - ) - ) - - database.run_in_transaction(insert_customers) - - -# [END spanner_alter_sequence] + print("Added the AlbumsByAlbumTitle2 index.") -# [START spanner_drop_sequence] -def drop_sequence(instance_id, database_id): - """Drops the Sequence""" +# [END spanner_create_storing_index] - from google.cloud.spanner_admin_database_v1.types import spanner_database_admin +def create_table_with_datatypes(instance_id, database_id): + """Creates a table with supported datatypes.""" + # [START spanner_create_table_with_datatypes] + # instance_id = "your-spanner-instance" + # database_id = "your-spanner-db-id" spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) database = instance.database(database_id) - request = spanner_database_admin.UpdateDatabaseDdlRequest( - database=database.name, - statements=[ - "ALTER TABLE Customers ALTER COLUMN CustomerId DROP DEFAULT", - "DROP SEQUENCE Seq", - ], + operation = database.update_ddl( + [ + """CREATE TABLE Venues ( + VenueId INT64 NOT NULL, + VenueName STRING(100), + VenueInfo BYTES(MAX), + Capacity INT64, + AvailableDates ARRAY, + LastContactDate DATE, + OutdoorVenue BOOL, + PopularityScore FLOAT64, + LastUpdateTime TIMESTAMP NOT NULL + OPTIONS(allow_commit_timestamp=true) + ) PRIMARY KEY (VenueId)""" + ] ) - operation = spanner_client.database_admin_api.update_database_ddl(request) - print("Waiting for operation to complete...") operation.result(OPERATION_TIMEOUT_SECONDS) print( - "Altered Customers table to drop DEFAULT from CustomerId column and dropped the Seq sequence on database {} on instance {}".format( + "Created Venues table on database {} on instance {}".format( database_id, instance_id ) ) + # [END spanner_create_table_with_datatypes] -# [END spanner_drop_sequence] - - -# [START spanner_add_column] -def add_column(instance_id, database_id): - """Adds a new column to the Albums table in the example database.""" - - from google.cloud.spanner_admin_database_v1.types import spanner_database_admin - +# [START spanner_create_table_with_foreign_key_delete_cascade] +def create_table_with_foreign_key_delete_cascade(instance_id, database_id): + """Creates a table with foreign key delete cascade action""" spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) database = instance.database(database_id) - request = spanner_database_admin.UpdateDatabaseDdlRequest( - database=database.name, - statements=[ - "ALTER TABLE Albums ADD COLUMN MarketingBudget INT64", - ], + operation = database.update_ddl( + [ + """CREATE TABLE Customers ( + CustomerId INT64 NOT NULL, + CustomerName STRING(62) NOT NULL, + ) PRIMARY KEY (CustomerId) + """, + """ + CREATE TABLE ShoppingCarts ( + CartId INT64 NOT NULL, + CustomerId INT64 NOT NULL, + CustomerName STRING(62) NOT NULL, + CONSTRAINT FKShoppingCartsCustomerId FOREIGN KEY (CustomerId) + REFERENCES Customers (CustomerId) ON DELETE CASCADE + ) PRIMARY KEY (CartId) + """, + ] ) - operation = spanner_client.database_admin_api.update_database_ddl(request) - print("Waiting for operation to complete...") operation.result(OPERATION_TIMEOUT_SECONDS) - print("Added the MarketingBudget column.") + print( + """Created Customers and ShoppingCarts table with FKShoppingCartsCustomerId + foreign key constraint on database {} on instance {}""".format( + database_id, instance_id + ) + ) -# [END spanner_add_column] +# [END spanner_create_table_with_foreign_key_delete_cascade] -# [START spanner_add_timestamp_column] -def add_timestamp_column(instance_id, database_id): - """Adds a new TIMESTAMP column to the Albums table in the example database.""" - from google.cloud.spanner_admin_database_v1.types import spanner_database_admin +# [START spanner_create_table_with_timestamp_column] +def create_table_with_timestamp(instance_id, database_id): + """Creates a table with a COMMIT_TIMESTAMP column.""" spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) - database = instance.database(database_id) - request = spanner_database_admin.UpdateDatabaseDdlRequest( - database=database.name, - statements=[ - "ALTER TABLE Albums ADD COLUMN LastUpdateTime TIMESTAMP " - "OPTIONS(allow_commit_timestamp=true)" - ], + operation = database.update_ddl( + [ + """CREATE TABLE Performances ( + SingerId INT64 NOT NULL, + VenueId INT64 NOT NULL, + EventDate Date, + Revenue INT64, + LastUpdateTime TIMESTAMP NOT NULL + OPTIONS(allow_commit_timestamp=true) + ) PRIMARY KEY (SingerId, VenueId, EventDate), + INTERLEAVE IN PARENT Singers ON DELETE CASCADE""" + ] ) - operation = spanner_client.database_admin_api.update_database_ddl(request) - print("Waiting for operation to complete...") operation.result(OPERATION_TIMEOUT_SECONDS) print( - 'Altered table "Albums" on database {} on instance {}.'.format( + "Created Performances table on database {} on instance {}".format( database_id, instance_id ) ) -# [END spanner_add_timestamp_column] - - -# [START spanner_create_index] -def add_index(instance_id, database_id): - """Adds a simple index to the example database.""" +# [END spanner_create_table_with_timestamp_column] - from google.cloud.spanner_admin_database_v1.types import spanner_database_admin +# [START spanner_drop_foreign_key_constraint_delete_cascade] +def drop_foreign_key_constraint_delete_cascade(instance_id, database_id): + """Alter table to drop foreign key delete cascade action""" spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) database = instance.database(database_id) - request = spanner_database_admin.UpdateDatabaseDdlRequest( - database=database.name, - statements=["CREATE INDEX AlbumsByAlbumTitle ON Albums(AlbumTitle)"], + operation = database.update_ddl( + [ + """ALTER TABLE ShoppingCarts + DROP CONSTRAINT FKShoppingCartsCustomerName""" + ] ) - operation = spanner_client.database_admin_api.update_database_ddl(request) - print("Waiting for operation to complete...") operation.result(OPERATION_TIMEOUT_SECONDS) - print("Added the AlbumsByAlbumTitle index.") - - -# [END spanner_create_index] + print( + """Altered ShoppingCarts table to drop FKShoppingCartsCustomerName + foreign key constraint on database {} on instance {}""".format( + database_id, instance_id + ) + ) -# [START spanner_create_storing_index] -def add_storing_index(instance_id, database_id): - """Adds an storing index to the example database.""" +# [END spanner_drop_foreign_key_constraint_delete_cascade] - from google.cloud.spanner_admin_database_v1.types import spanner_database_admin +# [START spanner_drop_sequence] +def drop_sequence(instance_id, database_id): + """Drops the Sequence""" spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) database = instance.database(database_id) - request = spanner_database_admin.UpdateDatabaseDdlRequest( - database=database.name, - statements=[ - "CREATE INDEX AlbumsByAlbumTitle2 ON Albums(AlbumTitle)" - "STORING (MarketingBudget)" - ], + operation = database.update_ddl( + [ + "ALTER TABLE Customers ALTER COLUMN CustomerId DROP DEFAULT", + "DROP SEQUENCE Seq", + ] ) - operation = spanner_client.database_admin_api.update_database_ddl(request) - print("Waiting for operation to complete...") operation.result(OPERATION_TIMEOUT_SECONDS) - print("Added the AlbumsByAlbumTitle2 index.") + print( + "Altered Customers table to drop DEFAULT from CustomerId column and dropped the Seq sequence on database {} on instance {}".format( + database_id, instance_id + ) + ) -# [END spanner_create_storing_index] +# [END spanner_drop_sequence] def enable_fine_grained_access( @@ -863,12 +694,6 @@ def enable_fine_grained_access( # iam_member = "user:alice@example.com" # database_role = "new_parent" # title = "condition title" - - from google.type import expr_pb2 - from google.iam.v1 import iam_policy_pb2 - from google.iam.v1 import options_pb2 - from google.iam.v1 import policy_pb2 - spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) database = instance.database(database_id) @@ -877,11 +702,7 @@ def enable_fine_grained_access( # that you specified, or it might use a lower policy version. For example, if you # specify version 3, but the policy has no conditional role bindings, the response # uses version 1. Valid values are 0, 1, and 3. - request = iam_policy_pb2.GetIamPolicyRequest( - resource=database.name, - options=options_pb2.GetPolicyOptions(requested_policy_version=3), - ) - policy = spanner_client.database_admin_api.get_iam_policy(request=request) + policy = database.get_iam_policy(3) if policy.version < 3: policy.version = 3 @@ -896,14 +717,108 @@ def enable_fine_grained_access( policy.version = 3 policy.bindings.append(new_binding) - set_request = iam_policy_pb2.SetIamPolicyRequest( - resource=database.name, - policy=policy, - ) - spanner_client.database_admin_api.set_iam_policy(set_request) + database.set_iam_policy(policy) - new_policy = spanner_client.database_admin_api.get_iam_policy(request=request) + new_policy = database.get_iam_policy(3) print( f"Enabled fine-grained access in IAM. New policy has version {new_policy.version}" ) # [END spanner_enable_fine_grained_access] + + +def list_database_roles(instance_id, database_id): + """Showcases how to list Database Roles.""" + # [START spanner_list_database_roles] + # instance_id = "your-spanner-instance" + # database_id = "your-spanner-db-id" + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + database = instance.database(database_id) + + # List database roles. + print("Database Roles are:") + for role in database.list_database_roles(): + print(role.name.split("/")[-1]) + # [END spanner_list_database_roles] + + +# [START spanner_list_databases] +def list_databases(instance_id): + """Lists databases and their leader options.""" + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + + databases = list(instance.list_databases()) + for database in databases: + print( + "Database {} has default leader {}".format( + database.name, database.default_leader + ) + ) + + +# [END spanner_list_databases] + + +# [START spanner_list_instance_configs] +def list_instance_config(): + """Lists the available instance configurations.""" + spanner_client = spanner.Client() + configs = spanner_client.list_instance_configs() + for config in configs: + print( + "Available leader options for instance config {}: {}".format( + config.name, config.leader_options + ) + ) + + +# [END spanner_list_instance_configs] + + +# [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_update_database_with_default_leader] +def update_database_with_default_leader(instance_id, database_id, default_leader): + """Updates a database with tables with a default leader.""" + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + + database = instance.database(database_id) + + operation = database.update_ddl( + [ + "ALTER DATABASE {}" + " SET OPTIONS (default_leader = '{}')".format(database_id, default_leader) + ] + ) + operation.result(OPERATION_TIMEOUT_SECONDS) + + database.reload() + + print( + "Database {} updated with default leader {}".format( + database.name, database.default_leader + ) + ) + + +# [END spanner_update_database_with_default_leader] diff --git a/samples/samples/admin/samples_test.py b/samples/samples/archived/samples_test.py similarity index 95% rename from samples/samples/admin/samples_test.py rename to samples/samples/archived/samples_test.py index 959c2f48fc..6435dc5311 100644 --- a/samples/samples/admin/samples_test.py +++ b/samples/samples/archived/samples_test.py @@ -206,6 +206,12 @@ def test_update_database(capsys, instance_id, sample_database): op.result() +def test_list_databases(capsys, instance_id): + samples.list_databases(instance_id) + out, _ = capsys.readouterr() + assert "has default leader" in out + + @pytest.mark.dependency( name="add_and_drop_database_roles", depends=["create_table_with_datatypes"] ) @@ -216,6 +222,19 @@ def test_add_and_drop_database_roles(capsys, instance_id, sample_database): assert "Revoked privileges and dropped role new_child" in out +@pytest.mark.dependency(depends=["add_and_drop_database_roles"]) +def test_list_database_roles(capsys, instance_id, sample_database): + samples.list_database_roles(instance_id, sample_database.database_id) + out, _ = capsys.readouterr() + assert "new_parent" in out + + +def test_list_instance_config(capsys): + samples.list_instance_config() + out, _ = capsys.readouterr() + assert "regional-us-central1" in out + + @pytest.mark.dependency(name="create_table_with_datatypes") def test_create_table_with_datatypes(capsys, instance_id, sample_database): samples.create_table_with_datatypes(instance_id, sample_database.database_id) diff --git a/samples/samples/autocommit_test.py b/samples/samples/autocommit_test.py index 8150058f1c..a22f74e6b4 100644 --- a/samples/samples/autocommit_test.py +++ b/samples/samples/autocommit_test.py @@ -4,8 +4,8 @@ # license that can be found in the LICENSE file or at # https://developers.google.com/open-source/licenses/bsd -from google.api_core.exceptions import Aborted import pytest +from google.api_core.exceptions import Aborted from test_utils.retry import RetryErrors import autocommit diff --git a/samples/samples/backup_sample.py b/samples/samples/backup_sample.py index 01d3e4bf60..d72dde87a6 100644 --- a/samples/samples/backup_sample.py +++ b/samples/samples/backup_sample.py @@ -19,35 +19,46 @@ """ import argparse -from datetime import datetime, timedelta import time +from datetime import datetime, timedelta +from google.api_core import protobuf_helpers from google.cloud import spanner +from google.cloud.exceptions import NotFound # [START spanner_create_backup] def create_backup(instance_id, database_id, backup_id, version_time): """Creates a backup for a database.""" + + from google.cloud.spanner_admin_database_v1.types import \ + backup as backup_pb + spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) database = instance.database(database_id) # Create a backup expire_time = datetime.utcnow() + timedelta(days=14) - backup = instance.backup( - backup_id, database=database, expire_time=expire_time, version_time=version_time + + request = backup_pb.CreateBackupRequest( + parent=instance.name, + backup_id=backup_id, + backup=backup_pb.Backup( + database=database.name, + expire_time=expire_time, + version_time=version_time, + ), ) - operation = backup.create() + + operation = spanner_client.database_admin_api.create_backup(request) # Wait for backup operation to complete. - operation.result(2100) + backup = operation.result(2100) # Verify that the backup is ready. - backup.reload() - assert backup.is_ready() is True + assert backup.state == backup_pb.Backup.State.READY - # Get the name, create time and backup size. - backup.reload() print( "Backup {} of size {} bytes was created at {} for version of database at {}".format( backup.name, backup.size_bytes, backup.create_time, backup.version_time @@ -57,12 +68,17 @@ def create_backup(instance_id, database_id, backup_id, version_time): # [END spanner_create_backup] + # [START spanner_create_backup_with_encryption_key] def create_backup_with_encryption_key( instance_id, database_id, backup_id, kms_key_name ): """Creates a backup for a database using a Customer Managed Encryption Key (CMEK).""" - from google.cloud.spanner_admin_database_v1 import CreateBackupEncryptionConfig + + from google.cloud.spanner_admin_database_v1 import \ + CreateBackupEncryptionConfig + from google.cloud.spanner_admin_database_v1.types import \ + backup as backup_pb spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) @@ -74,23 +90,24 @@ def create_backup_with_encryption_key( "encryption_type": CreateBackupEncryptionConfig.EncryptionType.CUSTOMER_MANAGED_ENCRYPTION, "kms_key_name": kms_key_name, } - backup = instance.backup( - backup_id, - database=database, - expire_time=expire_time, + request = backup_pb.CreateBackupRequest( + parent=instance.name, + backup_id=backup_id, + backup=backup_pb.Backup( + database=database.name, + expire_time=expire_time, + ), encryption_config=encryption_config, ) - operation = backup.create() + operation = spanner_client.database_admin_api.create_backup(request) # Wait for backup operation to complete. - operation.result(2100) + backup = operation.result(2100) # Verify that the backup is ready. - backup.reload() - assert backup.is_ready() is True + assert backup.state == backup_pb.Backup.State.READY # Get the name, create time, backup size and encryption key. - backup.reload() print( "Backup {} of size {} bytes was created at {} using encryption key {}".format( backup.name, backup.size_bytes, backup.create_time, kms_key_name @@ -104,21 +121,24 @@ def create_backup_with_encryption_key( # [START spanner_restore_backup] def restore_database(instance_id, new_database_id, backup_id): """Restores a database from a backup.""" + from google.cloud.spanner_admin_database_v1 import RestoreDatabaseRequest + spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) - # Create a backup on database_id. # Start restoring an existing backup to a new database. - backup = instance.backup(backup_id) - new_database = instance.database(new_database_id) - operation = new_database.restore(backup) + request = RestoreDatabaseRequest( + parent=instance.name, + database_id=new_database_id, + backup="{}/backups/{}".format(instance.name, backup_id), + ) + operation = spanner_client.database_admin_api.restore_database(request) # Wait for restore operation to complete. - operation.result(1600) + db = operation.result(1600) # Newly created database has restore information. - new_database.reload() - restore_info = new_database.restore_info + restore_info = db.restore_info print( "Database {} restored to {} from backup {} with version time {}.".format( restore_info.backup_info.source_database, @@ -137,34 +157,37 @@ def restore_database_with_encryption_key( instance_id, new_database_id, backup_id, kms_key_name ): """Restores a database from a backup using a Customer Managed Encryption Key (CMEK).""" - from google.cloud.spanner_admin_database_v1 import RestoreDatabaseEncryptionConfig + from google.cloud.spanner_admin_database_v1 import ( + RestoreDatabaseEncryptionConfig, RestoreDatabaseRequest) spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) # Start restoring an existing backup to a new database. - backup = instance.backup(backup_id) encryption_config = { "encryption_type": RestoreDatabaseEncryptionConfig.EncryptionType.CUSTOMER_MANAGED_ENCRYPTION, "kms_key_name": kms_key_name, } - new_database = instance.database( - new_database_id, encryption_config=encryption_config + + request = RestoreDatabaseRequest( + parent=instance.name, + database_id=new_database_id, + backup="{}/backups/{}".format(instance.name, backup_id), + encryption_config=encryption_config, ) - operation = new_database.restore(backup) + operation = spanner_client.database_admin_api.restore_database(request) # Wait for restore operation to complete. - operation.result(1600) + db = operation.result(1600) # Newly created database has restore information. - new_database.reload() - restore_info = new_database.restore_info + restore_info = db.restore_info print( "Database {} restored to {} from backup {} with using encryption key {}.".format( restore_info.backup_info.source_database, new_database_id, restore_info.backup_info.backup, - new_database.encryption_config.kms_key_name, + db.encryption_config.kms_key_name, ) ) @@ -174,6 +197,9 @@ def restore_database_with_encryption_key( # [START spanner_cancel_backup_create] def cancel_backup(instance_id, database_id, backup_id): + from google.cloud.spanner_admin_database_v1.types import \ + backup as backup_pb + spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) database = instance.database(database_id) @@ -181,9 +207,16 @@ def cancel_backup(instance_id, database_id, backup_id): expire_time = datetime.utcnow() + timedelta(days=30) # Create a backup. - backup = instance.backup(backup_id, database=database, expire_time=expire_time) - operation = backup.create() + request = backup_pb.CreateBackupRequest( + parent=instance.name, + backup_id=backup_id, + backup=backup_pb.Backup( + database=database.name, + expire_time=expire_time, + ), + ) + operation = spanner_client.database_admin_api.create_backup(request) # Cancel backup creation. operation.cancel() @@ -192,13 +225,22 @@ def cancel_backup(instance_id, database_id, backup_id): while not operation.done(): time.sleep(300) # 5 mins - # Deal with resource if the operation succeeded. - if backup.exists(): - print("Backup was created before the cancel completed.") - backup.delete() - print("Backup deleted.") - else: + try: + spanner_client.database_admin_api.get_backup( + backup_pb.GetBackupRequest( + name="{}/backups/{}".format(instance.name, backup_id) + ) + ) + except NotFound: print("Backup creation was successfully cancelled.") + return + print("Backup was created before the cancel completed.") + spanner_client.database_admin_api.delete_backup( + backup_pb.DeleteBackupRequest( + name="{}/backups/{}".format(instance.name, backup_id) + ) + ) + print("Backup deleted.") # [END spanner_cancel_backup_create] @@ -206,6 +248,9 @@ def cancel_backup(instance_id, database_id, backup_id): # [START spanner_list_backup_operations] def list_backup_operations(instance_id, database_id, backup_id): + from google.cloud.spanner_admin_database_v1.types import \ + backup as backup_pb + spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) @@ -215,9 +260,14 @@ def list_backup_operations(instance_id, database_id, backup_id): "google.spanner.admin.database.v1.CreateBackupMetadata) " "AND (metadata.database:{})" ).format(database_id) - operations = instance.list_backup_operations(filter_=filter_) + request = backup_pb.ListBackupOperationsRequest( + parent=instance.name, filter=filter_ + ) + operations = spanner_client.database_admin_api.list_backup_operations(request) for op in operations: - metadata = op.metadata + metadata = protobuf_helpers.from_any_pb( + backup_pb.CreateBackupMetadata, op.metadata + ) print( "Backup {} on database {}: {}% complete.".format( metadata.name, metadata.database, metadata.progress.progress_percent @@ -229,9 +279,14 @@ def list_backup_operations(instance_id, database_id, backup_id): "(metadata.@type:type.googleapis.com/google.spanner.admin.database.v1.CopyBackupMetadata) " "AND (metadata.source_backup:{})" ).format(backup_id) - operations = instance.list_backup_operations(filter_=filter_) + request = backup_pb.ListBackupOperationsRequest( + parent=instance.name, filter=filter_ + ) + operations = spanner_client.database_admin_api.list_backup_operations(request) for op in operations: - metadata = op.metadata + metadata = protobuf_helpers.from_any_pb( + backup_pb.CopyBackupMetadata, op.metadata + ) print( "Backup {} on source backup {}: {}% complete.".format( metadata.name, @@ -246,6 +301,9 @@ def list_backup_operations(instance_id, database_id, backup_id): # [START spanner_list_database_operations] def list_database_operations(instance_id): + from google.cloud.spanner_admin_database_v1.types import \ + spanner_database_admin + spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) @@ -254,11 +312,17 @@ def list_database_operations(instance_id): "(metadata.@type:type.googleapis.com/" "google.spanner.admin.database.v1.OptimizeRestoredDatabaseMetadata)" ) - operations = instance.list_database_operations(filter_=filter_) + request = spanner_database_admin.ListDatabaseOperationsRequest( + parent=instance.name, filter=filter_ + ) + operations = spanner_client.database_admin_api.list_database_operations(request) for op in operations: + metadata = protobuf_helpers.from_any_pb( + spanner_database_admin.OptimizeRestoredDatabaseMetadata, op.metadata + ) print( "Database {} restored from backup is {}% optimized.".format( - op.metadata.name, op.metadata.progress.progress_percent + metadata.name, metadata.progress.progress_percent ) ) @@ -268,22 +332,35 @@ def list_database_operations(instance_id): # [START spanner_list_backups] def list_backups(instance_id, database_id, backup_id): + from google.cloud.spanner_admin_database_v1.types import \ + backup as backup_pb + spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) # List all backups. print("All backups:") - for backup in instance.list_backups(): + request = backup_pb.ListBackupsRequest(parent=instance.name, filter="") + operations = spanner_client.database_admin_api.list_backups(request) + for backup in operations: print(backup.name) # List all backups that contain a name. print('All backups with backup name containing "{}":'.format(backup_id)) - for backup in instance.list_backups(filter_="name:{}".format(backup_id)): + request = backup_pb.ListBackupsRequest( + parent=instance.name, filter="name:{}".format(backup_id) + ) + operations = spanner_client.database_admin_api.list_backups(request) + for backup in operations: print(backup.name) # List all backups for a database that contains a name. print('All backups with database name containing "{}":'.format(database_id)) - for backup in instance.list_backups(filter_="database:{}".format(database_id)): + request = backup_pb.ListBackupsRequest( + parent=instance.name, filter="database:{}".format(database_id) + ) + operations = spanner_client.database_admin_api.list_backups(request) + for backup in operations: print(backup.name) # List all backups that expire before a timestamp. @@ -293,14 +370,21 @@ def list_backups(instance_id, database_id, backup_id): *expire_time.timetuple() ) ) - for backup in instance.list_backups( - filter_='expire_time < "{}-{}-{}T{}:{}:{}Z"'.format(*expire_time.timetuple()) - ): + request = backup_pb.ListBackupsRequest( + parent=instance.name, + filter='expire_time < "{}-{}-{}T{}:{}:{}Z"'.format(*expire_time.timetuple()), + ) + operations = spanner_client.database_admin_api.list_backups(request) + for backup in operations: print(backup.name) # List all backups with a size greater than some bytes. print("All backups with backup size more than 100 bytes:") - for backup in instance.list_backups(filter_="size_bytes > 100"): + request = backup_pb.ListBackupsRequest( + parent=instance.name, filter="size_bytes > 100" + ) + operations = spanner_client.database_admin_api.list_backups(request) + for backup in operations: print(backup.name) # List backups that were created after a timestamp that are also ready. @@ -310,18 +394,23 @@ def list_backups(instance_id, database_id, backup_id): *create_time.timetuple() ) ) - for backup in instance.list_backups( - filter_='create_time >= "{}-{}-{}T{}:{}:{}Z" AND state:READY'.format( + request = backup_pb.ListBackupsRequest( + parent=instance.name, + filter='create_time >= "{}-{}-{}T{}:{}:{}Z" AND state:READY'.format( *create_time.timetuple() - ) - ): + ), + ) + operations = spanner_client.database_admin_api.list_backups(request) + for backup in operations: print(backup.name) print("All backups with pagination") # If there are multiple pages, additional ``ListBackup`` # requests will be made as needed while iterating. paged_backups = set() - for backup in instance.list_backups(page_size=2): + request = backup_pb.ListBackupsRequest(parent=instance.name, page_size=2) + operations = spanner_client.database_admin_api.list_backups(request) + for backup in operations: paged_backups.add(backup.name) for backup in paged_backups: print(backup) @@ -332,22 +421,39 @@ def list_backups(instance_id, database_id, backup_id): # [START spanner_delete_backup] def delete_backup(instance_id, backup_id): + from google.cloud.spanner_admin_database_v1.types import \ + backup as backup_pb + spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) - backup = instance.backup(backup_id) - backup.reload() + backup = spanner_client.database_admin_api.get_backup( + backup_pb.GetBackupRequest( + name="{}/backups/{}".format(instance.name, backup_id) + ) + ) # Wait for databases that reference this backup to finish optimizing. while backup.referencing_databases: time.sleep(30) - backup.reload() + backup = spanner_client.database_admin_api.get_backup( + backup_pb.GetBackupRequest( + name="{}/backups/{}".format(instance.name, backup_id) + ) + ) # Delete the backup. - backup.delete() + spanner_client.database_admin_api.delete_backup( + backup_pb.DeleteBackupRequest(name=backup.name) + ) # Verify that the backup is deleted. - assert backup.exists() is False - print("Backup {} has been deleted.".format(backup.name)) + try: + backup = spanner_client.database_admin_api.get_backup( + backup_pb.GetBackupRequest(name=backup.name) + ) + except NotFound: + print("Backup {} has been deleted.".format(backup.name)) + return # [END spanner_delete_backup] @@ -355,16 +461,28 @@ def delete_backup(instance_id, backup_id): # [START spanner_update_backup] def update_backup(instance_id, backup_id): + from google.cloud.spanner_admin_database_v1.types import \ + backup as backup_pb + spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) - backup = instance.backup(backup_id) - backup.reload() + + backup = spanner_client.database_admin_api.get_backup( + backup_pb.GetBackupRequest( + name="{}/backups/{}".format(instance.name, backup_id) + ) + ) # Expire time must be within 366 days of the create time of the backup. old_expire_time = backup.expire_time # New expire time should be less than the max expire time new_expire_time = min(backup.max_expire_time, old_expire_time + timedelta(days=30)) - backup.update_expire_time(new_expire_time) + spanner_client.database_admin_api.update_backup( + backup_pb.UpdateBackupRequest( + backup=backup_pb.Backup(name=backup.name, expire_time=new_expire_time), + update_mask={"paths": ["expire_time"]}, + ) + ) print( "Backup {} expire time was updated from {} to {}.".format( backup.name, old_expire_time, new_expire_time @@ -380,6 +498,10 @@ def create_database_with_version_retention_period( instance_id, database_id, retention_period ): """Creates a database with a version retention period.""" + + from google.cloud.spanner_admin_database_v1.types import \ + spanner_database_admin + spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) ddl_statements = [ @@ -400,20 +522,24 @@ def create_database_with_version_retention_period( database_id, retention_period ), ] - db = instance.database(database_id, ddl_statements) - operation = db.create() - - operation.result(30) - - db.reload() + operation = spanner_client.database_admin_api.create_database( + request=spanner_database_admin.CreateDatabaseRequest( + parent=instance.name, + create_statement="CREATE DATABASE `{}`".format(database_id), + extra_statements=ddl_statements, + ) + ) + db = operation.result(30) print( "Database {} created with version retention period {} and earliest version time {}".format( - db.database_id, db.version_retention_period, db.earliest_version_time + db.name, db.version_retention_period, db.earliest_version_time ) ) - db.drop() + spanner_client.database_admin_api.drop_database( + spanner_database_admin.DropDatabaseRequest(database=db.name) + ) # [END spanner_create_database_with_version_retention_period] @@ -422,22 +548,29 @@ def create_database_with_version_retention_period( # [START spanner_copy_backup] def copy_backup(instance_id, backup_id, source_backup_path): """Copies a backup.""" + + from google.cloud.spanner_admin_database_v1.types import \ + backup as backup_pb + spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) # Create a backup object and wait for copy backup operation to complete. expire_time = datetime.utcnow() + timedelta(days=14) - copy_backup = instance.copy_backup( - backup_id=backup_id, source_backup=source_backup_path, expire_time=expire_time + request = backup_pb.CopyBackupRequest( + parent=instance.name, + backup_id=backup_id, + source_backup=source_backup_path, + expire_time=expire_time, ) - operation = copy_backup.create() - # Wait for copy backup operation to complete. - operation.result(2100) + operation = spanner_client.database_admin_api.copy_backup(request) + + # Wait for backup operation to complete. + copy_backup = operation.result(2100) # Verify that the copy backup is ready. - copy_backup.reload() - assert copy_backup.is_ready() is True + assert copy_backup.state == backup_pb.Backup.State.READY print( "Backup {} of size {} bytes was created at {} with version time {}".format( diff --git a/samples/samples/backup_sample_test.py b/samples/samples/backup_sample_test.py index 5f094e7a77..6d656c5545 100644 --- a/samples/samples/backup_sample_test.py +++ b/samples/samples/backup_sample_test.py @@ -13,8 +13,8 @@ # limitations under the License. import uuid -from google.api_core.exceptions import DeadlineExceeded import pytest +from google.api_core.exceptions import DeadlineExceeded from test_utils.retry import RetryErrors import backup_sample diff --git a/samples/samples/conftest.py b/samples/samples/conftest.py index 5b1af63876..9f0b7d12a0 100644 --- a/samples/samples/conftest.py +++ b/samples/samples/conftest.py @@ -16,15 +16,11 @@ import time import uuid +import pytest from google.api_core import exceptions - from google.cloud import spanner_admin_database_v1 from google.cloud.spanner_admin_database_v1.types.common import DatabaseDialect -from google.cloud.spanner_v1 import backup -from google.cloud.spanner_v1 import client -from google.cloud.spanner_v1 import database -from google.cloud.spanner_v1 import instance -import pytest +from google.cloud.spanner_v1 import backup, client, database, instance from test_utils import retry INSTANCE_CREATION_TIMEOUT = 560 # seconds diff --git a/samples/samples/pg_snippets.py b/samples/samples/pg_snippets.py index 51ddec6906..fe5ebab02c 100644 --- a/samples/samples/pg_snippets.py +++ b/samples/samples/pg_snippets.py @@ -68,26 +68,34 @@ def create_instance(instance_id): # [START spanner_postgresql_create_database] def create_database(instance_id, database_id): """Creates a PostgreSql database and tables for sample data.""" + + from google.cloud.spanner_admin_database_v1.types import \ + spanner_database_admin + spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) - database = instance.database( - database_id, + request = spanner_database_admin.CreateDatabaseRequest( + parent=instance.name, + create_statement=f'CREATE DATABASE "{database_id}"', database_dialect=DatabaseDialect.POSTGRESQL, ) - operation = database.create() + operation = spanner_client.database_admin_api.create_database(request=request) print("Waiting for operation to complete...") - operation.result(OPERATION_TIMEOUT_SECONDS) + database = operation.result(OPERATION_TIMEOUT_SECONDS) create_table_using_ddl(database.name) print("Created database {} on instance {}".format(database_id, instance_id)) def create_table_using_ddl(database_name): + from google.cloud.spanner_admin_database_v1.types import \ + spanner_database_admin + spanner_client = spanner.Client() - request = spanner_admin_database_v1.UpdateDatabaseDdlRequest( + request = spanner_database_admin.UpdateDatabaseDdlRequest( database=database_name, statements=[ """CREATE TABLE Singers ( @@ -231,13 +239,19 @@ def read_data(instance_id, database_id): # [START spanner_postgresql_add_column] def add_column(instance_id, database_id): """Adds a new column to the Albums table in the example database.""" + + from google.cloud.spanner_admin_database_v1.types import \ + spanner_database_admin + spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) database = instance.database(database_id) - operation = database.update_ddl( - ["ALTER TABLE Albums ADD COLUMN MarketingBudget BIGINT"] + request = spanner_database_admin.UpdateDatabaseDdlRequest( + database=database.name, + statements=["ALTER TABLE Albums ADD COLUMN MarketingBudget BIGINT"], ) + operation = spanner_client.database_admin_api.update_database_ddl(request) print("Waiting for operation to complete...") operation.result(OPERATION_TIMEOUT_SECONDS) @@ -390,6 +404,7 @@ def add_index(instance_id, database_id): # [END spanner_postgresql_create_index] + # [START spanner_postgresql_read_data_with_index] def read_data_with_index(instance_id, database_id): """Reads sample data from the database using an index. @@ -424,17 +439,24 @@ def read_data_with_index(instance_id, database_id): # [START spanner_postgresql_create_storing_index] def add_storing_index(instance_id, database_id): """Adds an storing index to the example database.""" + + from google.cloud.spanner_admin_database_v1.types import \ + spanner_database_admin + spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) database = instance.database(database_id) - operation = database.update_ddl( - [ + request = spanner_database_admin.UpdateDatabaseDdlRequest( + database=database.name, + statements=[ "CREATE INDEX AlbumsByAlbumTitle2 ON Albums(AlbumTitle)" "INCLUDE (MarketingBudget)" - ] + ], ) + operation = spanner_client.database_admin_api.update_database_ddl(request) + print("Waiting for operation to complete...") operation.result(OPERATION_TIMEOUT_SECONDS) @@ -1066,11 +1088,15 @@ def create_table_with_datatypes(instance_id, database_id): # [START spanner_postgresql_create_table_with_datatypes] # instance_id = "your-spanner-instance" # database_id = "your-spanner-db-id" + + from google.cloud.spanner_admin_database_v1.types import \ + spanner_database_admin + spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) database = instance.database(database_id) - request = spanner_admin_database_v1.UpdateDatabaseDdlRequest( + request = spanner_database_admin.UpdateDatabaseDdlRequest( database=database.name, statements=[ """CREATE TABLE Venues ( @@ -1447,14 +1473,20 @@ def add_jsonb_column(instance_id, database_id): # instance_id = "your-spanner-instance" # database_id = "your-spanner-db-id" + from google.cloud.spanner_admin_database_v1.types import \ + spanner_database_admin + spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) database = instance.database(database_id) - operation = database.update_ddl( - ["ALTER TABLE Venues ADD COLUMN VenueDetails JSONB"] + request = spanner_database_admin.UpdateDatabaseDdlRequest( + database=database.name, + statements=["ALTER TABLE Venues ADD COLUMN VenueDetails JSONB"], ) + operation = spanner_client.database_admin_api.update_database_ddl(request) + print("Waiting for operation to complete...") operation.result(OPERATION_TIMEOUT_SECONDS) @@ -1524,6 +1556,7 @@ def update_data_with_jsonb(instance_id, database_id): # [END spanner_postgresql_jsonb_update_data] + # [START spanner_postgresql_jsonb_query_parameter] def query_data_with_jsonb_parameter(instance_id, database_id): """Queries sample data using SQL with a JSONB parameter.""" @@ -1555,11 +1588,15 @@ def query_data_with_jsonb_parameter(instance_id, database_id): # [START spanner_postgresql_create_sequence] def create_sequence(instance_id, database_id): """Creates the Sequence and insert data""" + + from google.cloud.spanner_admin_database_v1.types import \ + spanner_database_admin + spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) database = instance.database(database_id) - request = spanner_admin_database_v1.UpdateDatabaseDdlRequest( + request = spanner_database_admin.UpdateDatabaseDdlRequest( database=database.name, statements=[ "CREATE SEQUENCE Seq BIT_REVERSED_POSITIVE", @@ -1601,14 +1638,23 @@ def insert_customers(transaction): # [END spanner_postgresql_create_sequence] + # [START spanner_postgresql_alter_sequence] def alter_sequence(instance_id, database_id): """Alters the Sequence and insert data""" + + from google.cloud.spanner_admin_database_v1.types import \ + spanner_database_admin + spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) database = instance.database(database_id) - operation = database.update_ddl(["ALTER SEQUENCE Seq SKIP RANGE 1000 5000000"]) + request = spanner_database_admin.UpdateDatabaseDdlRequest( + database=database.name, + statements=["ALTER SEQUENCE Seq SKIP RANGE 1000 5000000"], + ) + operation = spanner_client.database_admin_api.update_database_ddl(request) print("Waiting for operation to complete...") operation.result(OPERATION_TIMEOUT_SECONDS) @@ -1640,19 +1686,26 @@ def insert_customers(transaction): # [END spanner_postgresql_alter_sequence] + # [START spanner_postgresql_drop_sequence] def drop_sequence(instance_id, database_id): """Drops the Sequence""" + + from google.cloud.spanner_admin_database_v1.types import \ + spanner_database_admin + spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) database = instance.database(database_id) - operation = database.update_ddl( - [ + request = spanner_database_admin.UpdateDatabaseDdlRequest( + database=database.name, + statements=[ "ALTER TABLE Customers ALTER COLUMN CustomerId DROP DEFAULT", "DROP SEQUENCE Seq", - ] + ], ) + operation = spanner_client.database_admin_api.update_database_ddl(request) print("Waiting for operation to complete...") operation.result(OPERATION_TIMEOUT_SECONDS) diff --git a/samples/samples/pg_snippets_test.py b/samples/samples/pg_snippets_test.py index d4f08499d2..1b5d2971c1 100644 --- a/samples/samples/pg_snippets_test.py +++ b/samples/samples/pg_snippets_test.py @@ -15,9 +15,9 @@ import time import uuid +import pytest from google.api_core import exceptions from google.cloud.spanner_admin_database_v1.types.common import DatabaseDialect -import pytest from test_utils.retry import RetryErrors import pg_snippets as snippets diff --git a/samples/samples/quickstart.py b/samples/samples/quickstart.py index aa330dd3ca..f2d355d931 100644 --- a/samples/samples/quickstart.py +++ b/samples/samples/quickstart.py @@ -25,7 +25,6 @@ def run_quickstart(instance_id, database_id): # # Your Cloud Spanner database ID. # database_id = "my-database-id" - # Instantiate a client. spanner_client = spanner.Client() diff --git a/samples/samples/snippets.py b/samples/samples/snippets.py index 3ffd579f4a..3cef929309 100644 --- a/samples/samples/snippets.py +++ b/samples/samples/snippets.py @@ -30,10 +30,7 @@ from google.cloud import spanner from google.cloud.spanner_admin_instance_v1.types import spanner_instance_admin -from google.cloud.spanner_v1 import param_types -from google.cloud.spanner_v1 import DirectedReadOptions -from google.type import expr_pb2 -from google.iam.v1 import policy_pb2 +from google.cloud.spanner_v1 import DirectedReadOptions, param_types from google.cloud.spanner_v1.data_types import JsonObject from google.protobuf import field_mask_pb2 # type: ignore @@ -43,26 +40,30 @@ # [START spanner_create_instance] def create_instance(instance_id): """Creates an instance.""" + from google.cloud.spanner_admin_instance_v1.types import \ + spanner_instance_admin + spanner_client = spanner.Client() config_name = "{}/instanceConfigs/regional-us-central1".format( spanner_client.project_name ) - instance = spanner_client.instance( - instance_id, - configuration_name=config_name, - display_name="This is a display name.", - node_count=1, - labels={ - "cloud_spanner_samples": "true", - "sample_name": "snippets-create_instance-explicit", - "created": str(int(time.time())), - }, + operation = spanner_client.instance_admin_api.create_instance( + parent=spanner_client.project_name, + instance_id=instance_id, + instance=spanner_instance_admin.Instance( + config=config_name, + display_name="This is a display name.", + node_count=1, + labels={ + "cloud_spanner_samples": "true", + "sample_name": "snippets-create_instance-explicit", + "created": str(int(time.time())), + }, + ), ) - operation = instance.create() - print("Waiting for operation to complete...") operation.result(OPERATION_TIMEOUT_SECONDS) @@ -75,28 +76,34 @@ def create_instance(instance_id): # [START spanner_create_instance_with_processing_units] def create_instance_with_processing_units(instance_id, processing_units): """Creates an instance.""" + from google.cloud.spanner_admin_instance_v1.types import \ + spanner_instance_admin + spanner_client = spanner.Client() config_name = "{}/instanceConfigs/regional-us-central1".format( spanner_client.project_name ) - instance = spanner_client.instance( - instance_id, - configuration_name=config_name, - display_name="This is a display name.", - processing_units=processing_units, - labels={ - "cloud_spanner_samples": "true", - "sample_name": "snippets-create_instance_with_processing_units", - "created": str(int(time.time())), - }, + request = spanner_instance_admin.CreateInstanceRequest( + parent=spanner_client.project_name, + instance_id=instance_id, + instance=spanner_instance_admin.Instance( + config=config_name, + display_name="This is a display name.", + processing_units=processing_units, + labels={ + "cloud_spanner_samples": "true", + "sample_name": "snippets-create_instance_with_processing_units", + "created": str(int(time.time())), + }, + ), ) - operation = instance.create() + operation = spanner_client.instance_admin_api.create_instance(request=request) print("Waiting for operation to complete...") - operation.result(OPERATION_TIMEOUT_SECONDS) + instance = operation.result(OPERATION_TIMEOUT_SECONDS) print( "Created instance {} with {} processing units".format( @@ -129,9 +136,17 @@ def get_instance_config(instance_config): # [START spanner_list_instance_configs] def list_instance_config(): """Lists the available instance configurations.""" + from google.cloud.spanner_admin_instance_v1.types import \ + spanner_instance_admin + spanner_client = spanner.Client() - configs = spanner_client.list_instance_configs() - for config in configs: + + request = spanner_instance_admin.ListInstanceConfigsRequest( + parent=spanner_client.project_name + ) + for config in spanner_client.instance_admin_api.list_instance_configs( + request=request + ): print( "Available leader options for instance config {}: {}".format( config.name, config.leader_options @@ -145,11 +160,15 @@ def list_instance_config(): # [START spanner_list_databases] def list_databases(instance_id): """Lists databases and their leader options.""" + from google.cloud.spanner_admin_database_v1.types import \ + spanner_database_admin + spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) - databases = list(instance.list_databases()) - for database in databases: + request = spanner_database_admin.ListDatabasesRequest(parent=instance.name) + + for database in spanner_client.database_admin_api.list_databases(request=request): print( "Database {} has default leader {}".format( database.name, database.default_leader @@ -163,12 +182,16 @@ def list_databases(instance_id): # [START spanner_create_database] def create_database(instance_id, database_id): """Creates a database and tables for sample data.""" + from google.cloud.spanner_admin_database_v1.types import \ + spanner_database_admin + spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) - database = instance.database( - database_id, - ddl_statements=[ + request = spanner_database_admin.CreateDatabaseRequest( + parent=instance.name, + create_statement=f"CREATE DATABASE `{database_id}`", + extra_statements=[ """CREATE TABLE Singers ( SingerId INT64 NOT NULL, FirstName STRING(1024), @@ -187,12 +210,12 @@ def create_database(instance_id, database_id): ], ) - operation = database.create() + operation = spanner_client.database_admin_api.create_database(request=request) print("Waiting for operation to complete...") - operation.result(OPERATION_TIMEOUT_SECONDS) + database = operation.result(OPERATION_TIMEOUT_SECONDS) - print("Created database {} on instance {}".format(database_id, instance_id)) + print("Created database {} on instance {}".format(database.name, instance.name)) # [END spanner_create_database] @@ -201,18 +224,28 @@ def create_database(instance_id, database_id): # [START spanner_update_database] def update_database(instance_id, database_id): """Updates the drop protection setting for a database.""" + from google.cloud.spanner_admin_database_v1.types import \ + spanner_database_admin + 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)) + request = spanner_database_admin.UpdateDatabaseRequest( + database=spanner_database_admin.Database( + name="{}/databases/{}".format(instance.name, database_id), + enable_drop_protection=True, + ), + update_mask={"paths": ["enable_drop_protection"]}, + ) + operation = spanner_client.database_admin_api.update_database(request=request) + print( + "Waiting for update operation for {}/databases/{} to complete...".format( + instance.name, database_id + ) + ) operation.result(OPERATION_TIMEOUT_SECONDS) - print("Updated database {}.".format(db.name)) + print("Updated database {}/databases/{}.".format(instance.name, database_id)) # [END spanner_update_database] @@ -221,12 +254,17 @@ def update_database(instance_id, database_id): # [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).""" + from google.cloud.spanner_admin_database_v1 import EncryptionConfig + from google.cloud.spanner_admin_database_v1.types import \ + spanner_database_admin + spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) - database = instance.database( - database_id, - ddl_statements=[ + request = spanner_database_admin.CreateDatabaseRequest( + parent=instance.name, + create_statement=f"CREATE DATABASE `{database_id}`", + extra_statements=[ """CREATE TABLE Singers ( SingerId INT64 NOT NULL, FirstName STRING(1024), @@ -240,13 +278,13 @@ def create_database_with_encryption_key(instance_id, database_id, kms_key_name): ) PRIMARY KEY (SingerId, AlbumId), INTERLEAVE IN PARENT Singers ON DELETE CASCADE""", ], - encryption_config={"kms_key_name": kms_key_name}, + encryption_config=EncryptionConfig(kms_key_name=kms_key_name), ) - operation = database.create() + operation = spanner_client.database_admin_api.create_database(request=request) print("Waiting for operation to complete...") - operation.result(OPERATION_TIMEOUT_SECONDS) + database = operation.result(OPERATION_TIMEOUT_SECONDS) print( "Database {} created with encryption key {}".format( @@ -261,34 +299,39 @@ def create_database_with_encryption_key(instance_id, database_id, kms_key_name): # [START spanner_create_database_with_default_leader] def create_database_with_default_leader(instance_id, database_id, default_leader): """Creates a database with tables with a default leader.""" - spanner_client = spanner.Client() - instance = spanner_client.instance(instance_id) - - database = instance.database( - database_id, - ddl_statements=[ - """CREATE TABLE Singers ( - SingerId INT64 NOT NULL, - FirstName STRING(1024), - LastName STRING(1024), - SingerInfo BYTES(MAX) - ) PRIMARY KEY (SingerId)""", - """CREATE TABLE Albums ( - SingerId INT64 NOT NULL, - AlbumId INT64 NOT NULL, - AlbumTitle STRING(MAX) - ) PRIMARY KEY (SingerId, AlbumId), - INTERLEAVE IN PARENT Singers ON DELETE CASCADE""", - "ALTER DATABASE {}" - " SET OPTIONS (default_leader = '{}')".format(database_id, default_leader), - ], + from google.cloud.spanner_admin_database_v1.types import \ + spanner_database_admin + + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + + operation = spanner_client.database_admin_api.create_database( + request=spanner_database_admin.CreateDatabaseRequest( + parent=instance.name, + create_statement=f"CREATE DATABASE `{database_id}`", + extra_statements=[ + """CREATE TABLE Singers ( + SingerId INT64 NOT NULL, + FirstName STRING(1024), + LastName STRING(1024), + SingerInfo BYTES(MAX) + ) PRIMARY KEY (SingerId)""", + """CREATE TABLE Albums ( + SingerId INT64 NOT NULL, + AlbumId INT64 NOT NULL, + AlbumTitle STRING(MAX) + ) PRIMARY KEY (SingerId, AlbumId), + INTERLEAVE IN PARENT Singers ON DELETE CASCADE""", + "ALTER DATABASE {}" + " SET OPTIONS (default_leader = '{}')".format( + database_id, default_leader + ), + ], + ) ) - operation = database.create() print("Waiting for operation to complete...") - operation.result(OPERATION_TIMEOUT_SECONDS) - - database.reload() + database = operation.result(OPERATION_TIMEOUT_SECONDS) print( "Database {} created with default leader {}".format( @@ -303,25 +346,26 @@ def create_database_with_default_leader(instance_id, database_id, default_leader # [START spanner_update_database_with_default_leader] def update_database_with_default_leader(instance_id, database_id, default_leader): """Updates a database with tables with a default leader.""" + from google.cloud.spanner_admin_database_v1.types import \ + spanner_database_admin + spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) - database = instance.database(database_id) - operation = database.update_ddl( - [ + request = spanner_database_admin.UpdateDatabaseDdlRequest( + database=database.name, + statements=[ "ALTER DATABASE {}" " SET OPTIONS (default_leader = '{}')".format(database_id, default_leader) - ] + ], ) - operation.result(OPERATION_TIMEOUT_SECONDS) + operation = spanner_client.database_admin_api.update_database_ddl(request) - database.reload() + operation.result(OPERATION_TIMEOUT_SECONDS) print( - "Database {} updated with default leader {}".format( - database.name, database.default_leader - ) + "Database {} updated with default leader {}".format(database_id, default_leader) ) @@ -590,14 +634,21 @@ def query_data_with_new_column(instance_id, database_id): # [START spanner_create_index] def add_index(instance_id, database_id): """Adds a simple index to the example database.""" + + from google.cloud.spanner_admin_database_v1.types import \ + spanner_database_admin + spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) database = instance.database(database_id) - operation = database.update_ddl( - ["CREATE INDEX AlbumsByAlbumTitle ON Albums(AlbumTitle)"] + request = spanner_database_admin.UpdateDatabaseDdlRequest( + database=database.name, + statements=["CREATE INDEX AlbumsByAlbumTitle ON Albums(AlbumTitle)"], ) + operation = spanner_client.database_admin_api.update_database_ddl(request) + print("Waiting for operation to complete...") operation.result(OPERATION_TIMEOUT_SECONDS) @@ -686,17 +737,24 @@ def read_data_with_index(instance_id, database_id): # [START spanner_create_storing_index] def add_storing_index(instance_id, database_id): """Adds an storing index to the example database.""" + + from google.cloud.spanner_admin_database_v1.types import \ + spanner_database_admin + spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) database = instance.database(database_id) - operation = database.update_ddl( - [ + request = spanner_database_admin.UpdateDatabaseDdlRequest( + database=database.name, + statements=[ "CREATE INDEX AlbumsByAlbumTitle2 ON Albums(AlbumTitle)" "STORING (MarketingBudget)" - ] + ], ) + operation = spanner_client.database_admin_api.update_database_ddl(request) + print("Waiting for operation to complete...") operation.result(OPERATION_TIMEOUT_SECONDS) @@ -742,17 +800,25 @@ def read_data_with_storing_index(instance_id, database_id): # [START spanner_add_column] def add_column(instance_id, database_id): """Adds a new column to the Albums table in the example database.""" + + from google.cloud.spanner_admin_database_v1.types import \ + spanner_database_admin + spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) database = instance.database(database_id) - operation = database.update_ddl( - ["ALTER TABLE Albums ADD COLUMN MarketingBudget INT64"] + request = spanner_database_admin.UpdateDatabaseDdlRequest( + database=database.name, + statements=[ + "ALTER TABLE Albums ADD COLUMN MarketingBudget INT64", + ], ) + operation = spanner_client.database_admin_api.update_database_ddl(request) + print("Waiting for operation to complete...") operation.result(OPERATION_TIMEOUT_SECONDS) - print("Added the MarketingBudget column.") @@ -897,12 +963,16 @@ def read_only_transaction(instance_id, database_id): def create_table_with_timestamp(instance_id, database_id): """Creates a table with a COMMIT_TIMESTAMP column.""" + from google.cloud.spanner_admin_database_v1.types import \ + spanner_database_admin + spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) database = instance.database(database_id) - operation = database.update_ddl( - [ + request = spanner_database_admin.UpdateDatabaseDdlRequest( + database=database.name, + statements=[ """CREATE TABLE Performances ( SingerId INT64 NOT NULL, VenueId INT64 NOT NULL, @@ -912,9 +982,11 @@ def create_table_with_timestamp(instance_id, database_id): OPTIONS(allow_commit_timestamp=true) ) PRIMARY KEY (SingerId, VenueId, EventDate), INTERLEAVE IN PARENT Singers ON DELETE CASCADE""" - ] + ], ) + operation = spanner_client.database_admin_api.update_database_ddl(request) + print("Waiting for operation to complete...") operation.result(OPERATION_TIMEOUT_SECONDS) @@ -957,18 +1029,25 @@ def insert_data_with_timestamp(instance_id, database_id): # [START spanner_add_timestamp_column] def add_timestamp_column(instance_id, database_id): """Adds a new TIMESTAMP column to the Albums table in the example database.""" + + from google.cloud.spanner_admin_database_v1.types import \ + spanner_database_admin + spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) database = instance.database(database_id) - operation = database.update_ddl( - [ + request = spanner_database_admin.UpdateDatabaseDdlRequest( + database=database.name, + statements=[ "ALTER TABLE Albums ADD COLUMN LastUpdateTime TIMESTAMP " "OPTIONS(allow_commit_timestamp=true)" - ] + ], ) + operation = spanner_client.database_admin_api.update_database_ddl(request) + print("Waiting for operation to complete...") operation.result(OPERATION_TIMEOUT_SECONDS) @@ -1054,12 +1133,20 @@ def query_data_with_timestamp(instance_id, database_id): # [START spanner_add_numeric_column] def add_numeric_column(instance_id, database_id): """Adds a new NUMERIC column to the Venues table in the example database.""" + + from google.cloud.spanner_admin_database_v1.types import \ + spanner_database_admin + spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) - database = instance.database(database_id) - operation = database.update_ddl(["ALTER TABLE Venues ADD COLUMN Revenue NUMERIC"]) + request = spanner_database_admin.UpdateDatabaseDdlRequest( + database=database.name, + statements=["ALTER TABLE Venues ADD COLUMN Revenue NUMERIC"], + ) + + operation = spanner_client.database_admin_api.update_database_ddl(request) print("Waiting for operation to complete...") operation.result(OPERATION_TIMEOUT_SECONDS) @@ -1111,12 +1198,22 @@ def update_data_with_numeric(instance_id, database_id): # [START spanner_add_json_column] def add_json_column(instance_id, database_id): """Adds a new JSON column to the Venues table in the example database.""" + # instance_id = "your-spanner-instance" + # database_id = "your-spanner-db-id" + + from google.cloud.spanner_admin_database_v1.types import \ + spanner_database_admin + spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) - database = instance.database(database_id) - operation = database.update_ddl(["ALTER TABLE Venues ADD COLUMN VenueDetails JSON"]) + request = spanner_database_admin.UpdateDatabaseDdlRequest( + database=database.name, + statements=["ALTER TABLE Venues ADD COLUMN VenueDetails JSON"], + ) + + operation = spanner_client.database_admin_api.update_database_ddl(request) print("Waiting for operation to complete...") operation.result(OPERATION_TIMEOUT_SECONDS) @@ -1374,6 +1471,7 @@ def insert_singers(transaction): # [START spanner_get_commit_stats] def log_commit_stats(instance_id, database_id): """Inserts sample data using DML and displays the commit statistics.""" + # By default, commit statistics are logged via stdout at level Info. # This sample uses a custom logger to access the commit statistics. class CommitStatsSampleLogger(logging.Logger): @@ -1812,12 +1910,17 @@ def create_table_with_datatypes(instance_id, database_id): # [START spanner_create_table_with_datatypes] # instance_id = "your-spanner-instance" # database_id = "your-spanner-db-id" + + from google.cloud.spanner_admin_database_v1.types import \ + spanner_database_admin + spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) database = instance.database(database_id) - operation = database.update_ddl( - [ + request = spanner_database_admin.UpdateDatabaseDdlRequest( + database=database.name, + statements=[ """CREATE TABLE Venues ( VenueId INT64 NOT NULL, VenueName STRING(100), @@ -1830,8 +1933,9 @@ def create_table_with_datatypes(instance_id, database_id): LastUpdateTime TIMESTAMP NOT NULL OPTIONS(allow_commit_timestamp=true) ) PRIMARY KEY (VenueId)""" - ] + ], ) + operation = spanner_client.database_admin_api.update_database_ddl(request) print("Waiting for operation to complete...") operation.result(OPERATION_TIMEOUT_SECONDS) @@ -2333,6 +2437,7 @@ def create_instance_config(user_config_name, base_config_id): # [END spanner_create_instance_config] + # [START spanner_update_instance_config] def update_instance_config(user_config_name): """Updates the user-managed instance configuration.""" @@ -2357,6 +2462,7 @@ def update_instance_config(user_config_name): # [END spanner_update_instance_config] + # [START spanner_delete_instance_config] def delete_instance_config(user_config_id): """Deleted the user-managed instance configuration.""" @@ -2398,31 +2504,42 @@ def add_and_drop_database_roles(instance_id, database_id): # [START spanner_add_and_drop_database_role] # instance_id = "your-spanner-instance" # database_id = "your-spanner-db-id" + + from google.cloud.spanner_admin_database_v1.types import \ + spanner_database_admin + spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) database = instance.database(database_id) + role_parent = "new_parent" role_child = "new_child" - operation = database.update_ddl( - [ + request = spanner_database_admin.UpdateDatabaseDdlRequest( + database=database.name, + statements=[ "CREATE ROLE {}".format(role_parent), "GRANT SELECT ON TABLE Singers TO ROLE {}".format(role_parent), "CREATE ROLE {}".format(role_child), "GRANT ROLE {} TO ROLE {}".format(role_parent, role_child), - ] + ], ) + operation = spanner_client.database_admin_api.update_database_ddl(request) + operation.result(OPERATION_TIMEOUT_SECONDS) print( "Created roles {} and {} and granted privileges".format(role_parent, role_child) ) - operation = database.update_ddl( - [ + request = spanner_database_admin.UpdateDatabaseDdlRequest( + database=database.name, + statements=[ "REVOKE ROLE {} FROM ROLE {}".format(role_parent, role_child), "DROP ROLE {}".format(role_child), - ] + ], ) + operation = spanner_client.database_admin_api.update_database_ddl(request) + operation.result(OPERATION_TIMEOUT_SECONDS) print("Revoked privileges and dropped role {}".format(role_child)) @@ -2452,13 +2569,17 @@ def list_database_roles(instance_id, database_id): # [START spanner_list_database_roles] # instance_id = "your-spanner-instance" # database_id = "your-spanner-db-id" + from google.cloud.spanner_admin_database_v1.types import \ + spanner_database_admin + spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) database = instance.database(database_id) + request = spanner_database_admin.ListDatabaseRolesRequest(parent=database.name) # List database roles. print("Database Roles are:") - for role in database.list_database_roles(): + for role in spanner_client.database_admin_api.list_database_roles(request): print(role.name.split("/")[-1]) # [END spanner_list_database_roles] @@ -2477,6 +2598,10 @@ def enable_fine_grained_access( # iam_member = "user:alice@example.com" # database_role = "new_parent" # title = "condition title" + + from google.iam.v1 import iam_policy_pb2, options_pb2, policy_pb2 + from google.type import expr_pb2 + spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) database = instance.database(database_id) @@ -2485,7 +2610,11 @@ def enable_fine_grained_access( # that you specified, or it might use a lower policy version. For example, if you # specify version 3, but the policy has no conditional role bindings, the response # uses version 1. Valid values are 0, 1, and 3. - policy = database.get_iam_policy(3) + request = iam_policy_pb2.GetIamPolicyRequest( + resource=database.name, + options=options_pb2.GetPolicyOptions(requested_policy_version=3), + ) + policy = spanner_client.database_admin_api.get_iam_policy(request=request) if policy.version < 3: policy.version = 3 @@ -2500,9 +2629,13 @@ def enable_fine_grained_access( policy.version = 3 policy.bindings.append(new_binding) - database.set_iam_policy(policy) + set_request = iam_policy_pb2.SetIamPolicyRequest( + resource=database.name, + policy=policy, + ) + spanner_client.database_admin_api.set_iam_policy(set_request) - new_policy = database.get_iam_policy(3) + new_policy = spanner_client.database_admin_api.get_iam_policy(request=request) print( f"Enabled fine-grained access in IAM. New policy has version {new_policy.version}" ) @@ -2512,12 +2645,17 @@ def enable_fine_grained_access( # [START spanner_create_table_with_foreign_key_delete_cascade] def create_table_with_foreign_key_delete_cascade(instance_id, database_id): """Creates a table with foreign key delete cascade action""" + + from google.cloud.spanner_admin_database_v1.types import \ + spanner_database_admin + spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) database = instance.database(database_id) - operation = database.update_ddl( - [ + request = spanner_database_admin.UpdateDatabaseDdlRequest( + database=database.name, + statements=[ """CREATE TABLE Customers ( CustomerId INT64 NOT NULL, CustomerName STRING(62) NOT NULL, @@ -2532,9 +2670,11 @@ def create_table_with_foreign_key_delete_cascade(instance_id, database_id): REFERENCES Customers (CustomerId) ON DELETE CASCADE ) PRIMARY KEY (CartId) """, - ] + ], ) + operation = spanner_client.database_admin_api.update_database_ddl(request) + print("Waiting for operation to complete...") operation.result(OPERATION_TIMEOUT_SECONDS) @@ -2552,20 +2692,27 @@ def create_table_with_foreign_key_delete_cascade(instance_id, database_id): # [START spanner_alter_table_with_foreign_key_delete_cascade] def alter_table_with_foreign_key_delete_cascade(instance_id, database_id): """Alters a table with foreign key delete cascade action""" + + from google.cloud.spanner_admin_database_v1.types import \ + spanner_database_admin + spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) database = instance.database(database_id) - operation = database.update_ddl( - [ + request = spanner_database_admin.UpdateDatabaseDdlRequest( + database=database.name, + statements=[ """ALTER TABLE ShoppingCarts ADD CONSTRAINT FKShoppingCartsCustomerName FOREIGN KEY (CustomerName) REFERENCES Customers(CustomerName) ON DELETE CASCADE""" - ] + ], ) + operation = spanner_client.database_admin_api.update_database_ddl(request) + print("Waiting for operation to complete...") operation.result(OPERATION_TIMEOUT_SECONDS) @@ -2583,17 +2730,24 @@ def alter_table_with_foreign_key_delete_cascade(instance_id, database_id): # [START spanner_drop_foreign_key_constraint_delete_cascade] def drop_foreign_key_constraint_delete_cascade(instance_id, database_id): """Alter table to drop foreign key delete cascade action""" + + from google.cloud.spanner_admin_database_v1.types import \ + spanner_database_admin + spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) database = instance.database(database_id) - operation = database.update_ddl( - [ + request = spanner_database_admin.UpdateDatabaseDdlRequest( + database=database.name, + statements=[ """ALTER TABLE ShoppingCarts DROP CONSTRAINT FKShoppingCartsCustomerName""" - ] + ], ) + operation = spanner_client.database_admin_api.update_database_ddl(request) + print("Waiting for operation to complete...") operation.result(OPERATION_TIMEOUT_SECONDS) @@ -2611,20 +2765,27 @@ def drop_foreign_key_constraint_delete_cascade(instance_id, database_id): # [START spanner_create_sequence] def create_sequence(instance_id, database_id): """Creates the Sequence and insert data""" + + from google.cloud.spanner_admin_database_v1.types import \ + spanner_database_admin + spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) database = instance.database(database_id) - operation = database.update_ddl( - [ + request = spanner_database_admin.UpdateDatabaseDdlRequest( + database=database.name, + statements=[ "CREATE SEQUENCE Seq OPTIONS (sequence_kind = 'bit_reversed_positive')", """CREATE TABLE Customers ( CustomerId INT64 DEFAULT (GET_NEXT_SEQUENCE_VALUE(Sequence Seq)), CustomerName STRING(1024) ) PRIMARY KEY (CustomerId)""", - ] + ], ) + operation = spanner_client.database_admin_api.update_database_ddl(request) + print("Waiting for operation to complete...") operation.result(OPERATION_TIMEOUT_SECONDS) @@ -2655,19 +2816,27 @@ def insert_customers(transaction): # [END spanner_create_sequence] + # [START spanner_alter_sequence] def alter_sequence(instance_id, database_id): """Alters the Sequence and insert data""" + + from google.cloud.spanner_admin_database_v1.types import \ + spanner_database_admin + spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) database = instance.database(database_id) - operation = database.update_ddl( - [ - "ALTER SEQUENCE Seq SET OPTIONS (skip_range_min = 1000, skip_range_max = 5000000)" - ] + request = spanner_database_admin.UpdateDatabaseDdlRequest( + database=database.name, + statements=[ + "ALTER SEQUENCE Seq SET OPTIONS (skip_range_min = 1000, skip_range_max = 5000000)", + ], ) + operation = spanner_client.database_admin_api.update_database_ddl(request) + print("Waiting for operation to complete...") operation.result(OPERATION_TIMEOUT_SECONDS) @@ -2698,20 +2867,28 @@ def insert_customers(transaction): # [END spanner_alter_sequence] + # [START spanner_drop_sequence] def drop_sequence(instance_id, database_id): """Drops the Sequence""" + + from google.cloud.spanner_admin_database_v1.types import \ + spanner_database_admin + spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) database = instance.database(database_id) - operation = database.update_ddl( - [ + request = spanner_database_admin.UpdateDatabaseDdlRequest( + database=database.name, + statements=[ "ALTER TABLE Customers ALTER COLUMN CustomerId DROP DEFAULT", "DROP SEQUENCE Seq", - ] + ], ) + operation = spanner_client.database_admin_api.update_database_ddl(request) + print("Waiting for operation to complete...") operation.result(OPERATION_TIMEOUT_SECONDS) diff --git a/samples/samples/snippets_test.py b/samples/samples/snippets_test.py index a49a4ee480..6942f8fa79 100644 --- a/samples/samples/snippets_test.py +++ b/samples/samples/snippets_test.py @@ -15,10 +15,10 @@ import time import uuid +import pytest from google.api_core import exceptions from google.cloud import spanner from google.cloud.spanner_admin_database_v1.types.common import DatabaseDialect -import pytest from test_utils.retry import RetryErrors import snippets From 3ab74b267b651b430e96712be22088e2859d7e79 Mon Sep 17 00:00:00 2001 From: rahul2393 Date: Mon, 4 Mar 2024 19:12:00 +0530 Subject: [PATCH 14/17] docs: use autogenerated methods to get names from admin samples (#1110) * docs: use autogenerated methods the fetch names from admin samples * use database_admin_api for instance_path * incorporate changes --- samples/samples/backup_sample.py | 171 +++++++++++-------- samples/samples/pg_snippets.py | 75 ++++---- samples/samples/snippets.py | 284 ++++++++++++++++++------------- 3 files changed, 308 insertions(+), 222 deletions(-) diff --git a/samples/samples/backup_sample.py b/samples/samples/backup_sample.py index d72dde87a6..d3c2c667c5 100644 --- a/samples/samples/backup_sample.py +++ b/samples/samples/backup_sample.py @@ -35,23 +35,24 @@ def create_backup(instance_id, database_id, backup_id, version_time): backup as backup_pb spanner_client = spanner.Client() - instance = spanner_client.instance(instance_id) - database = instance.database(database_id) + database_admin_api = spanner_client.database_admin_api # Create a backup expire_time = datetime.utcnow() + timedelta(days=14) request = backup_pb.CreateBackupRequest( - parent=instance.name, + parent=database_admin_api.instance_path(spanner_client.project, instance_id), backup_id=backup_id, backup=backup_pb.Backup( - database=database.name, + database=database_admin_api.database_path( + spanner_client.project, instance_id, database_id + ), expire_time=expire_time, version_time=version_time, ), ) - operation = spanner_client.database_admin_api.create_backup(request) + operation = database_admin_api.create_backup(request) # Wait for backup operation to complete. backup = operation.result(2100) @@ -81,8 +82,7 @@ def create_backup_with_encryption_key( backup as backup_pb spanner_client = spanner.Client() - instance = spanner_client.instance(instance_id) - database = instance.database(database_id) + database_admin_api = spanner_client.database_admin_api # Create a backup expire_time = datetime.utcnow() + timedelta(days=14) @@ -91,15 +91,17 @@ def create_backup_with_encryption_key( "kms_key_name": kms_key_name, } request = backup_pb.CreateBackupRequest( - parent=instance.name, + parent=database_admin_api.instance_path(spanner_client.project, instance_id), backup_id=backup_id, backup=backup_pb.Backup( - database=database.name, + database=database_admin_api.database_path( + spanner_client.project, instance_id, database_id + ), expire_time=expire_time, ), encryption_config=encryption_config, ) - operation = spanner_client.database_admin_api.create_backup(request) + operation = database_admin_api.create_backup(request) # Wait for backup operation to complete. backup = operation.result(2100) @@ -124,15 +126,17 @@ def restore_database(instance_id, new_database_id, backup_id): from google.cloud.spanner_admin_database_v1 import RestoreDatabaseRequest spanner_client = spanner.Client() - instance = spanner_client.instance(instance_id) + database_admin_api = spanner_client.database_admin_api # Start restoring an existing backup to a new database. request = RestoreDatabaseRequest( - parent=instance.name, + parent=database_admin_api.instance_path(spanner_client.project, instance_id), database_id=new_database_id, - backup="{}/backups/{}".format(instance.name, backup_id), + backup=database_admin_api.backup_path( + spanner_client.project, instance_id, backup_id + ), ) - operation = spanner_client.database_admin_api.restore_database(request) + operation = database_admin_api.restore_database(request) # Wait for restore operation to complete. db = operation.result(1600) @@ -161,7 +165,7 @@ def restore_database_with_encryption_key( RestoreDatabaseEncryptionConfig, RestoreDatabaseRequest) spanner_client = spanner.Client() - instance = spanner_client.instance(instance_id) + database_admin_api = spanner_client.database_admin_api # Start restoring an existing backup to a new database. encryption_config = { @@ -170,12 +174,14 @@ def restore_database_with_encryption_key( } request = RestoreDatabaseRequest( - parent=instance.name, + parent=database_admin_api.instance_path(spanner_client.project, instance_id), database_id=new_database_id, - backup="{}/backups/{}".format(instance.name, backup_id), + backup=database_admin_api.backup_path( + spanner_client.project, instance_id, backup_id + ), encryption_config=encryption_config, ) - operation = spanner_client.database_admin_api.restore_database(request) + operation = database_admin_api.restore_database(request) # Wait for restore operation to complete. db = operation.result(1600) @@ -201,43 +207,48 @@ def cancel_backup(instance_id, database_id, backup_id): backup as backup_pb spanner_client = spanner.Client() - instance = spanner_client.instance(instance_id) - database = instance.database(database_id) + database_admin_api = spanner_client.database_admin_api expire_time = datetime.utcnow() + timedelta(days=30) # Create a backup. request = backup_pb.CreateBackupRequest( - parent=instance.name, + parent=database_admin_api.instance_path(spanner_client.project, instance_id), backup_id=backup_id, backup=backup_pb.Backup( - database=database.name, + database=database_admin_api.database_path( + spanner_client.project, instance_id, database_id + ), expire_time=expire_time, ), ) - operation = spanner_client.database_admin_api.create_backup(request) + operation = database_admin_api.create_backup(request) # Cancel backup creation. operation.cancel() - # Cancel operations are best effort so either it will complete or + # Cancel operations are the best effort so either it will complete or # be cancelled. while not operation.done(): time.sleep(300) # 5 mins try: - spanner_client.database_admin_api.get_backup( + database_admin_api.get_backup( backup_pb.GetBackupRequest( - name="{}/backups/{}".format(instance.name, backup_id) + name=database_admin_api.backup_path( + spanner_client.project, instance_id, backup_id + ), ) ) except NotFound: print("Backup creation was successfully cancelled.") return print("Backup was created before the cancel completed.") - spanner_client.database_admin_api.delete_backup( + database_admin_api.delete_backup( backup_pb.DeleteBackupRequest( - name="{}/backups/{}".format(instance.name, backup_id) + name=database_admin_api.backup_path( + spanner_client.project, instance_id, backup_id + ), ) ) print("Backup deleted.") @@ -252,7 +263,7 @@ def list_backup_operations(instance_id, database_id, backup_id): backup as backup_pb spanner_client = spanner.Client() - instance = spanner_client.instance(instance_id) + database_admin_api = spanner_client.database_admin_api # List the CreateBackup operations. filter_ = ( @@ -261,9 +272,10 @@ def list_backup_operations(instance_id, database_id, backup_id): "AND (metadata.database:{})" ).format(database_id) request = backup_pb.ListBackupOperationsRequest( - parent=instance.name, filter=filter_ + parent=database_admin_api.instance_path(spanner_client.project, instance_id), + filter=filter_, ) - operations = spanner_client.database_admin_api.list_backup_operations(request) + operations = database_admin_api.list_backup_operations(request) for op in operations: metadata = protobuf_helpers.from_any_pb( backup_pb.CreateBackupMetadata, op.metadata @@ -280,9 +292,10 @@ def list_backup_operations(instance_id, database_id, backup_id): "AND (metadata.source_backup:{})" ).format(backup_id) request = backup_pb.ListBackupOperationsRequest( - parent=instance.name, filter=filter_ + parent=database_admin_api.instance_path(spanner_client.project, instance_id), + filter=filter_, ) - operations = spanner_client.database_admin_api.list_backup_operations(request) + operations = database_admin_api.list_backup_operations(request) for op in operations: metadata = protobuf_helpers.from_any_pb( backup_pb.CopyBackupMetadata, op.metadata @@ -305,7 +318,7 @@ def list_database_operations(instance_id): spanner_database_admin spanner_client = spanner.Client() - instance = spanner_client.instance(instance_id) + database_admin_api = spanner_client.database_admin_api # List the progress of restore. filter_ = ( @@ -313,9 +326,10 @@ def list_database_operations(instance_id): "google.spanner.admin.database.v1.OptimizeRestoredDatabaseMetadata)" ) request = spanner_database_admin.ListDatabaseOperationsRequest( - parent=instance.name, filter=filter_ + parent=database_admin_api.instance_path(spanner_client.project, instance_id), + filter=filter_, ) - operations = spanner_client.database_admin_api.list_database_operations(request) + operations = database_admin_api.list_database_operations(request) for op in operations: metadata = protobuf_helpers.from_any_pb( spanner_database_admin.OptimizeRestoredDatabaseMetadata, op.metadata @@ -336,30 +350,35 @@ def list_backups(instance_id, database_id, backup_id): backup as backup_pb spanner_client = spanner.Client() - instance = spanner_client.instance(instance_id) + database_admin_api = spanner_client.database_admin_api # List all backups. print("All backups:") - request = backup_pb.ListBackupsRequest(parent=instance.name, filter="") - operations = spanner_client.database_admin_api.list_backups(request) + request = backup_pb.ListBackupsRequest( + parent=database_admin_api.instance_path(spanner_client.project, instance_id), + filter="", + ) + operations = database_admin_api.list_backups(request) for backup in operations: print(backup.name) # List all backups that contain a name. print('All backups with backup name containing "{}":'.format(backup_id)) request = backup_pb.ListBackupsRequest( - parent=instance.name, filter="name:{}".format(backup_id) + parent=database_admin_api.instance_path(spanner_client.project, instance_id), + filter="name:{}".format(backup_id), ) - operations = spanner_client.database_admin_api.list_backups(request) + operations = database_admin_api.list_backups(request) for backup in operations: print(backup.name) # List all backups for a database that contains a name. print('All backups with database name containing "{}":'.format(database_id)) request = backup_pb.ListBackupsRequest( - parent=instance.name, filter="database:{}".format(database_id) + parent=database_admin_api.instance_path(spanner_client.project, instance_id), + filter="database:{}".format(database_id), ) - operations = spanner_client.database_admin_api.list_backups(request) + operations = database_admin_api.list_backups(request) for backup in operations: print(backup.name) @@ -371,19 +390,20 @@ def list_backups(instance_id, database_id, backup_id): ) ) request = backup_pb.ListBackupsRequest( - parent=instance.name, + parent=database_admin_api.instance_path(spanner_client.project, instance_id), filter='expire_time < "{}-{}-{}T{}:{}:{}Z"'.format(*expire_time.timetuple()), ) - operations = spanner_client.database_admin_api.list_backups(request) + operations = database_admin_api.list_backups(request) for backup in operations: print(backup.name) # List all backups with a size greater than some bytes. print("All backups with backup size more than 100 bytes:") request = backup_pb.ListBackupsRequest( - parent=instance.name, filter="size_bytes > 100" + parent=database_admin_api.instance_path(spanner_client.project, instance_id), + filter="size_bytes > 100", ) - operations = spanner_client.database_admin_api.list_backups(request) + operations = database_admin_api.list_backups(request) for backup in operations: print(backup.name) @@ -395,12 +415,12 @@ def list_backups(instance_id, database_id, backup_id): ) ) request = backup_pb.ListBackupsRequest( - parent=instance.name, + parent=database_admin_api.instance_path(spanner_client.project, instance_id), filter='create_time >= "{}-{}-{}T{}:{}:{}Z" AND state:READY'.format( *create_time.timetuple() ), ) - operations = spanner_client.database_admin_api.list_backups(request) + operations = database_admin_api.list_backups(request) for backup in operations: print(backup.name) @@ -408,8 +428,11 @@ def list_backups(instance_id, database_id, backup_id): # If there are multiple pages, additional ``ListBackup`` # requests will be made as needed while iterating. paged_backups = set() - request = backup_pb.ListBackupsRequest(parent=instance.name, page_size=2) - operations = spanner_client.database_admin_api.list_backups(request) + request = backup_pb.ListBackupsRequest( + parent=database_admin_api.instance_path(spanner_client.project, instance_id), + page_size=2, + ) + operations = database_admin_api.list_backups(request) for backup in operations: paged_backups.add(backup.name) for backup in paged_backups: @@ -425,30 +448,32 @@ def delete_backup(instance_id, backup_id): backup as backup_pb spanner_client = spanner.Client() - instance = spanner_client.instance(instance_id) - backup = spanner_client.database_admin_api.get_backup( + database_admin_api = spanner_client.database_admin_api + backup = database_admin_api.get_backup( backup_pb.GetBackupRequest( - name="{}/backups/{}".format(instance.name, backup_id) + name=database_admin_api.backup_path( + spanner_client.project, instance_id, backup_id + ), ) ) # Wait for databases that reference this backup to finish optimizing. while backup.referencing_databases: time.sleep(30) - backup = spanner_client.database_admin_api.get_backup( + backup = database_admin_api.get_backup( backup_pb.GetBackupRequest( - name="{}/backups/{}".format(instance.name, backup_id) + name=database_admin_api.backup_path( + spanner_client.project, instance_id, backup_id + ), ) ) # Delete the backup. - spanner_client.database_admin_api.delete_backup( - backup_pb.DeleteBackupRequest(name=backup.name) - ) + database_admin_api.delete_backup(backup_pb.DeleteBackupRequest(name=backup.name)) # Verify that the backup is deleted. try: - backup = spanner_client.database_admin_api.get_backup( + backup = database_admin_api.get_backup( backup_pb.GetBackupRequest(name=backup.name) ) except NotFound: @@ -465,11 +490,13 @@ def update_backup(instance_id, backup_id): backup as backup_pb spanner_client = spanner.Client() - instance = spanner_client.instance(instance_id) + database_admin_api = spanner_client.database_admin_api - backup = spanner_client.database_admin_api.get_backup( + backup = database_admin_api.get_backup( backup_pb.GetBackupRequest( - name="{}/backups/{}".format(instance.name, backup_id) + name=database_admin_api.backup_path( + spanner_client.project, instance_id, backup_id + ), ) ) @@ -477,7 +504,7 @@ def update_backup(instance_id, backup_id): old_expire_time = backup.expire_time # New expire time should be less than the max expire time new_expire_time = min(backup.max_expire_time, old_expire_time + timedelta(days=30)) - spanner_client.database_admin_api.update_backup( + database_admin_api.update_backup( backup_pb.UpdateBackupRequest( backup=backup_pb.Backup(name=backup.name, expire_time=new_expire_time), update_mask={"paths": ["expire_time"]}, @@ -503,7 +530,7 @@ def create_database_with_version_retention_period( spanner_database_admin spanner_client = spanner.Client() - instance = spanner_client.instance(instance_id) + database_admin_api = spanner_client.database_admin_api ddl_statements = [ "CREATE TABLE Singers (" + " SingerId INT64 NOT NULL," @@ -522,9 +549,11 @@ def create_database_with_version_retention_period( database_id, retention_period ), ] - operation = spanner_client.database_admin_api.create_database( + operation = database_admin_api.create_database( request=spanner_database_admin.CreateDatabaseRequest( - parent=instance.name, + parent=database_admin_api.instance_path( + spanner_client.project, instance_id + ), create_statement="CREATE DATABASE `{}`".format(database_id), extra_statements=ddl_statements, ) @@ -537,7 +566,7 @@ def create_database_with_version_retention_period( ) ) - spanner_client.database_admin_api.drop_database( + database_admin_api.drop_database( spanner_database_admin.DropDatabaseRequest(database=db.name) ) @@ -553,18 +582,18 @@ def copy_backup(instance_id, backup_id, source_backup_path): backup as backup_pb spanner_client = spanner.Client() - instance = spanner_client.instance(instance_id) + database_admin_api = spanner_client.database_admin_api # Create a backup object and wait for copy backup operation to complete. expire_time = datetime.utcnow() + timedelta(days=14) request = backup_pb.CopyBackupRequest( - parent=instance.name, + parent=database_admin_api.instance_path(spanner_client.project, instance_id), backup_id=backup_id, source_backup=source_backup_path, expire_time=expire_time, ) - operation = spanner_client.database_admin_api.copy_backup(request) + operation = database_admin_api.copy_backup(request) # Wait for backup operation to complete. copy_backup = operation.result(2100) diff --git a/samples/samples/pg_snippets.py b/samples/samples/pg_snippets.py index fe5ebab02c..ad8744794a 100644 --- a/samples/samples/pg_snippets.py +++ b/samples/samples/pg_snippets.py @@ -73,15 +73,15 @@ def create_database(instance_id, database_id): spanner_database_admin spanner_client = spanner.Client() - instance = spanner_client.instance(instance_id) + database_admin_api = spanner_client.database_admin_api request = spanner_database_admin.CreateDatabaseRequest( - parent=instance.name, + parent=database_admin_api.instance_path(spanner_client.project, instance_id), create_statement=f'CREATE DATABASE "{database_id}"', database_dialect=DatabaseDialect.POSTGRESQL, ) - operation = spanner_client.database_admin_api.create_database(request=request) + operation = database_admin_api.create_database(request=request) print("Waiting for operation to complete...") database = operation.result(OPERATION_TIMEOUT_SECONDS) @@ -244,14 +244,15 @@ def add_column(instance_id, database_id): spanner_database_admin spanner_client = spanner.Client() - instance = spanner_client.instance(instance_id) - database = instance.database(database_id) + database_admin_api = spanner_client.database_admin_api request = spanner_database_admin.UpdateDatabaseDdlRequest( - database=database.name, + database=database_admin_api.database_path( + spanner_client.project, instance_id, database_id + ), statements=["ALTER TABLE Albums ADD COLUMN MarketingBudget BIGINT"], ) - operation = spanner_client.database_admin_api.update_database_ddl(request) + operation = database_admin_api.update_database_ddl(request) print("Waiting for operation to complete...") operation.result(OPERATION_TIMEOUT_SECONDS) @@ -444,18 +445,19 @@ def add_storing_index(instance_id, database_id): spanner_database_admin spanner_client = spanner.Client() - instance = spanner_client.instance(instance_id) - database = instance.database(database_id) + database_admin_api = spanner_client.database_admin_api request = spanner_database_admin.UpdateDatabaseDdlRequest( - database=database.name, + database=database_admin_api.database_path( + spanner_client.project, instance_id, database_id + ), statements=[ "CREATE INDEX AlbumsByAlbumTitle2 ON Albums(AlbumTitle)" "INCLUDE (MarketingBudget)" ], ) - operation = spanner_client.database_admin_api.update_database_ddl(request) + operation = database_admin_api.update_database_ddl(request) print("Waiting for operation to complete...") operation.result(OPERATION_TIMEOUT_SECONDS) @@ -1093,11 +1095,12 @@ def create_table_with_datatypes(instance_id, database_id): spanner_database_admin spanner_client = spanner.Client() - instance = spanner_client.instance(instance_id) - database = instance.database(database_id) + database_admin_api = spanner_client.database_admin_api request = spanner_database_admin.UpdateDatabaseDdlRequest( - database=database.name, + database=database_admin_api.database_path( + spanner_client.project, instance_id, database_id + ), statements=[ """CREATE TABLE Venues ( VenueId BIGINT NOT NULL, @@ -1111,7 +1114,7 @@ def create_table_with_datatypes(instance_id, database_id): PRIMARY KEY (VenueId))""" ], ) - operation = spanner_client.database_admin_api.update_database_ddl(request) + operation = database_admin_api.update_database_ddl(request) print("Waiting for operation to complete...") operation.result(OPERATION_TIMEOUT_SECONDS) @@ -1477,15 +1480,16 @@ def add_jsonb_column(instance_id, database_id): spanner_database_admin spanner_client = spanner.Client() - instance = spanner_client.instance(instance_id) - database = instance.database(database_id) + database_admin_api = spanner_client.database_admin_api request = spanner_database_admin.UpdateDatabaseDdlRequest( - database=database.name, + database=database_admin_api.database_path( + spanner_client.project, instance_id, database_id + ), statements=["ALTER TABLE Venues ADD COLUMN VenueDetails JSONB"], ) - operation = spanner_client.database_admin_api.update_database_ddl(request) + operation = database_admin_api.update_database_ddl(request) print("Waiting for operation to complete...") operation.result(OPERATION_TIMEOUT_SECONDS) @@ -1593,11 +1597,12 @@ def create_sequence(instance_id, database_id): spanner_database_admin spanner_client = spanner.Client() - instance = spanner_client.instance(instance_id) - database = instance.database(database_id) + database_admin_api = spanner_client.database_admin_api request = spanner_database_admin.UpdateDatabaseDdlRequest( - database=database.name, + database=database_admin_api.database_path( + spanner_client.project, instance_id, database_id + ), statements=[ "CREATE SEQUENCE Seq BIT_REVERSED_POSITIVE", """CREATE TABLE Customers ( @@ -1607,7 +1612,7 @@ def create_sequence(instance_id, database_id): )""", ], ) - operation = spanner_client.database_admin_api.update_database_ddl(request) + operation = database_admin_api.update_database_ddl(request) print("Waiting for operation to complete...") operation.result(OPERATION_TIMEOUT_SECONDS) @@ -1633,6 +1638,9 @@ def insert_customers(transaction): ) ) + instance = spanner_client.instance(instance_id) + database = instance.database(database_id) + database.run_in_transaction(insert_customers) @@ -1647,14 +1655,15 @@ def alter_sequence(instance_id, database_id): spanner_database_admin spanner_client = spanner.Client() - instance = spanner_client.instance(instance_id) - database = instance.database(database_id) + database_admin_api = spanner_client.database_admin_api request = spanner_database_admin.UpdateDatabaseDdlRequest( - database=database.name, + database=database_admin_api.database_path( + spanner_client.project, instance_id, database_id + ), statements=["ALTER SEQUENCE Seq SKIP RANGE 1000 5000000"], ) - operation = spanner_client.database_admin_api.update_database_ddl(request) + operation = database_admin_api.update_database_ddl(request) print("Waiting for operation to complete...") operation.result(OPERATION_TIMEOUT_SECONDS) @@ -1681,6 +1690,9 @@ def insert_customers(transaction): ) ) + instance = spanner_client.instance(instance_id) + database = instance.database(database_id) + database.run_in_transaction(insert_customers) @@ -1695,17 +1707,18 @@ def drop_sequence(instance_id, database_id): spanner_database_admin spanner_client = spanner.Client() - instance = spanner_client.instance(instance_id) - database = instance.database(database_id) + database_admin_api = spanner_client.database_admin_api request = spanner_database_admin.UpdateDatabaseDdlRequest( - database=database.name, + database=database_admin_api.database_path( + spanner_client.project, instance_id, database_id + ), statements=[ "ALTER TABLE Customers ALTER COLUMN CustomerId DROP DEFAULT", "DROP SEQUENCE Seq", ], ) - operation = spanner_client.database_admin_api.update_database_ddl(request) + operation = database_admin_api.update_database_ddl(request) print("Waiting for operation to complete...") operation.result(OPERATION_TIMEOUT_SECONDS) diff --git a/samples/samples/snippets.py b/samples/samples/snippets.py index 3cef929309..5cd1cc8e8b 100644 --- a/samples/samples/snippets.py +++ b/samples/samples/snippets.py @@ -164,11 +164,13 @@ def list_databases(instance_id): spanner_database_admin spanner_client = spanner.Client() - instance = spanner_client.instance(instance_id) + database_admin_api = spanner_client.database_admin_api - request = spanner_database_admin.ListDatabasesRequest(parent=instance.name) + request = spanner_database_admin.ListDatabasesRequest( + parent=database_admin_api.instance_path(spanner_client.project, instance_id) + ) - for database in spanner_client.database_admin_api.list_databases(request=request): + for database in database_admin_api.list_databases(request=request): print( "Database {} has default leader {}".format( database.name, database.default_leader @@ -186,10 +188,10 @@ def create_database(instance_id, database_id): spanner_database_admin spanner_client = spanner.Client() - instance = spanner_client.instance(instance_id) + database_admin_api = spanner_client.database_admin_api request = spanner_database_admin.CreateDatabaseRequest( - parent=instance.name, + parent=database_admin_api.instance_path(spanner_client.project, instance_id), create_statement=f"CREATE DATABASE `{database_id}`", extra_statements=[ """CREATE TABLE Singers ( @@ -210,12 +212,17 @@ def create_database(instance_id, database_id): ], ) - operation = spanner_client.database_admin_api.create_database(request=request) + operation = database_admin_api.create_database(request=request) print("Waiting for operation to complete...") database = operation.result(OPERATION_TIMEOUT_SECONDS) - print("Created database {} on instance {}".format(database.name, instance.name)) + print( + "Created database {} on instance {}".format( + database.name, + database_admin_api.instance_path(spanner_client.project, instance_id), + ) + ) # [END spanner_create_database] @@ -228,24 +235,32 @@ def update_database(instance_id, database_id): spanner_database_admin spanner_client = spanner.Client() - instance = spanner_client.instance(instance_id) + database_admin_api = spanner_client.database_admin_api request = spanner_database_admin.UpdateDatabaseRequest( database=spanner_database_admin.Database( - name="{}/databases/{}".format(instance.name, database_id), + name=database_admin_api.database_path( + spanner_client.project, instance_id, database_id + ), enable_drop_protection=True, ), update_mask={"paths": ["enable_drop_protection"]}, ) - operation = spanner_client.database_admin_api.update_database(request=request) + operation = database_admin_api.update_database(request=request) print( "Waiting for update operation for {}/databases/{} to complete...".format( - instance.name, database_id + database_admin_api.instance_path(spanner_client.project, instance_id), + database_id, ) ) operation.result(OPERATION_TIMEOUT_SECONDS) - print("Updated database {}/databases/{}.".format(instance.name, database_id)) + print( + "Updated database {}/databases/{}.".format( + database_admin_api.instance_path(spanner_client.project, instance_id), + database_id, + ) + ) # [END spanner_update_database] @@ -259,10 +274,10 @@ def create_database_with_encryption_key(instance_id, database_id, kms_key_name): spanner_database_admin spanner_client = spanner.Client() - instance = spanner_client.instance(instance_id) + database_admin_api = spanner_client.database_admin_api request = spanner_database_admin.CreateDatabaseRequest( - parent=instance.name, + parent=database_admin_api.instance_path(spanner_client.project, instance_id), create_statement=f"CREATE DATABASE `{database_id}`", extra_statements=[ """CREATE TABLE Singers ( @@ -281,7 +296,7 @@ def create_database_with_encryption_key(instance_id, database_id, kms_key_name): encryption_config=EncryptionConfig(kms_key_name=kms_key_name), ) - operation = spanner_client.database_admin_api.create_database(request=request) + operation = database_admin_api.create_database(request=request) print("Waiting for operation to complete...") database = operation.result(OPERATION_TIMEOUT_SECONDS) @@ -303,32 +318,29 @@ def create_database_with_default_leader(instance_id, database_id, default_leader spanner_database_admin spanner_client = spanner.Client() - instance = spanner_client.instance(instance_id) + database_admin_api = spanner_client.database_admin_api - operation = spanner_client.database_admin_api.create_database( - request=spanner_database_admin.CreateDatabaseRequest( - parent=instance.name, - create_statement=f"CREATE DATABASE `{database_id}`", - extra_statements=[ - """CREATE TABLE Singers ( - SingerId INT64 NOT NULL, - FirstName STRING(1024), - LastName STRING(1024), - SingerInfo BYTES(MAX) - ) PRIMARY KEY (SingerId)""", - """CREATE TABLE Albums ( - SingerId INT64 NOT NULL, - AlbumId INT64 NOT NULL, - AlbumTitle STRING(MAX) - ) PRIMARY KEY (SingerId, AlbumId), - INTERLEAVE IN PARENT Singers ON DELETE CASCADE""", - "ALTER DATABASE {}" - " SET OPTIONS (default_leader = '{}')".format( - database_id, default_leader - ), - ], - ) + request = spanner_database_admin.CreateDatabaseRequest( + parent=database_admin_api.instance_path(spanner_client.project, instance_id), + create_statement=f"CREATE DATABASE `{database_id}`", + extra_statements=[ + """CREATE TABLE Singers ( + SingerId INT64 NOT NULL, + FirstName STRING(1024), + LastName STRING(1024), + SingerInfo BYTES(MAX) + ) PRIMARY KEY (SingerId)""", + """CREATE TABLE Albums ( + SingerId INT64 NOT NULL, + AlbumId INT64 NOT NULL, + AlbumTitle STRING(MAX) + ) PRIMARY KEY (SingerId, AlbumId), + INTERLEAVE IN PARENT Singers ON DELETE CASCADE""", + "ALTER DATABASE {}" + " SET OPTIONS (default_leader = '{}')".format(database_id, default_leader), + ], ) + operation = database_admin_api.create_database(request=request) print("Waiting for operation to complete...") database = operation.result(OPERATION_TIMEOUT_SECONDS) @@ -350,17 +362,18 @@ def update_database_with_default_leader(instance_id, database_id, default_leader spanner_database_admin spanner_client = spanner.Client() - instance = spanner_client.instance(instance_id) - database = instance.database(database_id) + database_admin_api = spanner_client.database_admin_api request = spanner_database_admin.UpdateDatabaseDdlRequest( - database=database.name, + database=database_admin_api.database_path( + spanner_client.project, instance_id, database_id + ), statements=[ "ALTER DATABASE {}" " SET OPTIONS (default_leader = '{}')".format(database_id, default_leader) ], ) - operation = spanner_client.database_admin_api.update_database_ddl(request) + operation = database_admin_api.update_database_ddl(request) operation.result(OPERATION_TIMEOUT_SECONDS) @@ -376,9 +389,12 @@ def update_database_with_default_leader(instance_id, database_id, default_leader def get_database_ddl(instance_id, database_id): """Gets the database DDL statements.""" spanner_client = spanner.Client() - instance = spanner_client.instance(instance_id) - database = instance.database(database_id) - ddl = spanner_client.database_admin_api.get_database_ddl(database=database.name) + database_admin_api = spanner_client.database_admin_api + ddl = database_admin_api.get_database_ddl( + database=database_admin_api.database_path( + spanner_client.project, instance_id, database_id + ) + ) print("Retrieved database DDL for {}".format(database_id)) for statement in ddl.statements: print(statement) @@ -639,15 +655,16 @@ def add_index(instance_id, database_id): spanner_database_admin spanner_client = spanner.Client() - instance = spanner_client.instance(instance_id) - database = instance.database(database_id) + database_admin_api = spanner_client.database_admin_api request = spanner_database_admin.UpdateDatabaseDdlRequest( - database=database.name, + database=database_admin_api.database_path( + spanner_client.project, instance_id, database_id + ), statements=["CREATE INDEX AlbumsByAlbumTitle ON Albums(AlbumTitle)"], ) - operation = spanner_client.database_admin_api.update_database_ddl(request) + operation = database_admin_api.update_database_ddl(request) print("Waiting for operation to complete...") operation.result(OPERATION_TIMEOUT_SECONDS) @@ -742,18 +759,19 @@ def add_storing_index(instance_id, database_id): spanner_database_admin spanner_client = spanner.Client() - instance = spanner_client.instance(instance_id) - database = instance.database(database_id) + database_admin_api = spanner_client.database_admin_api request = spanner_database_admin.UpdateDatabaseDdlRequest( - database=database.name, + database=database_admin_api.database_path( + spanner_client.project, instance_id, database_id + ), statements=[ "CREATE INDEX AlbumsByAlbumTitle2 ON Albums(AlbumTitle)" "STORING (MarketingBudget)" ], ) - operation = spanner_client.database_admin_api.update_database_ddl(request) + operation = database_admin_api.update_database_ddl(request) print("Waiting for operation to complete...") operation.result(OPERATION_TIMEOUT_SECONDS) @@ -805,17 +823,18 @@ def add_column(instance_id, database_id): spanner_database_admin spanner_client = spanner.Client() - instance = spanner_client.instance(instance_id) - database = instance.database(database_id) + database_admin_api = spanner_client.database_admin_api request = spanner_database_admin.UpdateDatabaseDdlRequest( - database=database.name, + database=database_admin_api.database_path( + spanner_client.project, instance_id, database_id + ), statements=[ "ALTER TABLE Albums ADD COLUMN MarketingBudget INT64", ], ) - operation = spanner_client.database_admin_api.update_database_ddl(request) + operation = database_admin_api.update_database_ddl(request) print("Waiting for operation to complete...") operation.result(OPERATION_TIMEOUT_SECONDS) @@ -967,11 +986,12 @@ def create_table_with_timestamp(instance_id, database_id): spanner_database_admin spanner_client = spanner.Client() - instance = spanner_client.instance(instance_id) - database = instance.database(database_id) + database_admin_api = spanner_client.database_admin_api request = spanner_database_admin.UpdateDatabaseDdlRequest( - database=database.name, + database=database_admin_api.database_path( + spanner_client.project, instance_id, database_id + ), statements=[ """CREATE TABLE Performances ( SingerId INT64 NOT NULL, @@ -985,7 +1005,7 @@ def create_table_with_timestamp(instance_id, database_id): ], ) - operation = spanner_client.database_admin_api.update_database_ddl(request) + operation = database_admin_api.update_database_ddl(request) print("Waiting for operation to complete...") operation.result(OPERATION_TIMEOUT_SECONDS) @@ -1034,19 +1054,19 @@ def add_timestamp_column(instance_id, database_id): spanner_database_admin spanner_client = spanner.Client() - instance = spanner_client.instance(instance_id) - - database = instance.database(database_id) + database_admin_api = spanner_client.database_admin_api request = spanner_database_admin.UpdateDatabaseDdlRequest( - database=database.name, + database=database_admin_api.database_path( + spanner_client.project, instance_id, database_id + ), statements=[ "ALTER TABLE Albums ADD COLUMN LastUpdateTime TIMESTAMP " "OPTIONS(allow_commit_timestamp=true)" ], ) - operation = spanner_client.database_admin_api.update_database_ddl(request) + operation = database_admin_api.update_database_ddl(request) print("Waiting for operation to complete...") operation.result(OPERATION_TIMEOUT_SECONDS) @@ -1138,15 +1158,16 @@ def add_numeric_column(instance_id, database_id): spanner_database_admin spanner_client = spanner.Client() - instance = spanner_client.instance(instance_id) - database = instance.database(database_id) + database_admin_api = spanner_client.database_admin_api request = spanner_database_admin.UpdateDatabaseDdlRequest( - database=database.name, + database=database_admin_api.database_path( + spanner_client.project, instance_id, database_id + ), statements=["ALTER TABLE Venues ADD COLUMN Revenue NUMERIC"], ) - operation = spanner_client.database_admin_api.update_database_ddl(request) + operation = database_admin_api.update_database_ddl(request) print("Waiting for operation to complete...") operation.result(OPERATION_TIMEOUT_SECONDS) @@ -1205,15 +1226,16 @@ def add_json_column(instance_id, database_id): spanner_database_admin spanner_client = spanner.Client() - instance = spanner_client.instance(instance_id) - database = instance.database(database_id) + database_admin_api = spanner_client.database_admin_api request = spanner_database_admin.UpdateDatabaseDdlRequest( - database=database.name, + database=database_admin_api.database_path( + spanner_client.project, instance_id, database_id + ), statements=["ALTER TABLE Venues ADD COLUMN VenueDetails JSON"], ) - operation = spanner_client.database_admin_api.update_database_ddl(request) + operation = database_admin_api.update_database_ddl(request) print("Waiting for operation to complete...") operation.result(OPERATION_TIMEOUT_SECONDS) @@ -1915,11 +1937,12 @@ def create_table_with_datatypes(instance_id, database_id): spanner_database_admin spanner_client = spanner.Client() - instance = spanner_client.instance(instance_id) - database = instance.database(database_id) + database_admin_api = spanner_client.database_admin_api request = spanner_database_admin.UpdateDatabaseDdlRequest( - database=database.name, + database=database_admin_api.database_path( + spanner_client.project, instance_id, database_id + ), statements=[ """CREATE TABLE Venues ( VenueId INT64 NOT NULL, @@ -1935,7 +1958,7 @@ def create_table_with_datatypes(instance_id, database_id): ) PRIMARY KEY (VenueId)""" ], ) - operation = spanner_client.database_admin_api.update_database_ddl(request) + operation = database_admin_api.update_database_ddl(request) print("Waiting for operation to complete...") operation.result(OPERATION_TIMEOUT_SECONDS) @@ -2509,14 +2532,15 @@ def add_and_drop_database_roles(instance_id, database_id): spanner_database_admin spanner_client = spanner.Client() - instance = spanner_client.instance(instance_id) - database = instance.database(database_id) + database_admin_api = spanner_client.database_admin_api role_parent = "new_parent" role_child = "new_child" request = spanner_database_admin.UpdateDatabaseDdlRequest( - database=database.name, + database=database_admin_api.database_path( + spanner_client.project, instance_id, database_id + ), statements=[ "CREATE ROLE {}".format(role_parent), "GRANT SELECT ON TABLE Singers TO ROLE {}".format(role_parent), @@ -2524,7 +2548,7 @@ def add_and_drop_database_roles(instance_id, database_id): "GRANT ROLE {} TO ROLE {}".format(role_parent, role_child), ], ) - operation = spanner_client.database_admin_api.update_database_ddl(request) + operation = database_admin_api.update_database_ddl(request) operation.result(OPERATION_TIMEOUT_SECONDS) print( @@ -2532,13 +2556,15 @@ def add_and_drop_database_roles(instance_id, database_id): ) request = spanner_database_admin.UpdateDatabaseDdlRequest( - database=database.name, + database=database_admin_api.database_path( + spanner_client.project, instance_id, database_id + ), statements=[ "REVOKE ROLE {} FROM ROLE {}".format(role_parent, role_child), "DROP ROLE {}".format(role_child), ], ) - operation = spanner_client.database_admin_api.update_database_ddl(request) + operation = database_admin_api.update_database_ddl(request) operation.result(OPERATION_TIMEOUT_SECONDS) print("Revoked privileges and dropped role {}".format(role_child)) @@ -2573,13 +2599,16 @@ def list_database_roles(instance_id, database_id): spanner_database_admin spanner_client = spanner.Client() - instance = spanner_client.instance(instance_id) - database = instance.database(database_id) + database_admin_api = spanner_client.database_admin_api - request = spanner_database_admin.ListDatabaseRolesRequest(parent=database.name) + request = spanner_database_admin.ListDatabaseRolesRequest( + parent=database_admin_api.database_path( + spanner_client.project, instance_id, database_id + ) + ) # List database roles. print("Database Roles are:") - for role in spanner_client.database_admin_api.list_database_roles(request): + for role in database_admin_api.list_database_roles(request): print(role.name.split("/")[-1]) # [END spanner_list_database_roles] @@ -2603,18 +2632,19 @@ def enable_fine_grained_access( from google.type import expr_pb2 spanner_client = spanner.Client() - instance = spanner_client.instance(instance_id) - database = instance.database(database_id) + database_admin_api = spanner_client.database_admin_api # The policy in the response from getDatabaseIAMPolicy might use the policy version # that you specified, or it might use a lower policy version. For example, if you # specify version 3, but the policy has no conditional role bindings, the response # uses version 1. Valid values are 0, 1, and 3. request = iam_policy_pb2.GetIamPolicyRequest( - resource=database.name, + resource=database_admin_api.database_path( + spanner_client.project, instance_id, database_id + ), options=options_pb2.GetPolicyOptions(requested_policy_version=3), ) - policy = spanner_client.database_admin_api.get_iam_policy(request=request) + policy = database_admin_api.get_iam_policy(request=request) if policy.version < 3: policy.version = 3 @@ -2630,12 +2660,14 @@ def enable_fine_grained_access( policy.version = 3 policy.bindings.append(new_binding) set_request = iam_policy_pb2.SetIamPolicyRequest( - resource=database.name, + resource=database_admin_api.database_path( + spanner_client.project, instance_id, database_id + ), policy=policy, ) - spanner_client.database_admin_api.set_iam_policy(set_request) + database_admin_api.set_iam_policy(set_request) - new_policy = spanner_client.database_admin_api.get_iam_policy(request=request) + new_policy = database_admin_api.get_iam_policy(request=request) print( f"Enabled fine-grained access in IAM. New policy has version {new_policy.version}" ) @@ -2650,11 +2682,12 @@ def create_table_with_foreign_key_delete_cascade(instance_id, database_id): spanner_database_admin spanner_client = spanner.Client() - instance = spanner_client.instance(instance_id) - database = instance.database(database_id) + database_admin_api = spanner_client.database_admin_api request = spanner_database_admin.UpdateDatabaseDdlRequest( - database=database.name, + database=database_admin_api.database_path( + spanner_client.project, instance_id, database_id + ), statements=[ """CREATE TABLE Customers ( CustomerId INT64 NOT NULL, @@ -2673,7 +2706,7 @@ def create_table_with_foreign_key_delete_cascade(instance_id, database_id): ], ) - operation = spanner_client.database_admin_api.update_database_ddl(request) + operation = database_admin_api.update_database_ddl(request) print("Waiting for operation to complete...") operation.result(OPERATION_TIMEOUT_SECONDS) @@ -2697,11 +2730,12 @@ def alter_table_with_foreign_key_delete_cascade(instance_id, database_id): spanner_database_admin spanner_client = spanner.Client() - instance = spanner_client.instance(instance_id) - database = instance.database(database_id) + database_admin_api = spanner_client.database_admin_api request = spanner_database_admin.UpdateDatabaseDdlRequest( - database=database.name, + database=database_admin_api.database_path( + spanner_client.project, instance_id, database_id + ), statements=[ """ALTER TABLE ShoppingCarts ADD CONSTRAINT FKShoppingCartsCustomerName @@ -2711,7 +2745,7 @@ def alter_table_with_foreign_key_delete_cascade(instance_id, database_id): ], ) - operation = spanner_client.database_admin_api.update_database_ddl(request) + operation = database_admin_api.update_database_ddl(request) print("Waiting for operation to complete...") operation.result(OPERATION_TIMEOUT_SECONDS) @@ -2735,18 +2769,19 @@ def drop_foreign_key_constraint_delete_cascade(instance_id, database_id): spanner_database_admin spanner_client = spanner.Client() - instance = spanner_client.instance(instance_id) - database = instance.database(database_id) + database_admin_api = spanner_client.database_admin_api request = spanner_database_admin.UpdateDatabaseDdlRequest( - database=database.name, + database=database_admin_api.database_path( + spanner_client.project, instance_id, database_id + ), statements=[ """ALTER TABLE ShoppingCarts DROP CONSTRAINT FKShoppingCartsCustomerName""" ], ) - operation = spanner_client.database_admin_api.update_database_ddl(request) + operation = database_admin_api.update_database_ddl(request) print("Waiting for operation to complete...") operation.result(OPERATION_TIMEOUT_SECONDS) @@ -2770,11 +2805,12 @@ def create_sequence(instance_id, database_id): spanner_database_admin spanner_client = spanner.Client() - instance = spanner_client.instance(instance_id) - database = instance.database(database_id) + database_admin_api = spanner_client.database_admin_api request = spanner_database_admin.UpdateDatabaseDdlRequest( - database=database.name, + database=database_admin_api.database_path( + spanner_client.project, instance_id, database_id + ), statements=[ "CREATE SEQUENCE Seq OPTIONS (sequence_kind = 'bit_reversed_positive')", """CREATE TABLE Customers ( @@ -2784,7 +2820,7 @@ def create_sequence(instance_id, database_id): ], ) - operation = spanner_client.database_admin_api.update_database_ddl(request) + operation = database_admin_api.update_database_ddl(request) print("Waiting for operation to complete...") operation.result(OPERATION_TIMEOUT_SECONDS) @@ -2811,6 +2847,9 @@ def insert_customers(transaction): ) ) + instance = spanner_client.instance(instance_id) + database = instance.database(database_id) + database.run_in_transaction(insert_customers) @@ -2825,17 +2864,18 @@ def alter_sequence(instance_id, database_id): spanner_database_admin spanner_client = spanner.Client() - instance = spanner_client.instance(instance_id) - database = instance.database(database_id) + database_admin_api = spanner_client.database_admin_api request = spanner_database_admin.UpdateDatabaseDdlRequest( - database=database.name, + database=database_admin_api.database_path( + spanner_client.project, instance_id, database_id + ), statements=[ "ALTER SEQUENCE Seq SET OPTIONS (skip_range_min = 1000, skip_range_max = 5000000)", ], ) - operation = spanner_client.database_admin_api.update_database_ddl(request) + operation = database_admin_api.update_database_ddl(request) print("Waiting for operation to complete...") operation.result(OPERATION_TIMEOUT_SECONDS) @@ -2862,6 +2902,9 @@ def insert_customers(transaction): ) ) + instance = spanner_client.instance(instance_id) + database = instance.database(database_id) + database.run_in_transaction(insert_customers) @@ -2876,18 +2919,19 @@ def drop_sequence(instance_id, database_id): spanner_database_admin spanner_client = spanner.Client() - instance = spanner_client.instance(instance_id) - database = instance.database(database_id) + database_admin_api = spanner_client.database_admin_api request = spanner_database_admin.UpdateDatabaseDdlRequest( - database=database.name, + database=database_admin_api.database_path( + spanner_client.project, instance_id, database_id + ), statements=[ "ALTER TABLE Customers ALTER COLUMN CustomerId DROP DEFAULT", "DROP SEQUENCE Seq", ], ) - operation = spanner_client.database_admin_api.update_database_ddl(request) + operation = database_admin_api.update_database_ddl(request) print("Waiting for operation to complete...") operation.result(OPERATION_TIMEOUT_SECONDS) From 9c919faf5cb21809e18893a5e1e1bcec425e7ff2 Mon Sep 17 00:00:00 2001 From: rahul2393 Date: Wed, 6 Mar 2024 11:55:22 +0530 Subject: [PATCH 15/17] test: skip sample tests if no changes detected (#1106) * test: skip sample tests if no changes detected * add exception for test file --- .kokoro/test-samples-impl.sh | 12 ++++++++++++ owlbot.py | 1 + 2 files changed, 13 insertions(+) diff --git a/.kokoro/test-samples-impl.sh b/.kokoro/test-samples-impl.sh index 5a0f5fab6a..776365a831 100755 --- a/.kokoro/test-samples-impl.sh +++ b/.kokoro/test-samples-impl.sh @@ -20,6 +20,8 @@ set -eo pipefail # Enables `**` to include files nested inside sub-folders shopt -s globstar +DIFF_FROM="origin/main..." + # Exit early if samples don't exist if ! find samples -name 'requirements.txt' | grep -q .; then echo "No tests run. './samples/**/requirements.txt' not found" @@ -71,6 +73,16 @@ for file in samples/**/requirements.txt; do file=$(dirname "$file") cd "$file" + # If $DIFF_FROM is set, use it to check for changes in this directory. + if [[ -n "${DIFF_FROM:-}" ]]; then + git diff --quiet "$DIFF_FROM" . + CHANGED=$? + if [[ "$CHANGED" -eq 0 ]]; then + # echo -e "\n Skipping $file: no changes in folder.\n" + continue + fi + fi + echo "------------------------------------------------------------" echo "- testing $file" echo "------------------------------------------------------------" diff --git a/owlbot.py b/owlbot.py index 7c249527b2..f2251da864 100644 --- a/owlbot.py +++ b/owlbot.py @@ -137,6 +137,7 @@ def get_staging_dirs( ".github/workflows", # exclude gh actions as credentials are needed for tests "README.rst", ".github/release-please.yml", + ".kokoro/test-samples-impl.sh", ], ) From 4f6340b0930bb1b5430209c4a1ff196c42b834d0 Mon Sep 17 00:00:00 2001 From: Sri Harsha CH <57220027+harshachinta@users.noreply.github.com> Date: Wed, 6 Mar 2024 18:19:16 +0530 Subject: [PATCH 16/17] feat: add retry and timeout for batch dml (#1107) * feat(spanner): add retry, timeout for batch update * feat(spanner): add samples for retry, timeout * feat(spanner): update unittest * feat(spanner): update comments * feat(spanner): update code for retry * feat(spanner): update comment --- google/cloud/spanner_v1/transaction.py | 17 ++++++++- samples/samples/snippets.py | 50 ++++++++++++++++++++++++++ samples/samples/snippets_test.py | 7 ++++ tests/unit/test_spanner.py | 14 ++++++++ tests/unit/test_transaction.py | 25 +++++++++++-- 5 files changed, 110 insertions(+), 3 deletions(-) diff --git a/google/cloud/spanner_v1/transaction.py b/google/cloud/spanner_v1/transaction.py index 1f5ff1098a..b02a43e8d2 100644 --- a/google/cloud/spanner_v1/transaction.py +++ b/google/cloud/spanner_v1/transaction.py @@ -410,7 +410,14 @@ def execute_update( return response.stats.row_count_exact - def batch_update(self, statements, request_options=None): + def batch_update( + self, + statements, + request_options=None, + *, + retry=gapic_v1.method.DEFAULT, + timeout=gapic_v1.method.DEFAULT, + ): """Perform a batch of DML statements via an ``ExecuteBatchDml`` request. :type statements: @@ -431,6 +438,12 @@ def batch_update(self, statements, request_options=None): If a dict is provided, it must be of the same form as the protobuf message :class:`~google.cloud.spanner_v1.types.RequestOptions`. + :type retry: :class:`~google.api_core.retry.Retry` + :param retry: (Optional) The retry settings for this request. + + :type timeout: float + :param timeout: (Optional) The timeout for this request. + :rtype: Tuple(status, Sequence[int]) :returns: @@ -486,6 +499,8 @@ def batch_update(self, statements, request_options=None): api.execute_batch_dml, request=request, metadata=metadata, + retry=retry, + timeout=timeout, ) if self._transaction_id is None: diff --git a/samples/samples/snippets.py b/samples/samples/snippets.py index 5cd1cc8e8b..23d9d8aff1 100644 --- a/samples/samples/snippets.py +++ b/samples/samples/snippets.py @@ -3017,6 +3017,51 @@ def directed_read_options( # [END spanner_directed_read] +def set_custom_timeout_and_retry(instance_id, database_id): + """Executes a snapshot read with custom timeout and retry.""" + # [START spanner_set_custom_timeout_and_retry] + from google.api_core import retry + from google.api_core import exceptions as core_exceptions + + # instance_id = "your-spanner-instance" + # database_id = "your-spanner-db-id" + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + database = instance.database(database_id) + + retry = retry.Retry( + # Customize retry with an initial wait time of 500 milliseconds. + initial=0.5, + # Customize retry with a maximum wait time of 16 seconds. + maximum=16, + # Customize retry with a wait time multiplier per iteration of 1.5. + multiplier=1.5, + # Customize retry with a timeout on + # how long a certain RPC may be retried in + # case the server returns an error. + timeout=60, + # Configure which errors should be retried. + predicate=retry.if_exception_type( + core_exceptions.ServiceUnavailable, + ), + ) + + # Set a custom retry and timeout setting. + with database.snapshot() as snapshot: + results = snapshot.execute_sql( + "SELECT SingerId, AlbumId, AlbumTitle FROM Albums", + # Set custom retry setting for this request + retry=retry, + # Set custom timeout of 60 seconds for this request + timeout=60, + ) + + for row in results: + print("SingerId: {}, AlbumId: {}, AlbumTitle: {}".format(*row)) + + # [END spanner_set_custom_timeout_and_retry] + + if __name__ == "__main__": # noqa: C901 parser = argparse.ArgumentParser( description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter @@ -3157,6 +3202,9 @@ def directed_read_options( ) enable_fine_grained_access_parser.add_argument("--title", default="condition title") subparsers.add_parser("directed_read_options", help=directed_read_options.__doc__) + subparsers.add_parser( + "set_custom_timeout_and_retry", help=set_custom_timeout_and_retry.__doc__ + ) args = parser.parse_args() @@ -3290,3 +3338,5 @@ def directed_read_options( ) elif args.command == "directed_read_options": directed_read_options(args.instance_id, args.database_id) + elif args.command == "set_custom_timeout_and_retry": + set_custom_timeout_and_retry(args.instance_id, args.database_id) diff --git a/samples/samples/snippets_test.py b/samples/samples/snippets_test.py index 6942f8fa79..7c8de8ab96 100644 --- a/samples/samples/snippets_test.py +++ b/samples/samples/snippets_test.py @@ -859,3 +859,10 @@ def test_directed_read_options(capsys, instance_id, sample_database): snippets.directed_read_options(instance_id, sample_database.database_id) out, _ = capsys.readouterr() assert "SingerId: 1, AlbumId: 1, AlbumTitle: Total Junk" in out + + +@pytest.mark.dependency(depends=["insert_data"]) +def test_set_custom_timeout_and_retry(capsys, instance_id, sample_database): + snippets.set_custom_timeout_and_retry(instance_id, sample_database.database_id) + out, _ = capsys.readouterr() + assert "SingerId: 1, AlbumId: 1, AlbumTitle: Total Junk" in out diff --git a/tests/unit/test_spanner.py b/tests/unit/test_spanner.py index 3663d8bdc9..0c7feed5ac 100644 --- a/tests/unit/test_spanner.py +++ b/tests/unit/test_spanner.py @@ -556,6 +556,8 @@ def test_transaction_should_include_begin_with_first_batch_update(self): ("google-cloud-resource-prefix", database.name), ("x-goog-spanner-route-to-leader", "true"), ], + retry=RETRY, + timeout=TIMEOUT, ) def test_transaction_should_use_transaction_id_if_error_with_first_batch_update( @@ -574,6 +576,8 @@ def test_transaction_should_use_transaction_id_if_error_with_first_batch_update( ("google-cloud-resource-prefix", database.name), ("x-goog-spanner-route-to-leader", "true"), ], + retry=RETRY, + timeout=TIMEOUT, ) self._execute_update_helper(transaction=transaction, api=api) api.execute_sql.assert_called_once_with( @@ -715,6 +719,8 @@ def test_transaction_should_use_transaction_id_returned_by_first_read(self): ("google-cloud-resource-prefix", database.name), ("x-goog-spanner-route-to-leader", "true"), ], + retry=RETRY, + timeout=TIMEOUT, ) def test_transaction_should_use_transaction_id_returned_by_first_batch_update(self): @@ -729,6 +735,8 @@ def test_transaction_should_use_transaction_id_returned_by_first_batch_update(se ("google-cloud-resource-prefix", database.name), ("x-goog-spanner-route-to-leader", "true"), ], + retry=RETRY, + timeout=TIMEOUT, ) self._read_helper(transaction=transaction, api=api) api.streaming_read.assert_called_once_with( @@ -797,6 +805,8 @@ def test_transaction_for_concurrent_statement_should_begin_one_transaction_with_ ("google-cloud-resource-prefix", database.name), ("x-goog-spanner-route-to-leader", "true"), ], + retry=RETRY, + timeout=TIMEOUT, ) self.assertEqual(api.execute_sql.call_count, 2) @@ -846,6 +856,8 @@ def test_transaction_for_concurrent_statement_should_begin_one_transaction_with_ ("google-cloud-resource-prefix", database.name), ("x-goog-spanner-route-to-leader", "true"), ], + retry=RETRY, + timeout=TIMEOUT, ) api.execute_batch_dml.assert_any_call( @@ -854,6 +866,8 @@ def test_transaction_for_concurrent_statement_should_begin_one_transaction_with_ ("google-cloud-resource-prefix", database.name), ("x-goog-spanner-route-to-leader", "true"), ], + retry=RETRY, + timeout=TIMEOUT, ) self.assertEqual(api.execute_sql.call_count, 1) diff --git a/tests/unit/test_transaction.py b/tests/unit/test_transaction.py index a673eabb83..b40ae8843f 100644 --- a/tests/unit/test_transaction.py +++ b/tests/unit/test_transaction.py @@ -662,7 +662,14 @@ def test_batch_update_other_error(self): with self.assertRaises(RuntimeError): transaction.batch_update(statements=[DML_QUERY]) - def _batch_update_helper(self, error_after=None, count=0, request_options=None): + def _batch_update_helper( + self, + error_after=None, + count=0, + request_options=None, + retry=gapic_v1.method.DEFAULT, + timeout=gapic_v1.method.DEFAULT, + ): from google.rpc.status_pb2 import Status from google.protobuf.struct_pb2 import Struct from google.cloud.spanner_v1 import param_types @@ -716,7 +723,10 @@ def _batch_update_helper(self, error_after=None, count=0, request_options=None): request_options = RequestOptions(request_options) status, row_counts = transaction.batch_update( - dml_statements, request_options=request_options + dml_statements, + request_options=request_options, + retry=retry, + timeout=timeout, ) self.assertEqual(status, expected_status) @@ -753,6 +763,8 @@ def _batch_update_helper(self, error_after=None, count=0, request_options=None): ("google-cloud-resource-prefix", database.name), ("x-goog-spanner-route-to-leader", "true"), ], + retry=retry, + timeout=timeout, ) self.assertEqual(transaction._execute_sql_count, count + 1) @@ -826,6 +838,15 @@ def test_batch_update_error(self): self.assertEqual(transaction._execute_sql_count, 1) + def test_batch_update_w_timeout_param(self): + self._batch_update_helper(timeout=2.0) + + def test_batch_update_w_retry_param(self): + self._batch_update_helper(retry=gapic_v1.method.DEFAULT) + + def test_batch_update_w_timeout_and_retry_params(self): + self._batch_update_helper(retry=gapic_v1.method.DEFAULT, timeout=2.0) + def test_context_mgr_success(self): import datetime from google.cloud.spanner_v1 import CommitResponse From 2aee2540d460b5865ce4a838f750ea7e6e543d1d Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Wed, 6 Mar 2024 18:59:24 +0530 Subject: [PATCH 17/17] chore(main): release 3.43.0 (#1093) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- .release-please-manifest.json | 2 +- CHANGELOG.md | 20 +++++++++++++++++++ .../gapic_version.py | 2 +- .../gapic_version.py | 2 +- google/cloud/spanner_v1/gapic_version.py | 2 +- ...data_google.spanner.admin.database.v1.json | 2 +- ...data_google.spanner.admin.instance.v1.json | 2 +- .../snippet_metadata_google.spanner.v1.json | 2 +- 8 files changed, 27 insertions(+), 7 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index e1589c3bdf..e5cbfafe9d 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "3.42.0" + ".": "3.43.0" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 01e5229479..40d7b46ef4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,26 @@ [1]: https://pypi.org/project/google-cloud-spanner/#history +## [3.43.0](https://github.com/googleapis/python-spanner/compare/v3.42.0...v3.43.0) (2024-03-06) + + +### Features + +* Add retry and timeout for batch dml ([#1107](https://github.com/googleapis/python-spanner/issues/1107)) ([4f6340b](https://github.com/googleapis/python-spanner/commit/4f6340b0930bb1b5430209c4a1ff196c42b834d0)) +* Add support for max commit delay ([#1050](https://github.com/googleapis/python-spanner/issues/1050)) ([d5acc26](https://github.com/googleapis/python-spanner/commit/d5acc263d86fcbde7d5f972930255119e2f60e76)) +* Exposing Spanner client in dbapi connection ([#1100](https://github.com/googleapis/python-spanner/issues/1100)) ([9299212](https://github.com/googleapis/python-spanner/commit/9299212fb8aa6ed27ca40367e8d5aaeeba80c675)) +* Include RENAME in DDL regex ([#1075](https://github.com/googleapis/python-spanner/issues/1075)) ([3669303](https://github.com/googleapis/python-spanner/commit/3669303fb50b4207975b380f356227aceaa1189a)) +* Support partitioned dml in dbapi ([#1103](https://github.com/googleapis/python-spanner/issues/1103)) ([3aab0ed](https://github.com/googleapis/python-spanner/commit/3aab0ed5ed3cd078835812dae183a333fe1d3a20)) +* Untyped param ([#1001](https://github.com/googleapis/python-spanner/issues/1001)) ([1750328](https://github.com/googleapis/python-spanner/commit/1750328bbc7f8a1125f8e0c38024ced8e195a1b9)) + + +### Documentation + +* Samples and tests for admin backup APIs ([#1105](https://github.com/googleapis/python-spanner/issues/1105)) ([5410c32](https://github.com/googleapis/python-spanner/commit/5410c32febbef48d4623d8023a6eb9f07a65c2f5)) +* Samples and tests for admin database APIs ([#1099](https://github.com/googleapis/python-spanner/issues/1099)) ([c25376c](https://github.com/googleapis/python-spanner/commit/c25376c8513af293c9db752ffc1970dbfca1c5b8)) +* Update all public documents to use auto-generated admin clients. ([#1109](https://github.com/googleapis/python-spanner/issues/1109)) ([d683a14](https://github.com/googleapis/python-spanner/commit/d683a14ccc574e49cefd4e2b2f8b6d9bfd3663ec)) +* Use autogenerated methods to get names from admin samples ([#1110](https://github.com/googleapis/python-spanner/issues/1110)) ([3ab74b2](https://github.com/googleapis/python-spanner/commit/3ab74b267b651b430e96712be22088e2859d7e79)) + ## [3.42.0](https://github.com/googleapis/python-spanner/compare/v3.41.0...v3.42.0) (2024-01-30) diff --git a/google/cloud/spanner_admin_database_v1/gapic_version.py b/google/cloud/spanner_admin_database_v1/gapic_version.py index 5acda5fd9b..9519d06159 100644 --- a/google/cloud/spanner_admin_database_v1/gapic_version.py +++ b/google/cloud/spanner_admin_database_v1/gapic_version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # -__version__ = "3.42.0" # {x-release-please-version} +__version__ = "3.43.0" # {x-release-please-version} diff --git a/google/cloud/spanner_admin_instance_v1/gapic_version.py b/google/cloud/spanner_admin_instance_v1/gapic_version.py index 5acda5fd9b..9519d06159 100644 --- a/google/cloud/spanner_admin_instance_v1/gapic_version.py +++ b/google/cloud/spanner_admin_instance_v1/gapic_version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # -__version__ = "3.42.0" # {x-release-please-version} +__version__ = "3.43.0" # {x-release-please-version} diff --git a/google/cloud/spanner_v1/gapic_version.py b/google/cloud/spanner_v1/gapic_version.py index 5acda5fd9b..9519d06159 100644 --- a/google/cloud/spanner_v1/gapic_version.py +++ b/google/cloud/spanner_v1/gapic_version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # -__version__ = "3.42.0" # {x-release-please-version} +__version__ = "3.43.0" # {x-release-please-version} diff --git a/samples/generated_samples/snippet_metadata_google.spanner.admin.database.v1.json b/samples/generated_samples/snippet_metadata_google.spanner.admin.database.v1.json index eadd88950b..d82a3d122c 100644 --- a/samples/generated_samples/snippet_metadata_google.spanner.admin.database.v1.json +++ b/samples/generated_samples/snippet_metadata_google.spanner.admin.database.v1.json @@ -8,7 +8,7 @@ ], "language": "PYTHON", "name": "google-cloud-spanner-admin-database", - "version": "3.42.0" + "version": "3.43.0" }, "snippets": [ { diff --git a/samples/generated_samples/snippet_metadata_google.spanner.admin.instance.v1.json b/samples/generated_samples/snippet_metadata_google.spanner.admin.instance.v1.json index 63d632ab61..d5bccd9177 100644 --- a/samples/generated_samples/snippet_metadata_google.spanner.admin.instance.v1.json +++ b/samples/generated_samples/snippet_metadata_google.spanner.admin.instance.v1.json @@ -8,7 +8,7 @@ ], "language": "PYTHON", "name": "google-cloud-spanner-admin-instance", - "version": "3.42.0" + "version": "3.43.0" }, "snippets": [ { diff --git a/samples/generated_samples/snippet_metadata_google.spanner.v1.json b/samples/generated_samples/snippet_metadata_google.spanner.v1.json index ecec16b3e3..468b6aac82 100644 --- a/samples/generated_samples/snippet_metadata_google.spanner.v1.json +++ b/samples/generated_samples/snippet_metadata_google.spanner.v1.json @@ -8,7 +8,7 @@ ], "language": "PYTHON", "name": "google-cloud-spanner", - "version": "3.42.0" + "version": "3.43.0" }, "snippets": [ {