sqlalchemy-spanner 1.6.2__tar.gz → 1.7.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (23) hide show
  1. {sqlalchemy-spanner-1.6.2 → sqlalchemy_spanner-1.7.0}/PKG-INFO +8 -2
  2. {sqlalchemy-spanner-1.6.2 → sqlalchemy_spanner-1.7.0}/google/cloud/sqlalchemy_spanner/_opentelemetry_tracing.py +13 -0
  3. {sqlalchemy-spanner-1.6.2 → sqlalchemy_spanner-1.7.0}/google/cloud/sqlalchemy_spanner/requirements.py +1 -1
  4. {sqlalchemy-spanner-1.6.2 → sqlalchemy_spanner-1.7.0}/google/cloud/sqlalchemy_spanner/sqlalchemy_spanner.py +139 -19
  5. {sqlalchemy-spanner-1.6.2 → sqlalchemy_spanner-1.7.0}/sqlalchemy_spanner.egg-info/PKG-INFO +8 -2
  6. {sqlalchemy-spanner-1.6.2 → sqlalchemy_spanner-1.7.0}/test/test_suite_13.py +126 -1
  7. {sqlalchemy-spanner-1.6.2 → sqlalchemy_spanner-1.7.0}/test/test_suite_14.py +131 -1
  8. {sqlalchemy-spanner-1.6.2 → sqlalchemy_spanner-1.7.0}/test/test_suite_20.py +195 -12
  9. {sqlalchemy-spanner-1.6.2 → sqlalchemy_spanner-1.7.0}/LICENSE +0 -0
  10. {sqlalchemy-spanner-1.6.2 → sqlalchemy_spanner-1.7.0}/README.rst +0 -0
  11. {sqlalchemy-spanner-1.6.2 → sqlalchemy_spanner-1.7.0}/google/__init__.py +0 -0
  12. {sqlalchemy-spanner-1.6.2 → sqlalchemy_spanner-1.7.0}/google/cloud/__init__.py +0 -0
  13. {sqlalchemy-spanner-1.6.2 → sqlalchemy_spanner-1.7.0}/google/cloud/sqlalchemy_spanner/__init__.py +0 -0
  14. {sqlalchemy-spanner-1.6.2 → sqlalchemy_spanner-1.7.0}/google/cloud/sqlalchemy_spanner/provision.py +0 -0
  15. {sqlalchemy-spanner-1.6.2 → sqlalchemy_spanner-1.7.0}/setup.cfg +0 -0
  16. {sqlalchemy-spanner-1.6.2 → sqlalchemy_spanner-1.7.0}/setup.py +0 -0
  17. {sqlalchemy-spanner-1.6.2 → sqlalchemy_spanner-1.7.0}/sqlalchemy_spanner.egg-info/SOURCES.txt +0 -0
  18. {sqlalchemy-spanner-1.6.2 → sqlalchemy_spanner-1.7.0}/sqlalchemy_spanner.egg-info/dependency_links.txt +0 -0
  19. {sqlalchemy-spanner-1.6.2 → sqlalchemy_spanner-1.7.0}/sqlalchemy_spanner.egg-info/entry_points.txt +0 -0
  20. {sqlalchemy-spanner-1.6.2 → sqlalchemy_spanner-1.7.0}/sqlalchemy_spanner.egg-info/namespace_packages.txt +0 -0
  21. {sqlalchemy-spanner-1.6.2 → sqlalchemy_spanner-1.7.0}/sqlalchemy_spanner.egg-info/not-zip-safe +0 -0
  22. {sqlalchemy-spanner-1.6.2 → sqlalchemy_spanner-1.7.0}/sqlalchemy_spanner.egg-info/requires.txt +0 -0
  23. {sqlalchemy-spanner-1.6.2 → sqlalchemy_spanner-1.7.0}/sqlalchemy_spanner.egg-info/top_level.txt +0 -0
@@ -1,13 +1,19 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sqlalchemy-spanner
3
- Version: 1.6.2
3
+ Version: 1.7.0
4
4
  Summary: SQLAlchemy dialect integrated into Cloud Spanner database
5
5
  Home-page: https://github.com/cloudspannerecosystem/python-spanner-sqlalchemy
6
6
  Author: Google LLC
7
7
  Author-email: cloud-spanner-developers@googlegroups.com
8
8
  Classifier: Intended Audience :: Developers
9
- Provides-Extra: tracing
10
9
  License-File: LICENSE
10
+ Requires-Dist: sqlalchemy>=1.1.13
11
+ Requires-Dist: google-cloud-spanner>=3.12.0
12
+ Requires-Dist: alembic
13
+ Provides-Extra: tracing
14
+ Requires-Dist: opentelemetry-api>=1.1.0; extra == "tracing"
15
+ Requires-Dist: opentelemetry-sdk>=1.1.0; extra == "tracing"
16
+ Requires-Dist: opentelemetry-instrumentation>=0.20b0; extra == "tracing"
11
17
 
12
18
  Spanner dialect for SQLAlchemy
13
19
  ==============================
@@ -14,6 +14,9 @@
14
14
 
15
15
  """Manages OpenTelemetry trace creation and handling"""
16
16
 
17
+ import collections
18
+ import os
19
+
17
20
  from contextlib import contextmanager
18
21
 
19
22
  from google.api_core.exceptions import GoogleAPICallError
@@ -46,6 +49,16 @@ def trace_call(name, extra_attributes=None):
46
49
  }
47
50
 
48
51
  if extra_attributes:
52
+ if os.environ.get("SQLALCHEMY_SPANNER_TRACE_HIDE_QUERY_PARAMETERS"):
53
+ extra_attributes.pop("db.params", None)
54
+
55
+ # Stringify "db.params" sequence values before sending to OpenTelemetry,
56
+ # otherwise OpenTelemetry may log a Warning if types differ.
57
+ if isinstance(extra_attributes, dict):
58
+ for k, v in extra_attributes.items():
59
+ if k == "db.params" and isinstance(v, collections.abc.Sequence):
60
+ extra_attributes[k] = [str(e) for e in v]
61
+
49
62
  attributes.update(extra_attributes)
50
63
 
51
64
  with tracer.start_as_current_span(
@@ -81,7 +81,7 @@ class Requirements(SuiteRequirements): # pragma: no cover
81
81
 
82
82
  @property
83
83
  def sequences(self):
84
- return exclusions.closed()
84
+ return exclusions.open()
85
85
 
86
86
  @property
87
87
  def temporary_tables(self):
@@ -23,6 +23,7 @@ from alembic.ddl.base import (
23
23
  format_type,
24
24
  )
25
25
  from sqlalchemy.exc import NoSuchTableError
26
+ from sqlalchemy.sql import elements
26
27
  from sqlalchemy import ForeignKeyConstraint, types
27
28
  from sqlalchemy.engine.base import Engine
28
29
  from sqlalchemy.engine.default import DefaultDialect, DefaultExecutionContext
@@ -40,6 +41,7 @@ from sqlalchemy.sql.compiler import (
40
41
  )
41
42
  from sqlalchemy.sql.default_comparator import operator_lookup
42
43
  from sqlalchemy.sql.operators import json_getitem_op
44
+ from sqlalchemy.sql import expression
43
45
 
44
46
  from google.cloud.spanner_v1.data_types import JsonObject
45
47
  from google.cloud import spanner_dbapi
@@ -173,6 +175,16 @@ class SpannerExecutionContext(DefaultExecutionContext):
173
175
  if priority is not None:
174
176
  self._dbapi_connection.connection.request_priority = priority
175
177
 
178
+ def fire_sequence(self, seq, type_):
179
+ """Builds a statement for fetching next value of the sequence."""
180
+ return self._execute_scalar(
181
+ (
182
+ "SELECT GET_NEXT_SEQUENCE_VALUE(SEQUENCE %s)"
183
+ % self.identifier_preparer.format_sequence(seq)
184
+ ),
185
+ type_,
186
+ )
187
+
176
188
 
177
189
  class SpannerIdentifierPreparer(IdentifierPreparer):
178
190
  """Identifiers compiler.
@@ -303,8 +315,14 @@ class SpannerSQLCompiler(SQLCompiler):
303
315
  in string. Override the method to add additional escape before using it to
304
316
  generate a SQL statement.
305
317
  """
318
+ if value is None and not type_.should_evaluate_none:
319
+ # issue #10535 - handle NULL in the compiler without placing
320
+ # this onto each type, except for "evaluate None" types
321
+ # (e.g. JSON)
322
+ return self.process(elements.Null._instance())
323
+
306
324
  raw = ["\\", "'", '"', "\n", "\t", "\r"]
307
- if type(value) == str and any(single in value for single in raw):
325
+ if isinstance(value, str) and any(single in value for single in raw):
308
326
  value = 'r"""{}"""'.format(value)
309
327
  return value
310
328
  else:
@@ -343,6 +361,20 @@ class SpannerSQLCompiler(SQLCompiler):
343
361
  text += " OFFSET " + self.process(select._offset_clause, **kw)
344
362
  return text
345
363
 
364
+ def returning_clause(self, stmt, returning_cols, **kw):
365
+ columns = [
366
+ self._label_select_column(None, c, True, False, {})
367
+ for c in expression._select_iterables(returning_cols)
368
+ ]
369
+
370
+ return "THEN RETURN " + ", ".join(columns)
371
+
372
+ def visit_sequence(self, seq, **kw):
373
+ """Builds a statement for fetching next value of the sequence."""
374
+ return " GET_NEXT_SEQUENCE_VALUE(SEQUENCE %s)" % self.preparer.format_sequence(
375
+ seq
376
+ )
377
+
346
378
 
347
379
  class SpannerDDLCompiler(DDLCompiler):
348
380
  """Spanner DDL statements compiler."""
@@ -406,7 +438,7 @@ class SpannerDDLCompiler(DDLCompiler):
406
438
  for index in drop_table.element.indexes:
407
439
  indexes += "DROP INDEX {};".format(self.preparer.quote(index.name))
408
440
 
409
- return indexes + constrs + str(drop_table)
441
+ return indexes + constrs + super().visit_drop_table(drop_table)
410
442
 
411
443
  def visit_primary_key_constraint(self, constraint, **kw):
412
444
  """Build primary key definition.
@@ -457,6 +489,24 @@ class SpannerDDLCompiler(DDLCompiler):
457
489
 
458
490
  return post_cmds
459
491
 
492
+ def get_identity_options(self, identity_options):
493
+ text = ["sequence_kind = 'bit_reversed_positive'"]
494
+ if identity_options.start is not None:
495
+ text.append("start_with_counter = %d" % identity_options.start)
496
+ return ", ".join(text)
497
+
498
+ def visit_create_sequence(self, create, prefix=None, **kw):
499
+ """Builds a ``CREATE SEQUENCE`` statement for the sequence."""
500
+ text = "CREATE SEQUENCE %s" % self.preparer.format_sequence(create.element)
501
+ options = self.get_identity_options(create.element)
502
+ if options:
503
+ text += " OPTIONS (" + options + ")"
504
+ return text
505
+
506
+ def visit_drop_sequence(self, drop, **kw):
507
+ """Builds a ``DROP SEQUENCE`` statement for the sequence."""
508
+ return "DROP SEQUENCE %s" % self.preparer.format_sequence(drop.element)
509
+
460
510
 
461
511
  class SpannerTypeCompiler(GenericTypeCompiler):
462
512
  """Spanner types compiler.
@@ -531,7 +581,8 @@ class SpannerDialect(DefaultDialect):
531
581
  supports_sane_rowcount = False
532
582
  supports_sane_multi_rowcount = False
533
583
  supports_default_values = False
534
- supports_sequences = False
584
+ supports_sequences = True
585
+ sequences_optional = False
535
586
  supports_native_enum = True
536
587
  supports_native_boolean = True
537
588
  supports_native_decimal = True
@@ -694,6 +745,36 @@ class SpannerDialect(DefaultDialect):
694
745
 
695
746
  return all_views
696
747
 
748
+ @engine_to_connection
749
+ def get_sequence_names(self, connection, schema=None, **kw):
750
+ """
751
+ Return a list of all sequence names available in the database.
752
+
753
+ The method is used by SQLAlchemy introspection systems.
754
+
755
+ Args:
756
+ connection (sqlalchemy.engine.base.Connection):
757
+ SQLAlchemy connection or engine object.
758
+ schema (str): Optional. Schema name
759
+
760
+ Returns:
761
+ list: List of sequence names.
762
+ """
763
+ sql = """
764
+ SELECT name
765
+ FROM information_schema.sequences
766
+ WHERE SCHEMA='{}'
767
+ """.format(
768
+ schema or ""
769
+ )
770
+ all_sequences = []
771
+ with connection.connection.database.snapshot() as snap:
772
+ rows = list(snap.execute_sql(sql))
773
+ for seq in rows:
774
+ all_sequences.append(seq[0])
775
+
776
+ return all_sequences
777
+
697
778
  @engine_to_connection
698
779
  def get_view_definition(self, connection, view_name, schema=None, **kw):
699
780
  """
@@ -765,7 +846,7 @@ class SpannerDialect(DefaultDialect):
765
846
  col.spanner_type, col.is_nullable, col.generation_expression
766
847
  FROM information_schema.columns as col
767
848
  JOIN information_schema.tables AS t
768
- ON col.table_name = t.table_name
849
+ USING (TABLE_CATALOG, TABLE_SCHEMA, TABLE_NAME)
769
850
  WHERE
770
851
  {table_filter_query}
771
852
  {table_type_query}
@@ -898,9 +979,14 @@ class SpannerDialect(DefaultDialect):
898
979
  ARRAY_AGG(ic.column_ordering)
899
980
  FROM information_schema.indexes as i
900
981
  JOIN information_schema.index_columns AS ic
901
- ON ic.index_name = i.index_name AND ic.table_name = i.table_name
982
+ ON ic.index_name = i.index_name
983
+ AND ic.table_catalog = i.table_catalog
984
+ AND ic.table_schema = i.table_schema
985
+ AND ic.table_name = i.table_name
902
986
  JOIN information_schema.tables AS t
903
- ON i.table_name = t.table_name
987
+ ON i.table_catalog = t.table_catalog
988
+ AND i.table_schema = t.table_schema
989
+ AND i.table_name = t.table_name
904
990
  WHERE
905
991
  {table_filter_query}
906
992
  {table_type_query}
@@ -995,9 +1081,11 @@ class SpannerDialect(DefaultDialect):
995
1081
  SELECT tc.table_schema, tc.table_name, ccu.COLUMN_NAME
996
1082
  FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS AS tc
997
1083
  JOIN INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE AS ccu
998
- ON ccu.CONSTRAINT_NAME = tc.CONSTRAINT_NAME
1084
+ USING (TABLE_CATALOG, TABLE_SCHEMA, CONSTRAINT_NAME)
999
1085
  JOIN information_schema.tables AS t
1000
- ON tc.table_name = t.table_name
1086
+ ON tc.TABLE_CATALOG = t.TABLE_CATALOG
1087
+ AND tc.TABLE_SCHEMA = t.TABLE_SCHEMA
1088
+ AND tc.TABLE_NAME = t.TABLE_NAME
1001
1089
  WHERE {table_filter_query} {table_type_query}
1002
1090
  {schema_filter_query} tc.CONSTRAINT_TYPE = "PRIMARY KEY"
1003
1091
  """.format(
@@ -1115,13 +1203,19 @@ class SpannerDialect(DefaultDialect):
1115
1203
  )
1116
1204
  FROM information_schema.table_constraints AS tc
1117
1205
  JOIN information_schema.constraint_column_usage AS ccu
1118
- ON ccu.constraint_name = tc.constraint_name
1206
+ USING (table_catalog, table_schema, constraint_name)
1119
1207
  JOIN information_schema.constraint_table_usage AS ctu
1120
- ON ctu.constraint_name = tc.constraint_name
1208
+ ON ctu.table_catalog = tc.table_catalog
1209
+ and ctu.table_schema = tc.table_schema
1210
+ and ctu.constraint_name = tc.constraint_name
1121
1211
  JOIN information_schema.key_column_usage AS kcu
1122
- ON kcu.constraint_name = tc.constraint_name
1212
+ ON kcu.table_catalog = tc.table_catalog
1213
+ and kcu.table_schema = tc.table_schema
1214
+ and kcu.constraint_name = tc.constraint_name
1123
1215
  JOIN information_schema.tables AS t
1124
- ON tc.table_name = t.table_name
1216
+ ON t.table_catalog = tc.table_catalog
1217
+ and t.table_schema = tc.table_schema
1218
+ and t.table_name = tc.table_name
1125
1219
  WHERE
1126
1220
  {table_filter_query}
1127
1221
  {table_type_query}
@@ -1242,15 +1336,14 @@ WHERE table_type = 'BASE TABLE' AND table_schema = '{schema}'
1242
1336
  SELECT ccu.CONSTRAINT_NAME, ccu.COLUMN_NAME
1243
1337
  FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS AS tc
1244
1338
  JOIN INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE AS ccu
1245
- ON ccu.CONSTRAINT_NAME = tc.CONSTRAINT_NAME
1246
- LEFT JOIN INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS AS rc
1247
- on tc.CONSTRAINT_NAME = rc.CONSTRAINT_NAME
1339
+ USING (TABLE_CATALOG, TABLE_SCHEMA, CONSTRAINT_NAME)
1248
1340
  WHERE
1249
1341
  tc.TABLE_NAME="{table_name}"
1342
+ AND tc.TABLE_SCHEMA="{table_schema}"
1250
1343
  AND tc.CONSTRAINT_TYPE = "UNIQUE"
1251
- AND rc.CONSTRAINT_NAME IS NOT NULL
1344
+ AND tc.CONSTRAINT_NAME IS NOT NULL
1252
1345
  """.format(
1253
- table_name=table_name
1346
+ table_schema=schema or "", table_name=table_name
1254
1347
  )
1255
1348
 
1256
1349
  cols = []
@@ -1282,10 +1375,37 @@ WHERE
1282
1375
  """
1283
1376
  SELECT true
1284
1377
  FROM INFORMATION_SCHEMA.TABLES
1285
- WHERE TABLE_NAME="{table_name}"
1378
+ WHERE TABLE_SCHEMA="{table_schema}" AND TABLE_NAME="{table_name}"
1286
1379
  LIMIT 1
1287
1380
  """.format(
1288
- table_name=table_name
1381
+ table_schema=schema or "", table_name=table_name
1382
+ )
1383
+ )
1384
+
1385
+ for _ in rows:
1386
+ return True
1387
+
1388
+ return False
1389
+
1390
+ @engine_to_connection
1391
+ def has_sequence(self, connection, sequence_name, schema=None, **kw):
1392
+ """Check the existence of a particular sequence in the database.
1393
+
1394
+ Given a :class:`_engine.Connection` object and a string
1395
+ `sequence_name`, return True if the given sequence exists in
1396
+ the database, False otherwise.
1397
+ """
1398
+
1399
+ with connection.connection.database.snapshot() as snap:
1400
+ rows = snap.execute_sql(
1401
+ """
1402
+ SELECT true
1403
+ FROM INFORMATION_SCHEMA.SEQUENCES
1404
+ WHERE NAME="{sequence_name}"
1405
+ AND SCHEMA="{schema}"
1406
+ LIMIT 1
1407
+ """.format(
1408
+ sequence_name=sequence_name, schema=schema or ""
1289
1409
  )
1290
1410
  )
1291
1411
 
@@ -1,13 +1,19 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sqlalchemy-spanner
3
- Version: 1.6.2
3
+ Version: 1.7.0
4
4
  Summary: SQLAlchemy dialect integrated into Cloud Spanner database
5
5
  Home-page: https://github.com/cloudspannerecosystem/python-spanner-sqlalchemy
6
6
  Author: Google LLC
7
7
  Author-email: cloud-spanner-developers@googlegroups.com
8
8
  Classifier: Intended Audience :: Developers
9
- Provides-Extra: tracing
10
9
  License-File: LICENSE
10
+ Requires-Dist: sqlalchemy>=1.1.13
11
+ Requires-Dist: google-cloud-spanner>=3.12.0
12
+ Requires-Dist: alembic
13
+ Provides-Extra: tracing
14
+ Requires-Dist: opentelemetry-api>=1.1.0; extra == "tracing"
15
+ Requires-Dist: opentelemetry-sdk>=1.1.0; extra == "tracing"
16
+ Requires-Dist: opentelemetry-instrumentation>=0.20b0; extra == "tracing"
11
17
 
12
18
  Spanner dialect for SQLAlchemy
13
19
  ==============================
@@ -37,6 +37,7 @@ from sqlalchemy.schema import Computed
37
37
  from sqlalchemy.testing import config
38
38
  from sqlalchemy.testing import engines
39
39
  from sqlalchemy.testing import eq_
40
+ from sqlalchemy.testing import is_instance_of
40
41
  from sqlalchemy.testing import provide_metadata, emits_warning
41
42
  from sqlalchemy.testing import fixtures
42
43
  from sqlalchemy.testing import is_true
@@ -73,7 +74,10 @@ from sqlalchemy.testing.suite.test_insert import * # noqa: F401, F403
73
74
  from sqlalchemy.testing.suite.test_reflection import * # noqa: F401, F403
74
75
  from sqlalchemy.testing.suite.test_results import * # noqa: F401, F403
75
76
  from sqlalchemy.testing.suite.test_select import * # noqa: F401, F403
76
- from sqlalchemy.testing.suite.test_sequence import * # noqa: F401, F403
77
+ from sqlalchemy.testing.suite.test_sequence import (
78
+ SequenceTest as _SequenceTest,
79
+ HasSequenceTest as _HasSequenceTest,
80
+ ) # noqa: F401, F403
77
81
  from sqlalchemy.testing.suite.test_update_delete import * # noqa: F401, F403
78
82
 
79
83
  from sqlalchemy.testing.suite.test_cte import CTETest as _CTETest
@@ -2059,3 +2063,124 @@ class CreateEngineWithoutDatabaseTest(fixtures.TestBase):
2059
2063
  engine = create_engine(get_db_url().split("/database")[0])
2060
2064
  with engine.connect() as connection:
2061
2065
  assert connection.connection.database is None
2066
+
2067
+
2068
+ @pytest.mark.skipif(
2069
+ bool(os.environ.get("SPANNER_EMULATOR_HOST")), reason="Skipped on emulator"
2070
+ )
2071
+ class SequenceTest(_SequenceTest):
2072
+ @classmethod
2073
+ def define_tables(cls, metadata):
2074
+ Table(
2075
+ "seq_pk",
2076
+ metadata,
2077
+ Column(
2078
+ "id",
2079
+ Integer,
2080
+ sqlalchemy.Sequence("tab_id_seq"),
2081
+ primary_key=True,
2082
+ ),
2083
+ Column("data", String(50)),
2084
+ )
2085
+
2086
+ Table(
2087
+ "seq_opt_pk",
2088
+ metadata,
2089
+ Column(
2090
+ "id",
2091
+ Integer,
2092
+ sqlalchemy.Sequence("tab_id_seq_opt", data_type=Integer, optional=True),
2093
+ primary_key=True,
2094
+ ),
2095
+ Column("data", String(50)),
2096
+ )
2097
+
2098
+ Table(
2099
+ "seq_no_returning",
2100
+ metadata,
2101
+ Column(
2102
+ "id",
2103
+ Integer,
2104
+ sqlalchemy.Sequence("noret_id_seq"),
2105
+ primary_key=True,
2106
+ ),
2107
+ Column("data", String(50)),
2108
+ implicit_returning=False,
2109
+ )
2110
+
2111
+ def test_insert_lastrowid(self, connection):
2112
+ r = connection.execute(self.tables.seq_pk.insert(), dict(data="some data"))
2113
+ assert len(r.inserted_primary_key) == 1
2114
+ is_instance_of(r.inserted_primary_key[0], int)
2115
+
2116
+ def test_nextval_direct(self, connection):
2117
+ r = connection.execute(self.tables.seq_pk.c.id.default)
2118
+ is_instance_of(r, int)
2119
+
2120
+ def _assert_round_trip(self, table, conn):
2121
+ row = conn.execute(table.select()).first()
2122
+ id, name = row
2123
+ is_instance_of(id, int)
2124
+ eq_(name, "some data")
2125
+
2126
+ @testing.combinations((True,), (False,), argnames="implicit_returning")
2127
+ @testing.requires.schemas
2128
+ @pytest.mark.skip("Not supported by Cloud Spanner")
2129
+ def test_insert_roundtrip_translate(self, connection, implicit_returning):
2130
+ pass
2131
+
2132
+ @testing.requires.schemas
2133
+ @pytest.mark.skip("Not supported by Cloud Spanner")
2134
+ def test_nextval_direct_schema_translate(self, connection):
2135
+ pass
2136
+
2137
+
2138
+ @pytest.mark.skipif(
2139
+ bool(os.environ.get("SPANNER_EMULATOR_HOST")), reason="Skipped on emulator"
2140
+ )
2141
+ class HasSequenceTest(_HasSequenceTest):
2142
+ @classmethod
2143
+ def define_tables(cls, metadata):
2144
+ sqlalchemy.Sequence("user_id_seq", metadata=metadata)
2145
+ sqlalchemy.Sequence(
2146
+ "other_seq", metadata=metadata, nomaxvalue=True, nominvalue=True
2147
+ )
2148
+ Table(
2149
+ "user_id_table",
2150
+ metadata,
2151
+ Column("id", Integer, primary_key=True),
2152
+ )
2153
+
2154
+ @pytest.mark.skip("Not supported by Cloud Spanner")
2155
+ def test_has_sequence_cache(self, connection, metadata):
2156
+ pass
2157
+
2158
+ @testing.requires.schemas
2159
+ @pytest.mark.skip("Not supported by Cloud Spanner")
2160
+ def test_has_sequence_schema(self, connection):
2161
+ pass
2162
+
2163
+ @testing.requires.schemas
2164
+ @pytest.mark.skip("Not supported by Cloud Spanner")
2165
+ def test_has_sequence_schemas_neg(self, connection):
2166
+ pass
2167
+
2168
+ @testing.requires.schemas
2169
+ @pytest.mark.skip("Not supported by Cloud Spanner")
2170
+ def test_has_sequence_default_not_in_remote(self, connection):
2171
+ pass
2172
+
2173
+ @testing.requires.schemas
2174
+ @pytest.mark.skip("Not supported by Cloud Spanner")
2175
+ def test_has_sequence_remote_not_in_default(self, connection):
2176
+ pass
2177
+
2178
+ @testing.requires.schemas
2179
+ @pytest.mark.skip("Not supported by Cloud Spanner")
2180
+ def test_get_sequence_names_no_sequence_schema(self, connection):
2181
+ pass
2182
+
2183
+ @testing.requires.schemas
2184
+ @pytest.mark.skip("Not supported by Cloud Spanner")
2185
+ def test_get_sequence_names_sequences_schema(self, connection):
2186
+ pass
@@ -37,6 +37,7 @@ from sqlalchemy.schema import Computed
37
37
  from sqlalchemy.testing import config
38
38
  from sqlalchemy.testing import engines
39
39
  from sqlalchemy.testing import eq_
40
+ from sqlalchemy.testing import is_instance_of
40
41
  from sqlalchemy.testing import provide_metadata, emits_warning
41
42
  from sqlalchemy.testing import fixtures
42
43
  from sqlalchemy.testing.provision import temp_table_keyword_args
@@ -76,7 +77,11 @@ from sqlalchemy.testing.suite.test_insert import * # noqa: F401, F403
76
77
  from sqlalchemy.testing.suite.test_reflection import * # noqa: F401, F403
77
78
  from sqlalchemy.testing.suite.test_results import * # noqa: F401, F403
78
79
  from sqlalchemy.testing.suite.test_select import * # noqa: F401, F403
79
- from sqlalchemy.testing.suite.test_sequence import * # noqa: F401, F403
80
+ from sqlalchemy.testing.suite.test_sequence import (
81
+ SequenceTest as _SequenceTest,
82
+ HasSequenceTest as _HasSequenceTest,
83
+ HasSequenceTestEmpty as _HasSequenceTestEmpty,
84
+ ) # noqa: F401, F403
80
85
  from sqlalchemy.testing.suite.test_update_delete import * # noqa: F401, F403
81
86
  from sqlalchemy.testing.suite.test_cte import CTETest as _CTETest
82
87
  from sqlalchemy.testing.suite.test_ddl import TableDDLTest as _TableDDLTest
@@ -2392,3 +2397,128 @@ class CreateEngineWithoutDatabaseTest(fixtures.TestBase):
2392
2397
  engine = create_engine(get_db_url().split("/database")[0])
2393
2398
  with engine.connect() as connection:
2394
2399
  assert connection.connection.database is None
2400
+
2401
+
2402
+ @pytest.mark.skipif(
2403
+ bool(os.environ.get("SPANNER_EMULATOR_HOST")), reason="Skipped on emulator"
2404
+ )
2405
+ class SequenceTest(_SequenceTest):
2406
+ @classmethod
2407
+ def define_tables(cls, metadata):
2408
+ Table(
2409
+ "seq_pk",
2410
+ metadata,
2411
+ Column(
2412
+ "id",
2413
+ Integer,
2414
+ sqlalchemy.Sequence("tab_id_seq"),
2415
+ primary_key=True,
2416
+ ),
2417
+ Column("data", String(50)),
2418
+ )
2419
+
2420
+ Table(
2421
+ "seq_opt_pk",
2422
+ metadata,
2423
+ Column(
2424
+ "id",
2425
+ Integer,
2426
+ sqlalchemy.Sequence("tab_id_seq_opt", data_type=Integer, optional=True),
2427
+ primary_key=True,
2428
+ ),
2429
+ Column("data", String(50)),
2430
+ )
2431
+
2432
+ Table(
2433
+ "seq_no_returning",
2434
+ metadata,
2435
+ Column(
2436
+ "id",
2437
+ Integer,
2438
+ sqlalchemy.Sequence("noret_id_seq"),
2439
+ primary_key=True,
2440
+ ),
2441
+ Column("data", String(50)),
2442
+ implicit_returning=False,
2443
+ )
2444
+
2445
+ def test_insert_lastrowid(self, connection):
2446
+ r = connection.execute(self.tables.seq_pk.insert(), dict(data="some data"))
2447
+ assert len(r.inserted_primary_key) == 1
2448
+ is_instance_of(r.inserted_primary_key[0], int)
2449
+
2450
+ def test_nextval_direct(self, connection):
2451
+ r = connection.execute(self.tables.seq_pk.c.id.default)
2452
+ is_instance_of(r, int)
2453
+
2454
+ def _assert_round_trip(self, table, conn):
2455
+ row = conn.execute(table.select()).first()
2456
+ id, name = row
2457
+ is_instance_of(id, int)
2458
+ eq_(name, "some data")
2459
+
2460
+ @testing.combinations((True,), (False,), argnames="implicit_returning")
2461
+ @testing.requires.schemas
2462
+ @pytest.mark.skip("Spanner doesn't support user defined schemas")
2463
+ def test_insert_roundtrip_translate(self, connection, implicit_returning):
2464
+ pass
2465
+
2466
+ @testing.requires.schemas
2467
+ @pytest.mark.skip("Spanner doesn't support user defined schemas")
2468
+ def test_nextval_direct_schema_translate(self, connection):
2469
+ pass
2470
+
2471
+
2472
+ @pytest.mark.skipif(
2473
+ bool(os.environ.get("SPANNER_EMULATOR_HOST")), reason="Skipped on emulator"
2474
+ )
2475
+ class HasSequenceTest(_HasSequenceTest):
2476
+ @classmethod
2477
+ def define_tables(cls, metadata):
2478
+ sqlalchemy.Sequence("user_id_seq", metadata=metadata)
2479
+ sqlalchemy.Sequence(
2480
+ "other_seq", metadata=metadata, nomaxvalue=True, nominvalue=True
2481
+ )
2482
+ Table(
2483
+ "user_id_table",
2484
+ metadata,
2485
+ Column("id", Integer, primary_key=True),
2486
+ )
2487
+
2488
+ @testing.requires.schemas
2489
+ @pytest.mark.skip("Spanner doesn't support user defined schemas")
2490
+ def test_has_sequence_schema(self, connection):
2491
+ pass
2492
+
2493
+ @testing.requires.schemas
2494
+ @pytest.mark.skip("Spanner doesn't support user defined schemas")
2495
+ def test_has_sequence_schemas_neg(self, connection):
2496
+ pass
2497
+
2498
+ @testing.requires.schemas
2499
+ @pytest.mark.skip("Spanner doesn't support user defined schemas")
2500
+ def test_has_sequence_default_not_in_remote(self, connection):
2501
+ pass
2502
+
2503
+ @testing.requires.schemas
2504
+ @pytest.mark.skip("Spanner doesn't support user defined schemas")
2505
+ def test_has_sequence_remote_not_in_default(self, connection):
2506
+ pass
2507
+
2508
+ @testing.requires.schemas
2509
+ @pytest.mark.skip("Spanner doesn't support user defined schemas")
2510
+ def test_get_sequence_names_no_sequence_schema(self, connection):
2511
+ pass
2512
+
2513
+ @testing.requires.schemas
2514
+ @pytest.mark.skip("Spanner doesn't support user defined schemas")
2515
+ def test_get_sequence_names_sequences_schema(self, connection):
2516
+ pass
2517
+
2518
+
2519
+ @pytest.mark.skipif(
2520
+ bool(os.environ.get("SPANNER_EMULATOR_HOST")), reason="Skipped on emulator"
2521
+ )
2522
+ class HasSequenceTestEmpty(_HasSequenceTestEmpty):
2523
+ def test_get_sequence_names_no_sequence(self, connection):
2524
+ super().test_get_sequence_names_no_sequence(connection)
@@ -39,6 +39,7 @@ from sqlalchemy.schema import Computed
39
39
  from sqlalchemy.testing import config
40
40
  from sqlalchemy.testing import engines
41
41
  from sqlalchemy.testing import eq_
42
+ from sqlalchemy.testing import is_instance_of
42
43
  from sqlalchemy.testing import provide_metadata, emits_warning
43
44
  from sqlalchemy.testing import fixtures
44
45
  from sqlalchemy.testing.provision import temp_table_keyword_args
@@ -80,7 +81,11 @@ from sqlalchemy.testing.suite.test_reflection import * # noqa: F401, F403
80
81
  from sqlalchemy.testing.suite.test_deprecations import * # noqa: F401, F403
81
82
  from sqlalchemy.testing.suite.test_results import * # noqa: F401, F403
82
83
  from sqlalchemy.testing.suite.test_select import * # noqa: F401, F403
83
- from sqlalchemy.testing.suite.test_sequence import * # noqa: F401, F403
84
+ from sqlalchemy.testing.suite.test_sequence import (
85
+ SequenceTest as _SequenceTest,
86
+ HasSequenceTest as _HasSequenceTest,
87
+ HasSequenceTestEmpty as _HasSequenceTestEmpty,
88
+ ) # noqa: F401, F403
84
89
  from sqlalchemy.testing.suite.test_unicode_ddl import * # noqa: F401, F403
85
90
  from sqlalchemy.testing.suite.test_update_delete import * # noqa: F401, F403
86
91
  from sqlalchemy.testing.suite.test_cte import CTETest as _CTETest
@@ -144,7 +149,10 @@ from sqlalchemy.testing.suite.test_types import ( # noqa: F401, F403
144
149
  UnicodeTextTest as _UnicodeTextTest,
145
150
  _UnicodeFixture as __UnicodeFixture,
146
151
  ) # noqa: F401, F403
147
- from test._helpers import get_db_url, get_project
152
+ from test._helpers import (
153
+ get_db_url,
154
+ get_project,
155
+ )
148
156
 
149
157
  config.test_schema = ""
150
158
 
@@ -157,7 +165,7 @@ class BooleanTest(_BooleanTest):
157
165
  def test_render_literal_bool(self):
158
166
  pass
159
167
 
160
- def test_render_literal_bool_true(self, literal_round_trip):
168
+ def test_render_literal_bool_true(self, literal_round_trip_spanner):
161
169
  """
162
170
  SPANNER OVERRIDE:
163
171
 
@@ -166,9 +174,9 @@ class BooleanTest(_BooleanTest):
166
174
  following insertions will fail with `Row [] already exists".
167
175
  Overriding the test to avoid the same failure.
168
176
  """
169
- literal_round_trip(Boolean(), [True], [True])
177
+ literal_round_trip_spanner(Boolean(), [True], [True])
170
178
 
171
- def test_render_literal_bool_false(self, literal_round_trip):
179
+ def test_render_literal_bool_false(self, literal_round_trip_spanner):
172
180
  """
173
181
  SPANNER OVERRIDE:
174
182
 
@@ -177,7 +185,7 @@ class BooleanTest(_BooleanTest):
177
185
  following insertions will fail with `Row [] already exists".
178
186
  Overriding the test to avoid the same failure.
179
187
  """
180
- literal_round_trip(Boolean(), [False], [False])
188
+ literal_round_trip_spanner(Boolean(), [False], [False])
181
189
 
182
190
  @pytest.mark.skip("Not supported by Cloud Spanner")
183
191
  def test_whereclause(self):
@@ -1998,6 +2006,9 @@ class IntegerTest(_IntegerTest):
1998
2006
  intvalue,
1999
2007
  )
2000
2008
 
2009
+ def test_literal(self, literal_round_trip_spanner):
2010
+ literal_round_trip_spanner(Integer, [5], [5])
2011
+
2001
2012
 
2002
2013
  class _UnicodeFixture(__UnicodeFixture):
2003
2014
  @classmethod
@@ -2111,6 +2122,10 @@ class InsertBehaviorTest(_InsertBehaviorTest):
2111
2122
  def test_insert_from_select_autoinc(self):
2112
2123
  pass
2113
2124
 
2125
+ @pytest.mark.skip("Spanner does not support auto increment")
2126
+ def test_no_results_for_non_returning_insert(self, connection, style, executemany):
2127
+ pass
2128
+
2114
2129
  def test_autoclose_on_insert(self):
2115
2130
  """
2116
2131
  SPANNER OVERRIDE:
@@ -2180,6 +2195,19 @@ class StringTest(_StringTest):
2180
2195
  args[1],
2181
2196
  )
2182
2197
 
2198
+ def test_literal(self, literal_round_trip_spanner):
2199
+ # note that in Python 3, this invokes the Unicode
2200
+ # datatype for the literal part because all strings are unicode
2201
+ literal_round_trip_spanner(String(40), ["some text"], ["some text"])
2202
+
2203
+ def test_literal_quoting(self, literal_round_trip_spanner):
2204
+ data = """some 'text' hey "hi there" that's text"""
2205
+ literal_round_trip_spanner(String(40), [data], [data])
2206
+
2207
+ def test_literal_backslashes(self, literal_round_trip_spanner):
2208
+ data = r"backslash one \ backslash two \\ end"
2209
+ literal_round_trip_spanner(String(40), [data], [data])
2210
+
2183
2211
 
2184
2212
  class TextTest(_TextTest):
2185
2213
  @classmethod
@@ -2215,6 +2243,21 @@ class TextTest(_TextTest):
2215
2243
  def test_text_null_strings(self, connection):
2216
2244
  pass
2217
2245
 
2246
+ def test_literal(self, literal_round_trip_spanner):
2247
+ literal_round_trip_spanner(Text, ["some text"], ["some text"])
2248
+
2249
+ def test_literal_quoting(self, literal_round_trip_spanner):
2250
+ data = """some 'text' hey "hi there" that's text"""
2251
+ literal_round_trip_spanner(Text, [data], [data])
2252
+
2253
+ def test_literal_backslashes(self, literal_round_trip_spanner):
2254
+ data = r"backslash one \ backslash two \\ end"
2255
+ literal_round_trip_spanner(Text, [data], [data])
2256
+
2257
+ def test_literal_percentsigns(self, literal_round_trip_spanner):
2258
+ data = r"percent % signs %% percent"
2259
+ literal_round_trip_spanner(Text, [data], [data])
2260
+
2218
2261
 
2219
2262
  class NumericTest(_NumericTest):
2220
2263
  @testing.fixture
@@ -2245,7 +2288,7 @@ class NumericTest(_NumericTest):
2245
2288
  return run
2246
2289
 
2247
2290
  @emits_warning(r".*does \*not\* support Decimal objects natively")
2248
- def test_render_literal_numeric(self, literal_round_trip):
2291
+ def test_render_literal_numeric(self, literal_round_trip_spanner):
2249
2292
  """
2250
2293
  SPANNER OVERRIDE:
2251
2294
 
@@ -2254,14 +2297,14 @@ class NumericTest(_NumericTest):
2254
2297
  following insertions will fail with `Row [] already exists".
2255
2298
  Overriding the test to avoid the same failure.
2256
2299
  """
2257
- literal_round_trip(
2300
+ literal_round_trip_spanner(
2258
2301
  Numeric(precision=8, scale=4),
2259
2302
  [decimal.Decimal("15.7563")],
2260
2303
  [decimal.Decimal("15.7563")],
2261
2304
  )
2262
2305
 
2263
2306
  @emits_warning(r".*does \*not\* support Decimal objects natively")
2264
- def test_render_literal_numeric_asfloat(self, literal_round_trip):
2307
+ def test_render_literal_numeric_asfloat(self, literal_round_trip_spanner):
2265
2308
  """
2266
2309
  SPANNER OVERRIDE:
2267
2310
 
@@ -2270,13 +2313,13 @@ class NumericTest(_NumericTest):
2270
2313
  following insertions will fail with `Row [] already exists".
2271
2314
  Overriding the test to avoid the same failure.
2272
2315
  """
2273
- literal_round_trip(
2316
+ literal_round_trip_spanner(
2274
2317
  Numeric(precision=8, scale=4, asdecimal=False),
2275
2318
  [decimal.Decimal("15.7563")],
2276
2319
  [15.7563],
2277
2320
  )
2278
2321
 
2279
- def test_render_literal_float(self, literal_round_trip):
2322
+ def test_render_literal_float(self, literal_round_trip_spanner):
2280
2323
  """
2281
2324
  SPANNER OVERRIDE:
2282
2325
 
@@ -2285,7 +2328,7 @@ class NumericTest(_NumericTest):
2285
2328
  following insertions will fail with `Row [] already exists".
2286
2329
  Overriding the test to avoid the same failure.
2287
2330
  """
2288
- literal_round_trip(
2331
+ literal_round_trip_spanner(
2289
2332
  Float(4),
2290
2333
  [decimal.Decimal("15.7563")],
2291
2334
  [15.7563],
@@ -2495,6 +2538,17 @@ class IsOrIsNotDistinctFromTest(_IsOrIsNotDistinctFromTest):
2495
2538
  pass
2496
2539
 
2497
2540
 
2541
+ @pytest.mark.skip("Spanner doesn't bizarre characters in foreign key names")
2542
+ class BizarroCharacterFKResolutionTest(fixtures.TestBase):
2543
+ pass
2544
+
2545
+
2546
+ class IsolationLevelTest(fixtures.TestBase):
2547
+ @pytest.mark.skip("Cloud Spanner does not support different isolation levels")
2548
+ def test_dialect_user_setting_is_restored(self, testing_engine):
2549
+ pass
2550
+
2551
+
2498
2552
  class OrderByLabelTest(_OrderByLabelTest):
2499
2553
  @pytest.mark.skip(
2500
2554
  "Spanner requires an alias for the GROUP BY list when specifying derived "
@@ -3041,3 +3095,132 @@ class CreateEngineWithoutDatabaseTest(fixtures.TestBase):
3041
3095
  engine = create_engine(get_db_url().split("/database")[0])
3042
3096
  with engine.connect() as connection:
3043
3097
  assert connection.connection.database is None
3098
+
3099
+
3100
+ @pytest.mark.skipif(
3101
+ bool(os.environ.get("SPANNER_EMULATOR_HOST")), reason="Skipped on emulator"
3102
+ )
3103
+ class SequenceTest(_SequenceTest):
3104
+ @classmethod
3105
+ def define_tables(cls, metadata):
3106
+ Table(
3107
+ "seq_pk",
3108
+ metadata,
3109
+ Column(
3110
+ "id",
3111
+ Integer,
3112
+ sqlalchemy.Sequence("tab_id_seq"),
3113
+ primary_key=True,
3114
+ ),
3115
+ Column("data", String(50)),
3116
+ )
3117
+
3118
+ Table(
3119
+ "seq_opt_pk",
3120
+ metadata,
3121
+ Column(
3122
+ "id",
3123
+ Integer,
3124
+ sqlalchemy.Sequence("tab_id_seq_opt", data_type=Integer, optional=True),
3125
+ primary_key=True,
3126
+ ),
3127
+ Column("data", String(50)),
3128
+ )
3129
+
3130
+ Table(
3131
+ "seq_no_returning",
3132
+ metadata,
3133
+ Column(
3134
+ "id",
3135
+ Integer,
3136
+ sqlalchemy.Sequence("noret_id_seq"),
3137
+ primary_key=True,
3138
+ ),
3139
+ Column("data", String(50)),
3140
+ implicit_returning=False,
3141
+ )
3142
+
3143
+ def test_insert_lastrowid(self, connection):
3144
+ r = connection.execute(self.tables.seq_pk.insert(), dict(data="some data"))
3145
+ assert len(r.inserted_primary_key) == 1
3146
+ is_instance_of(r.inserted_primary_key[0], int)
3147
+
3148
+ def test_nextval_direct(self, connection):
3149
+ r = connection.execute(self.tables.seq_pk.c.id.default)
3150
+ is_instance_of(r, int)
3151
+
3152
+ def _assert_round_trip(self, table, conn):
3153
+ row = conn.execute(table.select()).first()
3154
+ id, name = row
3155
+ is_instance_of(id, int)
3156
+ eq_(name, "some data")
3157
+
3158
+ @testing.combinations((True,), (False,), argnames="implicit_returning")
3159
+ @testing.requires.schemas
3160
+ @pytest.mark.skip("Not supported by Cloud Spanner")
3161
+ def test_insert_roundtrip_translate(self, connection, implicit_returning):
3162
+ pass
3163
+
3164
+ @testing.requires.schemas
3165
+ @pytest.mark.skip("Not supported by Cloud Spanner")
3166
+ def test_nextval_direct_schema_translate(self, connection):
3167
+ pass
3168
+
3169
+
3170
+ @pytest.mark.skipif(
3171
+ bool(os.environ.get("SPANNER_EMULATOR_HOST")), reason="Skipped on emulator"
3172
+ )
3173
+ class HasSequenceTest(_HasSequenceTest):
3174
+ @classmethod
3175
+ def define_tables(cls, metadata):
3176
+ sqlalchemy.Sequence("user_id_seq", metadata=metadata)
3177
+ sqlalchemy.Sequence(
3178
+ "other_seq", metadata=metadata, nomaxvalue=True, nominvalue=True
3179
+ )
3180
+ Table(
3181
+ "user_id_table",
3182
+ metadata,
3183
+ Column("id", Integer, primary_key=True),
3184
+ )
3185
+
3186
+ @pytest.mark.skip("Not supported by Cloud Spanner")
3187
+ def test_has_sequence_cache(self, connection, metadata):
3188
+ pass
3189
+
3190
+ @testing.requires.schemas
3191
+ @pytest.mark.skip("Not supported by Cloud Spanner")
3192
+ def test_has_sequence_schema(self, connection):
3193
+ pass
3194
+
3195
+ @testing.requires.schemas
3196
+ @pytest.mark.skip("Not supported by Cloud Spanner")
3197
+ def test_has_sequence_schemas_neg(self, connection):
3198
+ pass
3199
+
3200
+ @testing.requires.schemas
3201
+ @pytest.mark.skip("Not supported by Cloud Spanner")
3202
+ def test_has_sequence_default_not_in_remote(self, connection):
3203
+ pass
3204
+
3205
+ @testing.requires.schemas
3206
+ @pytest.mark.skip("Not supported by Cloud Spanner")
3207
+ def test_has_sequence_remote_not_in_default(self, connection):
3208
+ pass
3209
+
3210
+ @testing.requires.schemas
3211
+ @pytest.mark.skip("Not supported by Cloud Spanner")
3212
+ def test_get_sequence_names_no_sequence_schema(self, connection):
3213
+ pass
3214
+
3215
+ @testing.requires.schemas
3216
+ @pytest.mark.skip("Not supported by Cloud Spanner")
3217
+ def test_get_sequence_names_sequences_schema(self, connection):
3218
+ pass
3219
+
3220
+
3221
+ @pytest.mark.skipif(
3222
+ bool(os.environ.get("SPANNER_EMULATOR_HOST")), reason="Skipped on emulator"
3223
+ )
3224
+ class HasSequenceTestEmpty(_HasSequenceTestEmpty):
3225
+ def test_get_sequence_names_no_sequence(self, connection):
3226
+ super().test_get_sequence_names_no_sequence(connection)