diff --git a/CHANGELOG.md b/CHANGELOG.md index e09b232b92..0814c1e8dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,27 @@ [1]: https://pypi.org/project/google-cloud-spanner/#history +## [3.23.0](https://github.com/googleapis/python-spanner/compare/v3.22.1...v3.23.0) (2022-11-07) + + +### Features + +* Adding support and samples for jsonb ([#851](https://github.com/googleapis/python-spanner/issues/851)) ([268924d](https://github.com/googleapis/python-spanner/commit/268924d29fa2577103abb9b6cdc91585d7c349ce)) +* Support request priorities ([#834](https://github.com/googleapis/python-spanner/issues/834)) ([ef2159c](https://github.com/googleapis/python-spanner/commit/ef2159c554b866955c9030099b208d4d9d594e83)) +* Support requiest options in !autocommit mode ([#838](https://github.com/googleapis/python-spanner/issues/838)) ([ab768e4](https://github.com/googleapis/python-spanner/commit/ab768e45efe7334823ec6bcdccfac2a6dde73bd7)) +* Update result_set.proto to return undeclared parameters in ExecuteSql API ([#841](https://github.com/googleapis/python-spanner/issues/841)) ([0aa4cad](https://github.com/googleapis/python-spanner/commit/0aa4cadb1ba8590cdfab5573b869e8b16e8050f8)) +* Update transaction.proto to include different lock modes ([#845](https://github.com/googleapis/python-spanner/issues/845)) ([c191296](https://github.com/googleapis/python-spanner/commit/c191296df5a0322e6050786e59159999eff16cdd)) + + +### Bug Fixes + +* **deps:** Allow protobuf 3.19.5 ([#839](https://github.com/googleapis/python-spanner/issues/839)) ([06725fc](https://github.com/googleapis/python-spanner/commit/06725fcf7fb216ad0cffb2cb568f8da38243c32e)) + + +### Documentation + +* Describe DB API and transactions retry mechanism ([#844](https://github.com/googleapis/python-spanner/issues/844)) ([30a0666](https://github.com/googleapis/python-spanner/commit/30a0666decf3ac638568c613facbf999efec6f19)), closes [#791](https://github.com/googleapis/python-spanner/issues/791) + ## [3.22.1](https://github.com/googleapis/python-spanner/compare/v3.22.0...v3.22.1) (2022-10-04) diff --git a/README.rst b/README.rst index bebfe1fd5d..7e75685f2e 100644 --- a/README.rst +++ b/README.rst @@ -235,6 +235,31 @@ if any of the records does not already exist. ) +Connection API +-------------- +Connection API represents a wrap-around for Python Spanner API, written in accordance with PEP-249, and provides a simple way of communication with a Spanner database through connection objects: + +.. code:: python + + from google.cloud.spanner_dbapi.connection import connect + + connection = connect("instance-id", "database-id") + connection.autocommit = True + + cursor = connection.cursor() + cursor.execute("SELECT * FROM table_name") + + result = cursor.fetchall() + + +Aborted Transactions Retry Mechanism +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In ``!autocommit`` mode, transactions can be aborted due to transient errors. In most cases retry of an aborted transaction solves the problem. To simplify it, connection tracks SQL statements, executed in the current transaction. In case the transaction aborted, the connection initiates a new one and re-executes all the statements. In the process, the connection checks that retried statements are returning the same results that the original statements did. If results are different, the transaction is dropped, as the underlying data changed, and auto retry is impossible. + +Auto-retry of aborted transactions is enabled only for ``!autocommit`` mode, as in ``autocommit`` mode transactions are never aborted. + + Next Steps ~~~~~~~~~~ diff --git a/google/cloud/spanner_dbapi/_helpers.py b/google/cloud/spanner_dbapi/_helpers.py index ee4883d74f..02901ffc3a 100644 --- a/google/cloud/spanner_dbapi/_helpers.py +++ b/google/cloud/spanner_dbapi/_helpers.py @@ -47,15 +47,21 @@ } -def _execute_insert_heterogenous(transaction, sql_params_list): +def _execute_insert_heterogenous( + transaction, + sql_params_list, + request_options=None, +): for sql, params in sql_params_list: sql, params = sql_pyformat_args_to_spanner(sql, params) - transaction.execute_update(sql, params, get_param_types(params)) + transaction.execute_update( + sql, params, get_param_types(params), request_options=request_options + ) def handle_insert(connection, sql, params): return connection.database.run_in_transaction( - _execute_insert_heterogenous, ((sql, params),) + _execute_insert_heterogenous, ((sql, params),), connection.request_options ) diff --git a/google/cloud/spanner_dbapi/connection.py b/google/cloud/spanner_dbapi/connection.py index 91b63a2da1..75263400f8 100644 --- a/google/cloud/spanner_dbapi/connection.py +++ b/google/cloud/spanner_dbapi/connection.py @@ -20,6 +20,7 @@ from google.api_core.exceptions import Aborted from google.api_core.gapic_v1.client_info import ClientInfo from google.cloud import spanner_v1 as spanner +from google.cloud.spanner_v1 import RequestOptions from google.cloud.spanner_v1.session import _get_retry_delay from google.cloud.spanner_v1.snapshot import Snapshot @@ -103,6 +104,7 @@ def __init__(self, instance, database, read_only=False): self._own_pool = True self._read_only = read_only self._staleness = None + self.request_priority = None @property def autocommit(self): @@ -181,6 +183,21 @@ def read_only(self, value): ) self._read_only = value + @property + def request_options(self): + """Options for the next SQL operations. + + Returns: + google.cloud.spanner_v1.RequestOptions: + Request options. + """ + if self.request_priority is None: + return + + req_opts = RequestOptions(priority=self.request_priority) + self.request_priority = None + return req_opts + @property def staleness(self): """Current read staleness option value of this `Connection`. @@ -435,7 +452,7 @@ def run_statement(self, statement, retried=False): if statement.is_insert: _execute_insert_heterogenous( - transaction, ((statement.sql, statement.params),) + transaction, ((statement.sql, statement.params),), self.request_options ) return ( iter(()), @@ -447,6 +464,7 @@ def run_statement(self, statement, retried=False): statement.sql, statement.params, param_types=statement.param_types, + request_options=self.request_options, ), ResultsChecksum() if retried else statement.checksum, ) diff --git a/google/cloud/spanner_dbapi/cursor.py b/google/cloud/spanner_dbapi/cursor.py index 4ffeac1a70..f8220d2c68 100644 --- a/google/cloud/spanner_dbapi/cursor.py +++ b/google/cloud/spanner_dbapi/cursor.py @@ -172,7 +172,10 @@ def close(self): def _do_execute_update(self, transaction, sql, params): result = transaction.execute_update( - sql, params=params, param_types=get_param_types(params) + sql, + params=params, + param_types=get_param_types(params), + request_options=self.connection.request_options, ) self._itr = None if type(result) == int: @@ -278,7 +281,9 @@ def execute(self, sql, args=None): _helpers.handle_insert(self.connection, sql, args or None) else: self.connection.database.run_in_transaction( - self._do_execute_update, sql, args or None + self._do_execute_update, + sql, + args or None, ) except (AlreadyExists, FailedPrecondition, OutOfRange) as e: raise IntegrityError(getattr(e, "details", e)) from e @@ -421,7 +426,12 @@ def fetchmany(self, size=None): return items def _handle_DQL_with_snapshot(self, snapshot, sql, params): - self._result_set = snapshot.execute_sql(sql, params, get_param_types(params)) + self._result_set = snapshot.execute_sql( + sql, + params, + get_param_types(params), + request_options=self.connection.request_options, + ) # Read the first element so that the StreamedResultSet can # return the metadata after a DQL statement. self._itr = PeekIterator(self._result_set) diff --git a/google/cloud/spanner_v1/param_types.py b/google/cloud/spanner_v1/param_types.py index 22c4782b8d..0c03f7ecc6 100644 --- a/google/cloud/spanner_v1/param_types.py +++ b/google/cloud/spanner_v1/param_types.py @@ -31,6 +31,7 @@ NUMERIC = Type(code=TypeCode.NUMERIC) JSON = Type(code=TypeCode.JSON) PG_NUMERIC = Type(code=TypeCode.NUMERIC, type_annotation=TypeAnnotationCode.PG_NUMERIC) +PG_JSONB = Type(code=TypeCode.JSON, type_annotation=TypeAnnotationCode.PG_JSONB) def Array(element_type): diff --git a/google/cloud/spanner_v1/types/result_set.py b/google/cloud/spanner_v1/types/result_set.py index 68ff3700c5..2990a015b5 100644 --- a/google/cloud/spanner_v1/types/result_set.py +++ b/google/cloud/spanner_v1/types/result_set.py @@ -238,6 +238,20 @@ class ResultSetMetadata(proto.Message): If the read or SQL query began a transaction as a side-effect, the information about the new transaction is yielded here. + undeclared_parameters (google.cloud.spanner_v1.types.StructType): + A SQL query can be parameterized. In PLAN mode, these + parameters can be undeclared. This indicates the field names + and types for those undeclared parameters in the SQL query. + For example, a SQL query like + ``"SELECT * FROM Users where UserId = @userId and UserName = @userName "`` + could return a ``undeclared_parameters`` value like: + + :: + + "fields": [ + { "name": "UserId", "type": { "code": "INT64" } }, + { "name": "UserName", "type": { "code": "STRING" } }, + ] """ row_type = proto.Field( @@ -250,6 +264,11 @@ class ResultSetMetadata(proto.Message): number=2, message=gs_transaction.Transaction, ) + undeclared_parameters = proto.Field( + proto.MESSAGE, + number=3, + message=gs_type.StructType, + ) class ResultSetStats(proto.Message): diff --git a/google/cloud/spanner_v1/types/transaction.py b/google/cloud/spanner_v1/types/transaction.py index f6c24708a2..0c7cb06bf0 100644 --- a/google/cloud/spanner_v1/types/transaction.py +++ b/google/cloud/spanner_v1/types/transaction.py @@ -401,8 +401,25 @@ class ReadWrite(proto.Message): r"""Message type to initiate a read-write transaction. Currently this transaction type has no options. + Attributes: + read_lock_mode (google.cloud.spanner_v1.types.TransactionOptions.ReadWrite.ReadLockMode): + Read lock mode for the transaction. """ + class ReadLockMode(proto.Enum): + r"""``ReadLockMode`` is used to set the read lock mode for read-write + transactions. + """ + READ_LOCK_MODE_UNSPECIFIED = 0 + PESSIMISTIC = 1 + OPTIMISTIC = 2 + + read_lock_mode = proto.Field( + proto.ENUM, + number=1, + enum="TransactionOptions.ReadWrite.ReadLockMode", + ) + class PartitionedDml(proto.Message): r"""Message type to initiate a Partitioned DML transaction.""" diff --git a/samples/samples/autocommit.py b/samples/samples/autocommit.py index d5c44b0c53..873ed2b7bd 100644 --- a/samples/samples/autocommit.py +++ b/samples/samples/autocommit.py @@ -46,11 +46,14 @@ def enable_autocommit_mode(instance_id, database_id): if __name__ == "__main__": parser = argparse.ArgumentParser( - description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter, + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter, ) parser.add_argument("instance_id", help="Your Cloud Spanner instance ID.") parser.add_argument( - "--database-id", help="Your Cloud Spanner database ID.", default="example_db", + "--database-id", + help="Your Cloud Spanner database ID.", + default="example_db", ) subparsers = parser.add_subparsers(dest="command") subparsers.add_parser("enable_autocommit_mode", help=enable_autocommit_mode.__doc__) diff --git a/samples/samples/autocommit_test.py b/samples/samples/autocommit_test.py index 6b102da8fe..8150058f1c 100644 --- a/samples/samples/autocommit_test.py +++ b/samples/samples/autocommit_test.py @@ -25,7 +25,8 @@ def test_enable_autocommit_mode(capsys, instance_id, sample_database): op.result() autocommit.enable_autocommit_mode( - instance_id, sample_database.database_id, + instance_id, + sample_database.database_id, ) out, _ = capsys.readouterr() assert "Autocommit mode is enabled." in out diff --git a/samples/samples/backup_sample_test.py b/samples/samples/backup_sample_test.py index da50fbba46..5f094e7a77 100644 --- a/samples/samples/backup_sample_test.py +++ b/samples/samples/backup_sample_test.py @@ -26,12 +26,12 @@ def sample_name(): def unique_database_id(): - """ Creates a unique id for the database. """ + """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. """ + """Creates a unique id for the backup.""" return f"test-backup-{uuid.uuid4().hex[:10]}" @@ -52,7 +52,10 @@ def test_create_backup(capsys, instance_id, sample_database): version_time = list(results)[0][0] backup_sample.create_backup( - instance_id, sample_database.database_id, BACKUP_ID, version_time, + instance_id, + sample_database.database_id, + BACKUP_ID, + version_time, ) out, _ = capsys.readouterr() assert BACKUP_ID in out @@ -74,10 +77,16 @@ def test_copy_backup(capsys, instance_id, spanner_client): @pytest.mark.dependency(name="create_backup_with_encryption_key") def test_create_backup_with_encryption_key( - capsys, instance_id, sample_database, kms_key_name, + capsys, + instance_id, + sample_database, + kms_key_name, ): backup_sample.create_backup_with_encryption_key( - instance_id, sample_database.database_id, CMEK_BACKUP_ID, kms_key_name, + instance_id, + sample_database.database_id, + CMEK_BACKUP_ID, + kms_key_name, ) out, _ = capsys.readouterr() assert CMEK_BACKUP_ID in out @@ -97,7 +106,10 @@ def test_restore_database(capsys, instance_id, sample_database): @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, + capsys, + instance_id, + sample_database, + kms_key_name, ): backup_sample.restore_database_with_encryption_key( instance_id, CMEK_RESTORE_DB_ID, CMEK_BACKUP_ID, kms_key_name @@ -123,10 +135,14 @@ def test_list_backup_operations(capsys, instance_id, sample_database): @pytest.mark.dependency(name="list_backup", depends=["create_backup", "copy_backup"]) def test_list_backups( - capsys, instance_id, sample_database, + capsys, + instance_id, + sample_database, ): backup_sample.list_backups( - instance_id, sample_database.database_id, BACKUP_ID, + instance_id, + sample_database.database_id, + BACKUP_ID, ) out, _ = capsys.readouterr() id_count = out.count(BACKUP_ID) @@ -153,7 +169,9 @@ def test_delete_backup(capsys, instance_id): @pytest.mark.dependency(depends=["create_backup"]) def test_cancel_backup(capsys, instance_id, sample_database): backup_sample.cancel_backup( - instance_id, sample_database.database_id, BACKUP_ID, + instance_id, + sample_database.database_id, + BACKUP_ID, ) out, _ = capsys.readouterr() cancel_success = "Backup creation was successfully cancelled." in out @@ -166,7 +184,9 @@ def test_cancel_backup(capsys, instance_id, sample_database): @RetryErrors(exception=DeadlineExceeded, max_tries=2) def test_create_database_with_retention_period(capsys, sample_instance): backup_sample.create_database_with_version_retention_period( - sample_instance.instance_id, RETENTION_DATABASE_ID, RETENTION_PERIOD, + sample_instance.instance_id, + RETENTION_DATABASE_ID, + RETENTION_PERIOD, ) out, _ = capsys.readouterr() assert (RETENTION_DATABASE_ID + " created with ") in out diff --git a/samples/samples/batch_sample.py b/samples/samples/batch_sample.py index 553dc31517..73d9f5667e 100644 --- a/samples/samples/batch_sample.py +++ b/samples/samples/batch_sample.py @@ -57,7 +57,7 @@ def run_batch_query(instance_id, database_id): for future in concurrent.futures.as_completed(futures, timeout=3600): finish, row_ct = future.result() elapsed = finish - start - print(u"Completed {} rows in {} seconds".format(row_ct, elapsed)) + print("Completed {} rows in {} seconds".format(row_ct, elapsed)) # Clean up snapshot.close() @@ -68,7 +68,7 @@ def process(snapshot, partition): print("Started processing partition.") row_ct = 0 for row in snapshot.process_read_batch(partition): - print(u"SingerId: {}, AlbumId: {}, AlbumTitle: {}".format(*row)) + print("SingerId: {}, AlbumId: {}, AlbumTitle: {}".format(*row)) row_ct += 1 return time.time(), row_ct diff --git a/samples/samples/conftest.py b/samples/samples/conftest.py index 314c984920..c63548c460 100644 --- a/samples/samples/conftest.py +++ b/samples/samples/conftest.py @@ -17,6 +17,9 @@ import uuid 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 @@ -26,17 +29,32 @@ INSTANCE_CREATION_TIMEOUT = 560 # seconds +OPERATION_TIMEOUT_SECONDS = 120 # seconds + retry_429 = retry.RetryErrors(exceptions.ResourceExhausted, delay=15) @pytest.fixture(scope="module") def sample_name(): - """ Sample testcase modules must define this fixture. + """Sample testcase modules must define this fixture. + + The name is used to label the instance created by the sample, to + aid in debugging leaked instances. + """ + raise NotImplementedError( + "Define 'sample_name' fixture in sample test driver") + + +@pytest.fixture(scope="module") +def database_dialect(): + """Database dialect to be used for this sample. - The name is used to label the instance created by the sample, to - aid in debugging leaked instances. - """ - raise NotImplementedError("Define 'sample_name' fixture in sample test driver") + The dialect is used to initialize the dialect for the database. + It can either be GoogleStandardSql or PostgreSql. + """ + # By default, we consider GOOGLE_STANDARD_SQL dialect. Other specific tests + # can override this if required. + return DatabaseDialect.GOOGLE_STANDARD_SQL @pytest.fixture(scope="session") @@ -87,7 +105,7 @@ def multi_region_instance_id(): @pytest.fixture(scope="module") def instance_config(spanner_client): return "{}/instanceConfigs/{}".format( - spanner_client.project_name, "regional-us-central1" + spanner_client.project_name, "regional-us-central1" ) @@ -98,16 +116,20 @@ def multi_region_instance_config(spanner_client): @pytest.fixture(scope="module") def sample_instance( - spanner_client, cleanup_old_instances, instance_id, instance_config, sample_name, + spanner_client, + cleanup_old_instances, + instance_id, + instance_config, + sample_name, ): sample_instance = spanner_client.instance( - instance_id, - instance_config, - labels={ - "cloud_spanner_samples": "true", - "sample_name": sample_name, - "created": str(int(time.time())), - }, + instance_id, + instance_config, + labels={ + "cloud_spanner_samples": "true", + "sample_name": sample_name, + "created": str(int(time.time())), + }, ) op = retry_429(sample_instance.create)() op.result(INSTANCE_CREATION_TIMEOUT) # block until completion @@ -129,20 +151,20 @@ def sample_instance( @pytest.fixture(scope="module") def multi_region_instance( - spanner_client, - cleanup_old_instances, - multi_region_instance_id, - multi_region_instance_config, - sample_name, + spanner_client, + cleanup_old_instances, + multi_region_instance_id, + multi_region_instance_config, + sample_name, ): multi_region_instance = spanner_client.instance( - multi_region_instance_id, - multi_region_instance_config, - labels={ - "cloud_spanner_samples": "true", - "sample_name": sample_name, - "created": str(int(time.time())), - }, + multi_region_instance_id, + multi_region_instance_config, + labels={ + "cloud_spanner_samples": "true", + "sample_name": sample_name, + "created": str(int(time.time())), + }, ) op = retry_429(multi_region_instance.create)() op.result(INSTANCE_CREATION_TIMEOUT) # block until completion @@ -166,8 +188,8 @@ def multi_region_instance( def database_id(): """Id for the database used in samples. - Sample testcase modules can override as needed. - """ + Sample testcase modules can override as needed. + """ return "my-database-id" @@ -175,20 +197,50 @@ def database_id(): def database_ddl(): """Sequence of DDL statements used to set up the database. - Sample testcase modules can override as needed. - """ + Sample testcase modules can override as needed. + """ return [] @pytest.fixture(scope="module") -def sample_database(sample_instance, database_id, database_ddl): +def sample_database( + spanner_client, + sample_instance, + database_id, + database_ddl, + database_dialect): + if database_dialect == DatabaseDialect.POSTGRESQL: + sample_database = sample_instance.database( + database_id, + database_dialect=DatabaseDialect.POSTGRESQL, + ) + + if not sample_database.exists(): + operation = sample_database.create() + operation.result(OPERATION_TIMEOUT_SECONDS) + + request = spanner_admin_database_v1.UpdateDatabaseDdlRequest( + database=sample_database.name, + statements=database_ddl, + ) + + operation =\ + spanner_client.database_admin_api.update_database_ddl(request) + operation.result(OPERATION_TIMEOUT_SECONDS) + + yield sample_database + + sample_database.drop() + return sample_database = sample_instance.database( - database_id, ddl_statements=database_ddl, + database_id, + ddl_statements=database_ddl, ) if not sample_database.exists(): - sample_database.create() + operation = sample_database.create() + operation.result(OPERATION_TIMEOUT_SECONDS) yield sample_database @@ -198,8 +250,8 @@ def sample_database(sample_instance, database_id, database_ddl): @pytest.fixture(scope="module") def kms_key_name(spanner_client): return "projects/{}/locations/{}/keyRings/{}/cryptoKeys/{}".format( - spanner_client.project, - "us-central1", - "spanner-test-keyring", - "spanner-test-cmek", + spanner_client.project, + "us-central1", + "spanner-test-keyring", + "spanner-test-cmek", ) diff --git a/samples/samples/pg_snippets.py b/samples/samples/pg_snippets.py new file mode 100644 index 0000000000..87215b69b8 --- /dev/null +++ b/samples/samples/pg_snippets.py @@ -0,0 +1,1670 @@ +#!/usr/bin/env python + +# Copyright 2022 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. +""" +import argparse +import base64 +import datetime +import decimal +import time + +from google.cloud import spanner, spanner_admin_database_v1 +from google.cloud.spanner_admin_database_v1.types.common import DatabaseDialect +from google.cloud.spanner_v1 import param_types +from google.cloud.spanner_v1.data_types import JsonObject + +OPERATION_TIMEOUT_SECONDS = 240 + + +# [START spanner_postgresql_create_instance] +def create_instance(instance_id): + """Creates an instance.""" + 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 = instance.create() + + print("Waiting for operation to complete...") + operation.result(OPERATION_TIMEOUT_SECONDS) + + print("Created instance {}".format(instance_id)) + + +# [END spanner_postgresql_create_instance] + + +# [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, + database_dialect=DatabaseDialect.POSTGRESQL, + ) + + operation = database.create() + + print("Waiting for operation to complete...") + 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): + 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, + 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_insert_data] +def insert_data(instance_id, database_id): + """Inserts sample data into the given database. + + The database and table must already exist and can be created using + `create_database`. + """ + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + database = instance.database(database_id) + + with database.batch() as batch: + batch.insert( + table="Singers", + columns=("SingerId", "FirstName", "LastName"), + values=[ + (1, "Marc", "Richards"), + (2, "Catalina", "Smith"), + (3, "Alice", "Trentor"), + (4, "Lea", "Martin"), + (5, "David", "Lomond"), + ], + ) + + batch.insert( + table="Albums", + columns=("SingerId", "AlbumId", "AlbumTitle"), + values=[ + (1, 1, "Total Junk"), + (1, 2, "Go, Go, Go"), + (2, 1, "Green"), + (2, 2, "Forever Hold Your Peace"), + (2, 3, "Terrified"), + ], + ) + + print("Inserted data.") + + +# [END spanner_postgresql_insert_data] + + +# [START spanner_postgresql_delete_data] +def delete_data(instance_id, database_id): + """Deletes sample data from the given database. + + The database, table, and data must already exist and can be created using + `create_database` and `insert_data`. + """ + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + database = instance.database(database_id) + + # Delete individual rows + albums_to_delete = spanner.KeySet(keys=[[2, 1], [2, 3]]) + + # Delete a range of rows where the column key is >=3 and <5 + singers_range = spanner.KeyRange(start_closed=[3], end_open=[5]) + singers_to_delete = spanner.KeySet(ranges=[singers_range]) + + # Delete remaining Singers rows, which will also delete the remaining + # Albums rows because Albums was defined with ON DELETE CASCADE + remaining_singers = spanner.KeySet(all_=True) + + with database.batch() as batch: + batch.delete("Albums", albums_to_delete) + batch.delete("Singers", singers_to_delete) + batch.delete("Singers", remaining_singers) + + print("Deleted data.") + + +# [END spanner_postgresql_delete_data] + + +# [START spanner_postgresql_query_data] +def query_data(instance_id, database_id): + """Queries sample data from the database using SQL.""" + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + database = instance.database(database_id) + + with database.snapshot() as snapshot: + results = snapshot.execute_sql( + "SELECT SingerId, AlbumId, AlbumTitle FROM Albums" + ) + + for row in results: + print("SingerId: {}, AlbumId: {}, AlbumTitle: {}".format(*row)) + + +# [END spanner_postgresql_query_data] + + +# [START spanner_postgresql_read_data] +def read_data(instance_id, database_id): + """Reads sample data from the database.""" + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + database = instance.database(database_id) + + with database.snapshot() as snapshot: + keyset = spanner.KeySet(all_=True) + results = snapshot.read( + table="Albums", columns=("SingerId", "AlbumId", "AlbumTitle"), + keyset=keyset + ) + + for row in results: + print("SingerId: {}, AlbumId: {}, AlbumTitle: {}".format(*row)) + + +# [END spanner_postgresql_read_data] + + +# [START spanner_postgresql_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) + + operation = database.update_ddl( + ["ALTER TABLE Albums ADD COLUMN MarketingBudget BIGINT"] + ) + + print("Waiting for operation to complete...") + operation.result(OPERATION_TIMEOUT_SECONDS) + + print("Added the MarketingBudget column.") + + +# [END spanner_postgresql_add_column] + + +# [START spanner_postgresql_update_data] +def update_data(instance_id, database_id): + """Updates sample data in the database. + + This updates the `MarketingBudget` column which must be created before + running this sample. You can add the column by running the `add_column` + sample or by running this DDL statement against your database: + + ALTER TABLE Albums ADD COLUMN MarketingBudget INT64 + + """ + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + database = instance.database(database_id) + + with database.batch() as batch: + batch.update( + table="Albums", + columns=("SingerId", "AlbumId", "MarketingBudget"), + values=[(1, 1, 100000), (2, 2, 500000)], + ) + + print("Updated data.") + + +# [END spanner_postgresql_update_data] + + +# [START spanner_postgresql_read_write_transaction] +def read_write_transaction(instance_id, database_id): + """Performs a read-write transaction to update two sample records in the + database. + + This will transfer 200,000 from the `MarketingBudget` field for the second + Album to the first Album. If the `MarketingBudget` is too low, it will + raise an exception. + + Before running this sample, you will need to run the `update_data` sample + to populate the fields. + """ + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + database = instance.database(database_id) + + def update_albums(transaction): + # Read the second album budget. + second_album_keyset = spanner.KeySet(keys=[(2, 2)]) + second_album_result = transaction.read( + table="Albums", + columns=("MarketingBudget",), + keyset=second_album_keyset, + limit=1, + ) + second_album_row = list(second_album_result)[0] + second_album_budget = second_album_row[0] + + transfer_amount = 200000 + + if second_album_budget < transfer_amount: + # Raising an exception will automatically roll back the + # transaction. + raise ValueError( + "The second album doesn't have enough funds to transfer") + + # Read the first album's budget. + first_album_keyset = spanner.KeySet(keys=[(1, 1)]) + first_album_result = transaction.read( + table="Albums", + columns=("MarketingBudget",), + keyset=first_album_keyset, + limit=1, + ) + first_album_row = list(first_album_result)[0] + first_album_budget = first_album_row[0] + + # Update the budgets. + second_album_budget -= transfer_amount + first_album_budget += transfer_amount + print( + "Setting first album's budget to {} and the second album's " + "budget to {}.".format(first_album_budget, second_album_budget) + ) + + # Update the rows. + transaction.update( + table="Albums", + columns=("SingerId", "AlbumId", "MarketingBudget"), + values=[(1, 1, first_album_budget), (2, 2, second_album_budget)], + ) + + database.run_in_transaction(update_albums) + + print("Transaction complete.") + + +# [END spanner_postgresql_read_write_transaction] + + +# [START spanner_postgresql_query_data_with_new_column] +def query_data_with_new_column(instance_id, database_id): + """Queries sample data from the database using SQL. + + This sample uses the `MarketingBudget` column. You can add the column + by running the `add_column` sample or by running this DDL statement against + your database: + + ALTER TABLE Albums ADD COLUMN MarketingBudget INT64 + """ + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + database = instance.database(database_id) + + with database.snapshot() as snapshot: + results = snapshot.execute_sql( + "SELECT SingerId, AlbumId, MarketingBudget FROM Albums" + ) + + for row in results: + print("SingerId: {}, AlbumId: {}, MarketingBudget: {}".format(*row)) + + +# [END spanner_postgresql_query_data_with_new_column] + + +# [START spanner_postgresql_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) + + operation = database.update_ddl( + ["CREATE INDEX AlbumsByAlbumTitle ON Albums(AlbumTitle)"] + ) + + print("Waiting for operation to complete...") + operation.result(OPERATION_TIMEOUT_SECONDS) + + print("Added the AlbumsByAlbumTitle index.") + + +# [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. + + The index must exist before running this sample. You can add the index + by running the `add_index` sample or by running this DDL statement against + your database: + + CREATE INDEX AlbumsByAlbumTitle ON Albums(AlbumTitle) + + """ + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + database = instance.database(database_id) + + with database.snapshot() as snapshot: + keyset = spanner.KeySet(all_=True) + results = snapshot.read( + table="Albums", + columns=("AlbumId", "AlbumTitle"), + keyset=keyset, + index="AlbumsByAlbumTitle", + ) + + for row in results: + print("AlbumId: {}, AlbumTitle: {}".format(*row)) + + +# [END spanner_postgresql_read_data_with_index] + + +# [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)" + ] + ) + + 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_read_data_with_storing_index] +def read_data_with_storing_index(instance_id, database_id): + """Reads sample data from the database using an index with a storing + clause. + + The index must exist before running this sample. You can add the index + by running the `add_scoring_index` sample or by running this DDL statement + against your database: + + CREATE INDEX AlbumsByAlbumTitle2 ON Albums(AlbumTitle) + INCLUDE (MarketingBudget) + + """ + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + database = instance.database(database_id) + + with database.snapshot() as snapshot: + keyset = spanner.KeySet(all_=True) + results = snapshot.read( + table="Albums", + columns=("AlbumId", "AlbumTitle", "MarketingBudget"), + keyset=keyset, + index="AlbumsByAlbumTitle2", + ) + + for row in results: + print("AlbumId: {}, AlbumTitle: {}, " "MarketingBudget: {}".format( + *row)) + + +# [END spanner_postgresql_read_data_with_storing_index] + + +# [START spanner_postgresql_read_only_transaction] +def read_only_transaction(instance_id, database_id): + """Reads data inside of a read-only transaction. + + Within the read-only transaction, or "snapshot", the application sees + consistent view of the database at a particular timestamp. + """ + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + database = instance.database(database_id) + + with database.snapshot(multi_use=True) as snapshot: + # Read using SQL. + results = snapshot.execute_sql( + "SELECT SingerId, AlbumId, AlbumTitle FROM Albums" + ) + + print("Results from first read:") + for row in results: + print("SingerId: {}, AlbumId: {}, AlbumTitle: {}".format(*row)) + + # Perform another read using the `read` method. Even if the data + # is updated in-between the reads, the snapshot ensures that both + # return the same data. + keyset = spanner.KeySet(all_=True) + results = snapshot.read( + table="Albums", columns=("SingerId", "AlbumId", "AlbumTitle"), + keyset=keyset + ) + + print("Results from second read:") + for row in results: + print("SingerId: {}, AlbumId: {}, AlbumTitle: {}".format(*row)) + + +# [END spanner_postgresql_read_only_transaction] + + +def insert_with_dml(instance_id, database_id): + """Inserts data with a DML statement into the database.""" + # [START spanner_postgresql_dml_getting_started_insert] + # 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) + + def insert_singers(transaction): + row_ct = transaction.execute_update( + "INSERT INTO Singers (SingerId, FirstName, LastName) VALUES " + "(12, 'Melissa', 'Garcia'), " + "(13, 'Russell', 'Morales'), " + "(14, 'Jacqueline', 'Long'), " + "(15, 'Dylan', 'Shaw')" + ) + print("{} record(s) inserted.".format(row_ct)) + + database.run_in_transaction(insert_singers) + # [END spanner_postgresql_dml_getting_started_insert] + + +def query_data_with_parameter(instance_id, database_id): + """Queries sample data from the database using SQL with a parameter.""" + # [START spanner_postgresql_query_with_parameter] + # 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) + + with database.snapshot() as snapshot: + results = snapshot.execute_sql( + "SELECT SingerId, FirstName, LastName FROM Singers " "WHERE LastName = $1", + params={"p1": "Garcia"}, + param_types={"p1": spanner.param_types.STRING}, + ) + + for row in results: + print("SingerId: {}, FirstName: {}, LastName: {}".format(*row)) + # [END spanner_postgresql_query_with_parameter] + + +def write_with_dml_transaction(instance_id, database_id): + """Transfers part of a marketing budget from one album to another.""" + # [START spanner_postgresql_dml_getting_started_update] + # 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) + + def transfer_budget(transaction): + # Transfer marketing budget from one album to another. Performed in a + # single transaction to ensure that the transfer is atomic. + second_album_result = transaction.execute_sql( + "SELECT MarketingBudget from Albums " "WHERE SingerId = 2 and AlbumId = 2" + ) + second_album_row = list(second_album_result)[0] + second_album_budget = second_album_row[0] + + transfer_amount = 200000 + + # Transaction will only be committed if this condition still holds at + # the time of commit. Otherwise it will be aborted and the callable + # will be rerun by the client library + if second_album_budget >= transfer_amount: + first_album_result = transaction.execute_sql( + "SELECT MarketingBudget from Albums " + "WHERE SingerId = 1 and AlbumId = 1" + ) + first_album_row = list(first_album_result)[0] + first_album_budget = first_album_row[0] + + second_album_budget -= transfer_amount + first_album_budget += transfer_amount + + # Update first album + transaction.execute_update( + "UPDATE Albums " + "SET MarketingBudget = $1 " + "WHERE SingerId = 1 and AlbumId = 1", + params={"p1": first_album_budget}, + param_types={"p1": spanner.param_types.INT64}, + ) + + # Update second album + transaction.execute_update( + "UPDATE Albums " + "SET MarketingBudget = $1 " + "WHERE SingerId = 2 and AlbumId = 2", + params={"p1": second_album_budget}, + param_types={"p1": spanner.param_types.INT64}, + ) + + print( + "Transferred {} from Album2's budget to Album1's".format( + transfer_amount + ) + ) + + database.run_in_transaction(transfer_budget) + # [END spanner_postgresql_dml_getting_started_update] + + +# [START spanner_postgresql_read_stale_data] +def read_stale_data(instance_id, database_id): + """Reads sample data from the database. The data is exactly 15 seconds + stale.""" + import datetime + + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + database = instance.database(database_id) + staleness = datetime.timedelta(seconds=15) + + with database.snapshot(exact_staleness=staleness) as snapshot: + keyset = spanner.KeySet(all_=True) + results = snapshot.read( + table="Albums", + columns=("SingerId", "AlbumId", "MarketingBudget"), + keyset=keyset, + ) + + for row in results: + print("SingerId: {}, AlbumId: {}, MarketingBudget: {}".format(*row)) + + +# [END spanner_postgresql_read_stale_data] + + +# [START spanner_postgresql_update_data_with_timestamp_column] +def update_data_with_timestamp(instance_id, database_id): + """Updates Performances tables in the database with the COMMIT_TIMESTAMP + column. + + This updates the `MarketingBudget` column which must be created before + running this sample. You can add the column by running the `add_column` + sample or by running this DDL statement against your database: + + ALTER TABLE Albums ADD COLUMN MarketingBudget BIGINT + + In addition this update expects the LastUpdateTime column added by + applying this DDL statement against your database: + + ALTER TABLE Albums ADD COLUMN LastUpdateTime SPANNER.COMMIT_TIMESTAMP + """ + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + + database = instance.database(database_id) + + with database.batch() as batch: + batch.update( + table="Albums", + columns=( + "SingerId", "AlbumId", "MarketingBudget", "LastUpdateTime"), + values=[ + (1, 1, 1000000, spanner.COMMIT_TIMESTAMP), + (2, 2, 750000, spanner.COMMIT_TIMESTAMP), + ], + ) + + print("Updated data.") + + +# [END spanner_postgresql_update_data_with_timestamp_column] + + +# [START spanner_postgresql_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) + + database = instance.database(database_id) + + operation = database.update_ddl( + [ + "ALTER TABLE Albums ADD COLUMN LastUpdateTime SPANNER.COMMIT_TIMESTAMP"] + ) + + 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_postgresql_add_timestamp_column] + + +# [START spanner_postgresql_query_data_with_timestamp_column] +def query_data_with_timestamp(instance_id, database_id): + """Queries sample data from the database using SQL. + + This updates the `LastUpdateTime` column which must be created before + running this sample. You can add the column by running the + `add_timestamp_column` sample or by running this DDL statement + against your database: + + ALTER TABLE Performances ADD COLUMN LastUpdateTime TIMESTAMP + OPTIONS (allow_commit_timestamp=true) + + """ + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + + database = instance.database(database_id) + + with database.snapshot() as snapshot: + results = snapshot.execute_sql( + "SELECT SingerId, AlbumId, MarketingBudget FROM Albums " + "ORDER BY LastUpdateTime DESC" + ) + + for row in results: + print("SingerId: {}, AlbumId: {}, MarketingBudget: {}".format(*row)) + + +# [END spanner_postgresql_query_data_with_timestamp_column] + + +# [START spanner_postgresql_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_admin_database_v1.UpdateDatabaseDdlRequest( + database=database.name, + statements=[ + """CREATE TABLE Performances ( + SingerId BIGINT NOT NULL, + VenueId BIGINT NOT NULL, + EventDate Date, + Revenue BIGINT, + LastUpdateTime SPANNER.COMMIT_TIMESTAMP NOT NULL, +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_postgresql_create_table_with_timestamp_column] + + +# [START spanner_postgresql_insert_data_with_timestamp_column] +def insert_data_with_timestamp(instance_id, database_id): + """Inserts data with a COMMIT_TIMESTAMP field into a table.""" + + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + + database = instance.database(database_id) + + with database.batch() as batch: + batch.insert( + table="Performances", + columns=( + "SingerId", "VenueId", "EventDate", "Revenue", "LastUpdateTime"), + values=[ + (1, 4, "2017-10-05", 11000, spanner.COMMIT_TIMESTAMP), + (1, 19, "2017-11-02", 15000, spanner.COMMIT_TIMESTAMP), + (2, 42, "2017-12-23", 7000, spanner.COMMIT_TIMESTAMP), + ], + ) + + print("Inserted data.") + + +# [END spanner_postgresql_insert_data_with_timestamp_column] + + +def insert_data_with_dml(instance_id, database_id): + """Inserts sample data into the given database using a DML statement.""" + # [START spanner_postgresql_dml_standard_insert] + # 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) + + def insert_singers(transaction): + row_ct = transaction.execute_update( + "INSERT INTO Singers (SingerId, FirstName, LastName) " + " VALUES (10, 'Virginia', 'Watson')" + ) + + print("{} record(s) inserted.".format(row_ct)) + + database.run_in_transaction(insert_singers) + # [END spanner_postgresql_dml_standard_insert] + + +def update_data_with_dml(instance_id, database_id): + """Updates sample data from the database using a DML statement.""" + # [START spanner_postgresql_dml_standard_update] + # 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) + + def update_albums(transaction): + row_ct = transaction.execute_update( + "UPDATE Albums " + "SET MarketingBudget = MarketingBudget * 2 " + "WHERE SingerId = 1 and AlbumId = 1" + ) + + print("{} record(s) updated.".format(row_ct)) + + database.run_in_transaction(update_albums) + # [END spanner_postgresql_dml_standard_update] + + +def delete_data_with_dml(instance_id, database_id): + """Deletes sample data from the database using a DML statement.""" + # [START spanner_postgresql_dml_standard_delete] + # 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) + + def delete_singers(transaction): + row_ct = transaction.execute_update( + "DELETE FROM Singers WHERE FirstName = 'Alice'" + ) + + print("{} record(s) deleted.".format(row_ct)) + + database.run_in_transaction(delete_singers) + # [END spanner_postgresql_dml_standard_delete] + + +def dml_write_read_transaction(instance_id, database_id): + """First inserts data then reads it from within a transaction using DML.""" + # [START spanner_postgresql_dml_write_then_read] + # 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) + + def write_then_read(transaction): + # Insert record. + row_ct = transaction.execute_update( + "INSERT INTO Singers (SingerId, FirstName, LastName) " + " VALUES (11, 'Timothy', 'Campbell')" + ) + print("{} record(s) inserted.".format(row_ct)) + + # Read newly inserted record. + results = transaction.execute_sql( + "SELECT FirstName, LastName FROM Singers WHERE SingerId = 11" + ) + for result in results: + print("FirstName: {}, LastName: {}".format(*result)) + + database.run_in_transaction(write_then_read) + # [END spanner_postgresql_dml_write_then_read] + + +def update_data_with_partitioned_dml(instance_id, database_id): + """Update sample data with a partitioned DML statement.""" + # [START spanner_postgresql_dml_partitioned_update] + # 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) + + row_ct = database.execute_partitioned_dml( + "UPDATE Albums SET MarketingBudget = 100000 WHERE SingerId > 1" + ) + + print("{} records updated.".format(row_ct)) + # [END spanner_postgresql_dml_partitioned_update] + + +def delete_data_with_partitioned_dml(instance_id, database_id): + """Delete sample data with a partitioned DML statement.""" + # [START spanner_postgresql_dml_partitioned_delete] + # 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) + + row_ct = database.execute_partitioned_dml( + "DELETE FROM Singers WHERE SingerId > 10") + + print("{} record(s) deleted.".format(row_ct)) + # [END spanner_postgresql_dml_partitioned_delete] + + +def update_with_batch_dml(instance_id, database_id): + """Updates sample data in the database using Batch DML.""" + # [START spanner_postgresql_dml_batch_update] + from google.rpc.code_pb2 import OK + + # 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) + + insert_statement = ( + "INSERT INTO Albums " + "(SingerId, AlbumId, AlbumTitle, MarketingBudget) " + "VALUES (1, 3, 'Test Album Title', 10000)" + ) + + update_statement = ( + "UPDATE Albums " + "SET MarketingBudget = MarketingBudget * 2 " + "WHERE SingerId = 1 and AlbumId = 3" + ) + + def update_albums(transaction): + status, row_cts = transaction.batch_update( + [insert_statement, update_statement]) + + if status.code != OK: + # Do handling here. + # Note: the exception will still be raised when + # `commit` is called by `run_in_transaction`. + return + + print( + "Executed {} SQL statements using Batch DML.".format(len(row_cts))) + + database.run_in_transaction(update_albums) + # [END spanner_postgresql_dml_batch_update] + + +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_admin_database_v1.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] + + +def insert_datatypes_data(instance_id, database_id): + """Inserts data with supported datatypes into a table.""" + # [START spanner_postgresql_insert_datatypes_data] + # 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) + + exampleBytes1 = base64.b64encode("Hello World 1".encode()) + exampleBytes2 = base64.b64encode("Hello World 2".encode()) + exampleBytes3 = base64.b64encode("Hello World 3".encode()) + with database.batch() as batch: + batch.insert( + table="Venues", + columns=( + "VenueId", + "VenueName", + "VenueInfo", + "Capacity", + "OutdoorVenue", + "PopularityScore", + "Revenue", + "LastUpdateTime", + ), + values=[ + ( + 4, + "Venue 4", + exampleBytes1, + 1800, + False, + 0.85543, + decimal.Decimal("215100.10"), + spanner.COMMIT_TIMESTAMP, + ), + ( + 19, + "Venue 19", + exampleBytes2, + 6300, + True, + 0.98716, + decimal.Decimal("1200100.00"), + spanner.COMMIT_TIMESTAMP, + ), + ( + 42, + "Venue 42", + exampleBytes3, + 3000, + False, + 0.72598, + decimal.Decimal("390650.99"), + spanner.COMMIT_TIMESTAMP, + ), + ], + ) + + print("Inserted data.") + # [END spanner_postgresql_insert_datatypes_data] + + +def query_data_with_bool(instance_id, database_id): + """Queries sample data using SQL with a BOOL parameter.""" + # [START spanner_postgresql_query_with_bool_parameter] + # 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) + + exampleBool = True + param = {"p1": exampleBool} + param_type = {"p1": param_types.BOOL} + + with database.snapshot() as snapshot: + results = snapshot.execute_sql( + "SELECT VenueId, VenueName, OutdoorVenue FROM Venues " + "WHERE OutdoorVenue = $1", + params=param, + param_types=param_type, + ) + + for row in results: + print("VenueId: {}, VenueName: {}, OutdoorVenue: {}".format(*row)) + # [END spanner_postgresql_query_with_bool_parameter] + + +def query_data_with_bytes(instance_id, database_id): + """Queries sample data using SQL with a BYTES parameter.""" + # [START spanner_postgresql_query_with_bytes_parameter] + # 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) + + exampleBytes = base64.b64encode("Hello World 1".encode()) + param = {"p1": exampleBytes} + param_type = {"p1": param_types.BYTES} + + with database.snapshot() as snapshot: + results = snapshot.execute_sql( + "SELECT VenueId, VenueName FROM Venues " "WHERE VenueInfo = $1", + params=param, + param_types=param_type, + ) + + for row in results: + print("VenueId: {}, VenueName: {}".format(*row)) + # [END spanner_postgresql_query_with_bytes_parameter] + + +def query_data_with_float(instance_id, database_id): + """Queries sample data using SQL with a FLOAT8 parameter.""" + # [START spanner_postgresql_query_with_float_parameter] + # 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) + + exampleFloat = 0.8 + param = {"p1": exampleFloat} + param_type = {"p1": param_types.FLOAT64} + + with database.snapshot() as snapshot: + results = snapshot.execute_sql( + "SELECT VenueId, VenueName, PopularityScore FROM Venues " + "WHERE PopularityScore > $1", + params=param, + param_types=param_type, + ) + + for row in results: + print( + "VenueId: {}, VenueName: {}, PopularityScore: {}".format(*row)) + # [END spanner_postgresql_query_with_float_parameter] + + +def query_data_with_int(instance_id, database_id): + """Queries sample data using SQL with a BIGINT parameter.""" + # [START spanner_postgresql_query_with_int_parameter] + # 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) + + exampleInt = 3000 + param = {"p1": exampleInt} + param_type = {"p1": param_types.INT64} + + with database.snapshot() as snapshot: + results = snapshot.execute_sql( + "SELECT VenueId, VenueName, Capacity FROM Venues " "WHERE Capacity >= $1", + params=param, + param_types=param_type, + ) + + for row in results: + print("VenueId: {}, VenueName: {}, Capacity: {}".format(*row)) + # [END spanner_postgresql_query_with_int_parameter] + + +def query_data_with_string(instance_id, database_id): + """Queries sample data using SQL with a STRING parameter.""" + # [START spanner_postgresql_query_with_string_parameter] + # 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) + + exampleString = "Venue 42" + param = {"p1": exampleString} + param_type = {"p1": param_types.STRING} + + with database.snapshot() as snapshot: + results = snapshot.execute_sql( + "SELECT VenueId, VenueName FROM Venues " "WHERE VenueName = $1", + params=param, + param_types=param_type, + ) + + for row in results: + print("VenueId: {}, VenueName: {}".format(*row)) + # [END spanner_postgresql_query_with_string_parameter] + + +def query_data_with_timestamp_parameter(instance_id, database_id): + """Queries sample data using SQL with a TIMESTAMPTZ parameter.""" + # [START spanner_postgresql_query_with_timestamp_parameter] + # 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) + + example_timestamp = datetime.datetime.utcnow().isoformat() + "Z" + # [END spanner_postgresql_query_with_timestamp_parameter] + # Avoid time drift on the local machine. + # https://github.com/GoogleCloudPlatform/python-docs-samples/issues/4197. + example_timestamp = (datetime.datetime.utcnow() + datetime.timedelta(days=1) + ).isoformat() + "Z" + # [START spanner_postgresql_query_with_timestamp_parameter] + param = {"p1": example_timestamp} + param_type = {"p1": param_types.TIMESTAMP} + + with database.snapshot() as snapshot: + results = snapshot.execute_sql( + "SELECT VenueId, VenueName, LastUpdateTime FROM Venues " + "WHERE LastUpdateTime < $1", + params=param, + param_types=param_type, + ) + + for row in results: + print("VenueId: {}, VenueName: {}, LastUpdateTime: {}".format(*row)) + # [END spanner_postgresql_query_with_timestamp_parameter] + + +# [START spanner_postgresql_update_data_with_numeric_column] +def update_data_with_numeric(instance_id, database_id): + """Updates Venues tables in the database with the NUMERIC + column. + + This updates the `Revenue` column which must be created before + running this sample. You can add the column by running the + `add_numeric_column` sample or by running this DDL statement + against your database: + + ALTER TABLE Venues ADD COLUMN Revenue NUMERIC + """ + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + + database = instance.database(database_id) + + with database.batch() as batch: + batch.update( + table="Venues", + columns=("VenueId", "Revenue"), + values=[ + (4, decimal.Decimal("35000")), + (19, decimal.Decimal("104500")), + (42, decimal.Decimal("99999999999999999999999999999.99")), + ], + ) + + print("Updated data.") + + +# [END spanner_postgresql_update_data_with_numeric_column] + + +def query_data_with_numeric_parameter(instance_id, database_id): + """Queries sample data using SQL with a NUMERIC parameter.""" + # [START spanner_postgresql_query_with_numeric_parameter] + # 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) + + example_numeric = decimal.Decimal("300000") + param = {"p1": example_numeric} + param_type = {"p1": param_types.PG_NUMERIC} + + with database.snapshot() as snapshot: + results = snapshot.execute_sql( + "SELECT VenueId, Revenue FROM Venues WHERE Revenue < $1", + params=param, + param_types=param_type, + ) + + for row in results: + print("VenueId: {}, Revenue: {}".format(*row)) + # [END spanner_postgresql_query_with_numeric_parameter] + + +def create_client_with_query_options(instance_id, database_id): + """Create a client with query options.""" + # [START spanner_postgresql_create_client_with_query_options] + # instance_id = "your-spanner-instance" + # database_id = "your-spanner-db-id" + spanner_client = spanner.Client( + query_options={ + "optimizer_version": "1", + "optimizer_statistics_package": "latest", + } + ) + instance = spanner_client.instance(instance_id) + database = instance.database(database_id) + + with database.snapshot() as snapshot: + results = snapshot.execute_sql( + "SELECT VenueId, VenueName, LastUpdateTime FROM Venues" + ) + + for row in results: + print("VenueId: {}, VenueName: {}, LastUpdateTime: {}".format(*row)) + # [END spanner_postgresql_create_client_with_query_options] + + +def query_data_with_query_options(instance_id, database_id): + """Queries sample data using SQL with query options.""" + # [START spanner_postgresql_query_with_query_options] + # 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) + + with database.snapshot() as snapshot: + results = snapshot.execute_sql( + "SELECT VenueId, VenueName, LastUpdateTime FROM Venues", + query_options={ + "optimizer_version": "1", + "optimizer_statistics_package": "latest", + }, + ) + + for row in results: + print("VenueId: {}, VenueName: {}, LastUpdateTime: {}".format(*row)) + # [END spanner_postgresql_query_with_query_options] + + +# [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" + + 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"] + ) + + 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_jsonb_update_data] +def update_data_with_jsonb(instance_id, database_id): + """Updates Venues tables in the database with the JSONB + column. + This updates the `VenueDetails` column which must be created before + running this sample. You can add the column by running the + `add_jsonb_column` sample or by running this DDL statement + against your database: + ALTER TABLE Venues ADD COLUMN VenueDetails JSONB + """ + # 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) + + """ + PG JSONB takes the last value in the case of duplicate keys. + PG JSONB sorts first by key length and then lexicographically with + equivalent key length. + """ + + with database.batch() as batch: + batch.update( + table="Venues", + columns=("VenueId", "VenueDetails"), + values=[ + ( + 4, + JsonObject( + [ + JsonObject({"name": None, "open": True}), + JsonObject( + {"name": "room 2", "open": False} + ), + ] + ), + ), + (19, JsonObject(rating=9, open=True)), + ( + 42, + JsonObject( + { + "name": None, + "open": {"Monday": True, "Tuesday": False}, + "tags": ["large", "airy"], + } + ), + ), + ], + ) + + print("Updated data.") + + +# [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.""" + # 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) + + param = {"p1": 2} + param_type = {"p1": param_types.INT64} + + with database.snapshot() as snapshot: + results = snapshot.execute_sql( + "SELECT venueid, venuedetails FROM Venues" + + " WHERE CAST(venuedetails ->> 'rating' AS INTEGER) > $1", + params=param, + param_types=param_type, + ) + + for row in results: + print("VenueId: {}, VenueDetails: {}".format(*row)) + + +# [END spanner_postgresql_jsonb_query_parameter] + + +if __name__ == "__main__": # noqa: C901 + parser = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter + ) + parser.add_argument("instance_id", help="Your Cloud Spanner instance ID.") + parser.add_argument( + "--database-id", help="Your Cloud Spanner database ID.", + default="example_db" + ) + + subparsers = parser.add_subparsers(dest="command") + subparsers.add_parser("create_instance", help=create_instance.__doc__) + subparsers.add_parser("create_database", help=create_database.__doc__) + subparsers.add_parser("insert_data", help=insert_data.__doc__) + subparsers.add_parser("delete_data", help=delete_data.__doc__) + subparsers.add_parser("query_data", help=query_data.__doc__) + subparsers.add_parser("read_data", help=read_data.__doc__) + subparsers.add_parser("read_stale_data", help=read_stale_data.__doc__) + subparsers.add_parser("add_column", help=add_column.__doc__) + subparsers.add_parser("update_data", help=update_data.__doc__) + subparsers.add_parser( + "query_data_with_new_column", help=query_data_with_new_column.__doc__ + ) + subparsers.add_parser("read_write_transaction", + help=read_write_transaction.__doc__) + subparsers.add_parser("read_only_transaction", + help=read_only_transaction.__doc__) + subparsers.add_parser("add_index", help=add_index.__doc__) + subparsers.add_parser("read_data_with_index", + help=read_data_with_index.__doc__) + subparsers.add_parser("add_storing_index", help=add_storing_index.__doc__) + subparsers.add_parser("read_data_with_storing_index", + help=read_data_with_storing_index.__doc__) + subparsers.add_parser( + "create_table_with_timestamp", help=create_table_with_timestamp.__doc__ + ) + subparsers.add_parser( + "insert_data_with_timestamp", help=insert_data_with_timestamp.__doc__ + ) + subparsers.add_parser("add_timestamp_column", + help=add_timestamp_column.__doc__) + subparsers.add_parser( + "update_data_with_timestamp", help=update_data_with_timestamp.__doc__ + ) + subparsers.add_parser( + "query_data_with_timestamp", help=query_data_with_timestamp.__doc__ + ) + subparsers.add_parser("insert_data_with_dml", + help=insert_data_with_dml.__doc__) + subparsers.add_parser("update_data_with_dml", + help=update_data_with_dml.__doc__) + subparsers.add_parser("delete_data_with_dml", + help=delete_data_with_dml.__doc__) + subparsers.add_parser( + "dml_write_read_transaction", help=dml_write_read_transaction.__doc__ + ) + subparsers.add_parser("insert_with_dml", help=insert_with_dml.__doc__) + subparsers.add_parser( + "query_data_with_parameter", help=query_data_with_parameter.__doc__ + ) + subparsers.add_parser( + "write_with_dml_transaction", help=write_with_dml_transaction.__doc__ + ) + subparsers.add_parser( + "update_data_with_partitioned_dml", + help=update_data_with_partitioned_dml.__doc__, + ) + subparsers.add_parser( + "delete_data_with_partitioned_dml", + help=delete_data_with_partitioned_dml.__doc__, + ) + subparsers.add_parser("update_with_batch_dml", + help=update_with_batch_dml.__doc__) + subparsers.add_parser( + "create_table_with_datatypes", help=create_table_with_datatypes.__doc__ + ) + subparsers.add_parser("insert_datatypes_data", + help=insert_datatypes_data.__doc__) + subparsers.add_parser("query_data_with_bool", + help=query_data_with_bool.__doc__) + subparsers.add_parser("query_data_with_bytes", + help=query_data_with_bytes.__doc__) + subparsers.add_parser("query_data_with_float", + help=query_data_with_float.__doc__) + subparsers.add_parser("query_data_with_int", + help=query_data_with_int.__doc__) + subparsers.add_parser("query_data_with_string", + help=query_data_with_string.__doc__) + subparsers.add_parser( + "query_data_with_timestamp_parameter", + help=query_data_with_timestamp_parameter.__doc__, + ) + subparsers.add_parser( + "update_data_with_numeric", + help=update_data_with_numeric.__doc__, + ) + subparsers.add_parser( + "query_data_with_numeric_parameter", + help=query_data_with_numeric_parameter.__doc__, + ) + subparsers.add_parser( + "query_data_with_query_options", + help=query_data_with_query_options.__doc__ + ) + subparsers.add_parser( + "create_client_with_query_options", + help=create_client_with_query_options.__doc__, + ) + + args = parser.parse_args() + + if args.command == "create_instance": + create_instance(args.instance_id) + elif args.command == "create_database": + create_database(args.instance_id, args.database_id) + elif args.command == "insert_data": + insert_data(args.instance_id, args.database_id) + elif args.command == "delete_data": + delete_data(args.instance_id, args.database_id) + elif args.command == "query_data": + query_data(args.instance_id, args.database_id) + elif args.command == "read_data": + read_data(args.instance_id, args.database_id) + elif args.command == "read_stale_data": + read_stale_data(args.instance_id, args.database_id) + elif args.command == "add_column": + add_column(args.instance_id, args.database_id) + elif args.command == "update_data": + update_data(args.instance_id, args.database_id) + elif args.command == "query_data_with_new_column": + query_data_with_new_column(args.instance_id, args.database_id) + elif args.command == "read_write_transaction": + read_write_transaction(args.instance_id, args.database_id) + elif args.command == "read_only_transaction": + read_only_transaction(args.instance_id, args.database_id) + elif args.command == "add_index": + add_index(args.instance_id, args.database_id) + elif args.command == "read_data_with_index": + read_data_with_index(args.instance_id, args.database_id) + elif args.command == "add_storing_index": + add_storing_index(args.instance_id, args.database_id) + elif args.command == "read_data_with_storing_index": + read_data_with_storing_index(args.instance_id, args.database_id) + elif args.command == "create_table_with_timestamp": + create_table_with_timestamp(args.instance_id, args.database_id) + elif args.command == "insert_data_with_timestamp": + insert_data_with_timestamp(args.instance_id, args.database_id) + elif args.command == "add_timestamp_column": + add_timestamp_column(args.instance_id, args.database_id) + elif args.command == "update_data_with_timestamp": + update_data_with_timestamp(args.instance_id, args.database_id) + elif args.command == "query_data_with_timestamp": + query_data_with_timestamp(args.instance_id, args.database_id) + elif args.command == "insert_data_with_dml": + insert_data_with_dml(args.instance_id, args.database_id) + elif args.command == "update_data_with_dml": + update_data_with_dml(args.instance_id, args.database_id) + elif args.command == "delete_data_with_dml": + delete_data_with_dml(args.instance_id, args.database_id) + elif args.command == "dml_write_read_transaction": + dml_write_read_transaction(args.instance_id, args.database_id) + elif args.command == "insert_with_dml": + insert_with_dml(args.instance_id, args.database_id) + elif args.command == "query_data_with_parameter": + query_data_with_parameter(args.instance_id, args.database_id) + elif args.command == "write_with_dml_transaction": + write_with_dml_transaction(args.instance_id, args.database_id) + elif args.command == "update_data_with_partitioned_dml": + update_data_with_partitioned_dml(args.instance_id, args.database_id) + elif args.command == "delete_data_with_partitioned_dml": + delete_data_with_partitioned_dml(args.instance_id, args.database_id) + elif args.command == "update_with_batch_dml": + update_with_batch_dml(args.instance_id, args.database_id) + elif args.command == "create_table_with_datatypes": + create_table_with_datatypes(args.instance_id, args.database_id) + elif args.command == "insert_datatypes_data": + insert_datatypes_data(args.instance_id, args.database_id) + elif args.command == "query_data_with_bool": + query_data_with_bool(args.instance_id, args.database_id) + elif args.command == "query_data_with_bytes": + query_data_with_bytes(args.instance_id, args.database_id) + elif args.command == "query_data_with_float": + query_data_with_float(args.instance_id, args.database_id) + elif args.command == "query_data_with_int": + query_data_with_int(args.instance_id, args.database_id) + elif args.command == "query_data_with_string": + query_data_with_string(args.instance_id, args.database_id) + elif args.command == "query_data_with_timestamp_parameter": + query_data_with_timestamp_parameter(args.instance_id, args.database_id) + elif args.command == "update_data_with_numeric": + update_data_with_numeric(args.instance_id, args.database_id) + elif args.command == "query_data_with_numeric_parameter": + query_data_with_numeric_parameter(args.instance_id, args.database_id) + elif args.command == "query_data_with_query_options": + query_data_with_query_options(args.instance_id, args.database_id) + elif args.command == "create_client_with_query_options": + create_client_with_query_options(args.instance_id, args.database_id) diff --git a/samples/samples/pg_snippets_test.py b/samples/samples/pg_snippets_test.py new file mode 100644 index 0000000000..8937f34b7c --- /dev/null +++ b/samples/samples/pg_snippets_test.py @@ -0,0 +1,473 @@ +# Copyright 2022 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 time +import uuid + +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 + +CREATE_TABLE_SINGERS = """\ +CREATE TABLE Singers ( + SingerId BIGINT NOT NULL, + FirstName CHARACTER VARYING(1024), + LastName CHARACTER VARYING(1024), + SingerInfo BYTEA, + 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" + + +def test_create_instance_explicit(spanner_client, create_instance_id): + # Rather than re-use 'sample_isntance', we create a new instance, to + # ensure that the 'create_instance' snippet is tested. + retry_429(snippets.create_instance)(create_instance_id) + instance = spanner_client.instance(create_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. + snippets.create_database(sample_instance.instance_id, create_database_id) + database = sample_instance.database(create_database_id) + database.drop() + + +@pytest.mark.dependency(name="insert_data") +def test_insert_data(capsys, instance_id, sample_database): + snippets.insert_data(instance_id, sample_database.database_id) + out, _ = capsys.readouterr() + assert "Inserted data" in out + + +@pytest.mark.dependency(depends=["insert_data"]) +def test_delete_data(capsys, instance_id, sample_database): + snippets.delete_data(instance_id, sample_database.database_id) + # put it back for other tests + snippets.insert_data(instance_id, sample_database.database_id) + out, _ = capsys.readouterr() + assert "Deleted data" in out + + +@pytest.mark.dependency(depends=["insert_data"]) +def test_query_data(capsys, instance_id, sample_database): + snippets.query_data(instance_id, sample_database.database_id) + out, _ = capsys.readouterr() + assert "SingerId: 1, AlbumId: 1, AlbumTitle: Total Junk" in out + + +@pytest.mark.dependency(name="add_column", depends=["insert_data"]) +def test_add_column(capsys, instance_id, sample_database): + snippets.add_column(instance_id, sample_database.database_id) + out, _ = capsys.readouterr() + assert "Added the MarketingBudget column." in out + + +@pytest.mark.dependency(depends=["insert_data"]) +def test_read_data(capsys, instance_id, sample_database): + snippets.read_data(instance_id, sample_database.database_id) + out, _ = capsys.readouterr() + assert "SingerId: 1, AlbumId: 1, AlbumTitle: Total Junk" in out + + +@pytest.mark.dependency(name="update_data", depends=["add_column"]) +def test_update_data(capsys, instance_id, sample_database): + # Sleep for 15 seconds to ensure previous inserts will be + # 'stale' by the time test_read_stale_data is run. + time.sleep(15) + + snippets.update_data(instance_id, sample_database.database_id) + out, _ = capsys.readouterr() + assert "Updated data." in out + + +@pytest.mark.dependency(depends=["update_data"]) +def test_read_stale_data(capsys, instance_id, sample_database): + # This snippet relies on test_update_data inserting data + # at least 15 seconds after the previous insert + snippets.read_stale_data(instance_id, sample_database.database_id) + out, _ = capsys.readouterr() + assert "SingerId: 1, AlbumId: 1, MarketingBudget: None" in out + + +@pytest.mark.dependency(depends=["add_column"]) +def test_read_write_transaction(capsys, instance_id, sample_database): + snippets.read_write_transaction(instance_id, sample_database.database_id) + out, _ = capsys.readouterr() + assert "Transaction complete" in out + + +@pytest.mark.dependency(depends=["add_column"]) +def test_query_data_with_new_column(capsys, instance_id, sample_database): + snippets.query_data_with_new_column(instance_id, + sample_database.database_id) + out, _ = capsys.readouterr() + assert "SingerId: 1, AlbumId: 1, MarketingBudget: 300000" in out + assert "SingerId: 2, AlbumId: 2, MarketingBudget: 300000" in out + + +@pytest.mark.dependency(name="add_index", depends=["insert_data"]) +def test_add_index(capsys, instance_id, sample_database): + snippets.add_index(instance_id, sample_database.database_id) + out, _ = capsys.readouterr() + assert "Added the AlbumsByAlbumTitle index" in out + + +@pytest.mark.dependency(depends=["add_index"]) +def test_read_data_with_index(capsys, instance_id, sample_database): + snippets.read_data_with_index(instance_id, sample_database.database_id) + out, _ = capsys.readouterr() + assert "Go, Go, Go" in out + assert "Forever Hold Your Peace" in out + assert "Green" in out + + +@pytest.mark.dependency(name="add_storing_index", depends=["insert_data"]) +def test_add_storing_index(capsys, instance_id, sample_database): + snippets.add_storing_index(instance_id, sample_database.database_id) + out, _ = capsys.readouterr() + assert "Added the AlbumsByAlbumTitle2 index." in out + + +@pytest.mark.dependency(depends=["add_storing_index"]) +def test_read_data_with_storing_index(capsys, instance_id, sample_database): + snippets.read_data_with_storing_index(instance_id, + sample_database.database_id) + out, _ = capsys.readouterr() + assert "300000" in out + + +@pytest.mark.dependency(depends=["insert_data"]) +def test_read_only_transaction(capsys, instance_id, sample_database): + snippets.read_only_transaction(instance_id, sample_database.database_id) + out, _ = capsys.readouterr() + # Snippet does two reads, so entry should be listed twice + assert out.count("SingerId: 1, AlbumId: 1, AlbumTitle: Total Junk") == 2 + + +@pytest.mark.dependency(name="add_timestamp_column", depends=["insert_data"]) +def test_add_timestamp_column(capsys, instance_id, sample_database): + snippets.add_timestamp_column(instance_id, sample_database.database_id) + out, _ = capsys.readouterr() + assert 'Altered table "Albums" on database ' in out + + +@pytest.mark.dependency(depends=["add_timestamp_column"]) +def test_update_data_with_timestamp(capsys, instance_id, sample_database): + snippets.update_data_with_timestamp(instance_id, + sample_database.database_id) + out, _ = capsys.readouterr() + assert "Updated data" in out + + +@pytest.mark.dependency(depends=["add_timestamp_column"]) +def test_query_data_with_timestamp(capsys, instance_id, sample_database): + snippets.query_data_with_timestamp(instance_id, sample_database.database_id) + out, _ = capsys.readouterr() + assert "SingerId: 1, AlbumId: 1, MarketingBudget: 1000000" in out + assert "SingerId: 2, AlbumId: 2, MarketingBudget: 750000" in out + + +@pytest.mark.dependency(name="create_table_with_timestamp") +def test_create_table_with_timestamp(capsys, instance_id, sample_database): + snippets.create_table_with_timestamp(instance_id, + sample_database.database_id) + out, _ = capsys.readouterr() + assert "Created Performances table on database" in out + + +@pytest.mark.dependency(depends=["create_table_with_timestamp"]) +def test_insert_data_with_timestamp(capsys, instance_id, sample_database): + snippets.insert_data_with_timestamp(instance_id, + sample_database.database_id) + out, _ = capsys.readouterr() + assert "Inserted data." in out + + +@pytest.mark.dependency(name="insert_data_with_dml") +def test_insert_data_with_dml(capsys, instance_id, sample_database): + snippets.insert_data_with_dml(instance_id, sample_database.database_id) + out, _ = capsys.readouterr() + assert "1 record(s) inserted." in out + + +@pytest.mark.dependency(depends=["insert_data"]) +def test_update_data_with_dml(capsys, instance_id, sample_database): + snippets.update_data_with_dml(instance_id, sample_database.database_id) + out, _ = capsys.readouterr() + assert "1 record(s) updated." in out + + +@pytest.mark.dependency(depends=["insert_data"]) +def test_delete_data_with_dml(capsys, instance_id, sample_database): + snippets.delete_data_with_dml(instance_id, sample_database.database_id) + out, _ = capsys.readouterr() + assert "1 record(s) deleted." in out + + +@pytest.mark.dependency(name="dml_write_read_transaction") +def test_dml_write_read_transaction(capsys, instance_id, sample_database): + snippets.dml_write_read_transaction(instance_id, + sample_database.database_id) + out, _ = capsys.readouterr() + assert "1 record(s) inserted." in out + assert "FirstName: Timothy, LastName: Campbell" in out + + +@pytest.mark.dependency(name="insert_with_dml") +def test_insert_with_dml(capsys, instance_id, sample_database): + snippets.insert_with_dml(instance_id, sample_database.database_id) + out, _ = capsys.readouterr() + assert "4 record(s) inserted" in out + + +@pytest.mark.dependency(depends=["insert_with_dml"]) +def test_query_data_with_parameter(capsys, instance_id, sample_database): + snippets.query_data_with_parameter(instance_id, sample_database.database_id) + out, _ = capsys.readouterr() + assert "SingerId: 12, FirstName: Melissa, LastName: Garcia" in out + + +@pytest.mark.dependency(depends=["add_column"]) +def test_write_with_dml_transaction(capsys, instance_id, sample_database): + snippets.write_with_dml_transaction(instance_id, + sample_database.database_id) + out, _ = capsys.readouterr() + assert "Transferred 200000 from Album2's budget to Album1's" in out + + +@pytest.mark.dependency(depends=["add_column"]) +def update_data_with_partitioned_dml(capsys, instance_id, sample_database): + snippets.update_data_with_partitioned_dml(instance_id, + sample_database.database_id) + out, _ = capsys.readouterr() + assert "3 record(s) updated" in out + + +@pytest.mark.dependency(depends=["insert_with_dml"]) +def test_delete_data_with_partitioned_dml(capsys, instance_id, sample_database): + snippets.delete_data_with_partitioned_dml(instance_id, + sample_database.database_id) + out, _ = capsys.readouterr() + assert "5 record(s) deleted" in out + + +@pytest.mark.dependency(depends=["add_column"]) +def test_update_with_batch_dml(capsys, instance_id, sample_database): + snippets.update_with_batch_dml(instance_id, sample_database.database_id) + out, _ = capsys.readouterr() + assert "Executed 2 SQL statements using Batch DML" in out + + +@pytest.mark.dependency(name="create_table_with_datatypes") +def test_create_table_with_datatypes(capsys, instance_id, sample_database): + snippets.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="insert_datatypes_data", + depends=["create_table_with_datatypes"], +) +def test_insert_datatypes_data(capsys, instance_id, sample_database): + snippets.insert_datatypes_data(instance_id, sample_database.database_id) + out, _ = capsys.readouterr() + assert "Inserted data." in out + + +@pytest.mark.dependency(depends=["insert_datatypes_data"]) +def test_query_data_with_bool(capsys, instance_id, sample_database): + snippets.query_data_with_bool(instance_id, sample_database.database_id) + out, _ = capsys.readouterr() + assert "VenueId: 19, VenueName: Venue 19, OutdoorVenue: True" in out + + +@pytest.mark.dependency(depends=["insert_datatypes_data"]) +def test_query_data_with_bytes(capsys, instance_id, sample_database): + snippets.query_data_with_bytes(instance_id, sample_database.database_id) + out, _ = capsys.readouterr() + assert "VenueId: 4, VenueName: Venue 4" in out + + +@pytest.mark.dependency(depends=["insert_datatypes_data"]) +def test_query_data_with_float(capsys, instance_id, sample_database): + snippets.query_data_with_float(instance_id, sample_database.database_id) + out, _ = capsys.readouterr() + assert "VenueId: 4, VenueName: Venue 4, PopularityScore: 0.8" in out + assert "VenueId: 19, VenueName: Venue 19, PopularityScore: 0.9" in out + + +@pytest.mark.dependency(depends=["insert_datatypes_data"]) +def test_query_data_with_int(capsys, instance_id, sample_database): + snippets.query_data_with_int(instance_id, sample_database.database_id) + out, _ = capsys.readouterr() + assert "VenueId: 19, VenueName: Venue 19, Capacity: 6300" in out + assert "VenueId: 42, VenueName: Venue 42, Capacity: 3000" in out + + +@pytest.mark.dependency(depends=["insert_datatypes_data"]) +def test_query_data_with_string(capsys, instance_id, sample_database): + snippets.query_data_with_string(instance_id, sample_database.database_id) + out, _ = capsys.readouterr() + assert "VenueId: 42, VenueName: Venue 42" in out + + +@pytest.mark.dependency(depends=["insert_datatypes_data"]) +def test_update_data_with_numeric(capsys, instance_id, sample_database): + snippets.update_data_with_numeric(instance_id, sample_database.database_id) + out, _ = capsys.readouterr() + assert "Updated data" in out + + +@pytest.mark.dependency(depends=["insert_datatypes_data"]) +def test_query_data_with_numeric_parameter(capsys, instance_id, + sample_database): + snippets.query_data_with_numeric_parameter(instance_id, + sample_database.database_id) + out, _ = capsys.readouterr() + assert "VenueId: 4, Revenue: 35000" in out + + +@pytest.mark.dependency(depends=["insert_datatypes_data"]) +def test_query_data_with_timestamp_parameter(capsys, instance_id, + sample_database): + snippets.query_data_with_timestamp_parameter( + instance_id, sample_database.database_id + ) + out, _ = capsys.readouterr() + assert "VenueId: 4, VenueName: Venue 4, LastUpdateTime:" in out + assert "VenueId: 19, VenueName: Venue 19, LastUpdateTime:" in out + assert "VenueId: 42, VenueName: Venue 42, LastUpdateTime:" in out + + +@pytest.mark.dependency(depends=["insert_datatypes_data"]) +def test_query_data_with_query_options(capsys, instance_id, sample_database): + snippets.query_data_with_query_options(instance_id, + sample_database.database_id) + out, _ = capsys.readouterr() + assert "VenueId: 4, VenueName: Venue 4, LastUpdateTime:" in out + assert "VenueId: 19, VenueName: Venue 19, LastUpdateTime:" in out + assert "VenueId: 42, VenueName: Venue 42, LastUpdateTime:" in out + + +@pytest.mark.dependency(depends=["insert_datatypes_data"]) +def test_create_client_with_query_options(capsys, instance_id, sample_database): + snippets.create_client_with_query_options(instance_id, + sample_database.database_id) + out, _ = capsys.readouterr() + assert "VenueId: 4, VenueName: Venue 4, LastUpdateTime:" in out + assert "VenueId: 19, VenueName: Venue 19, LastUpdateTime:" in out + assert "VenueId: 42, VenueName: Venue 42, LastUpdateTime:" in out + + +@pytest.mark.dependency(name="add_jsonb_column", depends=["insert_datatypes_data"]) +def test_add_jsonb_column(capsys, instance_id, sample_database): + snippets.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="update_data_with_jsonb", depends=["add_jsonb_column"]) +def test_update_data_with_jsonb(capsys, instance_id, sample_database): + snippets.update_data_with_jsonb(instance_id, sample_database.database_id) + out, _ = capsys.readouterr() + assert "Updated data." in out + + +@pytest.mark.dependency(depends=["update_data_with_jsonb"]) +def test_query_data_with_jsonb_parameter(capsys, instance_id, sample_database): + snippets.query_data_with_jsonb_parameter(instance_id, sample_database.database_id) + out, _ = capsys.readouterr() + assert "VenueId: 19, VenueDetails: {'open': True, 'rating': 9}" in out diff --git a/samples/samples/requirements-test.txt b/samples/samples/requirements-test.txt index 30bdddbaac..55c9ea9350 100644 --- a/samples/samples/requirements-test.txt +++ b/samples/samples/requirements-test.txt @@ -1,4 +1,4 @@ -pytest==7.1.3 +pytest==7.2.0 pytest-dependency==0.5.1 mock==4.0.3 google-cloud-testutils==1.3.3 diff --git a/samples/samples/requirements.txt b/samples/samples/requirements.txt index c3216b3800..6caeb75060 100644 --- a/samples/samples/requirements.txt +++ b/samples/samples/requirements.txt @@ -1,2 +1,2 @@ -google-cloud-spanner==3.22.0 -futures==3.3.0; python_version < "3" +google-cloud-spanner==3.22.2 +futures==3.4.0; python_version < "3" diff --git a/samples/samples/snippets.py b/samples/samples/snippets.py index 3d65ab9c7b..7a64c2c818 100644 --- a/samples/samples/snippets.py +++ b/samples/samples/snippets.py @@ -31,8 +31,8 @@ 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.data_types import JsonObject from google.protobuf import field_mask_pb2 # type: ignore - OPERATION_TIMEOUT_SECONDS = 240 @@ -351,11 +351,11 @@ def insert_data(instance_id, database_id): table="Singers", columns=("SingerId", "FirstName", "LastName"), values=[ - (1, u"Marc", u"Richards"), - (2, u"Catalina", u"Smith"), - (3, u"Alice", u"Trentor"), - (4, u"Lea", u"Martin"), - (5, u"David", u"Lomond"), + (1, "Marc", "Richards"), + (2, "Catalina", "Smith"), + (3, "Alice", "Trentor"), + (4, "Lea", "Martin"), + (5, "David", "Lomond"), ], ) @@ -363,11 +363,11 @@ def insert_data(instance_id, database_id): table="Albums", columns=("SingerId", "AlbumId", "AlbumTitle"), values=[ - (1, 1, u"Total Junk"), - (1, 2, u"Go, Go, Go"), - (2, 1, u"Green"), - (2, 2, u"Forever Hold Your Peace"), - (2, 3, u"Terrified"), + (1, 1, "Total Junk"), + (1, 2, "Go, Go, Go"), + (2, 1, "Green"), + (2, 2, "Forever Hold Your Peace"), + (2, 3, "Terrified"), ], ) @@ -423,7 +423,7 @@ def query_data(instance_id, database_id): ) for row in results: - print(u"SingerId: {}, AlbumId: {}, AlbumTitle: {}".format(*row)) + print("SingerId: {}, AlbumId: {}, AlbumTitle: {}".format(*row)) # [END spanner_query_data] @@ -443,7 +443,7 @@ def read_data(instance_id, database_id): ) for row in results: - print(u"SingerId: {}, AlbumId: {}, AlbumTitle: {}".format(*row)) + print("SingerId: {}, AlbumId: {}, AlbumTitle: {}".format(*row)) # [END spanner_read_data] @@ -469,7 +469,7 @@ def read_stale_data(instance_id, database_id): ) for row in results: - print(u"SingerId: {}, AlbumId: {}, MarketingBudget: {}".format(*row)) + print("SingerId: {}, AlbumId: {}, MarketingBudget: {}".format(*row)) # [END spanner_read_stale_data] @@ -495,7 +495,7 @@ def query_data_with_new_column(instance_id, database_id): ) for row in results: - print(u"SingerId: {}, AlbumId: {}, MarketingBudget: {}".format(*row)) + print("SingerId: {}, AlbumId: {}, MarketingBudget: {}".format(*row)) # [END spanner_query_data_with_new_column] @@ -560,7 +560,7 @@ def query_data_with_index( ) for row in results: - print(u"AlbumId: {}, AlbumTitle: {}, " "MarketingBudget: {}".format(*row)) + print("AlbumId: {}, AlbumTitle: {}, " "MarketingBudget: {}".format(*row)) # [END spanner_query_data_with_index] @@ -626,7 +626,7 @@ def read_data_with_storing_index(instance_id, database_id): clause. The index must exist before running this sample. You can add the index - by running the `add_soring_index` sample or by running this DDL statement + by running the `add_scoring_index` sample or by running this DDL statement against your database: CREATE INDEX AlbumsByAlbumTitle2 ON Albums(AlbumTitle) @@ -647,7 +647,7 @@ def read_data_with_storing_index(instance_id, database_id): ) for row in results: - print(u"AlbumId: {}, AlbumTitle: {}, " "MarketingBudget: {}".format(*row)) + print("AlbumId: {}, AlbumTitle: {}, " "MarketingBudget: {}".format(*row)) # [END spanner_read_data_with_storing_index] @@ -789,7 +789,7 @@ def read_only_transaction(instance_id, database_id): print("Results from first read:") for row in results: - print(u"SingerId: {}, AlbumId: {}, AlbumTitle: {}".format(*row)) + print("SingerId: {}, AlbumId: {}, AlbumTitle: {}".format(*row)) # Perform another read using the `read` method. Even if the data # is updated in-between the reads, the snapshot ensures that both @@ -801,7 +801,7 @@ def read_only_transaction(instance_id, database_id): print("Results from second read:") for row in results: - print(u"SingerId: {}, AlbumId: {}, AlbumTitle: {}".format(*row)) + print("SingerId: {}, AlbumId: {}, AlbumTitle: {}".format(*row)) # [END spanner_read_only_transaction] @@ -844,7 +844,7 @@ def create_table_with_timestamp(instance_id, database_id): # [START spanner_insert_data_with_timestamp_column] def insert_data_with_timestamp(instance_id, database_id): - """Inserts data with a COMMIT_TIMESTAMP field into a table. """ + """Inserts data with a COMMIT_TIMESTAMP field into a table.""" spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) @@ -870,8 +870,7 @@ 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. - """ + """Adds a new TIMESTAMP column to the Albums table in the example database.""" spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) @@ -960,7 +959,7 @@ def query_data_with_timestamp(instance_id, database_id): ) for row in results: - print(u"SingerId: {}, AlbumId: {}, MarketingBudget: {}".format(*row)) + print("SingerId: {}, AlbumId: {}, MarketingBudget: {}".format(*row)) # [END spanner_query_data_with_timestamp_column] @@ -968,8 +967,7 @@ 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. - """ + """Adds a new NUMERIC column to the Venues table in the example database.""" spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) @@ -1026,8 +1024,7 @@ 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. - """ + """Adds a new JSON column to the Venues table in the example database.""" spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) @@ -1072,17 +1069,17 @@ def update_data_with_json(instance_id, database_id): values=[ ( 4, - json.dumps( + JsonObject( [ - {"name": "room 1", "open": True}, - {"name": "room 2", "open": False}, + JsonObject({"name": "room 1", "open": True}), + JsonObject({"name": "room 2", "open": False}), ] ), ), - (19, json.dumps({"rating": 9, "open": True})), + (19, JsonObject(rating=9, open=True)), ( 42, - json.dumps( + JsonObject( { "name": None, "open": {"Monday": True, "Tuesday": False}, @@ -1113,10 +1110,10 @@ def write_struct_data(instance_id, database_id): table="Singers", columns=("SingerId", "FirstName", "LastName"), values=[ - (6, u"Elena", u"Campbell"), - (7, u"Gabriel", u"Wright"), - (8, u"Benjamin", u"Martinez"), - (9, u"Hannah", u"Harris"), + (6, "Elena", "Campbell"), + (7, "Gabriel", "Wright"), + (8, "Benjamin", "Martinez"), + (9, "Hannah", "Harris"), ], ) @@ -1127,7 +1124,7 @@ def write_struct_data(instance_id, database_id): def query_with_struct(instance_id, database_id): - """Query a table using STRUCT parameters. """ + """Query a table using STRUCT parameters.""" # [START spanner_create_struct_with_data] record_type = param_types.Struct( [ @@ -1152,12 +1149,12 @@ def query_with_struct(instance_id, database_id): ) for row in results: - print(u"SingerId: {}".format(*row)) + print("SingerId: {}".format(*row)) # [END spanner_query_data_with_struct] def query_with_array_of_struct(instance_id, database_id): - """Query a table using an array of STRUCT parameters. """ + """Query a table using an array of STRUCT parameters.""" # [START spanner_create_user_defined_struct] name_type = param_types.Struct( [ @@ -1190,13 +1187,13 @@ def query_with_array_of_struct(instance_id, database_id): ) for row in results: - print(u"SingerId: {}".format(*row)) + print("SingerId: {}".format(*row)) # [END spanner_query_data_with_array_of_struct] # [START spanner_field_access_on_struct_parameters] def query_struct_field(instance_id, database_id): - """Query a table using field access on a STRUCT parameter. """ + """Query a table using field access on a STRUCT parameter.""" spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) database = instance.database(database_id) @@ -1216,7 +1213,7 @@ def query_struct_field(instance_id, database_id): ) for row in results: - print(u"SingerId: {}".format(*row)) + print("SingerId: {}".format(*row)) # [END spanner_field_access_on_struct_parameters] @@ -1224,7 +1221,7 @@ def query_struct_field(instance_id, database_id): # [START spanner_field_access_on_nested_struct_parameters] def query_nested_struct_field(instance_id, database_id): - """Query a table using nested field access on a STRUCT parameter. """ + """Query a table using nested field access on a STRUCT parameter.""" spanner_client = spanner.Client() instance = spanner_client.instance(instance_id) database = instance.database(database_id) @@ -1260,14 +1257,14 @@ def query_nested_struct_field(instance_id, database_id): ) for row in results: - print(u"SingerId: {} SongName: {}".format(*row)) + print("SingerId: {} SongName: {}".format(*row)) # [END spanner_field_access_on_nested_struct_parameters] def insert_data_with_dml(instance_id, database_id): - """Inserts sample data into the given database using a DML statement. """ + """Inserts sample data into the given database using a DML statement.""" # [START spanner_dml_standard_insert] # instance_id = "your-spanner-instance" # database_id = "your-spanner-db-id" @@ -1278,7 +1275,7 @@ def insert_data_with_dml(instance_id, database_id): def insert_singers(transaction): row_ct = transaction.execute_update( - "INSERT Singers (SingerId, FirstName, LastName) " + "INSERT INTO Singers (SingerId, FirstName, LastName) " " VALUES (10, 'Virginia', 'Watson')" ) @@ -1290,7 +1287,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. """ + """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): @@ -1325,7 +1322,7 @@ def insert_singers(transaction): def update_data_with_dml(instance_id, database_id): - """Updates sample data from the database using a DML statement. """ + """Updates sample data from the database using a DML statement.""" # [START spanner_dml_standard_update] # instance_id = "your-spanner-instance" # database_id = "your-spanner-db-id" @@ -1348,7 +1345,7 @@ def update_albums(transaction): def delete_data_with_dml(instance_id, database_id): - """Deletes sample data from the database using a DML statement. """ + """Deletes sample data from the database using a DML statement.""" # [START spanner_dml_standard_delete] # instance_id = "your-spanner-instance" # database_id = "your-spanner-db-id" @@ -1369,7 +1366,7 @@ def delete_singers(transaction): def update_data_with_dml_timestamp(instance_id, database_id): - """Updates data with Timestamp from the database using a DML statement. """ + """Updates data with Timestamp from the database using a DML statement.""" # [START spanner_dml_standard_update_with_timestamp] # instance_id = "your-spanner-instance" # database_id = "your-spanner-db-id" @@ -1404,7 +1401,7 @@ def dml_write_read_transaction(instance_id, database_id): def write_then_read(transaction): # Insert record. row_ct = transaction.execute_update( - "INSERT Singers (SingerId, FirstName, LastName) " + "INSERT INTO Singers (SingerId, FirstName, LastName) " " VALUES (11, 'Timothy', 'Campbell')" ) print("{} record(s) inserted.".format(row_ct)) @@ -1421,7 +1418,7 @@ def write_then_read(transaction): def update_data_with_dml_struct(instance_id, database_id): - """Updates data with a DML statement and STRUCT parameters. """ + """Updates data with a DML statement and STRUCT parameters.""" # [START spanner_dml_structs] # instance_id = "your-spanner-instance" # database_id = "your-spanner-db-id" @@ -1453,7 +1450,7 @@ def write_with_struct(transaction): def insert_with_dml(instance_id, database_id): - """Inserts data with a DML statement into the database. """ + """Inserts data with a DML statement into the database.""" # [START spanner_dml_getting_started_insert] # instance_id = "your-spanner-instance" # database_id = "your-spanner-db-id" @@ -1463,7 +1460,7 @@ def insert_with_dml(instance_id, database_id): def insert_singers(transaction): row_ct = transaction.execute_update( - "INSERT Singers (SingerId, FirstName, LastName) VALUES " + "INSERT INTO Singers (SingerId, FirstName, LastName) VALUES " "(12, 'Melissa', 'Garcia'), " "(13, 'Russell', 'Morales'), " "(14, 'Jacqueline', 'Long'), " @@ -1493,12 +1490,12 @@ def query_data_with_parameter(instance_id, database_id): ) for row in results: - print(u"SingerId: {}, FirstName: {}, LastName: {}".format(*row)) + print("SingerId: {}, FirstName: {}, LastName: {}".format(*row)) # [END spanner_query_with_parameter] def write_with_dml_transaction(instance_id, database_id): - """ Transfers part of a marketing budget from one album to another. """ + """Transfers part of a marketing budget from one album to another.""" # [START spanner_dml_getting_started_update] # instance_id = "your-spanner-instance" # database_id = "your-spanner-db-id" @@ -1561,7 +1558,7 @@ def transfer_budget(transaction): def update_data_with_partitioned_dml(instance_id, database_id): - """ Update sample data with a partitioned DML statement. """ + """Update sample data with a partitioned DML statement.""" # [START spanner_dml_partitioned_update] # instance_id = "your-spanner-instance" # database_id = "your-spanner-db-id" @@ -1579,7 +1576,7 @@ def update_data_with_partitioned_dml(instance_id, database_id): def delete_data_with_partitioned_dml(instance_id, database_id): - """ Delete sample data with a partitioned DML statement. """ + """Delete sample data with a partitioned DML statement.""" # [START spanner_dml_partitioned_delete] # instance_id = "your-spanner-instance" # database_id = "your-spanner-db-id" @@ -1594,7 +1591,7 @@ def delete_data_with_partitioned_dml(instance_id, database_id): def update_with_batch_dml(instance_id, database_id): - """Updates sample data in the database using Batch DML. """ + """Updates sample data in the database using Batch DML.""" # [START spanner_dml_batch_update] from google.rpc.code_pb2 import OK @@ -1633,7 +1630,7 @@ def update_albums(transaction): def create_table_with_datatypes(instance_id, database_id): - """Creates a table with supported dataypes. """ + """Creates a table with supported datatypes. """ # [START spanner_create_table_with_datatypes] # instance_id = "your-spanner-instance" # database_id = "your-spanner-db-id" @@ -1670,7 +1667,7 @@ def create_table_with_datatypes(instance_id, database_id): def insert_datatypes_data(instance_id, database_id): - """Inserts data with supported datatypes into a table. """ + """Inserts data with supported datatypes into a table.""" # [START spanner_insert_datatypes_data] # instance_id = "your-spanner-instance" # database_id = "your-spanner-db-id" @@ -1678,9 +1675,9 @@ def insert_datatypes_data(instance_id, database_id): instance = spanner_client.instance(instance_id) database = instance.database(database_id) - exampleBytes1 = base64.b64encode(u"Hello World 1".encode()) - exampleBytes2 = base64.b64encode(u"Hello World 2".encode()) - exampleBytes3 = base64.b64encode(u"Hello World 3".encode()) + exampleBytes1 = base64.b64encode("Hello World 1".encode()) + exampleBytes2 = base64.b64encode("Hello World 2".encode()) + exampleBytes3 = base64.b64encode("Hello World 3".encode()) available_dates1 = ["2020-12-01", "2020-12-02", "2020-12-03"] available_dates2 = ["2020-11-01", "2020-11-05", "2020-11-15"] available_dates3 = ["2020-10-01", "2020-10-07"] @@ -1701,7 +1698,7 @@ def insert_datatypes_data(instance_id, database_id): values=[ ( 4, - u"Venue 4", + "Venue 4", exampleBytes1, 1800, available_dates1, @@ -1712,7 +1709,7 @@ def insert_datatypes_data(instance_id, database_id): ), ( 19, - u"Venue 19", + "Venue 19", exampleBytes2, 6300, available_dates2, @@ -1723,7 +1720,7 @@ def insert_datatypes_data(instance_id, database_id): ), ( 42, - u"Venue 42", + "Venue 42", exampleBytes3, 3000, available_dates3, @@ -1740,7 +1737,7 @@ def insert_datatypes_data(instance_id, database_id): def query_data_with_array(instance_id, database_id): - """Queries sample data using SQL with an ARRAY parameter. """ + """Queries sample data using SQL with an ARRAY parameter.""" # [START spanner_query_with_array_parameter] # instance_id = "your-spanner-instance" # database_id = "your-spanner-db-id" @@ -1762,12 +1759,12 @@ def query_data_with_array(instance_id, database_id): ) for row in results: - print(u"VenueId: {}, VenueName: {}, AvailableDate: {}".format(*row)) + print("VenueId: {}, VenueName: {}, AvailableDate: {}".format(*row)) # [END spanner_query_with_array_parameter] def query_data_with_bool(instance_id, database_id): - """Queries sample data using SQL with a BOOL parameter. """ + """Queries sample data using SQL with a BOOL parameter.""" # [START spanner_query_with_bool_parameter] # instance_id = "your-spanner-instance" # database_id = "your-spanner-db-id" @@ -1788,12 +1785,12 @@ def query_data_with_bool(instance_id, database_id): ) for row in results: - print(u"VenueId: {}, VenueName: {}, OutdoorVenue: {}".format(*row)) + print("VenueId: {}, VenueName: {}, OutdoorVenue: {}".format(*row)) # [END spanner_query_with_bool_parameter] def query_data_with_bytes(instance_id, database_id): - """Queries sample data using SQL with a BYTES parameter. """ + """Queries sample data using SQL with a BYTES parameter.""" # [START spanner_query_with_bytes_parameter] # instance_id = "your-spanner-instance" # database_id = "your-spanner-db-id" @@ -1801,7 +1798,7 @@ def query_data_with_bytes(instance_id, database_id): instance = spanner_client.instance(instance_id) database = instance.database(database_id) - exampleBytes = base64.b64encode(u"Hello World 1".encode()) + exampleBytes = base64.b64encode("Hello World 1".encode()) param = {"venue_info": exampleBytes} param_type = {"venue_info": param_types.BYTES} @@ -1813,12 +1810,12 @@ def query_data_with_bytes(instance_id, database_id): ) for row in results: - print(u"VenueId: {}, VenueName: {}".format(*row)) + print("VenueId: {}, VenueName: {}".format(*row)) # [END spanner_query_with_bytes_parameter] def query_data_with_date(instance_id, database_id): - """Queries sample data using SQL with a DATE parameter. """ + """Queries sample data using SQL with a DATE parameter.""" # [START spanner_query_with_date_parameter] # instance_id = "your-spanner-instance" # database_id = "your-spanner-db-id" @@ -1839,12 +1836,12 @@ def query_data_with_date(instance_id, database_id): ) for row in results: - print(u"VenueId: {}, VenueName: {}, LastContactDate: {}".format(*row)) + print("VenueId: {}, VenueName: {}, LastContactDate: {}".format(*row)) # [END spanner_query_with_date_parameter] def query_data_with_float(instance_id, database_id): - """Queries sample data using SQL with a FLOAT64 parameter. """ + """Queries sample data using SQL with a FLOAT64 parameter.""" # [START spanner_query_with_float_parameter] # instance_id = "your-spanner-instance" # database_id = "your-spanner-db-id" @@ -1865,12 +1862,12 @@ def query_data_with_float(instance_id, database_id): ) for row in results: - print(u"VenueId: {}, VenueName: {}, PopularityScore: {}".format(*row)) + print("VenueId: {}, VenueName: {}, PopularityScore: {}".format(*row)) # [END spanner_query_with_float_parameter] def query_data_with_int(instance_id, database_id): - """Queries sample data using SQL with a INT64 parameter. """ + """Queries sample data using SQL with a INT64 parameter.""" # [START spanner_query_with_int_parameter] # instance_id = "your-spanner-instance" # database_id = "your-spanner-db-id" @@ -1891,12 +1888,12 @@ def query_data_with_int(instance_id, database_id): ) for row in results: - print(u"VenueId: {}, VenueName: {}, Capacity: {}".format(*row)) + print("VenueId: {}, VenueName: {}, Capacity: {}".format(*row)) # [END spanner_query_with_int_parameter] def query_data_with_string(instance_id, database_id): - """Queries sample data using SQL with a STRING parameter. """ + """Queries sample data using SQL with a STRING parameter.""" # [START spanner_query_with_string_parameter] # instance_id = "your-spanner-instance" # database_id = "your-spanner-db-id" @@ -1916,12 +1913,12 @@ def query_data_with_string(instance_id, database_id): ) for row in results: - print(u"VenueId: {}, VenueName: {}".format(*row)) + print("VenueId: {}, VenueName: {}".format(*row)) # [END spanner_query_with_string_parameter] def query_data_with_numeric_parameter(instance_id, database_id): - """Queries sample data using SQL with a NUMERIC parameter. """ + """Queries sample data using SQL with a NUMERIC parameter.""" # [START spanner_query_with_numeric_parameter] # instance_id = "your-spanner-instance" # database_id = "your-spanner-db-id" @@ -1941,12 +1938,12 @@ def query_data_with_numeric_parameter(instance_id, database_id): ) for row in results: - print(u"VenueId: {}, Revenue: {}".format(*row)) + print("VenueId: {}, Revenue: {}".format(*row)) # [END spanner_query_with_numeric_parameter] def query_data_with_json_parameter(instance_id, database_id): - """Queries sample data using SQL with a JSON parameter. """ + """Queries sample data using SQL with a JSON parameter.""" # [START spanner_query_with_json_parameter] # instance_id = "your-spanner-instance" # database_id = "your-spanner-db-id" @@ -1969,12 +1966,12 @@ def query_data_with_json_parameter(instance_id, database_id): ) for row in results: - print(u"VenueId: {}, VenueDetails: {}".format(*row)) + print("VenueId: {}, VenueDetails: {}".format(*row)) # [END spanner_query_with_json_parameter] def query_data_with_timestamp_parameter(instance_id, database_id): - """Queries sample data using SQL with a TIMESTAMP parameter. """ + """Queries sample data using SQL with a TIMESTAMP parameter.""" # [START spanner_query_with_timestamp_parameter] # instance_id = "your-spanner-instance" # database_id = "your-spanner-db-id" @@ -2002,7 +1999,7 @@ def query_data_with_timestamp_parameter(instance_id, database_id): ) for row in results: - print(u"VenueId: {}, VenueName: {}, LastUpdateTime: {}".format(*row)) + print("VenueId: {}, VenueName: {}, LastUpdateTime: {}".format(*row)) # [END spanner_query_with_timestamp_parameter] @@ -2025,7 +2022,7 @@ def query_data_with_query_options(instance_id, database_id): ) for row in results: - print(u"VenueId: {}, VenueName: {}, LastUpdateTime: {}".format(*row)) + print("VenueId: {}, VenueName: {}, LastUpdateTime: {}".format(*row)) # [END spanner_query_with_query_options] @@ -2049,7 +2046,7 @@ def create_client_with_query_options(instance_id, database_id): ) for row in results: - print(u"VenueId: {}, VenueName: {}, LastUpdateTime: {}".format(*row)) + print("VenueId: {}, VenueName: {}, LastUpdateTime: {}".format(*row)) # [END spanner_create_client_with_query_options] @@ -2113,7 +2110,7 @@ def set_request_tag(instance_id, database_id): ) for row in results: - print(u"SingerId: {}, AlbumId: {}, AlbumTitle: {}".format(*row)) + print("SingerId: {}, AlbumId: {}, AlbumTitle: {}".format(*row)) # [END spanner_set_request_tag] @@ -2126,7 +2123,8 @@ def create_instance_config(user_config_name, base_config_id): # base_config_id = `projects//instanceConfigs/nam11` spanner_client = spanner.Client() base_config = spanner_client.instance_admin_api.get_instance_config( - name=base_config_id) + name=base_config_id + ) # The replicas for the custom instance configuration must include all the replicas of the base # configuration, in addition to at least one from the list of optional replicas of the base @@ -2139,15 +2137,16 @@ def create_instance_config(user_config_name, base_config_id): parent=spanner_client.project_name, instance_config_id=user_config_name, instance_config=spanner_instance_admin.InstanceConfig( - name="{}/instanceConfigs/{}".format(spanner_client.project_name, user_config_name), + name="{}/instanceConfigs/{}".format( + spanner_client.project_name, user_config_name + ), display_name="custom-python-samples", config_type=spanner_instance_admin.InstanceConfig.Type.USER_MANAGED, replicas=replicas, base_config=base_config.name, - labels={ - "python_cloud_spanner_samples": "true" - } - )) + labels={"python_cloud_spanner_samples": "true"}, + ), + ) print("Waiting for operation to complete...") operation.result(OPERATION_TIMEOUT_SECONDS) @@ -2163,12 +2162,16 @@ def update_instance_config(user_config_name): # user_config_name = `custom-nam11` spanner_client = spanner.Client() config = spanner_client.instance_admin_api.get_instance_config( - name="{}/instanceConfigs/{}".format(spanner_client.project_name, user_config_name)) + name="{}/instanceConfigs/{}".format( + spanner_client.project_name, user_config_name + ) + ) config.display_name = "updated custom instance config" config.labels["updated"] = "true" - operation = spanner_client.instance_admin_api.update_instance_config(instance_config=config, - update_mask=field_mask_pb2.FieldMask( - paths=["display_name", "labels"])) + operation = spanner_client.instance_admin_api.update_instance_config( + instance_config=config, + update_mask=field_mask_pb2.FieldMask(paths=["display_name", "labels"]), + ) print("Waiting for operation to complete...") operation.result(OPERATION_TIMEOUT_SECONDS) print("Updated instance configuration {}".format(user_config_name)) @@ -2180,8 +2183,7 @@ def update_instance_config(user_config_name): def delete_instance_config(user_config_id): """Deleted the user-managed instance configuration.""" spanner_client = spanner.Client() - spanner_client.instance_admin_api.delete_instance_config( - name=user_config_id) + spanner_client.instance_admin_api.delete_instance_config(name=user_config_id) print("Instance config {} successfully deleted".format(user_config_id)) @@ -2193,10 +2195,15 @@ def list_instance_config_operations(): """List the user-managed instance configuration operations.""" spanner_client = spanner.Client() operations = spanner_client.instance_admin_api.list_instance_config_operations( - request=spanner_instance_admin.ListInstanceConfigOperationsRequest(parent=spanner_client.project_name, - filter="(metadata.@type=type.googleapis.com/google.spanner.admin.instance.v1.CreateInstanceConfigMetadata)")) + request=spanner_instance_admin.ListInstanceConfigOperationsRequest( + parent=spanner_client.project_name, + filter="(metadata.@type=type.googleapis.com/google.spanner.admin.instance.v1.CreateInstanceConfigMetadata)", + ) + ) for op in operations: - metadata = spanner_instance_admin.CreateInstanceConfigMetadata.pb(spanner_instance_admin.CreateInstanceConfigMetadata()) + metadata = spanner_instance_admin.CreateInstanceConfigMetadata.pb( + spanner_instance_admin.CreateInstanceConfigMetadata() + ) op.metadata.Unpack(metadata) print( "List instance config operations {} is {}% completed.".format( @@ -2238,9 +2245,9 @@ def list_instance_config_operations(): ) query_data_with_index_parser.add_argument("--start_title", default="Aardvark") query_data_with_index_parser.add_argument("--end_title", default="Goo") - subparsers.add_parser("read_data_with_index", help=insert_data.__doc__) + subparsers.add_parser("read_data_with_index", help=read_data_with_index.__doc__) subparsers.add_parser("add_storing_index", help=add_storing_index.__doc__) - subparsers.add_parser("read_data_with_storing_index", help=insert_data.__doc__) + subparsers.add_parser("read_data_with_storing_index", help=read_data_with_storing_index.__doc__) subparsers.add_parser( "create_table_with_timestamp", help=create_table_with_timestamp.__doc__ ) diff --git a/samples/samples/snippets_test.py b/samples/samples/snippets_test.py index f085a0e71c..d4143a2319 100644 --- a/samples/samples/snippets_test.py +++ b/samples/samples/snippets_test.py @@ -17,6 +17,7 @@ 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 @@ -48,6 +49,16 @@ def sample_name(): return "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.GOOGLE_STANDARD_SQL + + @pytest.fixture(scope="module") def create_instance_id(): """Id for the low-cost instance.""" @@ -99,8 +110,9 @@ def default_leader(): def user_managed_instance_config_name(spanner_client): name = f"custom-python-samples-config-{uuid.uuid4().hex[:10]}" yield name - snippets.delete_instance_config("{}/instanceConfigs/{}".format( - spanner_client.project_name, name)) + snippets.delete_instance_config( + "{}/instanceConfigs/{}".format(spanner_client.project_name, name) + ) return @@ -128,7 +140,8 @@ def test_create_database_explicit(sample_instance, create_database_id): def test_create_instance_with_processing_units(capsys, lci_instance_id): processing_units = 500 retry_429(snippets.create_instance_with_processing_units)( - lci_instance_id, processing_units, + lci_instance_id, + processing_units, ) out, _ = capsys.readouterr() assert lci_instance_id in out @@ -139,10 +152,10 @@ def test_create_instance_with_processing_units(capsys, lci_instance_id): def test_create_database_with_encryption_config( - capsys, instance_id, cmek_database_id, kms_key_name + capsys, instance_id, cmek_database_id, kms_key_name ): snippets.create_database_with_encryption_key( - instance_id, cmek_database_id, kms_key_name + instance_id, cmek_database_id, kms_key_name ) out, _ = capsys.readouterr() assert cmek_database_id in out @@ -163,8 +176,12 @@ def test_list_instance_config(capsys): @pytest.mark.dependency(name="create_instance_config") -def test_create_instance_config(capsys, user_managed_instance_config_name, base_instance_config_id): - snippets.create_instance_config(user_managed_instance_config_name, base_instance_config_id) +def test_create_instance_config( + capsys, user_managed_instance_config_name, base_instance_config_id +): + snippets.create_instance_config( + user_managed_instance_config_name, base_instance_config_id + ) out, _ = capsys.readouterr() assert "Created instance configuration" in out @@ -179,8 +196,11 @@ def test_update_instance_config(capsys, user_managed_instance_config_name): @pytest.mark.dependency(depends=["create_instance_config"]) def test_delete_instance_config(capsys, user_managed_instance_config_name): spanner_client = spanner.Client() - snippets.delete_instance_config("{}/instanceConfigs/{}".format( - spanner_client.project_name, user_managed_instance_config_name)) + snippets.delete_instance_config( + "{}/instanceConfigs/{}".format( + spanner_client.project_name, user_managed_instance_config_name + ) + ) out, _ = capsys.readouterr() assert "successfully deleted" in out @@ -198,15 +218,15 @@ def test_list_databases(capsys, instance_id): def test_create_database_with_default_leader( - capsys, - multi_region_instance, - multi_region_instance_id, - default_leader_database_id, - 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(snippets.create_database_with_default_leader)( - multi_region_instance_id, default_leader_database_id, default_leader + multi_region_instance_id, default_leader_database_id, default_leader ) out, _ = capsys.readouterr() assert default_leader_database_id in out @@ -214,15 +234,15 @@ def test_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, + capsys, + multi_region_instance, + multi_region_instance_id, + default_leader_database_id, + default_leader, ): retry_429 = RetryErrors(exceptions.ResourceExhausted, delay=15) retry_429(snippets.update_database_with_default_leader)( - multi_region_instance_id, default_leader_database_id, default_leader + multi_region_instance_id, default_leader_database_id, default_leader ) out, _ = capsys.readouterr() assert default_leader_database_id in out @@ -236,14 +256,14 @@ def test_get_database_ddl(capsys, instance_id, sample_database): def test_query_information_schema_database_options( - capsys, - multi_region_instance, - multi_region_instance_id, - default_leader_database_id, - default_leader, + capsys, + multi_region_instance, + multi_region_instance_id, + default_leader_database_id, + default_leader, ): snippets.query_information_schema_database_options( - multi_region_instance_id, default_leader_database_id + multi_region_instance_id, default_leader_database_id ) out, _ = capsys.readouterr() assert default_leader in out @@ -315,7 +335,8 @@ def test_read_write_transaction(capsys, instance_id, sample_database): @pytest.mark.dependency(depends=["add_column"]) def test_query_data_with_new_column(capsys, instance_id, sample_database): - snippets.query_data_with_new_column(instance_id, sample_database.database_id) + snippets.query_data_with_new_column(instance_id, + sample_database.database_id) out, _ = capsys.readouterr() assert "SingerId: 1, AlbumId: 1, MarketingBudget: 300000" in out assert "SingerId: 2, AlbumId: 2, MarketingBudget: 300000" in out @@ -355,7 +376,8 @@ def test_add_storing_index(capsys, instance_id, sample_database): @pytest.mark.dependency(depends=["add_storing_index"]) def test_read_data_with_storing_index(capsys, instance_id, sample_database): - snippets.read_data_with_storing_index(instance_id, sample_database.database_id) + snippets.read_data_with_storing_index(instance_id, + sample_database.database_id) out, _ = capsys.readouterr() assert "300000" in out @@ -377,7 +399,8 @@ def test_add_timestamp_column(capsys, instance_id, sample_database): @pytest.mark.dependency(depends=["add_timestamp_column"]) def test_update_data_with_timestamp(capsys, instance_id, sample_database): - snippets.update_data_with_timestamp(instance_id, sample_database.database_id) + snippets.update_data_with_timestamp(instance_id, + sample_database.database_id) out, _ = capsys.readouterr() assert "Updated data" in out @@ -392,14 +415,16 @@ def test_query_data_with_timestamp(capsys, instance_id, sample_database): @pytest.mark.dependency(name="create_table_with_timestamp") def test_create_table_with_timestamp(capsys, instance_id, sample_database): - snippets.create_table_with_timestamp(instance_id, sample_database.database_id) + snippets.create_table_with_timestamp(instance_id, + sample_database.database_id) out, _ = capsys.readouterr() assert "Created Performances table on database" in out -@pytest.mark.dependency(depends=["create_table_with_datatypes"]) +@pytest.mark.dependency(depends=["create_table_with_timestamp"]) def test_insert_data_with_timestamp(capsys, instance_id, sample_database): - snippets.insert_data_with_timestamp(instance_id, sample_database.database_id) + snippets.insert_data_with_timestamp(instance_id, + sample_database.database_id) out, _ = capsys.readouterr() assert "Inserted data." in out @@ -420,7 +445,8 @@ def test_query_with_struct(capsys, instance_id, sample_database): @pytest.mark.dependency(depends=["write_struct_data"]) def test_query_with_array_of_struct(capsys, instance_id, sample_database): - snippets.query_with_array_of_struct(instance_id, sample_database.database_id) + snippets.query_with_array_of_struct(instance_id, + sample_database.database_id) out, _ = capsys.readouterr() assert "SingerId: 8" in out assert "SingerId: 7" in out @@ -473,14 +499,16 @@ def test_delete_data_with_dml(capsys, instance_id, sample_database): @pytest.mark.dependency(depends=["add_timestamp_column"]) def test_update_data_with_dml_timestamp(capsys, instance_id, sample_database): - snippets.update_data_with_dml_timestamp(instance_id, sample_database.database_id) + snippets.update_data_with_dml_timestamp(instance_id, + sample_database.database_id) out, _ = capsys.readouterr() assert "2 record(s) updated." in out @pytest.mark.dependency(name="dml_write_read_transaction") def test_dml_write_read_transaction(capsys, instance_id, sample_database): - snippets.dml_write_read_transaction(instance_id, sample_database.database_id) + snippets.dml_write_read_transaction(instance_id, + sample_database.database_id) out, _ = capsys.readouterr() assert "1 record(s) inserted." in out assert "FirstName: Timothy, LastName: Campbell" in out @@ -488,7 +516,8 @@ def test_dml_write_read_transaction(capsys, instance_id, sample_database): @pytest.mark.dependency(depends=["dml_write_read_transaction"]) def test_update_data_with_dml_struct(capsys, instance_id, sample_database): - snippets.update_data_with_dml_struct(instance_id, sample_database.database_id) + snippets.update_data_with_dml_struct(instance_id, + sample_database.database_id) out, _ = capsys.readouterr() assert "1 record(s) updated" in out @@ -509,21 +538,24 @@ def test_query_data_with_parameter(capsys, instance_id, sample_database): @pytest.mark.dependency(depends=["add_column"]) def test_write_with_dml_transaction(capsys, instance_id, sample_database): - snippets.write_with_dml_transaction(instance_id, sample_database.database_id) + snippets.write_with_dml_transaction(instance_id, + sample_database.database_id) out, _ = capsys.readouterr() assert "Transferred 200000 from Album2's budget to Album1's" in out @pytest.mark.dependency(depends=["add_column"]) def update_data_with_partitioned_dml(capsys, instance_id, sample_database): - snippets.update_data_with_partitioned_dml(instance_id, sample_database.database_id) + snippets.update_data_with_partitioned_dml(instance_id, + sample_database.database_id) out, _ = capsys.readouterr() assert "3 record(s) updated" in out @pytest.mark.dependency(depends=["insert_with_dml"]) def test_delete_data_with_partitioned_dml(capsys, instance_id, sample_database): - snippets.delete_data_with_partitioned_dml(instance_id, sample_database.database_id) + snippets.delete_data_with_partitioned_dml(instance_id, + sample_database.database_id) out, _ = capsys.readouterr() assert "6 record(s) deleted" in out @@ -537,13 +569,15 @@ def test_update_with_batch_dml(capsys, instance_id, sample_database): @pytest.mark.dependency(name="create_table_with_datatypes") def test_create_table_with_datatypes(capsys, instance_id, sample_database): - snippets.create_table_with_datatypes(instance_id, sample_database.database_id) + snippets.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="insert_datatypes_data", depends=["create_table_with_datatypes"], + name="insert_datatypes_data", + depends=["create_table_with_datatypes"], ) def test_insert_datatypes_data(capsys, instance_id, sample_database): snippets.insert_datatypes_data(instance_id, sample_database.database_id) @@ -605,7 +639,8 @@ def test_query_data_with_string(capsys, instance_id, sample_database): @pytest.mark.dependency( - name="add_numeric_column", depends=["create_table_with_datatypes"], + name="add_numeric_column", + depends=["create_table_with_datatypes"], ) def test_add_numeric_column(capsys, instance_id, sample_database): snippets.add_numeric_column(instance_id, sample_database.database_id) @@ -621,14 +656,17 @@ def test_update_data_with_numeric(capsys, instance_id, sample_database): @pytest.mark.dependency(depends=["add_numeric_column"]) -def test_query_data_with_numeric_parameter(capsys, instance_id, sample_database): - snippets.query_data_with_numeric_parameter(instance_id, sample_database.database_id) +def test_query_data_with_numeric_parameter(capsys, instance_id, + sample_database): + snippets.query_data_with_numeric_parameter(instance_id, + sample_database.database_id) out, _ = capsys.readouterr() assert "VenueId: 4, Revenue: 35000" in out @pytest.mark.dependency( - name="add_json_column", depends=["create_table_with_datatypes"], + name="add_json_column", + depends=["create_table_with_datatypes"], ) def test_add_json_column(capsys, instance_id, sample_database): snippets.add_json_column(instance_id, sample_database.database_id) @@ -645,15 +683,17 @@ def test_update_data_with_json(capsys, instance_id, sample_database): @pytest.mark.dependency(depends=["add_json_column"]) def test_query_data_with_json_parameter(capsys, instance_id, sample_database): - snippets.query_data_with_json_parameter(instance_id, sample_database.database_id) + snippets.query_data_with_json_parameter(instance_id, + sample_database.database_id) out, _ = capsys.readouterr() assert "VenueId: 19, VenueDetails: {'open': True, 'rating': 9}" in out @pytest.mark.dependency(depends=["insert_datatypes_data"]) -def test_query_data_with_timestamp_parameter(capsys, instance_id, sample_database): +def test_query_data_with_timestamp_parameter(capsys, instance_id, + sample_database): snippets.query_data_with_timestamp_parameter( - instance_id, sample_database.database_id + instance_id, sample_database.database_id ) out, _ = capsys.readouterr() assert "VenueId: 4, VenueName: Venue 4, LastUpdateTime:" in out @@ -663,7 +703,8 @@ def test_query_data_with_timestamp_parameter(capsys, instance_id, sample_databas @pytest.mark.dependency(depends=["insert_datatypes_data"]) def test_query_data_with_query_options(capsys, instance_id, sample_database): - snippets.query_data_with_query_options(instance_id, sample_database.database_id) + snippets.query_data_with_query_options(instance_id, + sample_database.database_id) out, _ = capsys.readouterr() assert "VenueId: 4, VenueName: Venue 4, LastUpdateTime:" in out assert "VenueId: 19, VenueName: Venue 19, LastUpdateTime:" in out @@ -672,7 +713,8 @@ def test_query_data_with_query_options(capsys, instance_id, sample_database): @pytest.mark.dependency(depends=["insert_datatypes_data"]) def test_create_client_with_query_options(capsys, instance_id, sample_database): - snippets.create_client_with_query_options(instance_id, sample_database.database_id) + snippets.create_client_with_query_options(instance_id, + sample_database.database_id) out, _ = capsys.readouterr() assert "VenueId: 4, VenueName: Venue 4, LastUpdateTime:" in out assert "VenueId: 19, VenueName: Venue 19, LastUpdateTime:" in out diff --git a/setup.py b/setup.py index b14776ee2d..ff5ab61ef2 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ name = "google-cloud-spanner" description = "Cloud Spanner API client library" -version = "3.22.1" +version = "3.23.0" # Should be one of: # 'Development Status :: 3 - Alpha' # 'Development Status :: 4 - Beta' @@ -35,7 +35,7 @@ "proto-plus >= 1.22.0, <2.0.0dev", "sqlparse >= 0.3.0", "packaging >= 14.3", - "protobuf >= 3.20.2, <5.0.0dev", + "protobuf>=3.19.5,<5.0.0dev,!=3.20.0,!=3.20.1,!=4.21.0,!=4.21.1,!=4.21.2,!=4.21.3,!=4.21.4,!=4.21.5", ] extras = { "tracing": [ diff --git a/testing/constraints-3.7.txt b/testing/constraints-3.7.txt index 7391e756d0..5a63b04a4d 100644 --- a/testing/constraints-3.7.txt +++ b/testing/constraints-3.7.txt @@ -15,4 +15,4 @@ opentelemetry-api==1.1.0 opentelemetry-sdk==1.1.0 opentelemetry-instrumentation==0.20b0 packaging==14.3 -protobuf==3.20.2 +protobuf==3.19.5 diff --git a/tests/_fixtures.py b/tests/_fixtures.py index cea3054156..7bf55ee232 100644 --- a/tests/_fixtures.py +++ b/tests/_fixtures.py @@ -136,6 +136,7 @@ string_value VARCHAR(16), timestamp_value TIMESTAMPTZ, numeric_value NUMERIC, + jsonb_value JSONB, PRIMARY KEY (pkey) ); CREATE TABLE counters ( name VARCHAR(1024), diff --git a/tests/system/test_session_api.py b/tests/system/test_session_api.py index 6d38d7b17b..8e7b65d95e 100644 --- a/tests/system/test_session_api.py +++ b/tests/system/test_session_api.py @@ -89,6 +89,7 @@ LIVE_ALL_TYPES_COLUMNS[:1] + LIVE_ALL_TYPES_COLUMNS[1:7:2] + LIVE_ALL_TYPES_COLUMNS[9:17:2] + + ("jsonb_value",) ) AllTypesRowData = collections.namedtuple("AllTypesRowData", LIVE_ALL_TYPES_COLUMNS) @@ -120,7 +121,7 @@ AllTypesRowData(pkey=108, timestamp_value=NANO_TIME), AllTypesRowData(pkey=109, numeric_value=NUMERIC_1), AllTypesRowData(pkey=110, json_value=JSON_1), - AllTypesRowData(pkey=111, json_value=[JSON_1, JSON_2]), + AllTypesRowData(pkey=111, json_value=JsonObject([JSON_1, JSON_2])), # empty array values AllTypesRowData(pkey=201, int_array=[]), AllTypesRowData(pkey=202, bool_array=[]), @@ -184,12 +185,13 @@ PostGresAllTypesRowData(pkey=107, timestamp_value=SOME_TIME), PostGresAllTypesRowData(pkey=108, timestamp_value=NANO_TIME), PostGresAllTypesRowData(pkey=109, numeric_value=NUMERIC_1), + PostGresAllTypesRowData(pkey=110, jsonb_value=JSON_1), ) if _helpers.USE_EMULATOR: ALL_TYPES_COLUMNS = EMULATOR_ALL_TYPES_COLUMNS ALL_TYPES_ROWDATA = EMULATOR_ALL_TYPES_ROWDATA -elif _helpers.DATABASE_DIALECT: +elif _helpers.DATABASE_DIALECT == "POSTGRESQL": ALL_TYPES_COLUMNS = POSTGRES_ALL_TYPES_COLUMNS ALL_TYPES_ROWDATA = POSTGRES_ALL_TYPES_ROWDATA else: @@ -2105,6 +2107,18 @@ def test_execute_sql_w_json_bindings( ) +def test_execute_sql_w_jsonb_bindings( + not_emulator, not_google_standard_sql, sessions_database, database_dialect +): + _bind_test_helper( + sessions_database, + database_dialect, + spanner_v1.param_types.PG_JSONB, + JSON_1, + [JSON_1, JSON_2], + ) + + def test_execute_sql_w_query_param_struct(sessions_database, not_postgres): name = "Phred" count = 123 diff --git a/tests/unit/gapic/spanner_v1/test_spanner.py b/tests/unit/gapic/spanner_v1/test_spanner.py index d17741419e..0e70b5119a 100644 --- a/tests/unit/gapic/spanner_v1/test_spanner.py +++ b/tests/unit/gapic/spanner_v1/test_spanner.py @@ -2926,7 +2926,11 @@ def test_begin_transaction_flattened(): # using the keyword arguments to the method. client.begin_transaction( session="session_value", - options=transaction.TransactionOptions(read_write=None), + options=transaction.TransactionOptions( + read_write=transaction.TransactionOptions.ReadWrite( + read_lock_mode=transaction.TransactionOptions.ReadWrite.ReadLockMode.PESSIMISTIC + ) + ), ) # Establish that the underlying call was made with the expected @@ -2937,7 +2941,11 @@ def test_begin_transaction_flattened(): mock_val = "session_value" assert arg == mock_val arg = args[0].options - mock_val = transaction.TransactionOptions(read_write=None) + mock_val = transaction.TransactionOptions( + read_write=transaction.TransactionOptions.ReadWrite( + read_lock_mode=transaction.TransactionOptions.ReadWrite.ReadLockMode.PESSIMISTIC + ) + ) assert arg == mock_val @@ -2952,7 +2960,11 @@ def test_begin_transaction_flattened_error(): client.begin_transaction( spanner.BeginTransactionRequest(), session="session_value", - options=transaction.TransactionOptions(read_write=None), + options=transaction.TransactionOptions( + read_write=transaction.TransactionOptions.ReadWrite( + read_lock_mode=transaction.TransactionOptions.ReadWrite.ReadLockMode.PESSIMISTIC + ) + ), ) @@ -2976,7 +2988,11 @@ async def test_begin_transaction_flattened_async(): # using the keyword arguments to the method. response = await client.begin_transaction( session="session_value", - options=transaction.TransactionOptions(read_write=None), + options=transaction.TransactionOptions( + read_write=transaction.TransactionOptions.ReadWrite( + read_lock_mode=transaction.TransactionOptions.ReadWrite.ReadLockMode.PESSIMISTIC + ) + ), ) # Establish that the underlying call was made with the expected @@ -2987,7 +3003,11 @@ async def test_begin_transaction_flattened_async(): mock_val = "session_value" assert arg == mock_val arg = args[0].options - mock_val = transaction.TransactionOptions(read_write=None) + mock_val = transaction.TransactionOptions( + read_write=transaction.TransactionOptions.ReadWrite( + read_lock_mode=transaction.TransactionOptions.ReadWrite.ReadLockMode.PESSIMISTIC + ) + ) assert arg == mock_val @@ -3003,7 +3023,11 @@ async def test_begin_transaction_flattened_error_async(): await client.begin_transaction( spanner.BeginTransactionRequest(), session="session_value", - options=transaction.TransactionOptions(read_write=None), + options=transaction.TransactionOptions( + read_write=transaction.TransactionOptions.ReadWrite( + read_lock_mode=transaction.TransactionOptions.ReadWrite.ReadLockMode.PESSIMISTIC + ) + ), ) @@ -3168,7 +3192,11 @@ def test_commit_flattened(): mutations=[ mutation.Mutation(insert=mutation.Mutation.Write(table="table_value")) ], - single_use_transaction=transaction.TransactionOptions(read_write=None), + single_use_transaction=transaction.TransactionOptions( + read_write=transaction.TransactionOptions.ReadWrite( + read_lock_mode=transaction.TransactionOptions.ReadWrite.ReadLockMode.PESSIMISTIC + ) + ), ) # Establish that the underlying call was made with the expected @@ -3184,7 +3212,9 @@ def test_commit_flattened(): ] assert arg == mock_val assert args[0].single_use_transaction == transaction.TransactionOptions( - read_write=None + read_write=transaction.TransactionOptions.ReadWrite( + read_lock_mode=transaction.TransactionOptions.ReadWrite.ReadLockMode.PESSIMISTIC + ) ) @@ -3203,7 +3233,11 @@ def test_commit_flattened_error(): mutations=[ mutation.Mutation(insert=mutation.Mutation.Write(table="table_value")) ], - single_use_transaction=transaction.TransactionOptions(read_write=None), + single_use_transaction=transaction.TransactionOptions( + read_write=transaction.TransactionOptions.ReadWrite( + read_lock_mode=transaction.TransactionOptions.ReadWrite.ReadLockMode.PESSIMISTIC + ) + ), ) @@ -3229,7 +3263,11 @@ async def test_commit_flattened_async(): mutations=[ mutation.Mutation(insert=mutation.Mutation.Write(table="table_value")) ], - single_use_transaction=transaction.TransactionOptions(read_write=None), + single_use_transaction=transaction.TransactionOptions( + read_write=transaction.TransactionOptions.ReadWrite( + read_lock_mode=transaction.TransactionOptions.ReadWrite.ReadLockMode.PESSIMISTIC + ) + ), ) # Establish that the underlying call was made with the expected @@ -3245,7 +3283,9 @@ async def test_commit_flattened_async(): ] assert arg == mock_val assert args[0].single_use_transaction == transaction.TransactionOptions( - read_write=None + read_write=transaction.TransactionOptions.ReadWrite( + read_lock_mode=transaction.TransactionOptions.ReadWrite.ReadLockMode.PESSIMISTIC + ) ) @@ -3265,7 +3305,11 @@ async def test_commit_flattened_error_async(): mutations=[ mutation.Mutation(insert=mutation.Mutation.Write(table="table_value")) ], - single_use_transaction=transaction.TransactionOptions(read_write=None), + single_use_transaction=transaction.TransactionOptions( + read_write=transaction.TransactionOptions.ReadWrite( + read_lock_mode=transaction.TransactionOptions.ReadWrite.ReadLockMode.PESSIMISTIC + ) + ), ) diff --git a/tests/unit/spanner_dbapi/test__helpers.py b/tests/unit/spanner_dbapi/test__helpers.py index 1782978d62..c770ff6e4b 100644 --- a/tests/unit/spanner_dbapi/test__helpers.py +++ b/tests/unit/spanner_dbapi/test__helpers.py @@ -37,7 +37,9 @@ def test__execute_insert_heterogenous(self): mock_pyformat.assert_called_once_with(params[0], params[1]) mock_param_types.assert_called_once_with(None) - mock_update.assert_called_once_with(sql, None, None) + mock_update.assert_called_once_with( + sql, None, None, request_options=None + ) def test__execute_insert_heterogenous_error(self): from google.cloud.spanner_dbapi import _helpers @@ -62,7 +64,9 @@ def test__execute_insert_heterogenous_error(self): mock_pyformat.assert_called_once_with(params[0], params[1]) mock_param_types.assert_called_once_with(None) - mock_update.assert_called_once_with(sql, None, None) + mock_update.assert_called_once_with( + sql, None, None, request_options=None + ) def test_handle_insert(self): from google.cloud.spanner_dbapi import _helpers diff --git a/tests/unit/spanner_dbapi/test_connection.py b/tests/unit/spanner_dbapi/test_connection.py index e15f6af33b..23fc098afc 100644 --- a/tests/unit/spanner_dbapi/test_connection.py +++ b/tests/unit/spanner_dbapi/test_connection.py @@ -883,6 +883,42 @@ def test_staleness_single_use_readonly_autocommit(self): connection.database.snapshot.assert_called_with(read_timestamp=timestamp) + def test_request_priority(self): + from google.cloud.spanner_dbapi.checksum import ResultsChecksum + from google.cloud.spanner_dbapi.cursor import Statement + from google.cloud.spanner_v1 import RequestOptions + + sql = "SELECT 1" + params = [] + param_types = {} + priority = 2 + + connection = self._make_connection() + connection._transaction = mock.Mock(committed=False, rolled_back=False) + connection._transaction.execute_sql = mock.Mock() + + connection.request_priority = priority + + req_opts = RequestOptions(priority=priority) + + connection.run_statement( + Statement(sql, params, param_types, ResultsChecksum(), False) + ) + + connection._transaction.execute_sql.assert_called_with( + sql, params, param_types=param_types, request_options=req_opts + ) + assert connection.request_priority is None + + # check that priority is applied for only one request + connection.run_statement( + Statement(sql, params, param_types, ResultsChecksum(), False) + ) + + connection._transaction.execute_sql.assert_called_with( + sql, params, param_types=param_types, request_options=None + ) + def exit_ctx_func(self, exc_type, exc_value, traceback): """Context __exit__ method mock.""" diff --git a/tests/unit/spanner_dbapi/test_cursor.py b/tests/unit/spanner_dbapi/test_cursor.py index 3f379f96ac..75089362af 100644 --- a/tests/unit/spanner_dbapi/test_cursor.py +++ b/tests/unit/spanner_dbapi/test_cursor.py @@ -748,6 +748,29 @@ def test_handle_dql(self): self.assertIsInstance(cursor._itr, utils.PeekIterator) self.assertEqual(cursor._row_count, _UNSET_COUNT) + def test_handle_dql_priority(self): + from google.cloud.spanner_dbapi import utils + from google.cloud.spanner_dbapi.cursor import _UNSET_COUNT + from google.cloud.spanner_v1 import RequestOptions + + connection = self._make_connection(self.INSTANCE, mock.MagicMock()) + connection.database.snapshot.return_value.__enter__.return_value = ( + mock_snapshot + ) = mock.MagicMock() + connection.request_priority = 1 + + cursor = self._make_one(connection) + + sql = "sql" + mock_snapshot.execute_sql.return_value = ["0"] + cursor._handle_DQL(sql, params=None) + self.assertEqual(cursor._result_set, ["0"]) + self.assertIsInstance(cursor._itr, utils.PeekIterator) + self.assertEqual(cursor._row_count, _UNSET_COUNT) + mock_snapshot.execute_sql.assert_called_with( + sql, None, None, request_options=RequestOptions(priority=1) + ) + def test_context(self): connection = self._make_connection(self.INSTANCE, self.DATABASE) cursor = self._make_one(connection) diff --git a/tests/unit/test_param_types.py b/tests/unit/test_param_types.py index 0d6a17c613..02f41c1f25 100644 --- a/tests/unit/test_param_types.py +++ b/tests/unit/test_param_types.py @@ -54,3 +54,20 @@ def test_it(self): ) self.assertEqual(found, expected) + + +class Test_JsonbParamType(unittest.TestCase): + def test_it(self): + from google.cloud.spanner_v1 import Type + from google.cloud.spanner_v1 import TypeCode + from google.cloud.spanner_v1 import TypeAnnotationCode + from google.cloud.spanner_v1 import param_types + + expected = Type( + code=TypeCode.JSON, + type_annotation=TypeAnnotationCode(TypeAnnotationCode.PG_JSONB), + ) + + found = param_types.PG_JSONB + + self.assertEqual(found, expected)