From b13ba9b7f19f8fa4629daec9d41ccd13a484ef99 Mon Sep 17 00:00:00 2001 From: Sri Harsha CH Date: Mon, 31 Jul 2023 13:03:14 +0000 Subject: [PATCH 01/15] feat: support sequences in sqlalchemy spanner --- .../cloud/sqlalchemy_spanner/requirements.py | 2 +- .../sqlalchemy_spanner/sqlalchemy_spanner.py | 98 +++++++++++++- test/test_suite_13.py | 127 +++++++++++++++++- test/test_suite_14.py | 123 ++++++++++++++++- test/test_suite_20.py | 127 +++++++++++++++++- 5 files changed, 472 insertions(+), 5 deletions(-) diff --git a/google/cloud/sqlalchemy_spanner/requirements.py b/google/cloud/sqlalchemy_spanner/requirements.py index 791a6b10..a6e6dc00 100644 --- a/google/cloud/sqlalchemy_spanner/requirements.py +++ b/google/cloud/sqlalchemy_spanner/requirements.py @@ -81,7 +81,7 @@ def isolation_level(self): @property def sequences(self): - return exclusions.closed() + return exclusions.open() @property def temporary_tables(self): diff --git a/google/cloud/sqlalchemy_spanner/sqlalchemy_spanner.py b/google/cloud/sqlalchemy_spanner/sqlalchemy_spanner.py index 9fe09140..0a3a7929 100644 --- a/google/cloud/sqlalchemy_spanner/sqlalchemy_spanner.py +++ b/google/cloud/sqlalchemy_spanner/sqlalchemy_spanner.py @@ -40,6 +40,7 @@ ) from sqlalchemy.sql.default_comparator import operator_lookup from sqlalchemy.sql.operators import json_getitem_op +from sqlalchemy.sql import expression from google.cloud.spanner_v1.data_types import JsonObject from google.cloud import spanner_dbapi @@ -173,6 +174,15 @@ def pre_exec(self): if priority is not None: self._dbapi_connection.connection.request_priority = priority + def fire_sequence(self, seq, type_): + return self._execute_scalar( + ( + "SELECT GET_NEXT_SEQUENCE_VALUE(SEQUENCE %s)" + % self.identifier_preparer.format_sequence(seq) + ), + type_, + ) + class SpannerIdentifierPreparer(IdentifierPreparer): """Identifiers compiler. @@ -343,6 +353,19 @@ def limit_clause(self, select, **kw): text += " OFFSET " + self.process(select._offset_clause, **kw) return text + def returning_clause(self, stmt, returning_cols, **kw): + columns = [ + self._label_select_column(None, c, True, False, {}) + for c in expression._select_iterables(returning_cols) + ] + + return "THEN RETURN " + ", ".join(columns) + + def visit_sequence(self, seq, **kw): + return " GET_NEXT_SEQUENCE_VALUE(SEQUENCE %s)" % self.preparer.format_sequence( + seq + ) + class SpannerDDLCompiler(DDLCompiler): """Spanner DDL statements compiler.""" @@ -457,6 +480,22 @@ def post_create_table(self, table): return post_cmds + def get_identity_options(self, identity_options): + text = ["sequence_kind = 'bit_reversed_positive'"] + if identity_options.start is not None: + text.append("start_with_counter = %d" % identity_options.start) + return ", ".join(text) + + def visit_create_sequence(self, create, prefix=None, **kw): + text = "CREATE SEQUENCE %s" % self.preparer.format_sequence(create.element) + options = self.get_identity_options(create.element) + if options: + text += " OPTIONS (" + options + ")" + return text + + def visit_drop_sequence(self, drop, **kw): + return "DROP SEQUENCE %s" % self.preparer.format_sequence(drop.element) + class SpannerTypeCompiler(GenericTypeCompiler): """Spanner types compiler. @@ -531,7 +570,8 @@ class SpannerDialect(DefaultDialect): supports_sane_rowcount = False supports_sane_multi_rowcount = False supports_default_values = False - supports_sequences = False + supports_sequences = True + sequences_optional = False supports_native_enum = True supports_native_boolean = True supports_native_decimal = True @@ -694,6 +734,36 @@ def get_view_names(self, connection, schema=None, **kw): return all_views + @engine_to_connection + def get_sequence_names(self, connection, schema=None, **kw): + """ + Return a list of all sequence names available in the database. + + The method is used by SQLAlchemy introspection systems. + + Args: + connection (sqlalchemy.engine.base.Connection): + SQLAlchemy connection or engine object. + schema (str): Optional. Schema name + + Returns: + list: List of sequence names. + """ + sql = """ + SELECT name + FROM information_schema.sequences + WHERE SCHEMA='{}' + """.format( + schema or "" + ) + all_sequences = [] + with connection.connection.database.snapshot() as snap: + rows = list(snap.execute_sql(sql)) + for seq in rows: + all_sequences.append(seq[0]) + + return all_sequences + @engine_to_connection def get_view_definition(self, connection, view_name, schema=None, **kw): """ @@ -1294,6 +1364,32 @@ def has_table(self, connection, table_name, schema=None, **kw): return False + @engine_to_connection + def has_sequence(self, connection, sequence_name, schema=None, **kw): + """Check the existence of a particular sequence in the database. + + Given a :class:`_engine.Connection` object and a string + `sequence_name`, return True if the given sequence exists in + the database, False otherwise. + """ + + with connection.connection.database.snapshot() as snap: + rows = snap.execute_sql( + """ +SELECT true +FROM INFORMATION_SCHEMA.SEQUENCES +WHERE NAME="{sequence_name}" +LIMIT 1 +""".format( + sequence_name=sequence_name + ) + ) + + for _ in rows: + return True + + return False + def set_isolation_level(self, conn_proxy, level): """Set the connection isolation level. diff --git a/test/test_suite_13.py b/test/test_suite_13.py index 0561de5d..425257a8 100644 --- a/test/test_suite_13.py +++ b/test/test_suite_13.py @@ -37,6 +37,7 @@ from sqlalchemy.testing import config from sqlalchemy.testing import engines from sqlalchemy.testing import eq_ +from sqlalchemy.testing import is_instance_of from sqlalchemy.testing import provide_metadata, emits_warning from sqlalchemy.testing import fixtures from sqlalchemy.testing import is_true @@ -73,7 +74,12 @@ from sqlalchemy.testing.suite.test_reflection import * # noqa: F401, F403 from sqlalchemy.testing.suite.test_results import * # noqa: F401, F403 from sqlalchemy.testing.suite.test_select import * # noqa: F401, F403 -from sqlalchemy.testing.suite.test_sequence import * # noqa: F401, F403 +from sqlalchemy.testing.suite.test_sequence import ( + SequenceTest as _SequenceTest, + HasSequenceTest as _HasSequenceTest, + HasSequenceTestEmpty, + SequenceCompilerTest, +) # noqa: F401, F403 from sqlalchemy.testing.suite.test_update_delete import * # noqa: F401, F403 from sqlalchemy.testing.suite.test_cte import CTETest as _CTETest @@ -2059,3 +2065,122 @@ def test_create_engine_wo_database(self): engine = create_engine(get_db_url().split("/database")[0]) with engine.connect() as connection: assert connection.connection.database is None + + +class SequenceTest(_SequenceTest): + @classmethod + def define_tables(cls, metadata): + Table( + "seq_pk", + metadata, + Column( + "id", + Integer, + sqlalchemy.Sequence("tab_id_seq"), + primary_key=True, + ), + Column("data", String(50)), + ) + + Table( + "seq_opt_pk", + metadata, + Column( + "id", + Integer, + sqlalchemy.Sequence("tab_id_seq_opt", data_type=Integer, optional=True), + primary_key=True, + ), + Column("data", String(50)), + ) + + Table( + "seq_no_returning", + metadata, + Column( + "id", + Integer, + sqlalchemy.Sequence("noret_id_seq"), + primary_key=True, + ), + Column("data", String(50)), + implicit_returning=False, + ) + + def test_insert_roundtrip(self, connection): + connection.execute(self.tables.seq_pk.insert(), dict(data="some data")) + self._assert_round_trip(self.tables.seq_pk, connection) + + def test_insert_lastrowid(self, connection): + r = connection.execute(self.tables.seq_pk.insert(), dict(data="some data")) + assert len(r.inserted_primary_key) == 1 + is_instance_of(r.inserted_primary_key[0], int) + + def test_nextval_direct(self, connection): + r = connection.execute(self.tables.seq_pk.c.id.default) + is_instance_of(r, int) + + def _assert_round_trip(self, table, conn): + row = conn.execute(table.select()).first() + id, name = row + is_instance_of(id, int) + eq_(name, "some data") + + @testing.combinations((True,), (False,), argnames="implicit_returning") + @testing.requires.schemas + @pytest.mark.skip("Not supported by Cloud Spanner") + def test_insert_roundtrip_translate(self, connection, implicit_returning): + pass + + @testing.requires.schemas + @pytest.mark.skip("Not supported by Cloud Spanner") + def test_nextval_direct_schema_translate(self, connection): + pass + + +class HasSequenceTest(_HasSequenceTest): + @classmethod + def define_tables(cls, metadata): + sqlalchemy.Sequence("user_id_seq", metadata=metadata) + sqlalchemy.Sequence( + "other_seq", metadata=metadata, nomaxvalue=True, nominvalue=True + ) + Table( + "user_id_table", + metadata, + Column("id", Integer, primary_key=True), + ) + + @pytest.mark.skip("Not supported by Cloud Spanner") + def test_has_sequence_cache(self, connection, metadata): + pass + + @testing.requires.schemas + @pytest.mark.skip("Not supported by Cloud Spanner") + def test_has_sequence_schema(self, connection): + pass + + @testing.requires.schemas + @pytest.mark.skip("Not supported by Cloud Spanner") + def test_has_sequence_schemas_neg(self, connection): + pass + + @testing.requires.schemas + @pytest.mark.skip("Not supported by Cloud Spanner") + def test_has_sequence_default_not_in_remote(self, connection): + pass + + @testing.requires.schemas + @pytest.mark.skip("Not supported by Cloud Spanner") + def test_has_sequence_remote_not_in_default(self, connection): + pass + + @testing.requires.schemas + @pytest.mark.skip("Not supported by Cloud Spanner") + def test_get_sequence_names_no_sequence_schema(self, connection): + pass + + @testing.requires.schemas + @pytest.mark.skip("Not supported by Cloud Spanner") + def test_get_sequence_names_sequences_schema(self, connection): + pass diff --git a/test/test_suite_14.py b/test/test_suite_14.py index 3ff069b2..125a0c99 100644 --- a/test/test_suite_14.py +++ b/test/test_suite_14.py @@ -37,6 +37,7 @@ from sqlalchemy.testing import config from sqlalchemy.testing import engines from sqlalchemy.testing import eq_ +from sqlalchemy.testing import is_instance_of from sqlalchemy.testing import provide_metadata, emits_warning from sqlalchemy.testing import fixtures from sqlalchemy.testing.provision import temp_table_keyword_args @@ -76,7 +77,12 @@ from sqlalchemy.testing.suite.test_reflection import * # noqa: F401, F403 from sqlalchemy.testing.suite.test_results import * # noqa: F401, F403 from sqlalchemy.testing.suite.test_select import * # noqa: F401, F403 -from sqlalchemy.testing.suite.test_sequence import * # noqa: F401, F403 +from sqlalchemy.testing.suite.test_sequence import ( + SequenceTest as _SequenceTest, + HasSequenceTest as _HasSequenceTest, + HasSequenceTestEmpty, + SequenceCompilerTest, +) # noqa: F401, F403 from sqlalchemy.testing.suite.test_update_delete import * # noqa: F401, F403 from sqlalchemy.testing.suite.test_cte import CTETest as _CTETest from sqlalchemy.testing.suite.test_ddl import TableDDLTest as _TableDDLTest @@ -2392,3 +2398,118 @@ def test_create_engine_wo_database(self): engine = create_engine(get_db_url().split("/database")[0]) with engine.connect() as connection: assert connection.connection.database is None + + +class SequenceTest(_SequenceTest): + @classmethod + def define_tables(cls, metadata): + Table( + "seq_pk", + metadata, + Column( + "id", + Integer, + sqlalchemy.Sequence("tab_id_seq"), + primary_key=True, + ), + Column("data", String(50)), + ) + + Table( + "seq_opt_pk", + metadata, + Column( + "id", + Integer, + sqlalchemy.Sequence("tab_id_seq_opt", data_type=Integer, optional=True), + primary_key=True, + ), + Column("data", String(50)), + ) + + Table( + "seq_no_returning", + metadata, + Column( + "id", + Integer, + sqlalchemy.Sequence("noret_id_seq"), + primary_key=True, + ), + Column("data", String(50)), + implicit_returning=False, + ) + + def test_insert_roundtrip(self, connection): + connection.execute(self.tables.seq_pk.insert(), dict(data="some data")) + self._assert_round_trip(self.tables.seq_pk, connection) + + def test_insert_lastrowid(self, connection): + r = connection.execute(self.tables.seq_pk.insert(), dict(data="some data")) + assert len(r.inserted_primary_key) == 1 + is_instance_of(r.inserted_primary_key[0], int) + + def test_nextval_direct(self, connection): + r = connection.execute(self.tables.seq_pk.c.id.default) + is_instance_of(r, int) + + def _assert_round_trip(self, table, conn): + row = conn.execute(table.select()).first() + id, name = row + is_instance_of(id, int) + eq_(name, "some data") + + @testing.combinations((True,), (False,), argnames="implicit_returning") + @testing.requires.schemas + @pytest.mark.skip("Spanner doesn't support user defined schemas") + def test_insert_roundtrip_translate(self, connection, implicit_returning): + pass + + @testing.requires.schemas + @pytest.mark.skip("Spanner doesn't support user defined schemas") + def test_nextval_direct_schema_translate(self, connection): + pass + + +class HasSequenceTest(_HasSequenceTest): + @classmethod + def define_tables(cls, metadata): + sqlalchemy.Sequence("user_id_seq", metadata=metadata) + sqlalchemy.Sequence( + "other_seq", metadata=metadata, nomaxvalue=True, nominvalue=True + ) + Table( + "user_id_table", + metadata, + Column("id", Integer, primary_key=True), + ) + + @testing.requires.schemas + @pytest.mark.skip("Spanner doesn't support user defined schemas") + def test_has_sequence_schema(self, connection): + pass + + @testing.requires.schemas + @pytest.mark.skip("Spanner doesn't support user defined schemas") + def test_has_sequence_schemas_neg(self, connection): + pass + + @testing.requires.schemas + @pytest.mark.skip("Spanner doesn't support user defined schemas") + def test_has_sequence_default_not_in_remote(self, connection): + pass + + @testing.requires.schemas + @pytest.mark.skip("Spanner doesn't support user defined schemas") + def test_has_sequence_remote_not_in_default(self, connection): + pass + + @testing.requires.schemas + @pytest.mark.skip("Spanner doesn't support user defined schemas") + def test_get_sequence_names_no_sequence_schema(self, connection): + pass + + @testing.requires.schemas + @pytest.mark.skip("Spanner doesn't support user defined schemas") + def test_get_sequence_names_sequences_schema(self, connection): + pass diff --git a/test/test_suite_20.py b/test/test_suite_20.py index b4bf26fa..2675fbb8 100644 --- a/test/test_suite_20.py +++ b/test/test_suite_20.py @@ -39,6 +39,7 @@ from sqlalchemy.testing import config from sqlalchemy.testing import engines from sqlalchemy.testing import eq_ +from sqlalchemy.testing import is_instance_of from sqlalchemy.testing import provide_metadata, emits_warning from sqlalchemy.testing import fixtures from sqlalchemy.testing.provision import temp_table_keyword_args @@ -80,7 +81,12 @@ from sqlalchemy.testing.suite.test_deprecations import * # noqa: F401, F403 from sqlalchemy.testing.suite.test_results import * # noqa: F401, F403 from sqlalchemy.testing.suite.test_select import * # noqa: F401, F403 -from sqlalchemy.testing.suite.test_sequence import * # noqa: F401, F403 +from sqlalchemy.testing.suite.test_sequence import ( + SequenceTest as _SequenceTest, + HasSequenceTest as _HasSequenceTest, + HasSequenceTestEmpty, + SequenceCompilerTest, +) # noqa: F401, F403 from sqlalchemy.testing.suite.test_unicode_ddl import * # noqa: F401, F403 from sqlalchemy.testing.suite.test_update_delete import * # noqa: F401, F403 from sqlalchemy.testing.suite.test_cte import CTETest as _CTETest @@ -3041,3 +3047,122 @@ def test_create_engine_wo_database(self): engine = create_engine(get_db_url().split("/database")[0]) with engine.connect() as connection: assert connection.connection.database is None + + +class SequenceTest(_SequenceTest): + @classmethod + def define_tables(cls, metadata): + Table( + "seq_pk", + metadata, + Column( + "id", + Integer, + sqlalchemy.Sequence("tab_id_seq"), + primary_key=True, + ), + Column("data", String(50)), + ) + + Table( + "seq_opt_pk", + metadata, + Column( + "id", + Integer, + sqlalchemy.Sequence("tab_id_seq_opt", data_type=Integer, optional=True), + primary_key=True, + ), + Column("data", String(50)), + ) + + Table( + "seq_no_returning", + metadata, + Column( + "id", + Integer, + sqlalchemy.Sequence("noret_id_seq"), + primary_key=True, + ), + Column("data", String(50)), + implicit_returning=False, + ) + + def test_insert_roundtrip(self, connection): + connection.execute(self.tables.seq_pk.insert(), dict(data="some data")) + self._assert_round_trip(self.tables.seq_pk, connection) + + def test_insert_lastrowid(self, connection): + r = connection.execute(self.tables.seq_pk.insert(), dict(data="some data")) + assert len(r.inserted_primary_key) == 1 + is_instance_of(r.inserted_primary_key[0], int) + + def test_nextval_direct(self, connection): + r = connection.execute(self.tables.seq_pk.c.id.default) + is_instance_of(r, int) + + def _assert_round_trip(self, table, conn): + row = conn.execute(table.select()).first() + id, name = row + is_instance_of(id, int) + eq_(name, "some data") + + @testing.combinations((True,), (False,), argnames="implicit_returning") + @testing.requires.schemas + @pytest.mark.skip("Not supported by Cloud Spanner") + def test_insert_roundtrip_translate(self, connection, implicit_returning): + pass + + @testing.requires.schemas + @pytest.mark.skip("Not supported by Cloud Spanner") + def test_nextval_direct_schema_translate(self, connection): + pass + + +class HasSequenceTest(_HasSequenceTest): + @classmethod + def define_tables(cls, metadata): + sqlalchemy.Sequence("user_id_seq", metadata=metadata) + sqlalchemy.Sequence( + "other_seq", metadata=metadata, nomaxvalue=True, nominvalue=True + ) + Table( + "user_id_table", + metadata, + Column("id", Integer, primary_key=True), + ) + + @pytest.mark.skip("Not supported by Cloud Spanner") + def test_has_sequence_cache(self, connection, metadata): + pass + + @testing.requires.schemas + @pytest.mark.skip("Not supported by Cloud Spanner") + def test_has_sequence_schema(self, connection): + pass + + @testing.requires.schemas + @pytest.mark.skip("Not supported by Cloud Spanner") + def test_has_sequence_schemas_neg(self, connection): + pass + + @testing.requires.schemas + @pytest.mark.skip("Not supported by Cloud Spanner") + def test_has_sequence_default_not_in_remote(self, connection): + pass + + @testing.requires.schemas + @pytest.mark.skip("Not supported by Cloud Spanner") + def test_has_sequence_remote_not_in_default(self, connection): + pass + + @testing.requires.schemas + @pytest.mark.skip("Not supported by Cloud Spanner") + def test_get_sequence_names_no_sequence_schema(self, connection): + pass + + @testing.requires.schemas + @pytest.mark.skip("Not supported by Cloud Spanner") + def test_get_sequence_names_sequences_schema(self, connection): + pass From 4460f66fc93ab5cfd89a9703ddce8b84b046ea18 Mon Sep 17 00:00:00 2001 From: Sri Harsha CH Date: Tue, 1 Aug 2023 02:19:53 +0000 Subject: [PATCH 02/15] feat: remove unsupported test in 1.3 --- test/test_suite_13.py | 1 - 1 file changed, 1 deletion(-) diff --git a/test/test_suite_13.py b/test/test_suite_13.py index 425257a8..49fa70ee 100644 --- a/test/test_suite_13.py +++ b/test/test_suite_13.py @@ -77,7 +77,6 @@ from sqlalchemy.testing.suite.test_sequence import ( SequenceTest as _SequenceTest, HasSequenceTest as _HasSequenceTest, - HasSequenceTestEmpty, SequenceCompilerTest, ) # noqa: F401, F403 from sqlalchemy.testing.suite.test_update_delete import * # noqa: F401, F403 From 02f77197de43e47dc030631bbe51042fcbfe7471 Mon Sep 17 00:00:00 2001 From: Sri Harsha CH Date: Wed, 2 Aug 2023 08:43:22 +0000 Subject: [PATCH 03/15] feat: dummy commit for running build --- test/test_suite_13.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/test_suite_13.py b/test/test_suite_13.py index 49fa70ee..49b30c12 100644 --- a/test/test_suite_13.py +++ b/test/test_suite_13.py @@ -78,6 +78,7 @@ SequenceTest as _SequenceTest, HasSequenceTest as _HasSequenceTest, SequenceCompilerTest, + HasSequenceTestEmpty, ) # noqa: F401, F403 from sqlalchemy.testing.suite.test_update_delete import * # noqa: F401, F403 From bad117b233aef6e0b3ac850c62c60992711073b0 Mon Sep 17 00:00:00 2001 From: Sri Harsha CH Date: Wed, 2 Aug 2023 10:47:08 +0000 Subject: [PATCH 04/15] feat: skip emulator tests --- test/test_suite_14.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/test/test_suite_14.py b/test/test_suite_14.py index 125a0c99..473de2e6 100644 --- a/test/test_suite_14.py +++ b/test/test_suite_14.py @@ -80,7 +80,7 @@ from sqlalchemy.testing.suite.test_sequence import ( SequenceTest as _SequenceTest, HasSequenceTest as _HasSequenceTest, - HasSequenceTestEmpty, + HasSequenceTestEmpty as _HasSequenceTestEmpty, SequenceCompilerTest, ) # noqa: F401, F403 from sqlalchemy.testing.suite.test_update_delete import * # noqa: F401, F403 @@ -2400,6 +2400,9 @@ def test_create_engine_wo_database(self): assert connection.connection.database is None +@pytest.mark.skipif( + bool(os.environ.get("SPANNER_EMULATOR_HOST")), reason="Skipped on emulator" +) class SequenceTest(_SequenceTest): @classmethod def define_tables(cls, metadata): @@ -2471,6 +2474,9 @@ def test_nextval_direct_schema_translate(self, connection): pass +@pytest.mark.skipif( + bool(os.environ.get("SPANNER_EMULATOR_HOST")), reason="Skipped on emulator" +) class HasSequenceTest(_HasSequenceTest): @classmethod def define_tables(cls, metadata): @@ -2513,3 +2519,11 @@ def test_get_sequence_names_no_sequence_schema(self, connection): @pytest.mark.skip("Spanner doesn't support user defined schemas") def test_get_sequence_names_sequences_schema(self, connection): pass + + +@pytest.mark.skipif( + bool(os.environ.get("SPANNER_EMULATOR_HOST")), reason="Skipped on emulator" +) +class HasSequenceTestEmpty(_HasSequenceTestEmpty): + def test_get_sequence_names_no_sequence(self, connection): + super().test_get_sequence_names_no_sequence(connection) From f2824c6783b6facd65b0bd3d092370bcc0ce6fe8 Mon Sep 17 00:00:00 2001 From: Sri Harsha CH Date: Wed, 2 Aug 2023 10:51:25 +0000 Subject: [PATCH 05/15] feat: skip emulator tests for sequences --- test/test_suite_13.py | 9 ++++++--- test/test_suite_20.py | 17 ++++++++++++++--- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/test/test_suite_13.py b/test/test_suite_13.py index 49b30c12..a17d6a70 100644 --- a/test/test_suite_13.py +++ b/test/test_suite_13.py @@ -78,7 +78,6 @@ SequenceTest as _SequenceTest, HasSequenceTest as _HasSequenceTest, SequenceCompilerTest, - HasSequenceTestEmpty, ) # noqa: F401, F403 from sqlalchemy.testing.suite.test_update_delete import * # noqa: F401, F403 @@ -2066,7 +2065,9 @@ def test_create_engine_wo_database(self): with engine.connect() as connection: assert connection.connection.database is None - +@pytest.mark.skipif( + bool(os.environ.get("SPANNER_EMULATOR_HOST")), reason="Skipped on emulator" +) class SequenceTest(_SequenceTest): @classmethod def define_tables(cls, metadata): @@ -2137,7 +2138,9 @@ def test_insert_roundtrip_translate(self, connection, implicit_returning): def test_nextval_direct_schema_translate(self, connection): pass - +@pytest.mark.skipif( + bool(os.environ.get("SPANNER_EMULATOR_HOST")), reason="Skipped on emulator" +) class HasSequenceTest(_HasSequenceTest): @classmethod def define_tables(cls, metadata): diff --git a/test/test_suite_20.py b/test/test_suite_20.py index 2675fbb8..daee89f2 100644 --- a/test/test_suite_20.py +++ b/test/test_suite_20.py @@ -84,7 +84,7 @@ from sqlalchemy.testing.suite.test_sequence import ( SequenceTest as _SequenceTest, HasSequenceTest as _HasSequenceTest, - HasSequenceTestEmpty, + HasSequenceTestEmpty as _HasSequenceTestEmpty, SequenceCompilerTest, ) # noqa: F401, F403 from sqlalchemy.testing.suite.test_unicode_ddl import * # noqa: F401, F403 @@ -3048,7 +3048,9 @@ def test_create_engine_wo_database(self): with engine.connect() as connection: assert connection.connection.database is None - +@pytest.mark.skipif( + bool(os.environ.get("SPANNER_EMULATOR_HOST")), reason="Skipped on emulator" +) class SequenceTest(_SequenceTest): @classmethod def define_tables(cls, metadata): @@ -3119,7 +3121,9 @@ def test_insert_roundtrip_translate(self, connection, implicit_returning): def test_nextval_direct_schema_translate(self, connection): pass - +@pytest.mark.skipif( + bool(os.environ.get("SPANNER_EMULATOR_HOST")), reason="Skipped on emulator" +) class HasSequenceTest(_HasSequenceTest): @classmethod def define_tables(cls, metadata): @@ -3166,3 +3170,10 @@ def test_get_sequence_names_no_sequence_schema(self, connection): @pytest.mark.skip("Not supported by Cloud Spanner") def test_get_sequence_names_sequences_schema(self, connection): pass + +@pytest.mark.skipif( + bool(os.environ.get("SPANNER_EMULATOR_HOST")), reason="Skipped on emulator" +) +class HasSequenceTestEmpty(_HasSequenceTestEmpty): + def test_get_sequence_names_no_sequence(self, connection): + super().test_get_sequence_names_no_sequence(connection) From 855f1c0b8d66f7baf9ed1ab66f67f8175d7f9770 Mon Sep 17 00:00:00 2001 From: Sri Harsha CH Date: Wed, 2 Aug 2023 10:54:34 +0000 Subject: [PATCH 06/15] feat: fix lint --- test/test_suite_20.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/test_suite_20.py b/test/test_suite_20.py index daee89f2..24fe643e 100644 --- a/test/test_suite_20.py +++ b/test/test_suite_20.py @@ -3048,6 +3048,7 @@ def test_create_engine_wo_database(self): with engine.connect() as connection: assert connection.connection.database is None + @pytest.mark.skipif( bool(os.environ.get("SPANNER_EMULATOR_HOST")), reason="Skipped on emulator" ) @@ -3121,6 +3122,7 @@ def test_insert_roundtrip_translate(self, connection, implicit_returning): def test_nextval_direct_schema_translate(self, connection): pass + @pytest.mark.skipif( bool(os.environ.get("SPANNER_EMULATOR_HOST")), reason="Skipped on emulator" ) @@ -3171,6 +3173,7 @@ def test_get_sequence_names_no_sequence_schema(self, connection): def test_get_sequence_names_sequences_schema(self, connection): pass + @pytest.mark.skipif( bool(os.environ.get("SPANNER_EMULATOR_HOST")), reason="Skipped on emulator" ) From b0935886267326a0326c3c88ce95dc93c0d3d27c Mon Sep 17 00:00:00 2001 From: Sri Harsha CH Date: Wed, 2 Aug 2023 10:57:00 +0000 Subject: [PATCH 07/15] feat: fix lint --- test/test_suite_13.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/test_suite_13.py b/test/test_suite_13.py index a17d6a70..0c5f2799 100644 --- a/test/test_suite_13.py +++ b/test/test_suite_13.py @@ -2065,6 +2065,7 @@ def test_create_engine_wo_database(self): with engine.connect() as connection: assert connection.connection.database is None + @pytest.mark.skipif( bool(os.environ.get("SPANNER_EMULATOR_HOST")), reason="Skipped on emulator" ) @@ -2138,6 +2139,7 @@ def test_insert_roundtrip_translate(self, connection, implicit_returning): def test_nextval_direct_schema_translate(self, connection): pass + @pytest.mark.skipif( bool(os.environ.get("SPANNER_EMULATOR_HOST")), reason="Skipped on emulator" ) From b4dcf32333aeac77ef983169dbba0858b9a17df4 Mon Sep 17 00:00:00 2001 From: Sri Harsha CH Date: Wed, 2 Aug 2023 10:59:22 +0000 Subject: [PATCH 08/15] feat: remove unused imports --- test/test_suite_13.py | 1 - test/test_suite_14.py | 1 - test/test_suite_20.py | 1 - 3 files changed, 3 deletions(-) diff --git a/test/test_suite_13.py b/test/test_suite_13.py index 0c5f2799..6f2d4c66 100644 --- a/test/test_suite_13.py +++ b/test/test_suite_13.py @@ -77,7 +77,6 @@ from sqlalchemy.testing.suite.test_sequence import ( SequenceTest as _SequenceTest, HasSequenceTest as _HasSequenceTest, - SequenceCompilerTest, ) # noqa: F401, F403 from sqlalchemy.testing.suite.test_update_delete import * # noqa: F401, F403 diff --git a/test/test_suite_14.py b/test/test_suite_14.py index 473de2e6..8e9cf481 100644 --- a/test/test_suite_14.py +++ b/test/test_suite_14.py @@ -81,7 +81,6 @@ SequenceTest as _SequenceTest, HasSequenceTest as _HasSequenceTest, HasSequenceTestEmpty as _HasSequenceTestEmpty, - SequenceCompilerTest, ) # noqa: F401, F403 from sqlalchemy.testing.suite.test_update_delete import * # noqa: F401, F403 from sqlalchemy.testing.suite.test_cte import CTETest as _CTETest diff --git a/test/test_suite_20.py b/test/test_suite_20.py index 24fe643e..1eaccbe3 100644 --- a/test/test_suite_20.py +++ b/test/test_suite_20.py @@ -85,7 +85,6 @@ SequenceTest as _SequenceTest, HasSequenceTest as _HasSequenceTest, HasSequenceTestEmpty as _HasSequenceTestEmpty, - SequenceCompilerTest, ) # noqa: F401, F403 from sqlalchemy.testing.suite.test_unicode_ddl import * # noqa: F401, F403 from sqlalchemy.testing.suite.test_update_delete import * # noqa: F401, F403 From d41f082c84fb3c71f11844fde8db7675d5f81c5c Mon Sep 17 00:00:00 2001 From: Sri Harsha CH Date: Wed, 2 Aug 2023 11:47:45 +0000 Subject: [PATCH 09/15] feat: remove space --- test/test_suite_14.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_suite_14.py b/test/test_suite_14.py index 8e9cf481..b64667b6 100644 --- a/test/test_suite_14.py +++ b/test/test_suite_14.py @@ -81,7 +81,7 @@ SequenceTest as _SequenceTest, HasSequenceTest as _HasSequenceTest, HasSequenceTestEmpty as _HasSequenceTestEmpty, -) # noqa: F401, F403 +) # noqa: F401, F403 from sqlalchemy.testing.suite.test_update_delete import * # noqa: F401, F403 from sqlalchemy.testing.suite.test_cte import CTETest as _CTETest from sqlalchemy.testing.suite.test_ddl import TableDDLTest as _TableDDLTest From 7ee8a2130665d130feb213edfd568f63fc1670ee Mon Sep 17 00:00:00 2001 From: Sri Harsha CH Date: Wed, 2 Aug 2023 11:52:36 +0000 Subject: [PATCH 10/15] feat: fix lint --- test/test_suite_14.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_suite_14.py b/test/test_suite_14.py index b64667b6..8e9cf481 100644 --- a/test/test_suite_14.py +++ b/test/test_suite_14.py @@ -81,7 +81,7 @@ SequenceTest as _SequenceTest, HasSequenceTest as _HasSequenceTest, HasSequenceTestEmpty as _HasSequenceTestEmpty, -) # noqa: F401, F403 +) # noqa: F401, F403 from sqlalchemy.testing.suite.test_update_delete import * # noqa: F401, F403 from sqlalchemy.testing.suite.test_cte import CTETest as _CTETest from sqlalchemy.testing.suite.test_ddl import TableDDLTest as _TableDDLTest From ddcb01d237683ee7c2643dd64644a1e1f520dec5 Mon Sep 17 00:00:00 2001 From: Sri Harsha CH Date: Wed, 2 Aug 2023 12:49:06 +0000 Subject: [PATCH 11/15] feat: fix lint --- google/cloud/sqlalchemy_spanner/sqlalchemy_spanner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/google/cloud/sqlalchemy_spanner/sqlalchemy_spanner.py b/google/cloud/sqlalchemy_spanner/sqlalchemy_spanner.py index 0a3a7929..b4afb6d7 100644 --- a/google/cloud/sqlalchemy_spanner/sqlalchemy_spanner.py +++ b/google/cloud/sqlalchemy_spanner/sqlalchemy_spanner.py @@ -314,7 +314,7 @@ def render_literal_value(self, value, type_): generate a SQL statement. """ raw = ["\\", "'", '"', "\n", "\t", "\r"] - if type(value) == str and any(single in value for single in raw): + if isinstance(value, str) and any(single in value for single in raw): value = 'r"""{}"""'.format(value) return value else: From 0c1885c44a0eaf3b1a6c85c8e8a4c2730717c660 Mon Sep 17 00:00:00 2001 From: Sri Harsha CH Date: Wed, 2 Aug 2023 12:52:12 +0000 Subject: [PATCH 12/15] fix: lint --- google/cloud/sqlalchemy_spanner/sqlalchemy_spanner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/google/cloud/sqlalchemy_spanner/sqlalchemy_spanner.py b/google/cloud/sqlalchemy_spanner/sqlalchemy_spanner.py index b4afb6d7..cf762438 100644 --- a/google/cloud/sqlalchemy_spanner/sqlalchemy_spanner.py +++ b/google/cloud/sqlalchemy_spanner/sqlalchemy_spanner.py @@ -314,7 +314,7 @@ def render_literal_value(self, value, type_): generate a SQL statement. """ raw = ["\\", "'", '"', "\n", "\t", "\r"] - if isinstance(value, str) and any(single in value for single in raw): + if isinstance(value, str) and any(single in value for single in raw): value = 'r"""{}"""'.format(value) return value else: From f8e9705574e7418981b650481eab5248e7d1c576 Mon Sep 17 00:00:00 2001 From: Sri Harsha CH Date: Wed, 2 Aug 2023 12:53:00 +0000 Subject: [PATCH 13/15] fix: lint --- google/cloud/sqlalchemy_spanner/sqlalchemy_spanner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/google/cloud/sqlalchemy_spanner/sqlalchemy_spanner.py b/google/cloud/sqlalchemy_spanner/sqlalchemy_spanner.py index cf762438..b4afb6d7 100644 --- a/google/cloud/sqlalchemy_spanner/sqlalchemy_spanner.py +++ b/google/cloud/sqlalchemy_spanner/sqlalchemy_spanner.py @@ -314,7 +314,7 @@ def render_literal_value(self, value, type_): generate a SQL statement. """ raw = ["\\", "'", '"', "\n", "\t", "\r"] - if isinstance(value, str) and any(single in value for single in raw): + if isinstance(value, str) and any(single in value for single in raw): value = 'r"""{}"""'.format(value) return value else: From 10bac02368d751379574e7a83a440aa0c0b257ca Mon Sep 17 00:00:00 2001 From: Sri Harsha CH Date: Thu, 3 Aug 2023 07:39:15 +0000 Subject: [PATCH 14/15] feat: remove unchanged tests and lint --- google/cloud/sqlalchemy_spanner/sqlalchemy_spanner.py | 10 +++++----- test/test_suite_13.py | 4 ---- test/test_suite_14.py | 4 ---- test/test_suite_20.py | 4 ---- 4 files changed, 5 insertions(+), 17 deletions(-) diff --git a/google/cloud/sqlalchemy_spanner/sqlalchemy_spanner.py b/google/cloud/sqlalchemy_spanner/sqlalchemy_spanner.py index b4afb6d7..83351320 100644 --- a/google/cloud/sqlalchemy_spanner/sqlalchemy_spanner.py +++ b/google/cloud/sqlalchemy_spanner/sqlalchemy_spanner.py @@ -1376,11 +1376,11 @@ def has_sequence(self, connection, sequence_name, schema=None, **kw): with connection.connection.database.snapshot() as snap: rows = snap.execute_sql( """ -SELECT true -FROM INFORMATION_SCHEMA.SEQUENCES -WHERE NAME="{sequence_name}" -LIMIT 1 -""".format( + SELECT true + FROM INFORMATION_SCHEMA.SEQUENCES + WHERE NAME="{sequence_name}" + LIMIT 1 + """.format( sequence_name=sequence_name ) ) diff --git a/test/test_suite_13.py b/test/test_suite_13.py index 6f2d4c66..ca11979b 100644 --- a/test/test_suite_13.py +++ b/test/test_suite_13.py @@ -2108,10 +2108,6 @@ def define_tables(cls, metadata): implicit_returning=False, ) - def test_insert_roundtrip(self, connection): - connection.execute(self.tables.seq_pk.insert(), dict(data="some data")) - self._assert_round_trip(self.tables.seq_pk, connection) - def test_insert_lastrowid(self, connection): r = connection.execute(self.tables.seq_pk.insert(), dict(data="some data")) assert len(r.inserted_primary_key) == 1 diff --git a/test/test_suite_14.py b/test/test_suite_14.py index 8e9cf481..87437b83 100644 --- a/test/test_suite_14.py +++ b/test/test_suite_14.py @@ -2442,10 +2442,6 @@ def define_tables(cls, metadata): implicit_returning=False, ) - def test_insert_roundtrip(self, connection): - connection.execute(self.tables.seq_pk.insert(), dict(data="some data")) - self._assert_round_trip(self.tables.seq_pk, connection) - def test_insert_lastrowid(self, connection): r = connection.execute(self.tables.seq_pk.insert(), dict(data="some data")) assert len(r.inserted_primary_key) == 1 diff --git a/test/test_suite_20.py b/test/test_suite_20.py index 1eaccbe3..317e591a 100644 --- a/test/test_suite_20.py +++ b/test/test_suite_20.py @@ -3091,10 +3091,6 @@ def define_tables(cls, metadata): implicit_returning=False, ) - def test_insert_roundtrip(self, connection): - connection.execute(self.tables.seq_pk.insert(), dict(data="some data")) - self._assert_round_trip(self.tables.seq_pk, connection) - def test_insert_lastrowid(self, connection): r = connection.execute(self.tables.seq_pk.insert(), dict(data="some data")) assert len(r.inserted_primary_key) == 1 From 73d68d6e38cb2c05c7a81889f283253c25c712af Mon Sep 17 00:00:00 2001 From: Sri Harsha CH Date: Thu, 3 Aug 2023 11:11:06 +0000 Subject: [PATCH 15/15] docs: add comments --- google/cloud/sqlalchemy_spanner/sqlalchemy_spanner.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/google/cloud/sqlalchemy_spanner/sqlalchemy_spanner.py b/google/cloud/sqlalchemy_spanner/sqlalchemy_spanner.py index 83351320..7f0b44a9 100644 --- a/google/cloud/sqlalchemy_spanner/sqlalchemy_spanner.py +++ b/google/cloud/sqlalchemy_spanner/sqlalchemy_spanner.py @@ -175,6 +175,7 @@ def pre_exec(self): self._dbapi_connection.connection.request_priority = priority def fire_sequence(self, seq, type_): + """Builds a statement for fetching next value of the sequence.""" return self._execute_scalar( ( "SELECT GET_NEXT_SEQUENCE_VALUE(SEQUENCE %s)" @@ -362,6 +363,7 @@ def returning_clause(self, stmt, returning_cols, **kw): return "THEN RETURN " + ", ".join(columns) def visit_sequence(self, seq, **kw): + """Builds a statement for fetching next value of the sequence.""" return " GET_NEXT_SEQUENCE_VALUE(SEQUENCE %s)" % self.preparer.format_sequence( seq ) @@ -487,6 +489,7 @@ def get_identity_options(self, identity_options): return ", ".join(text) def visit_create_sequence(self, create, prefix=None, **kw): + """Builds a ``CREATE SEQUENCE`` statement for the sequence.""" text = "CREATE SEQUENCE %s" % self.preparer.format_sequence(create.element) options = self.get_identity_options(create.element) if options: @@ -494,6 +497,7 @@ def visit_create_sequence(self, create, prefix=None, **kw): return text def visit_drop_sequence(self, drop, **kw): + """Builds a ``DROP SEQUENCE`` statement for the sequence.""" return "DROP SEQUENCE %s" % self.preparer.format_sequence(drop.element)