sqlalchemy-spanner 1.7.0__tar.gz → 1.9.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 (24) hide show
  1. {sqlalchemy_spanner-1.7.0 → sqlalchemy_spanner-1.9.0}/PKG-INFO +32 -16
  2. {sqlalchemy_spanner-1.7.0 → sqlalchemy_spanner-1.9.0}/README.rst +31 -15
  3. sqlalchemy_spanner-1.9.0/google/cloud/sqlalchemy_spanner/dml.py +26 -0
  4. {sqlalchemy_spanner-1.7.0 → sqlalchemy_spanner-1.9.0}/google/cloud/sqlalchemy_spanner/sqlalchemy_spanner.py +108 -11
  5. {sqlalchemy_spanner-1.7.0 → sqlalchemy_spanner-1.9.0}/setup.py +25 -21
  6. {sqlalchemy_spanner-1.7.0 → sqlalchemy_spanner-1.9.0}/sqlalchemy_spanner.egg-info/PKG-INFO +32 -16
  7. {sqlalchemy_spanner-1.7.0 → sqlalchemy_spanner-1.9.0}/sqlalchemy_spanner.egg-info/SOURCES.txt +1 -0
  8. {sqlalchemy_spanner-1.7.0 → sqlalchemy_spanner-1.9.0}/test/test_suite_13.py +46 -0
  9. {sqlalchemy_spanner-1.7.0 → sqlalchemy_spanner-1.9.0}/test/test_suite_14.py +52 -1
  10. {sqlalchemy_spanner-1.7.0 → sqlalchemy_spanner-1.9.0}/test/test_suite_20.py +101 -8
  11. {sqlalchemy_spanner-1.7.0 → sqlalchemy_spanner-1.9.0}/LICENSE +0 -0
  12. {sqlalchemy_spanner-1.7.0 → sqlalchemy_spanner-1.9.0}/google/__init__.py +0 -0
  13. {sqlalchemy_spanner-1.7.0 → sqlalchemy_spanner-1.9.0}/google/cloud/__init__.py +0 -0
  14. {sqlalchemy_spanner-1.7.0 → sqlalchemy_spanner-1.9.0}/google/cloud/sqlalchemy_spanner/__init__.py +0 -0
  15. {sqlalchemy_spanner-1.7.0 → sqlalchemy_spanner-1.9.0}/google/cloud/sqlalchemy_spanner/_opentelemetry_tracing.py +0 -0
  16. {sqlalchemy_spanner-1.7.0 → sqlalchemy_spanner-1.9.0}/google/cloud/sqlalchemy_spanner/provision.py +0 -0
  17. {sqlalchemy_spanner-1.7.0 → sqlalchemy_spanner-1.9.0}/google/cloud/sqlalchemy_spanner/requirements.py +0 -0
  18. {sqlalchemy_spanner-1.7.0 → sqlalchemy_spanner-1.9.0}/setup.cfg +0 -0
  19. {sqlalchemy_spanner-1.7.0 → sqlalchemy_spanner-1.9.0}/sqlalchemy_spanner.egg-info/dependency_links.txt +0 -0
  20. {sqlalchemy_spanner-1.7.0 → sqlalchemy_spanner-1.9.0}/sqlalchemy_spanner.egg-info/entry_points.txt +0 -0
  21. {sqlalchemy_spanner-1.7.0 → sqlalchemy_spanner-1.9.0}/sqlalchemy_spanner.egg-info/namespace_packages.txt +0 -0
  22. {sqlalchemy_spanner-1.7.0 → sqlalchemy_spanner-1.9.0}/sqlalchemy_spanner.egg-info/not-zip-safe +0 -0
  23. {sqlalchemy_spanner-1.7.0 → sqlalchemy_spanner-1.9.0}/sqlalchemy_spanner.egg-info/requires.txt +0 -0
  24. {sqlalchemy_spanner-1.7.0 → sqlalchemy_spanner-1.9.0}/sqlalchemy_spanner.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sqlalchemy-spanner
3
- Version: 1.7.0
3
+ Version: 1.9.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
@@ -75,6 +75,13 @@ Next install the package from the package ``setup.py`` file:
75
75
 
76
76
  During setup the dialect will be registered with entry points.
77
77
 
78
+ Samples
79
+ -------------
80
+
81
+ The `samples directory <https://github.com/googleapis/python-spanner-sqlalchemy/blob/-/samples/README.md>`__
82
+ contains multiple examples for how to configure and use common Spanner features.
83
+
84
+
78
85
  A Minimal App
79
86
  -------------
80
87
 
@@ -90,7 +97,7 @@ on this step in a dialect prefix part:
90
97
  # for SQLAlchemy 1.3:
91
98
  spanner:///projects/project-id/instances/instance-id/databases/database-id
92
99
 
93
- # for SQLAlchemy 1.4:
100
+ # for SQLAlchemy 1.4 and 2.0:
94
101
  spanner+spanner:///projects/project-id/instances/instance-id/databases/database-id
95
102
 
96
103
  To pass your custom client object directly to be be used, create engine as following:
@@ -247,7 +254,7 @@ Unique constraints
247
254
  ~~~~~~~~~~~~~~~~~~
248
255
 
249
256
  Cloud Spanner doesn't support direct UNIQUE constraints creation. In
250
- order to achieve column values uniqueness UNIQUE indexes should be used.
257
+ order to achieve column values uniqueness, UNIQUE indexes should be used.
251
258
 
252
259
  Instead of direct UNIQUE constraint creation:
253
260
 
@@ -275,10 +282,16 @@ Autocommit mode
275
282
  ~~~~~~~~~~~~~~~
276
283
 
277
284
  Spanner dialect supports both ``SERIALIZABLE`` and ``AUTOCOMMIT``
278
- isolation levels. ``SERIALIZABLE`` is the default one, where
279
- transactions need to be committed manually. ``AUTOCOMMIT`` mode
280
- corresponds to automatically committing of a query right in its
281
- execution time.
285
+ isolation levels. ``SERIALIZABLE`` is the default isolation level.
286
+
287
+ ``AUTOCOMMIT`` mode corresponds to automatically committing each
288
+ insert/update/delete statement right after is has been executed.
289
+ Queries that are executed in ``AUTOCOMMIT`` mode use a single-use
290
+ read-only transaction. These do not take any locks and do not need
291
+ to be committed.
292
+
293
+ Workloads that only read data, should use either ``AUTOCOMMIT`` or
294
+ a read-only transaction.
282
295
 
283
296
  Isolation level change example:
284
297
 
@@ -289,7 +302,7 @@ Isolation level change example:
289
302
  eng = create_engine("spanner:///projects/project-id/instances/instance-id/databases/database-id")
290
303
  autocommit_engine = eng.execution_options(isolation_level="AUTOCOMMIT")
291
304
 
292
- Automatic transactions retry
305
+ Automatic transaction retry
293
306
  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
294
307
  In the default ``SERIALIZABLE`` mode transactions may fail with ``Aborted`` exception. This is a transient kind of errors, which mostly happen to prevent data corruption by concurrent modifications. Though the original transaction becomes non operational, a simple retry of the queries solves the issue.
295
308
 
@@ -297,8 +310,8 @@ This, however, may require to manually repeat a long list of operations, execute
297
310
 
298
311
  In ``AUTOCOMMIT`` mode automatic transactions retry mechanism is disabled, as every operation is committed just in time, and there is no way an ``Aborted`` exception can happen.
299
312
 
300
- Autoincremented IDs
301
- ~~~~~~~~~~~~~~~~~~~
313
+ Auto-incremented IDs
314
+ ~~~~~~~~~~~~~~~~~~~~
302
315
 
303
316
  Cloud Spanner doesn't support autoincremented IDs mechanism due to
304
317
  performance reasons (`see for more
@@ -354,8 +367,9 @@ ReadOnly transactions
354
367
  ~~~~~~~~~~~~~~~~~~~~~
355
368
 
356
369
  By default, transactions produced by a Spanner connection are in
357
- ReadWrite mode. However, some applications require an ability to grant
358
- ReadOnly access to users/methods; for these cases Spanner dialect
370
+ ReadWrite mode. However, workloads that only read data perform better
371
+ if they use read-only transactions, as Spanner does not need to take
372
+ locks for the data that is read; for these cases, the Spanner dialect
359
373
  supports the ``read_only`` execution option, which switches a connection
360
374
  into ReadOnly mode:
361
375
 
@@ -364,11 +378,13 @@ into ReadOnly mode:
364
378
  with engine.connect().execution_options(read_only=True) as connection:
365
379
  connection.execute(select(["*"], from_obj=table)).fetchall()
366
380
 
367
- Note that execution options are applied lazily - on the ``execute()``
368
- method call, right before it.
381
+ See the `Read-only transaction sample
382
+ <https://github.com/googleapis/python-spanner-sqlalchemy/blob/-/samples/read_only_transaction_sample.py>`__
383
+ for a concrete example.
369
384
 
370
385
  ReadOnly/ReadWrite mode of a connection can't be changed while a
371
- transaction is in progress - first you must commit or rollback it.
386
+ transaction is in progress - you must commit or rollback the current
387
+ transaction before changing the mode.
372
388
 
373
389
  Stale reads
374
390
  ~~~~~~~~~~~
@@ -525,7 +541,7 @@ run the tests the ``nox`` package commands can be used:
525
541
  Running tests on Spanner emulator
526
542
  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
527
543
 
528
- The dialect test suite can be runned on `Spanner
544
+ The dialect test suite can be run on `Spanner
529
545
  emulator <https://cloud.google.com/spanner/docs/emulator>`__. Several
530
546
  tests, relating to ``NULL`` values of data types, are skipped when
531
547
  executed on emulator.
@@ -58,6 +58,13 @@ Next install the package from the package ``setup.py`` file:
58
58
 
59
59
  During setup the dialect will be registered with entry points.
60
60
 
61
+ Samples
62
+ -------------
63
+
64
+ The `samples directory <https://github.com/googleapis/python-spanner-sqlalchemy/blob/-/samples/README.md>`__
65
+ contains multiple examples for how to configure and use common Spanner features.
66
+
67
+
61
68
  A Minimal App
62
69
  -------------
63
70
 
@@ -73,7 +80,7 @@ on this step in a dialect prefix part:
73
80
  # for SQLAlchemy 1.3:
74
81
  spanner:///projects/project-id/instances/instance-id/databases/database-id
75
82
 
76
- # for SQLAlchemy 1.4:
83
+ # for SQLAlchemy 1.4 and 2.0:
77
84
  spanner+spanner:///projects/project-id/instances/instance-id/databases/database-id
78
85
 
79
86
  To pass your custom client object directly to be be used, create engine as following:
@@ -230,7 +237,7 @@ Unique constraints
230
237
  ~~~~~~~~~~~~~~~~~~
231
238
 
232
239
  Cloud Spanner doesn't support direct UNIQUE constraints creation. In
233
- order to achieve column values uniqueness UNIQUE indexes should be used.
240
+ order to achieve column values uniqueness, UNIQUE indexes should be used.
234
241
 
235
242
  Instead of direct UNIQUE constraint creation:
236
243
 
@@ -258,10 +265,16 @@ Autocommit mode
258
265
  ~~~~~~~~~~~~~~~
259
266
 
260
267
  Spanner dialect supports both ``SERIALIZABLE`` and ``AUTOCOMMIT``
261
- isolation levels. ``SERIALIZABLE`` is the default one, where
262
- transactions need to be committed manually. ``AUTOCOMMIT`` mode
263
- corresponds to automatically committing of a query right in its
264
- execution time.
268
+ isolation levels. ``SERIALIZABLE`` is the default isolation level.
269
+
270
+ ``AUTOCOMMIT`` mode corresponds to automatically committing each
271
+ insert/update/delete statement right after is has been executed.
272
+ Queries that are executed in ``AUTOCOMMIT`` mode use a single-use
273
+ read-only transaction. These do not take any locks and do not need
274
+ to be committed.
275
+
276
+ Workloads that only read data, should use either ``AUTOCOMMIT`` or
277
+ a read-only transaction.
265
278
 
266
279
  Isolation level change example:
267
280
 
@@ -272,7 +285,7 @@ Isolation level change example:
272
285
  eng = create_engine("spanner:///projects/project-id/instances/instance-id/databases/database-id")
273
286
  autocommit_engine = eng.execution_options(isolation_level="AUTOCOMMIT")
274
287
 
275
- Automatic transactions retry
288
+ Automatic transaction retry
276
289
  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
277
290
  In the default ``SERIALIZABLE`` mode transactions may fail with ``Aborted`` exception. This is a transient kind of errors, which mostly happen to prevent data corruption by concurrent modifications. Though the original transaction becomes non operational, a simple retry of the queries solves the issue.
278
291
 
@@ -280,8 +293,8 @@ This, however, may require to manually repeat a long list of operations, execute
280
293
 
281
294
  In ``AUTOCOMMIT`` mode automatic transactions retry mechanism is disabled, as every operation is committed just in time, and there is no way an ``Aborted`` exception can happen.
282
295
 
283
- Autoincremented IDs
284
- ~~~~~~~~~~~~~~~~~~~
296
+ Auto-incremented IDs
297
+ ~~~~~~~~~~~~~~~~~~~~
285
298
 
286
299
  Cloud Spanner doesn't support autoincremented IDs mechanism due to
287
300
  performance reasons (`see for more
@@ -337,8 +350,9 @@ ReadOnly transactions
337
350
  ~~~~~~~~~~~~~~~~~~~~~
338
351
 
339
352
  By default, transactions produced by a Spanner connection are in
340
- ReadWrite mode. However, some applications require an ability to grant
341
- ReadOnly access to users/methods; for these cases Spanner dialect
353
+ ReadWrite mode. However, workloads that only read data perform better
354
+ if they use read-only transactions, as Spanner does not need to take
355
+ locks for the data that is read; for these cases, the Spanner dialect
342
356
  supports the ``read_only`` execution option, which switches a connection
343
357
  into ReadOnly mode:
344
358
 
@@ -347,11 +361,13 @@ into ReadOnly mode:
347
361
  with engine.connect().execution_options(read_only=True) as connection:
348
362
  connection.execute(select(["*"], from_obj=table)).fetchall()
349
363
 
350
- Note that execution options are applied lazily - on the ``execute()``
351
- method call, right before it.
364
+ See the `Read-only transaction sample
365
+ <https://github.com/googleapis/python-spanner-sqlalchemy/blob/-/samples/read_only_transaction_sample.py>`__
366
+ for a concrete example.
352
367
 
353
368
  ReadOnly/ReadWrite mode of a connection can't be changed while a
354
- transaction is in progress - first you must commit or rollback it.
369
+ transaction is in progress - you must commit or rollback the current
370
+ transaction before changing the mode.
355
371
 
356
372
  Stale reads
357
373
  ~~~~~~~~~~~
@@ -508,7 +524,7 @@ run the tests the ``nox`` package commands can be used:
508
524
  Running tests on Spanner emulator
509
525
  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
510
526
 
511
- The dialect test suite can be runned on `Spanner
527
+ The dialect test suite can be run on `Spanner
512
528
  emulator <https://cloud.google.com/spanner/docs/emulator>`__. Several
513
529
  tests, relating to ``NULL`` values of data types, are skipped when
514
530
  executed on emulator.
@@ -0,0 +1,26 @@
1
+ # Copyright 2024 Google LLC All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from sqlalchemy import Insert, insert
16
+ from sqlalchemy.sql._typing import _DMLTableArgument
17
+
18
+
19
+ def insert_or_update(table: _DMLTableArgument) -> Insert:
20
+ """Construct a Spanner-specific insert-or-update statement."""
21
+ return insert(table).prefix_with("OR UPDATE")
22
+
23
+
24
+ def insert_or_ignore(table: _DMLTableArgument) -> Insert:
25
+ """Construct a Spanner-specific insert-or-ignore statement."""
26
+ return insert(table).prefix_with("OR IGNORE")
@@ -22,6 +22,9 @@ from alembic.ddl.base import (
22
22
  alter_table,
23
23
  format_type,
24
24
  )
25
+ from google.api_core.client_options import ClientOptions
26
+ from google.auth.credentials import AnonymousCredentials
27
+ from google.cloud.spanner_v1 import Client
25
28
  from sqlalchemy.exc import NoSuchTableError
26
29
  from sqlalchemy.sql import elements
27
30
  from sqlalchemy import ForeignKeyConstraint, types
@@ -81,6 +84,7 @@ _type_map = {
81
84
  "BYTES": types.LargeBinary,
82
85
  "DATE": types.DATE,
83
86
  "DATETIME": types.DATETIME,
87
+ "FLOAT32": types.REAL,
84
88
  "FLOAT64": types.Float,
85
89
  "INT64": types.BIGINT,
86
90
  "NUMERIC": types.NUMERIC(precision=38, scale=9),
@@ -98,6 +102,7 @@ _type_map_inv = {
98
102
  types.LargeBinary: "BYTES(MAX)",
99
103
  types.DATE: "DATE",
100
104
  types.DATETIME: "DATETIME",
105
+ types.REAL: "FLOAT32",
101
106
  types.Float: "FLOAT64",
102
107
  types.BIGINT: "INT64",
103
108
  types.DECIMAL: "NUMERIC",
@@ -175,6 +180,14 @@ class SpannerExecutionContext(DefaultExecutionContext):
175
180
  if priority is not None:
176
181
  self._dbapi_connection.connection.request_priority = priority
177
182
 
183
+ transaction_tag = self.execution_options.get("transaction_tag")
184
+ if transaction_tag:
185
+ self._dbapi_connection.connection.transaction_tag = transaction_tag
186
+
187
+ request_tag = self.execution_options.get("request_tag")
188
+ if request_tag:
189
+ self.cursor.request_tag = request_tag
190
+
178
191
  def fire_sequence(self, seq, type_):
179
192
  """Builds a statement for fetching next value of the sequence."""
180
193
  return self._execute_scalar(
@@ -230,6 +243,9 @@ class SpannerSQLCompiler(SQLCompiler):
230
243
  """
231
244
  return text
232
245
 
246
+ def visit_now_func(self, func, **kwargs):
247
+ return "current_timestamp"
248
+
233
249
  def visit_empty_set_expr(self, type_, **kw):
234
250
  """Return an empty set expression of the given type.
235
251
 
@@ -489,6 +505,26 @@ class SpannerDDLCompiler(DDLCompiler):
489
505
 
490
506
  return post_cmds
491
507
 
508
+ def visit_create_index(
509
+ self, create, include_schema=False, include_table_schema=True, **kw
510
+ ):
511
+ text = super().visit_create_index(
512
+ create, include_schema, include_table_schema, **kw
513
+ )
514
+ index = create.element
515
+ if "spanner" in index.dialect_options:
516
+ options = index.dialect_options["spanner"]
517
+ if "storing" in options:
518
+ storing = options["storing"]
519
+ storing_columns = [
520
+ index.table.c[col] if isinstance(col, str) else col
521
+ for col in storing
522
+ ]
523
+ text += " STORING (%s)" % ", ".join(
524
+ [self.preparer.quote(c.name) for c in storing_columns]
525
+ )
526
+ return text
527
+
492
528
  def get_identity_options(self, identity_options):
493
529
  text = ["sequence_kind = 'bit_reversed_positive'"]
494
530
  if identity_options.start is not None:
@@ -517,9 +553,18 @@ class SpannerTypeCompiler(GenericTypeCompiler):
517
553
  def visit_INTEGER(self, type_, **kw):
518
554
  return "INT64"
519
555
 
556
+ def visit_DOUBLE(self, type_, **kw):
557
+ return "FLOAT64"
558
+
520
559
  def visit_FLOAT(self, type_, **kw):
560
+ # Note: This was added before Spanner supported FLOAT32.
561
+ # Changing this now to generate a FLOAT32 would be a breaking change.
562
+ # Users therefore have to use REAL to generate a FLOAT32 column.
521
563
  return "FLOAT64"
522
564
 
565
+ def visit_REAL(self, type_, **kw):
566
+ return "FLOAT32"
567
+
523
568
  def visit_TEXT(self, type_, **kw):
524
569
  return "STRING({})".format(type_.length or "MAX")
525
570
 
@@ -588,6 +633,10 @@ class SpannerDialect(DefaultDialect):
588
633
  supports_native_decimal = True
589
634
  supports_statement_cache = True
590
635
 
636
+ insert_returning = True
637
+ update_returning = True
638
+ delete_returning = True
639
+
591
640
  ddl_compiler = SpannerDDLCompiler
592
641
  preparer = SpannerIdentifierPreparer
593
642
  statement_compiler = SpannerSQLCompiler
@@ -709,9 +758,29 @@ class SpannerDialect(DefaultDialect):
709
758
  url.database,
710
759
  )
711
760
  dist = pkg_resources.get_distribution("sqlalchemy-spanner")
761
+ options = {"user_agent": f"gl-{dist.project_name}/{dist.version}"}
762
+ connect_opts = url.translate_connect_args()
763
+ if (
764
+ "host" in connect_opts
765
+ and "port" in connect_opts
766
+ and "password" in connect_opts
767
+ ):
768
+ # Create a test client that connects to a local Spanner (mock) server.
769
+ if (
770
+ connect_opts["host"] == "localhost"
771
+ and connect_opts["password"] == "AnonymousCredentials"
772
+ ):
773
+ client = Client(
774
+ project=match.group("project"),
775
+ credentials=AnonymousCredentials(),
776
+ client_options=ClientOptions(
777
+ api_endpoint=f"{connect_opts['host']}:{connect_opts['port']}",
778
+ ),
779
+ )
780
+ options["client"] = client
712
781
  return (
713
782
  [match.group("instance"), match.group("database"), match.group("project")],
714
- {"user_agent": f"gl-{dist.project_name}/{dist.version}"},
783
+ options,
715
784
  )
716
785
 
717
786
  @engine_to_connection
@@ -974,15 +1043,35 @@ class SpannerDialect(DefaultDialect):
974
1043
  i.table_schema,
975
1044
  i.table_name,
976
1045
  i.index_name,
977
- ARRAY_AGG(ic.column_name),
1046
+ (
1047
+ SELECT ARRAY_AGG(ic.column_name)
1048
+ FROM information_schema.index_columns ic
1049
+ WHERE ic.index_name = i.index_name
1050
+ AND ic.table_catalog = i.table_catalog
1051
+ AND ic.table_schema = i.table_schema
1052
+ AND ic.table_name = i.table_name
1053
+ AND ic.column_ordering is not null
1054
+ ) as columns,
978
1055
  i.is_unique,
979
- ARRAY_AGG(ic.column_ordering)
1056
+ (
1057
+ SELECT ARRAY_AGG(ic.column_ordering)
1058
+ FROM information_schema.index_columns ic
1059
+ WHERE ic.index_name = i.index_name
1060
+ AND ic.table_catalog = i.table_catalog
1061
+ AND ic.table_schema = i.table_schema
1062
+ AND ic.table_name = i.table_name
1063
+ AND ic.column_ordering is not null
1064
+ ) as column_orderings,
1065
+ (
1066
+ SELECT ARRAY_AGG(storing.column_name)
1067
+ FROM information_schema.index_columns storing
1068
+ WHERE storing.index_name = i.index_name
1069
+ AND storing.table_catalog = i.table_catalog
1070
+ AND storing.table_schema = i.table_schema
1071
+ AND storing.table_name = i.table_name
1072
+ AND storing.column_ordering is null
1073
+ ) as storing_columns,
980
1074
  FROM information_schema.indexes as i
981
- JOIN information_schema.index_columns AS ic
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
986
1075
  JOIN information_schema.tables AS t
987
1076
  ON i.table_catalog = t.table_catalog
988
1077
  AND i.table_schema = t.table_schema
@@ -993,7 +1082,8 @@ class SpannerDialect(DefaultDialect):
993
1082
  {schema_filter_query}
994
1083
  i.index_type != 'PRIMARY_KEY'
995
1084
  AND i.spanner_is_managed = FALSE
996
- GROUP BY i.table_schema, i.table_name, i.index_name, i.is_unique
1085
+ GROUP BY i.table_catalog, i.table_schema, i.table_name,
1086
+ i.index_name, i.is_unique
997
1087
  ORDER BY i.index_name
998
1088
  """.format(
999
1089
  table_filter_query=table_filter_query,
@@ -1006,13 +1096,19 @@ class SpannerDialect(DefaultDialect):
1006
1096
  result_dict = {}
1007
1097
 
1008
1098
  for row in rows:
1099
+ dialect_options = {}
1100
+ include_columns = row[6]
1101
+ if include_columns:
1102
+ dialect_options["spanner_storing"] = include_columns
1009
1103
  index_info = {
1010
1104
  "name": row[2],
1011
1105
  "column_names": row[3],
1012
1106
  "unique": row[4],
1013
1107
  "column_sorting": {
1014
- col: order for col, order in zip(row[3], row[5])
1108
+ col: order.lower() for col, order in zip(row[3], row[5])
1015
1109
  },
1110
+ "include_columns": include_columns if include_columns else [],
1111
+ "dialect_options": dialect_options,
1016
1112
  }
1017
1113
  row[0] = row[0] or None
1018
1114
  table_info = result_dict.get((row[0], row[1]), [])
@@ -1541,8 +1637,9 @@ def visit_column_nullable(
1541
1637
  def visit_column_type(
1542
1638
  element: "ColumnType", compiler: "SpannerDDLCompiler", **kw
1543
1639
  ) -> str:
1544
- return "%s %s %s" % (
1640
+ return "%s %s %s %s" % (
1545
1641
  alter_table(compiler, element.table_name, element.schema),
1546
1642
  alter_column(compiler, element.column_name),
1547
1643
  "%s" % format_type(compiler, element.type_),
1644
+ "" if element.existing_nullable else "NOT NULL",
1548
1645
  )
@@ -14,6 +14,8 @@
14
14
 
15
15
  import io
16
16
  import os
17
+ import warnings
18
+
17
19
  import setuptools
18
20
 
19
21
 
@@ -59,24 +61,26 @@ namespaces = ["google"]
59
61
  if "google.cloud" in packages:
60
62
  namespaces.append("google.cloud")
61
63
 
62
- setuptools.setup(
63
- author="Google LLC",
64
- author_email="cloud-spanner-developers@googlegroups.com",
65
- classifiers=["Intended Audience :: Developers"],
66
- description=description,
67
- long_description=readme,
68
- entry_points={
69
- "sqlalchemy.dialects": [
70
- "spanner.spanner = google.cloud.sqlalchemy_spanner:SpannerDialect"
71
- ]
72
- },
73
- install_requires=dependencies,
74
- extras_require=extras,
75
- name=name,
76
- namespace_packages=namespaces,
77
- packages=packages,
78
- url="https://github.com/cloudspannerecosystem/python-spanner-sqlalchemy",
79
- version=version,
80
- include_package_data=True,
81
- zip_safe=False,
82
- )
64
+ with warnings.catch_warnings():
65
+ warnings.simplefilter("ignore")
66
+ setuptools.setup(
67
+ author="Google LLC",
68
+ author_email="cloud-spanner-developers@googlegroups.com",
69
+ classifiers=["Intended Audience :: Developers"],
70
+ description=description,
71
+ long_description=readme,
72
+ entry_points={
73
+ "sqlalchemy.dialects": [
74
+ "spanner.spanner = google.cloud.sqlalchemy_spanner:SpannerDialect"
75
+ ]
76
+ },
77
+ install_requires=dependencies,
78
+ extras_require=extras,
79
+ name=name,
80
+ namespace_packages=namespaces,
81
+ packages=packages,
82
+ url="https://github.com/cloudspannerecosystem/python-spanner-sqlalchemy",
83
+ version=version,
84
+ include_package_data=True,
85
+ zip_safe=False,
86
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sqlalchemy-spanner
3
- Version: 1.7.0
3
+ Version: 1.9.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
@@ -75,6 +75,13 @@ Next install the package from the package ``setup.py`` file:
75
75
 
76
76
  During setup the dialect will be registered with entry points.
77
77
 
78
+ Samples
79
+ -------------
80
+
81
+ The `samples directory <https://github.com/googleapis/python-spanner-sqlalchemy/blob/-/samples/README.md>`__
82
+ contains multiple examples for how to configure and use common Spanner features.
83
+
84
+
78
85
  A Minimal App
79
86
  -------------
80
87
 
@@ -90,7 +97,7 @@ on this step in a dialect prefix part:
90
97
  # for SQLAlchemy 1.3:
91
98
  spanner:///projects/project-id/instances/instance-id/databases/database-id
92
99
 
93
- # for SQLAlchemy 1.4:
100
+ # for SQLAlchemy 1.4 and 2.0:
94
101
  spanner+spanner:///projects/project-id/instances/instance-id/databases/database-id
95
102
 
96
103
  To pass your custom client object directly to be be used, create engine as following:
@@ -247,7 +254,7 @@ Unique constraints
247
254
  ~~~~~~~~~~~~~~~~~~
248
255
 
249
256
  Cloud Spanner doesn't support direct UNIQUE constraints creation. In
250
- order to achieve column values uniqueness UNIQUE indexes should be used.
257
+ order to achieve column values uniqueness, UNIQUE indexes should be used.
251
258
 
252
259
  Instead of direct UNIQUE constraint creation:
253
260
 
@@ -275,10 +282,16 @@ Autocommit mode
275
282
  ~~~~~~~~~~~~~~~
276
283
 
277
284
  Spanner dialect supports both ``SERIALIZABLE`` and ``AUTOCOMMIT``
278
- isolation levels. ``SERIALIZABLE`` is the default one, where
279
- transactions need to be committed manually. ``AUTOCOMMIT`` mode
280
- corresponds to automatically committing of a query right in its
281
- execution time.
285
+ isolation levels. ``SERIALIZABLE`` is the default isolation level.
286
+
287
+ ``AUTOCOMMIT`` mode corresponds to automatically committing each
288
+ insert/update/delete statement right after is has been executed.
289
+ Queries that are executed in ``AUTOCOMMIT`` mode use a single-use
290
+ read-only transaction. These do not take any locks and do not need
291
+ to be committed.
292
+
293
+ Workloads that only read data, should use either ``AUTOCOMMIT`` or
294
+ a read-only transaction.
282
295
 
283
296
  Isolation level change example:
284
297
 
@@ -289,7 +302,7 @@ Isolation level change example:
289
302
  eng = create_engine("spanner:///projects/project-id/instances/instance-id/databases/database-id")
290
303
  autocommit_engine = eng.execution_options(isolation_level="AUTOCOMMIT")
291
304
 
292
- Automatic transactions retry
305
+ Automatic transaction retry
293
306
  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
294
307
  In the default ``SERIALIZABLE`` mode transactions may fail with ``Aborted`` exception. This is a transient kind of errors, which mostly happen to prevent data corruption by concurrent modifications. Though the original transaction becomes non operational, a simple retry of the queries solves the issue.
295
308
 
@@ -297,8 +310,8 @@ This, however, may require to manually repeat a long list of operations, execute
297
310
 
298
311
  In ``AUTOCOMMIT`` mode automatic transactions retry mechanism is disabled, as every operation is committed just in time, and there is no way an ``Aborted`` exception can happen.
299
312
 
300
- Autoincremented IDs
301
- ~~~~~~~~~~~~~~~~~~~
313
+ Auto-incremented IDs
314
+ ~~~~~~~~~~~~~~~~~~~~
302
315
 
303
316
  Cloud Spanner doesn't support autoincremented IDs mechanism due to
304
317
  performance reasons (`see for more
@@ -354,8 +367,9 @@ ReadOnly transactions
354
367
  ~~~~~~~~~~~~~~~~~~~~~
355
368
 
356
369
  By default, transactions produced by a Spanner connection are in
357
- ReadWrite mode. However, some applications require an ability to grant
358
- ReadOnly access to users/methods; for these cases Spanner dialect
370
+ ReadWrite mode. However, workloads that only read data perform better
371
+ if they use read-only transactions, as Spanner does not need to take
372
+ locks for the data that is read; for these cases, the Spanner dialect
359
373
  supports the ``read_only`` execution option, which switches a connection
360
374
  into ReadOnly mode:
361
375
 
@@ -364,11 +378,13 @@ into ReadOnly mode:
364
378
  with engine.connect().execution_options(read_only=True) as connection:
365
379
  connection.execute(select(["*"], from_obj=table)).fetchall()
366
380
 
367
- Note that execution options are applied lazily - on the ``execute()``
368
- method call, right before it.
381
+ See the `Read-only transaction sample
382
+ <https://github.com/googleapis/python-spanner-sqlalchemy/blob/-/samples/read_only_transaction_sample.py>`__
383
+ for a concrete example.
369
384
 
370
385
  ReadOnly/ReadWrite mode of a connection can't be changed while a
371
- transaction is in progress - first you must commit or rollback it.
386
+ transaction is in progress - you must commit or rollback the current
387
+ transaction before changing the mode.
372
388
 
373
389
  Stale reads
374
390
  ~~~~~~~~~~~
@@ -525,7 +541,7 @@ run the tests the ``nox`` package commands can be used:
525
541
  Running tests on Spanner emulator
526
542
  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
527
543
 
528
- The dialect test suite can be runned on `Spanner
544
+ The dialect test suite can be run on `Spanner
529
545
  emulator <https://cloud.google.com/spanner/docs/emulator>`__. Several
530
546
  tests, relating to ``NULL`` values of data types, are skipped when
531
547
  executed on emulator.
@@ -6,6 +6,7 @@ google/__init__.py
6
6
  google/cloud/__init__.py
7
7
  google/cloud/sqlalchemy_spanner/__init__.py
8
8
  google/cloud/sqlalchemy_spanner/_opentelemetry_tracing.py
9
+ google/cloud/sqlalchemy_spanner/dml.py
9
10
  google/cloud/sqlalchemy_spanner/provision.py
10
11
  google/cloud/sqlalchemy_spanner/requirements.py
11
12
  google/cloud/sqlalchemy_spanner/sqlalchemy_spanner.py
@@ -1300,6 +1300,9 @@ class NumericTest(_NumericTest):
1300
1300
  filter_=lambda n: n is not None and round(n, 5) or None,
1301
1301
  )
1302
1302
 
1303
+ def test_float_coerce_round_trip(self, connection):
1304
+ pass
1305
+
1303
1306
  @requires.precision_numerics_general
1304
1307
  def test_precision_decimal(self):
1305
1308
  """
@@ -2065,6 +2068,49 @@ class CreateEngineWithoutDatabaseTest(fixtures.TestBase):
2065
2068
  assert connection.connection.database is None
2066
2069
 
2067
2070
 
2071
+ class ReturningTest(fixtures.TestBase):
2072
+ def setUp(self):
2073
+ self._engine = create_engine(get_db_url())
2074
+ metadata = MetaData()
2075
+
2076
+ self._table = Table(
2077
+ "returning_test",
2078
+ metadata,
2079
+ Column("id", Integer, primary_key=True),
2080
+ Column("data", String(16), nullable=False),
2081
+ )
2082
+
2083
+ metadata.create_all(self._engine)
2084
+
2085
+ def test_returning_for_insert_and_update(self):
2086
+ random_id = random.randint(1, 1000)
2087
+ with self._engine.begin() as connection:
2088
+ stmt = (
2089
+ self._table.insert()
2090
+ .values(id=random_id, data="some % value")
2091
+ .returning(self._table.c.id)
2092
+ )
2093
+ row = connection.execute(stmt).fetchall()
2094
+ eq_(
2095
+ row,
2096
+ [(random_id,)],
2097
+ )
2098
+
2099
+ with self._engine.begin() as connection:
2100
+ update_text = "some + value"
2101
+ stmt = (
2102
+ self._table.update()
2103
+ .values(data=update_text)
2104
+ .where(self._table.c.id == random_id)
2105
+ .returning(self._table.c.data)
2106
+ )
2107
+ row = connection.execute(stmt).fetchall()
2108
+ eq_(
2109
+ row,
2110
+ [(update_text,)],
2111
+ )
2112
+
2113
+
2068
2114
  @pytest.mark.skipif(
2069
2115
  bool(os.environ.get("SPANNER_EMULATOR_HOST")), reason="Skipped on emulator"
2070
2116
  )
@@ -27,7 +27,7 @@ from unittest import mock
27
27
  from google.cloud.spanner_v1 import RequestOptions, Client
28
28
 
29
29
  import sqlalchemy
30
- from sqlalchemy import create_engine
30
+ from sqlalchemy import create_engine, literal, FLOAT
31
31
  from sqlalchemy import inspect
32
32
  from sqlalchemy import testing
33
33
  from sqlalchemy import ForeignKey
@@ -53,6 +53,7 @@ from sqlalchemy import Boolean
53
53
  from sqlalchemy import Float
54
54
  from sqlalchemy import LargeBinary
55
55
  from sqlalchemy import String
56
+ from sqlalchemy.sql.expression import cast
56
57
  from sqlalchemy.ext.declarative import declarative_base
57
58
  from sqlalchemy.orm import relation
58
59
  from sqlalchemy.orm import Session
@@ -1650,6 +1651,13 @@ class NumericTest(_NumericTest):
1650
1651
  filter_=lambda n: n is not None and round(n, 5) or None,
1651
1652
  )
1652
1653
 
1654
+ @testing.requires.literal_float_coercion
1655
+ def test_float_coerce_round_trip(self, connection):
1656
+ expr = 15.7563
1657
+
1658
+ val = connection.scalar(select(cast(literal(expr), FLOAT)))
1659
+ eq_(val, expr)
1660
+
1653
1661
  @requires.precision_numerics_general
1654
1662
  def test_precision_decimal(self, do_numeric_test):
1655
1663
  """
@@ -2399,6 +2407,49 @@ class CreateEngineWithoutDatabaseTest(fixtures.TestBase):
2399
2407
  assert connection.connection.database is None
2400
2408
 
2401
2409
 
2410
+ class ReturningTest(fixtures.TestBase):
2411
+ def setUp(self):
2412
+ self._engine = create_engine(get_db_url())
2413
+ metadata = MetaData()
2414
+
2415
+ self._table = Table(
2416
+ "returning_test",
2417
+ metadata,
2418
+ Column("id", Integer, primary_key=True),
2419
+ Column("data", String(16), nullable=False),
2420
+ )
2421
+
2422
+ metadata.create_all(self._engine)
2423
+
2424
+ def test_returning_for_insert_and_update(self):
2425
+ random_id = random.randint(1, 1000)
2426
+ with self._engine.begin() as connection:
2427
+ stmt = (
2428
+ self._table.insert()
2429
+ .values(id=random_id, data="some % value")
2430
+ .returning(self._table.c.id)
2431
+ )
2432
+ row = connection.execute(stmt).fetchall()
2433
+ eq_(
2434
+ row,
2435
+ [(random_id,)],
2436
+ )
2437
+
2438
+ with self._engine.begin() as connection:
2439
+ update_text = "some + value"
2440
+ stmt = (
2441
+ self._table.update()
2442
+ .values(data=update_text)
2443
+ .where(self._table.c.id == random_id)
2444
+ .returning(self._table.c.data)
2445
+ )
2446
+ row = connection.execute(stmt).fetchall()
2447
+ eq_(
2448
+ row,
2449
+ [(update_text,)],
2450
+ )
2451
+
2452
+
2402
2453
  @pytest.mark.skipif(
2403
2454
  bool(os.environ.get("SPANNER_EMULATOR_HOST")), reason="Skipped on emulator"
2404
2455
  )
@@ -26,7 +26,7 @@ from unittest import mock
26
26
 
27
27
  from google.cloud.spanner_v1 import RequestOptions, Client
28
28
  import sqlalchemy
29
- from sqlalchemy import create_engine
29
+ from sqlalchemy import create_engine, literal, FLOAT
30
30
  from sqlalchemy.engine import Inspector
31
31
  from sqlalchemy import inspect
32
32
  from sqlalchemy import testing
@@ -55,6 +55,7 @@ from sqlalchemy import Boolean
55
55
  from sqlalchemy import Float
56
56
  from sqlalchemy import LargeBinary
57
57
  from sqlalchemy import String
58
+ from sqlalchemy.sql.expression import cast
58
59
  from sqlalchemy.ext.declarative import declarative_base
59
60
  from sqlalchemy.orm import relationship
60
61
  from sqlalchemy.orm import Session
@@ -80,7 +81,9 @@ from sqlalchemy.testing.suite.test_insert import * # noqa: F401, F403
80
81
  from sqlalchemy.testing.suite.test_reflection import * # noqa: F401, F403
81
82
  from sqlalchemy.testing.suite.test_deprecations import * # noqa: F401, F403
82
83
  from sqlalchemy.testing.suite.test_results import * # noqa: F401, F403
83
- from sqlalchemy.testing.suite.test_select import * # noqa: F401, F403
84
+ from sqlalchemy.testing.suite.test_select import (
85
+ BitwiseTest as _BitwiseTest,
86
+ ) # noqa: F401, F403
84
87
  from sqlalchemy.testing.suite.test_sequence import (
85
88
  SequenceTest as _SequenceTest,
86
89
  HasSequenceTest as _HasSequenceTest,
@@ -192,6 +195,12 @@ class BooleanTest(_BooleanTest):
192
195
  pass
193
196
 
194
197
 
198
+ class BitwiseTest(_BitwiseTest):
199
+ @pytest.mark.skip("Causes too many problems with other tests")
200
+ def test_bitwise(self, case, expected, connection):
201
+ pass
202
+
203
+
195
204
  class ComponentReflectionTestExtra(_ComponentReflectionTestExtra):
196
205
  @testing.requires.table_reflection
197
206
  def test_nullable_reflection(self, connection, metadata):
@@ -1018,6 +1027,10 @@ class ComponentReflectionTest(_ComponentReflectionTest):
1018
1027
  tables to be read, and in Spanner all the tables are real,
1019
1028
  expected results override is required.
1020
1029
  """
1030
+ _ignore_tables = [
1031
+ "bitwise",
1032
+ ]
1033
+
1021
1034
  insp, kws, exp = get_multi_exp(
1022
1035
  schema,
1023
1036
  scope,
@@ -1030,6 +1043,8 @@ class ComponentReflectionTest(_ComponentReflectionTest):
1030
1043
  for kw in kws:
1031
1044
  insp.clear_cache()
1032
1045
  result = insp.get_multi_columns(**kw)
1046
+ for t in _ignore_tables:
1047
+ result.pop((schema, t), None)
1033
1048
  self._check_table_dict(result, exp, self._required_column_keys)
1034
1049
 
1035
1050
  @pytest.mark.skip(
@@ -1097,6 +1112,7 @@ class ComponentReflectionTest(_ComponentReflectionTest):
1097
1112
  _ignore_tables = [
1098
1113
  "account",
1099
1114
  "alembic_version",
1115
+ "bitwise",
1100
1116
  "bytes_table",
1101
1117
  "comment_test",
1102
1118
  "date_table",
@@ -1306,6 +1322,7 @@ class ComponentReflectionTest(_ComponentReflectionTest):
1306
1322
  expected results override is required.
1307
1323
  """
1308
1324
  _ignore_tables = [
1325
+ "bitwise",
1309
1326
  "comment_test",
1310
1327
  "noncol_idx_test_pk",
1311
1328
  "noncol_idx_test_nopk",
@@ -1398,7 +1415,7 @@ class ComponentReflectionTest(_ComponentReflectionTest):
1398
1415
  "include_columns": [],
1399
1416
  }
1400
1417
  if column_sorting:
1401
- res["column_sorting"] = {"q": "DESC"}
1418
+ res["column_sorting"] = {"q": "desc"}
1402
1419
  if duplicates:
1403
1420
  res["duplicates_constraint"] = name
1404
1421
  return [res]
@@ -1442,11 +1459,11 @@ class ComponentReflectionTest(_ComponentReflectionTest):
1442
1459
  *idx(
1443
1460
  "q",
1444
1461
  name="noncol_idx_nopk",
1445
- column_sorting={"q": "DESC"},
1462
+ column_sorting={"q": "desc"},
1446
1463
  )
1447
1464
  ],
1448
1465
  (schema, "noncol_idx_test_pk"): [
1449
- *idx("q", name="noncol_idx_pk", column_sorting={"q": "DESC"})
1466
+ *idx("q", name="noncol_idx_pk", column_sorting={"q": "desc"})
1450
1467
  ],
1451
1468
  (schema, self.temp_table_name()): [
1452
1469
  *idx("foo", name="user_tmp_ix"),
@@ -1688,7 +1705,7 @@ class DateTimeMicrosecondsTest(_DateTimeMicrosecondsTest, DateTest):
1688
1705
  connection.execute(date_table.insert(), {"date_data": self.data, "id": 250})
1689
1706
  row = connection.execute(select(date_table.c.date_data)).first()
1690
1707
 
1691
- compare = self.compare or self.data
1708
+ compare = self.compare or self.data.astimezone(timezone.utc)
1692
1709
  compare = compare.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
1693
1710
  eq_(row[0].rfc3339(), compare)
1694
1711
  assert isinstance(row[0], DatetimeWithNanoseconds)
@@ -1708,7 +1725,7 @@ class DateTimeMicrosecondsTest(_DateTimeMicrosecondsTest, DateTest):
1708
1725
 
1709
1726
  row = connection.execute(select(date_table.c.decorated_date_data)).first()
1710
1727
 
1711
- compare = self.compare or self.data
1728
+ compare = self.compare or self.data.astimezone(timezone.utc)
1712
1729
  compare = compare.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
1713
1730
  eq_(row[0].rfc3339(), compare)
1714
1731
  assert isinstance(row[0], DatetimeWithNanoseconds)
@@ -2105,7 +2122,7 @@ class RowFetchTest(_RowFetchTest):
2105
2122
 
2106
2123
  eq_(
2107
2124
  row.somelabel,
2108
- DatetimeWithNanoseconds(2006, 5, 12, 12, 0, 0, tzinfo=timezone.utc),
2125
+ DatetimeWithNanoseconds(2006, 5, 12, 12, 0, 0).astimezone(timezone.utc),
2109
2126
  )
2110
2127
 
2111
2128
 
@@ -2155,6 +2172,32 @@ class InsertBehaviorTest(_InsertBehaviorTest):
2155
2172
  assert r.is_insert
2156
2173
  assert not r.returns_rows
2157
2174
 
2175
+ def test_autoclose_on_insert_implicit_returning(self, connection):
2176
+ """
2177
+ SPANNER OVERRIDE:
2178
+
2179
+ Cloud Spanner doesn't support tables with an auto increment primary key,
2180
+ following insertions will fail with `400 id must not be NULL in table
2181
+ autoinc_pk`.
2182
+
2183
+ Overriding the tests and adding a manual primary key value to avoid the same
2184
+ failures.
2185
+ """
2186
+ r = connection.execute(
2187
+ # return_defaults() ensures RETURNING will be used,
2188
+ # new in 2.0 as sqlite/mariadb offer both RETURNING and
2189
+ # cursor.lastrowid
2190
+ self.tables.autoinc_pk.insert().return_defaults(),
2191
+ dict(id=2, data="some data"),
2192
+ )
2193
+ assert r._soft_closed
2194
+ assert not r.closed
2195
+ assert r.is_insert
2196
+
2197
+ # Spanner does not return any rows in this case, because the primary key
2198
+ # is not auto-generated.
2199
+ assert not r.returns_rows
2200
+
2158
2201
 
2159
2202
  class BytesTest(_LiteralRoundTripFixture, fixtures.TestBase):
2160
2203
  __backend__ = True
@@ -2413,6 +2456,13 @@ class NumericTest(_NumericTest):
2413
2456
  filter_=lambda n: n is not None and round(n, 5) or None,
2414
2457
  )
2415
2458
 
2459
+ @testing.requires.literal_float_coercion
2460
+ def test_float_coerce_round_trip(self, connection):
2461
+ expr = 15.7563
2462
+
2463
+ val = connection.scalar(select(cast(literal(expr), FLOAT)))
2464
+ eq_(val, expr)
2465
+
2416
2466
  @requires.precision_numerics_general
2417
2467
  def test_precision_decimal(self, do_numeric_test):
2418
2468
  """
@@ -3097,6 +3147,49 @@ class CreateEngineWithoutDatabaseTest(fixtures.TestBase):
3097
3147
  assert connection.connection.database is None
3098
3148
 
3099
3149
 
3150
+ class ReturningTest(fixtures.TestBase):
3151
+ def setUp(self):
3152
+ self._engine = create_engine(get_db_url())
3153
+ metadata = MetaData()
3154
+
3155
+ self._table = Table(
3156
+ "returning_test",
3157
+ metadata,
3158
+ Column("id", Integer, primary_key=True),
3159
+ Column("data", String(16), nullable=False),
3160
+ )
3161
+
3162
+ metadata.create_all(self._engine)
3163
+
3164
+ def test_returning_for_insert_and_update(self):
3165
+ random_id = random.randint(1, 1000)
3166
+ with self._engine.begin() as connection:
3167
+ stmt = (
3168
+ self._table.insert()
3169
+ .values(id=random_id, data="some % value")
3170
+ .returning(self._table.c.id)
3171
+ )
3172
+ row = connection.execute(stmt).fetchall()
3173
+ eq_(
3174
+ row,
3175
+ [(random_id,)],
3176
+ )
3177
+
3178
+ with self._engine.begin() as connection:
3179
+ update_text = "some + value"
3180
+ stmt = (
3181
+ self._table.update()
3182
+ .values(data=update_text)
3183
+ .where(self._table.c.id == random_id)
3184
+ .returning(self._table.c.data)
3185
+ )
3186
+ row = connection.execute(stmt).fetchall()
3187
+ eq_(
3188
+ row,
3189
+ [(update_text,)],
3190
+ )
3191
+
3192
+
3100
3193
  @pytest.mark.skipif(
3101
3194
  bool(os.environ.get("SPANNER_EMULATOR_HOST")), reason="Skipped on emulator"
3102
3195
  )