diff --git a/packages/google-cloud-spanner/google/cloud/spanner_v1/_async/database.py b/packages/google-cloud-spanner/google/cloud/spanner_v1/_async/database.py index 0fb7de8ec89e..5c7e82b092bd 100644 --- a/packages/google-cloud-spanner/google/cloud/spanner_v1/_async/database.py +++ b/packages/google-cloud-spanner/google/cloud/spanner_v1/_async/database.py @@ -688,18 +688,19 @@ async def create(self): """ api = self._instance._client.database_admin_api metadata = _metadata_with_prefix(self.name) - db_name = self.database_id - if "-" in db_name: - if self._database_dialect == DatabaseDialect.POSTGRESQL: - db_name = f'"{db_name}"' - else: - db_name = f"`{db_name}`" + database_id = str(self.database_id) + if self._database_dialect == DatabaseDialect.POSTGRESQL: + escaped = database_id.replace('"', '""') + quoted_database_id = f'"{escaped}"' + else: + escaped = database_id.replace("\\", "\\\\").replace("`", "\\`") + quoted_database_id = f"`{escaped}`" if type(self._encryption_config) is dict: self._encryption_config = EncryptionConfig(**self._encryption_config) request = CreateDatabaseRequest( parent=self._instance.name, - create_statement="CREATE DATABASE %s" % (db_name,), + create_statement=f"CREATE DATABASE {quoted_database_id}", extra_statements=list(self._ddl_statements), encryption_config=self._encryption_config, database_dialect=self._database_dialect, diff --git a/packages/google-cloud-spanner/google/cloud/spanner_v1/database.py b/packages/google-cloud-spanner/google/cloud/spanner_v1/database.py index 954302146c79..5d7b78c65875 100644 --- a/packages/google-cloud-spanner/google/cloud/spanner_v1/database.py +++ b/packages/google-cloud-spanner/google/cloud/spanner_v1/database.py @@ -599,17 +599,18 @@ def create(self): :raises NotFound: if the instance owning the database does not exist""" api = self._instance._client.database_admin_api metadata = _metadata_with_prefix(self.name) - db_name = self.database_id - if "-" in db_name: - if self._database_dialect == DatabaseDialect.POSTGRESQL: - db_name = f'"{db_name}"' - else: - db_name = f"`{db_name}`" + database_id = str(self.database_id) + if self._database_dialect == DatabaseDialect.POSTGRESQL: + escaped = database_id.replace('"', '""') + quoted_database_id = f'"{escaped}"' + else: + escaped = database_id.replace("\\", "\\\\").replace("`", "\\`") + quoted_database_id = f"`{escaped}`" if type(self._encryption_config) is dict: self._encryption_config = EncryptionConfig(**self._encryption_config) request = CreateDatabaseRequest( parent=self._instance.name, - create_statement="CREATE DATABASE %s" % (db_name,), + create_statement=f"CREATE DATABASE {quoted_database_id}", extra_statements=list(self._ddl_statements), encryption_config=self._encryption_config, database_dialect=self._database_dialect, diff --git a/packages/google-cloud-spanner/tests/unit/_async/test_database.py b/packages/google-cloud-spanner/tests/unit/_async/test_database.py index bc1f142e05c4..9bb2e0519da7 100644 --- a/packages/google-cloud-spanner/tests/unit/_async/test_database.py +++ b/packages/google-cloud-spanner/tests/unit/_async/test_database.py @@ -765,7 +765,7 @@ async def test_create_grpc_error(self): expected_request = CreateDatabaseRequest( parent=self.INSTANCE_NAME, - create_statement="CREATE DATABASE {}".format(self.DATABASE_ID), + create_statement="CREATE DATABASE `{}`".format(self.DATABASE_ID), extra_statements=[], encryption_config=None, ) @@ -834,7 +834,7 @@ async def test_create_instance_not_found(self): expected_request = CreateDatabaseRequest( parent=self.INSTANCE_NAME, - create_statement="CREATE DATABASE {}".format(self.DATABASE_ID), + create_statement="CREATE DATABASE `{}`".format(self.DATABASE_ID), extra_statements=[], encryption_config=None, ) @@ -879,7 +879,7 @@ async def test_create_success(self): expected_request = CreateDatabaseRequest( parent=self.INSTANCE_NAME, - create_statement="CREATE DATABASE {}".format(self.DATABASE_ID), + create_statement="CREATE DATABASE `{}`".format(self.DATABASE_ID), extra_statements=DDL_STATEMENTS, encryption_config=encryption_config, ) @@ -925,7 +925,7 @@ async def test_create_success_w_encryption_config_dict(self): expected_encryption_config = EncryptionConfig(**encryption_config) expected_request = CreateDatabaseRequest( parent=self.INSTANCE_NAME, - create_statement="CREATE DATABASE {}".format(self.DATABASE_ID), + create_statement="CREATE DATABASE `{}`".format(self.DATABASE_ID), extra_statements=DDL_STATEMENTS, encryption_config=expected_encryption_config, ) @@ -967,7 +967,7 @@ async def test_create_success_w_proto_descriptors(self): expected_request = CreateDatabaseRequest( parent=self.INSTANCE_NAME, - create_statement="CREATE DATABASE {}".format(self.DATABASE_ID), + create_statement="CREATE DATABASE `{}`".format(self.DATABASE_ID), extra_statements=DDL_STATEMENTS, proto_descriptors=proto_descriptors, ) @@ -983,6 +983,252 @@ async def test_create_success_w_proto_descriptors(self): ], ) + @CrossSync.pytest + async def test_create_reserved_keyword_googlesql(self): + from google.cloud.spanner_admin_database_v1 import CreateDatabaseRequest + + client = _Client() + api = client.database_admin_api = self._make_database_admin_api() + api.create_database.return_value = None + instance = _Instance(self.INSTANCE_NAME, client=client) + pool = _Pool() + database = await self._make_one("default", instance, pool=pool) + + await database.create() + + expected_request = CreateDatabaseRequest( + parent=self.INSTANCE_NAME, + create_statement="CREATE DATABASE `default`", + extra_statements=[], + encryption_config=None, + ) + api.create_database.assert_called_once_with( + request=expected_request, + metadata=[ + ("google-cloud-resource-prefix", database.name), + ( + "x-goog-spanner-request-id", + f"1.{REQ_RAND_PROCESS_ID}.{database._nth_client_id}.{database._channel_id}.1.1", + ), + ], + ) + + @CrossSync.pytest + async def test_create_reserved_keyword_postgresql(self): + from google.cloud.spanner_admin_database_v1 import CreateDatabaseRequest + from google.cloud.spanner_admin_database_v1.types import DatabaseDialect + + client = _Client() + api = client.database_admin_api = self._make_database_admin_api() + api.create_database.return_value = None + instance = _Instance(self.INSTANCE_NAME, client=client) + pool = _Pool() + database = await self._make_one( + "default", + instance, + pool=pool, + database_dialect=DatabaseDialect.POSTGRESQL, + ) + + await database.create() + + expected_request = CreateDatabaseRequest( + parent=self.INSTANCE_NAME, + create_statement='CREATE DATABASE "default"', + extra_statements=[], + encryption_config=None, + database_dialect=DatabaseDialect.POSTGRESQL, + ) + api.create_database.assert_called_once_with( + request=expected_request, + metadata=[ + ("google-cloud-resource-prefix", database.name), + ( + "x-goog-spanner-request-id", + f"1.{REQ_RAND_PROCESS_ID}.{database._nth_client_id}.{database._channel_id}.1.1", + ), + ], + ) + + @CrossSync.pytest + async def test_create_hyphen_postgresql(self): + from google.cloud.spanner_admin_database_v1 import CreateDatabaseRequest + from google.cloud.spanner_admin_database_v1.types import DatabaseDialect + + DATABASE_ID_HYPHEN = "database-id" + client = _Client() + api = client.database_admin_api = self._make_database_admin_api() + api.create_database.return_value = None + instance = _Instance(self.INSTANCE_NAME, client=client) + pool = _Pool() + database = await self._make_one( + DATABASE_ID_HYPHEN, + instance, + pool=pool, + database_dialect=DatabaseDialect.POSTGRESQL, + ) + + await database.create() + + expected_request = CreateDatabaseRequest( + parent=self.INSTANCE_NAME, + create_statement=f'CREATE DATABASE "{DATABASE_ID_HYPHEN}"', + extra_statements=[], + encryption_config=None, + database_dialect=DatabaseDialect.POSTGRESQL, + ) + api.create_database.assert_called_once_with( + request=expected_request, + metadata=[ + ("google-cloud-resource-prefix", database.name), + ( + "x-goog-spanner-request-id", + f"1.{REQ_RAND_PROCESS_ID}.{database._nth_client_id}.{database._channel_id}.1.1", + ), + ], + ) + + @CrossSync.pytest + async def test_create_delimiters_escaped_googlesql(self): + from google.cloud.spanner_admin_database_v1 import CreateDatabaseRequest + + client = _Client() + api = client.database_admin_api = self._make_database_admin_api() + api.create_database.return_value = None + instance = _Instance(self.INSTANCE_NAME, client=client) + pool = _Pool() + database = await self._make_one("db`name\\", instance, pool=pool) + + await database.create() + + expected_request = CreateDatabaseRequest( + parent=self.INSTANCE_NAME, + create_statement="CREATE DATABASE `db\\`name\\\\`", + extra_statements=[], + encryption_config=None, + ) + api.create_database.assert_called_once_with( + request=expected_request, + metadata=[ + ("google-cloud-resource-prefix", database.name), + ( + "x-goog-spanner-request-id", + f"1.{REQ_RAND_PROCESS_ID}.{database._nth_client_id}.{database._channel_id}.1.1", + ), + ], + ) + + @CrossSync.pytest + async def test_create_delimiters_escaped_postgresql(self): + from google.cloud.spanner_admin_database_v1 import CreateDatabaseRequest + from google.cloud.spanner_admin_database_v1.types import DatabaseDialect + + client = _Client() + api = client.database_admin_api = self._make_database_admin_api() + api.create_database.return_value = None + instance = _Instance(self.INSTANCE_NAME, client=client) + pool = _Pool() + database = await self._make_one( + 'db"name', + instance, + pool=pool, + database_dialect=DatabaseDialect.POSTGRESQL, + ) + + await database.create() + + expected_request = CreateDatabaseRequest( + parent=self.INSTANCE_NAME, + create_statement='CREATE DATABASE "db""name"', + extra_statements=[], + encryption_config=None, + database_dialect=DatabaseDialect.POSTGRESQL, + ) + api.create_database.assert_called_once_with( + request=expected_request, + metadata=[ + ("google-cloud-resource-prefix", database.name), + ( + "x-goog-spanner-request-id", + f"1.{REQ_RAND_PROCESS_ID}.{database._nth_client_id}.{database._channel_id}.1.1", + ), + ], + ) + + @CrossSync.pytest + async def test_create_explicit_googlesql(self): + from google.cloud.spanner_admin_database_v1 import CreateDatabaseRequest + from google.cloud.spanner_admin_database_v1.types import DatabaseDialect + + client = _Client() + api = client.database_admin_api = self._make_database_admin_api() + api.create_database.return_value = None + instance = _Instance(self.INSTANCE_NAME, client=client) + pool = _Pool() + database = await self._make_one( + self.DATABASE_ID, + instance, + pool=pool, + database_dialect=DatabaseDialect.GOOGLE_STANDARD_SQL, + ) + + await database.create() + + expected_request = CreateDatabaseRequest( + parent=self.INSTANCE_NAME, + create_statement=f"CREATE DATABASE `{self.DATABASE_ID}`", + extra_statements=[], + encryption_config=None, + database_dialect=DatabaseDialect.GOOGLE_STANDARD_SQL, + ) + api.create_database.assert_called_once_with( + request=expected_request, + metadata=[ + ("google-cloud-resource-prefix", database.name), + ( + "x-goog-spanner-request-id", + f"1.{REQ_RAND_PROCESS_ID}.{database._nth_client_id}.{database._channel_id}.1.1", + ), + ], + ) + + @CrossSync.pytest + async def test_create_standard_postgresql(self): + from google.cloud.spanner_admin_database_v1 import CreateDatabaseRequest + from google.cloud.spanner_admin_database_v1.types import DatabaseDialect + + client = _Client() + api = client.database_admin_api = self._make_database_admin_api() + api.create_database.return_value = None + instance = _Instance(self.INSTANCE_NAME, client=client) + pool = _Pool() + database = await self._make_one( + "standard_db", + instance, + pool=pool, + database_dialect=DatabaseDialect.POSTGRESQL, + ) + + await database.create() + + expected_request = CreateDatabaseRequest( + parent=self.INSTANCE_NAME, + create_statement='CREATE DATABASE "standard_db"', + extra_statements=[], + encryption_config=None, + database_dialect=DatabaseDialect.POSTGRESQL, + ) + api.create_database.assert_called_once_with( + request=expected_request, + metadata=[ + ("google-cloud-resource-prefix", database.name), + ( + "x-goog-spanner-request-id", + f"1.{REQ_RAND_PROCESS_ID}.{database._nth_client_id}.{database._channel_id}.1.1", + ), + ], + ) + @CrossSync.pytest async def test_exists_grpc_error(self): from google.api_core.exceptions import Unknown diff --git a/packages/google-cloud-spanner/tests/unit/test_database.py b/packages/google-cloud-spanner/tests/unit/test_database.py index 4b669ccbc433..4e912cff4f5a 100644 --- a/packages/google-cloud-spanner/tests/unit/test_database.py +++ b/packages/google-cloud-spanner/tests/unit/test_database.py @@ -698,7 +698,7 @@ def test_create_grpc_error(self): expected_request = CreateDatabaseRequest( parent=self.INSTANCE_NAME, - create_statement="CREATE DATABASE {}".format(self.DATABASE_ID), + create_statement="CREATE DATABASE `{}`".format(self.DATABASE_ID), extra_statements=[], encryption_config=None, ) @@ -765,7 +765,7 @@ def test_create_instance_not_found(self): expected_request = CreateDatabaseRequest( parent=self.INSTANCE_NAME, - create_statement="CREATE DATABASE {}".format(self.DATABASE_ID), + create_statement="CREATE DATABASE `{}`".format(self.DATABASE_ID), extra_statements=[], encryption_config=None, ) @@ -809,7 +809,7 @@ def test_create_success(self): expected_request = CreateDatabaseRequest( parent=self.INSTANCE_NAME, - create_statement="CREATE DATABASE {}".format(self.DATABASE_ID), + create_statement="CREATE DATABASE `{}`".format(self.DATABASE_ID), extra_statements=DDL_STATEMENTS, encryption_config=encryption_config, ) @@ -854,7 +854,7 @@ def test_create_success_w_encryption_config_dict(self): expected_encryption_config = EncryptionConfig(**encryption_config) expected_request = CreateDatabaseRequest( parent=self.INSTANCE_NAME, - create_statement="CREATE DATABASE {}".format(self.DATABASE_ID), + create_statement="CREATE DATABASE `{}`".format(self.DATABASE_ID), extra_statements=DDL_STATEMENTS, encryption_config=expected_encryption_config, ) @@ -895,7 +895,7 @@ def test_create_success_w_proto_descriptors(self): expected_request = CreateDatabaseRequest( parent=self.INSTANCE_NAME, - create_statement="CREATE DATABASE {}".format(self.DATABASE_ID), + create_statement="CREATE DATABASE `{}`".format(self.DATABASE_ID), extra_statements=DDL_STATEMENTS, proto_descriptors=proto_descriptors, ) @@ -911,6 +911,245 @@ def test_create_success_w_proto_descriptors(self): ], ) + def test_create_reserved_keyword_googlesql(self): + from google.cloud.spanner_admin_database_v1 import CreateDatabaseRequest + + client = _Client() + api = client.database_admin_api = self._make_database_admin_api() + api.create_database.return_value = None + instance = _Instance(self.INSTANCE_NAME, client=client) + pool = _Pool() + database = self._make_one("default", instance, pool=pool) + + database.create() + + expected_request = CreateDatabaseRequest( + parent=self.INSTANCE_NAME, + create_statement="CREATE DATABASE `default`", + extra_statements=[], + encryption_config=None, + ) + api.create_database.assert_called_once_with( + request=expected_request, + metadata=[ + ("google-cloud-resource-prefix", database.name), + ( + "x-goog-spanner-request-id", + f"1.{REQ_RAND_PROCESS_ID}.{database._nth_client_id}.{database._channel_id}.1.1", + ), + ], + ) + + def test_create_reserved_keyword_postgresql(self): + from google.cloud.spanner_admin_database_v1 import CreateDatabaseRequest + from google.cloud.spanner_admin_database_v1.types import DatabaseDialect + + client = _Client() + api = client.database_admin_api = self._make_database_admin_api() + api.create_database.return_value = None + instance = _Instance(self.INSTANCE_NAME, client=client) + pool = _Pool() + database = self._make_one( + "default", + instance, + pool=pool, + database_dialect=DatabaseDialect.POSTGRESQL, + ) + + database.create() + + expected_request = CreateDatabaseRequest( + parent=self.INSTANCE_NAME, + create_statement='CREATE DATABASE "default"', + extra_statements=[], + encryption_config=None, + database_dialect=DatabaseDialect.POSTGRESQL, + ) + api.create_database.assert_called_once_with( + request=expected_request, + metadata=[ + ("google-cloud-resource-prefix", database.name), + ( + "x-goog-spanner-request-id", + f"1.{REQ_RAND_PROCESS_ID}.{database._nth_client_id}.{database._channel_id}.1.1", + ), + ], + ) + + def test_create_hyphen_postgresql(self): + from google.cloud.spanner_admin_database_v1 import CreateDatabaseRequest + from google.cloud.spanner_admin_database_v1.types import DatabaseDialect + + DATABASE_ID_HYPHEN = "database-id" + client = _Client() + api = client.database_admin_api = self._make_database_admin_api() + api.create_database.return_value = None + instance = _Instance(self.INSTANCE_NAME, client=client) + pool = _Pool() + database = self._make_one( + DATABASE_ID_HYPHEN, + instance, + pool=pool, + database_dialect=DatabaseDialect.POSTGRESQL, + ) + + database.create() + + expected_request = CreateDatabaseRequest( + parent=self.INSTANCE_NAME, + create_statement=f'CREATE DATABASE "{DATABASE_ID_HYPHEN}"', + extra_statements=[], + encryption_config=None, + database_dialect=DatabaseDialect.POSTGRESQL, + ) + api.create_database.assert_called_once_with( + request=expected_request, + metadata=[ + ("google-cloud-resource-prefix", database.name), + ( + "x-goog-spanner-request-id", + f"1.{REQ_RAND_PROCESS_ID}.{database._nth_client_id}.{database._channel_id}.1.1", + ), + ], + ) + + def test_create_delimiters_escaped_googlesql(self): + from google.cloud.spanner_admin_database_v1 import CreateDatabaseRequest + + client = _Client() + api = client.database_admin_api = self._make_database_admin_api() + api.create_database.return_value = None + instance = _Instance(self.INSTANCE_NAME, client=client) + pool = _Pool() + database = self._make_one("db`name\\", instance, pool=pool) + + database.create() + + expected_request = CreateDatabaseRequest( + parent=self.INSTANCE_NAME, + create_statement="CREATE DATABASE `db\\`name\\\\`", + extra_statements=[], + encryption_config=None, + ) + api.create_database.assert_called_once_with( + request=expected_request, + metadata=[ + ("google-cloud-resource-prefix", database.name), + ( + "x-goog-spanner-request-id", + f"1.{REQ_RAND_PROCESS_ID}.{database._nth_client_id}.{database._channel_id}.1.1", + ), + ], + ) + + def test_create_delimiters_escaped_postgresql(self): + from google.cloud.spanner_admin_database_v1 import CreateDatabaseRequest + from google.cloud.spanner_admin_database_v1.types import DatabaseDialect + + client = _Client() + api = client.database_admin_api = self._make_database_admin_api() + api.create_database.return_value = None + instance = _Instance(self.INSTANCE_NAME, client=client) + pool = _Pool() + database = self._make_one( + 'db"name', + instance, + pool=pool, + database_dialect=DatabaseDialect.POSTGRESQL, + ) + + database.create() + + expected_request = CreateDatabaseRequest( + parent=self.INSTANCE_NAME, + create_statement='CREATE DATABASE "db""name"', + extra_statements=[], + encryption_config=None, + database_dialect=DatabaseDialect.POSTGRESQL, + ) + api.create_database.assert_called_once_with( + request=expected_request, + metadata=[ + ("google-cloud-resource-prefix", database.name), + ( + "x-goog-spanner-request-id", + f"1.{REQ_RAND_PROCESS_ID}.{database._nth_client_id}.{database._channel_id}.1.1", + ), + ], + ) + + def test_create_explicit_googlesql(self): + from google.cloud.spanner_admin_database_v1 import CreateDatabaseRequest + from google.cloud.spanner_admin_database_v1.types import DatabaseDialect + + client = _Client() + api = client.database_admin_api = self._make_database_admin_api() + api.create_database.return_value = None + instance = _Instance(self.INSTANCE_NAME, client=client) + pool = _Pool() + database = self._make_one( + self.DATABASE_ID, + instance, + pool=pool, + database_dialect=DatabaseDialect.GOOGLE_STANDARD_SQL, + ) + + database.create() + + expected_request = CreateDatabaseRequest( + parent=self.INSTANCE_NAME, + create_statement=f"CREATE DATABASE `{self.DATABASE_ID}`", + extra_statements=[], + encryption_config=None, + database_dialect=DatabaseDialect.GOOGLE_STANDARD_SQL, + ) + api.create_database.assert_called_once_with( + request=expected_request, + metadata=[ + ("google-cloud-resource-prefix", database.name), + ( + "x-goog-spanner-request-id", + f"1.{REQ_RAND_PROCESS_ID}.{database._nth_client_id}.{database._channel_id}.1.1", + ), + ], + ) + + def test_create_standard_postgresql(self): + from google.cloud.spanner_admin_database_v1 import CreateDatabaseRequest + from google.cloud.spanner_admin_database_v1.types import DatabaseDialect + + client = _Client() + api = client.database_admin_api = self._make_database_admin_api() + api.create_database.return_value = None + instance = _Instance(self.INSTANCE_NAME, client=client) + pool = _Pool() + database = self._make_one( + "standard_db", + instance, + pool=pool, + database_dialect=DatabaseDialect.POSTGRESQL, + ) + + database.create() + + expected_request = CreateDatabaseRequest( + parent=self.INSTANCE_NAME, + create_statement='CREATE DATABASE "standard_db"', + extra_statements=[], + encryption_config=None, + database_dialect=DatabaseDialect.POSTGRESQL, + ) + api.create_database.assert_called_once_with( + request=expected_request, + metadata=[ + ("google-cloud-resource-prefix", database.name), + ( + "x-goog-spanner-request-id", + f"1.{REQ_RAND_PROCESS_ID}.{database._nth_client_id}.{database._channel_id}.1.1", + ), + ], + ) + def test_exists_grpc_error(self): from google.api_core.exceptions import Unknown