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.
- {sqlalchemy_spanner-1.7.0 → sqlalchemy_spanner-1.9.0}/PKG-INFO +32 -16
- {sqlalchemy_spanner-1.7.0 → sqlalchemy_spanner-1.9.0}/README.rst +31 -15
- sqlalchemy_spanner-1.9.0/google/cloud/sqlalchemy_spanner/dml.py +26 -0
- {sqlalchemy_spanner-1.7.0 → sqlalchemy_spanner-1.9.0}/google/cloud/sqlalchemy_spanner/sqlalchemy_spanner.py +108 -11
- {sqlalchemy_spanner-1.7.0 → sqlalchemy_spanner-1.9.0}/setup.py +25 -21
- {sqlalchemy_spanner-1.7.0 → sqlalchemy_spanner-1.9.0}/sqlalchemy_spanner.egg-info/PKG-INFO +32 -16
- {sqlalchemy_spanner-1.7.0 → sqlalchemy_spanner-1.9.0}/sqlalchemy_spanner.egg-info/SOURCES.txt +1 -0
- {sqlalchemy_spanner-1.7.0 → sqlalchemy_spanner-1.9.0}/test/test_suite_13.py +46 -0
- {sqlalchemy_spanner-1.7.0 → sqlalchemy_spanner-1.9.0}/test/test_suite_14.py +52 -1
- {sqlalchemy_spanner-1.7.0 → sqlalchemy_spanner-1.9.0}/test/test_suite_20.py +101 -8
- {sqlalchemy_spanner-1.7.0 → sqlalchemy_spanner-1.9.0}/LICENSE +0 -0
- {sqlalchemy_spanner-1.7.0 → sqlalchemy_spanner-1.9.0}/google/__init__.py +0 -0
- {sqlalchemy_spanner-1.7.0 → sqlalchemy_spanner-1.9.0}/google/cloud/__init__.py +0 -0
- {sqlalchemy_spanner-1.7.0 → sqlalchemy_spanner-1.9.0}/google/cloud/sqlalchemy_spanner/__init__.py +0 -0
- {sqlalchemy_spanner-1.7.0 → sqlalchemy_spanner-1.9.0}/google/cloud/sqlalchemy_spanner/_opentelemetry_tracing.py +0 -0
- {sqlalchemy_spanner-1.7.0 → sqlalchemy_spanner-1.9.0}/google/cloud/sqlalchemy_spanner/provision.py +0 -0
- {sqlalchemy_spanner-1.7.0 → sqlalchemy_spanner-1.9.0}/google/cloud/sqlalchemy_spanner/requirements.py +0 -0
- {sqlalchemy_spanner-1.7.0 → sqlalchemy_spanner-1.9.0}/setup.cfg +0 -0
- {sqlalchemy_spanner-1.7.0 → sqlalchemy_spanner-1.9.0}/sqlalchemy_spanner.egg-info/dependency_links.txt +0 -0
- {sqlalchemy_spanner-1.7.0 → sqlalchemy_spanner-1.9.0}/sqlalchemy_spanner.egg-info/entry_points.txt +0 -0
- {sqlalchemy_spanner-1.7.0 → sqlalchemy_spanner-1.9.0}/sqlalchemy_spanner.egg-info/namespace_packages.txt +0 -0
- {sqlalchemy_spanner-1.7.0 → sqlalchemy_spanner-1.9.0}/sqlalchemy_spanner.egg-info/not-zip-safe +0 -0
- {sqlalchemy_spanner-1.7.0 → sqlalchemy_spanner-1.9.0}/sqlalchemy_spanner.egg-info/requires.txt +0 -0
- {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.
|
|
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
|
|
279
|
-
|
|
280
|
-
corresponds to automatically committing
|
|
281
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
358
|
-
|
|
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
|
-
|
|
368
|
-
|
|
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 -
|
|
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
|
|
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
|
|
262
|
-
|
|
263
|
-
corresponds to automatically committing
|
|
264
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
341
|
-
|
|
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
|
-
|
|
351
|
-
|
|
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 -
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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.
|
|
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
|
|
279
|
-
|
|
280
|
-
corresponds to automatically committing
|
|
281
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
358
|
-
|
|
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
|
-
|
|
368
|
-
|
|
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 -
|
|
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
|
|
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.
|
{sqlalchemy_spanner-1.7.0 → sqlalchemy_spanner-1.9.0}/sqlalchemy_spanner.egg-info/SOURCES.txt
RENAMED
|
@@ -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
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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
|
|
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
|
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sqlalchemy_spanner-1.7.0 → sqlalchemy_spanner-1.9.0}/google/cloud/sqlalchemy_spanner/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{sqlalchemy_spanner-1.7.0 → sqlalchemy_spanner-1.9.0}/google/cloud/sqlalchemy_spanner/provision.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sqlalchemy_spanner-1.7.0 → sqlalchemy_spanner-1.9.0}/sqlalchemy_spanner.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
{sqlalchemy_spanner-1.7.0 → sqlalchemy_spanner-1.9.0}/sqlalchemy_spanner.egg-info/not-zip-safe
RENAMED
|
File without changes
|
{sqlalchemy_spanner-1.7.0 → sqlalchemy_spanner-1.9.0}/sqlalchemy_spanner.egg-info/requires.txt
RENAMED
|
File without changes
|
{sqlalchemy_spanner-1.7.0 → sqlalchemy_spanner-1.9.0}/sqlalchemy_spanner.egg-info/top_level.txt
RENAMED
|
File without changes
|