diff --git a/google/cloud/spanner_v1/_helpers.py b/google/cloud/spanner_v1/_helpers.py index 91e8c8d29c..6437c65e7f 100644 --- a/google/cloud/spanner_v1/_helpers.py +++ b/google/cloud/spanner_v1/_helpers.py @@ -15,6 +15,7 @@ """Helper functions for Cloud Spanner.""" import datetime +import decimal import math import six @@ -127,6 +128,8 @@ def _make_value_pb(value): return Value(string_value=value) if isinstance(value, ListValue): return Value(list_value=value) + if isinstance(value, decimal.Decimal): + return Value(string_value=str(value)) raise ValueError("Unknown type: %s" % (value,)) @@ -201,6 +204,8 @@ def _parse_value_pb(value_pb, field_type): _parse_value_pb(item_pb, field_type.struct_type.fields[i].type) for (i, item_pb) in enumerate(value_pb.list_value.values) ] + elif field_type.code == type_pb2.NUMERIC: + result = decimal.Decimal(value_pb.string_value) else: raise ValueError("Unknown type: %s" % (field_type,)) return result diff --git a/google/cloud/spanner_v1/param_types.py b/google/cloud/spanner_v1/param_types.py index 47442bfc4b..c672d818ca 100644 --- a/google/cloud/spanner_v1/param_types.py +++ b/google/cloud/spanner_v1/param_types.py @@ -25,6 +25,7 @@ FLOAT64 = type_pb2.Type(code=type_pb2.FLOAT64) DATE = type_pb2.Type(code=type_pb2.DATE) TIMESTAMP = type_pb2.Type(code=type_pb2.TIMESTAMP) +NUMERIC = type_pb2.Type(code=type_pb2.NUMERIC) def Array(element_type): # pylint: disable=invalid-name diff --git a/tests/_fixtures.py b/tests/_fixtures.py index d0b78c0ba5..efca8a9042 100644 --- a/tests/_fixtures.py +++ b/tests/_fixtures.py @@ -16,6 +16,58 @@ DDL = """\ +CREATE TABLE contacts ( + contact_id INT64, + first_name STRING(1024), + last_name STRING(1024), + email STRING(1024) ) + PRIMARY KEY (contact_id); +CREATE TABLE contact_phones ( + contact_id INT64, + phone_type STRING(1024), + phone_number STRING(1024) ) + PRIMARY KEY (contact_id, phone_type), + INTERLEAVE IN PARENT contacts ON DELETE CASCADE; +CREATE TABLE all_types ( + pkey INT64 NOT NULL, + int_value INT64, + int_array ARRAY, + bool_value BOOL, + bool_array ARRAY, + bytes_value BYTES(16), + bytes_array ARRAY, + date_value DATE, + date_array ARRAY, + float_value FLOAT64, + float_array ARRAY, + string_value STRING(16), + string_array ARRAY, + timestamp_value TIMESTAMP, + timestamp_array ARRAY, + numeric_value NUMERIC, + numeric_array ARRAY) + PRIMARY KEY (pkey); +CREATE TABLE counters ( + name STRING(1024), + value INT64 ) + PRIMARY KEY (name); +CREATE TABLE string_plus_array_of_string ( + id INT64, + name STRING(16), + tags ARRAY ) + PRIMARY KEY (id); +CREATE INDEX name ON contacts(first_name, last_name); +CREATE TABLE users_history ( + id INT64 NOT NULL, + commit_ts TIMESTAMP NOT NULL OPTIONS + (allow_commit_timestamp=true), + name STRING(MAX) NOT NULL, + email STRING(MAX), + deleted BOOL NOT NULL ) + PRIMARY KEY(id, commit_ts DESC); +""" + +EMULATOR_DDL = """\ CREATE TABLE contacts ( contact_id INT64, first_name STRING(1024), @@ -66,3 +118,6 @@ """ DDL_STATEMENTS = [stmt.strip() for stmt in DDL.split(";") if stmt.strip()] +EMULATOR_DDL_STATEMENTS = [ + stmt.strip() for stmt in EMULATOR_DDL.split(";") if stmt.strip() +] diff --git a/tests/system/test_system.py b/tests/system/test_system.py index 7779769c8f..65cc0ef1f9 100644 --- a/tests/system/test_system.py +++ b/tests/system/test_system.py @@ -14,6 +14,7 @@ import collections import datetime +import decimal import math import operator import os @@ -38,6 +39,7 @@ from google.cloud.spanner_v1.proto.type_pb2 import INT64 from google.cloud.spanner_v1.proto.type_pb2 import STRING from google.cloud.spanner_v1.proto.type_pb2 import TIMESTAMP +from google.cloud.spanner_v1.proto.type_pb2 import NUMERIC from google.cloud.spanner_v1.proto.type_pb2 import Type from google.cloud._helpers import UTC @@ -52,11 +54,13 @@ from test_utils.retry import RetryResult from test_utils.system import unique_resource_id from tests._fixtures import DDL_STATEMENTS +from tests._fixtures import EMULATOR_DDL_STATEMENTS from tests._helpers import OpenTelemetryBase, HAS_OPENTELEMETRY_INSTALLED CREATE_INSTANCE = os.getenv("GOOGLE_CLOUD_TESTS_CREATE_SPANNER_INSTANCE") is not None USE_EMULATOR = os.getenv("SPANNER_EMULATOR_HOST") is not None +SKIP_BACKUP_TESTS = os.getenv("SKIP_BACKUP_TESTS") is not None if CREATE_INSTANCE: INSTANCE_ID = "google-cloud" + unique_resource_id("-") @@ -92,7 +96,8 @@ class Config(object): def _has_all_ddl(database): - return len(database.ddl_statements) == len(DDL_STATEMENTS) + ddl_statements = EMULATOR_DDL_STATEMENTS if USE_EMULATOR else DDL_STATEMENTS + return len(database.ddl_statements) == len(ddl_statements) def _list_instances(): @@ -284,8 +289,9 @@ class TestDatabaseAPI(unittest.TestCase, _TestData): @classmethod def setUpClass(cls): pool = BurstyPool(labels={"testcase": "database_api"}) + ddl_statements = EMULATOR_DDL_STATEMENTS if USE_EMULATOR else DDL_STATEMENTS cls._db = Config.INSTANCE.database( - cls.DATABASE_NAME, ddl_statements=DDL_STATEMENTS, pool=pool + cls.DATABASE_NAME, ddl_statements=ddl_statements, pool=pool ) operation = cls._db.create() operation.result(30) # raises on failure / timeout. @@ -359,12 +365,13 @@ def test_update_database_ddl_with_operation_id(self): temp_db = Config.INSTANCE.database(temp_db_id, pool=pool) create_op = temp_db.create() self.to_delete.append(temp_db) + ddl_statements = EMULATOR_DDL_STATEMENTS if USE_EMULATOR else DDL_STATEMENTS # We want to make sure the operation completes. create_op.result(240) # raises on failure / timeout. # random but shortish always start with letter operation_id = "a" + str(uuid.uuid4())[:8] - operation = temp_db.update_ddl(DDL_STATEMENTS, operation_id=operation_id) + operation = temp_db.update_ddl(ddl_statements, operation_id=operation_id) self.assertEqual(operation_id, operation.operation.name.split("/")[-1]) @@ -373,7 +380,7 @@ def test_update_database_ddl_with_operation_id(self): temp_db.reload() - self.assertEqual(len(temp_db.ddl_statements), len(DDL_STATEMENTS)) + self.assertEqual(len(temp_db.ddl_statements), len(ddl_statements)) def test_db_batch_insert_then_db_snapshot_read(self): retry = RetryInstanceState(_has_all_ddl) @@ -447,6 +454,7 @@ def _unit_of_work(transaction, name): @unittest.skipIf(USE_EMULATOR, "Skipping backup tests") +@unittest.skipIf(SKIP_BACKUP_TESTS, "Skipping backup tests") class TestBackupAPI(unittest.TestCase, _TestData): DATABASE_NAME = "test_database" + unique_resource_id("_") DATABASE_NAME_2 = "test_database2" + unique_resource_id("_") @@ -454,8 +462,9 @@ class TestBackupAPI(unittest.TestCase, _TestData): @classmethod def setUpClass(cls): pool = BurstyPool(labels={"testcase": "database_api"}) + ddl_statements = EMULATOR_DDL_STATEMENTS if USE_EMULATOR else DDL_STATEMENTS db1 = Config.INSTANCE.database( - cls.DATABASE_NAME, ddl_statements=DDL_STATEMENTS, pool=pool + cls.DATABASE_NAME, ddl_statements=ddl_statements, pool=pool ) db2 = Config.INSTANCE.database(cls.DATABASE_NAME_2, pool=pool) cls._db = db1 @@ -736,6 +745,8 @@ def test_list_backups(self): (OTHER_NAN,) = struct.unpack("