SQLAlchemy 2.0.43__py3-none-any.whl → 2.0.44__py3-none-any.whl
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/__init__.py +1 -1
- sqlalchemy/connectors/asyncio.py +81 -3
- sqlalchemy/dialects/mssql/base.py +5 -0
- sqlalchemy/dialects/mysql/aiomysql.py +11 -5
- sqlalchemy/dialects/mysql/asyncmy.py +11 -5
- sqlalchemy/dialects/oracle/oracledb.py +2 -4
- sqlalchemy/dialects/postgresql/array.py +15 -5
- sqlalchemy/dialects/postgresql/asyncpg.py +18 -27
- sqlalchemy/dialects/postgresql/json.py +28 -0
- sqlalchemy/dialects/postgresql/psycopg.py +3 -0
- sqlalchemy/dialects/sqlite/aiosqlite.py +3 -0
- sqlalchemy/dialects/sqlite/base.py +36 -13
- sqlalchemy/dialects/sqlite/pysqlite.py +68 -38
- sqlalchemy/event/__init__.py +1 -0
- sqlalchemy/event/attr.py +35 -14
- sqlalchemy/event/legacy.py +15 -3
- sqlalchemy/ext/asyncio/engine.py +4 -2
- sqlalchemy/ext/asyncio/result.py +3 -0
- sqlalchemy/ext/asyncio/scoping.py +0 -14
- sqlalchemy/ext/asyncio/session.py +0 -14
- sqlalchemy/ext/hybrid.py +8 -6
- sqlalchemy/ext/indexable.py +37 -19
- sqlalchemy/ext/mutable.py +26 -37
- sqlalchemy/orm/__init__.py +1 -0
- sqlalchemy/orm/decl_api.py +95 -11
- sqlalchemy/orm/events.py +60 -77
- sqlalchemy/orm/properties.py +39 -11
- sqlalchemy/orm/query.py +16 -10
- sqlalchemy/orm/scoping.py +0 -14
- sqlalchemy/orm/session.py +0 -14
- sqlalchemy/orm/writeonly.py +2 -2
- sqlalchemy/sql/dml.py +19 -6
- sqlalchemy/sql/elements.py +36 -1
- sqlalchemy/sql/functions.py +1 -1
- sqlalchemy/sql/schema.py +7 -3
- sqlalchemy/sql/selectable.py +40 -24
- sqlalchemy/sql/sqltypes.py +3 -3
- sqlalchemy/testing/plugin/pytestplugin.py +9 -3
- sqlalchemy/testing/profiling.py +3 -0
- sqlalchemy/testing/requirements.py +12 -1
- sqlalchemy/testing/suite/test_reflection.py +20 -0
- sqlalchemy/util/__init__.py +2 -0
- sqlalchemy/util/compat.py +13 -0
- sqlalchemy/util/concurrency.py +2 -0
- sqlalchemy/util/langhelpers.py +8 -5
- sqlalchemy/util/typing.py +3 -2
- {sqlalchemy-2.0.43.dist-info → sqlalchemy-2.0.44.dist-info}/METADATA +2 -2
- {sqlalchemy-2.0.43.dist-info → sqlalchemy-2.0.44.dist-info}/RECORD +51 -51
- {sqlalchemy-2.0.43.dist-info → sqlalchemy-2.0.44.dist-info}/WHEEL +0 -0
- {sqlalchemy-2.0.43.dist-info → sqlalchemy-2.0.44.dist-info}/licenses/LICENSE +0 -0
- {sqlalchemy-2.0.43.dist-info → sqlalchemy-2.0.44.dist-info}/top_level.txt +0 -0
sqlalchemy/__init__.py
CHANGED
sqlalchemy/connectors/asyncio.py
CHANGED
|
@@ -19,11 +19,15 @@ from typing import Iterator
|
|
|
19
19
|
from typing import NoReturn
|
|
20
20
|
from typing import Optional
|
|
21
21
|
from typing import Sequence
|
|
22
|
+
from typing import Tuple
|
|
23
|
+
from typing import Type
|
|
22
24
|
from typing import TYPE_CHECKING
|
|
23
25
|
|
|
24
26
|
from ..engine import AdaptedConnection
|
|
27
|
+
from ..util import EMPTY_DICT
|
|
25
28
|
from ..util.concurrency import await_fallback
|
|
26
29
|
from ..util.concurrency import await_only
|
|
30
|
+
from ..util.concurrency import in_greenlet
|
|
27
31
|
from ..util.typing import Protocol
|
|
28
32
|
|
|
29
33
|
if TYPE_CHECKING:
|
|
@@ -129,8 +133,11 @@ class AsyncAdapt_dbapi_cursor:
|
|
|
129
133
|
"await_",
|
|
130
134
|
"_cursor",
|
|
131
135
|
"_rows",
|
|
136
|
+
"_soft_closed_memoized",
|
|
132
137
|
)
|
|
133
138
|
|
|
139
|
+
_awaitable_cursor_close: bool = True
|
|
140
|
+
|
|
134
141
|
_cursor: AsyncIODBAPICursor
|
|
135
142
|
_adapt_connection: AsyncAdapt_dbapi_connection
|
|
136
143
|
_connection: AsyncIODBAPIConnection
|
|
@@ -144,7 +151,7 @@ class AsyncAdapt_dbapi_cursor:
|
|
|
144
151
|
|
|
145
152
|
cursor = self._make_new_cursor(self._connection)
|
|
146
153
|
self._cursor = self._aenter_cursor(cursor)
|
|
147
|
-
|
|
154
|
+
self._soft_closed_memoized = EMPTY_DICT
|
|
148
155
|
if not self.server_side:
|
|
149
156
|
self._rows = collections.deque()
|
|
150
157
|
|
|
@@ -158,6 +165,8 @@ class AsyncAdapt_dbapi_cursor:
|
|
|
158
165
|
|
|
159
166
|
@property
|
|
160
167
|
def description(self) -> Optional[_DBAPICursorDescription]:
|
|
168
|
+
if "description" in self._soft_closed_memoized:
|
|
169
|
+
return self._soft_closed_memoized["description"] # type: ignore[no-any-return] # noqa: E501
|
|
161
170
|
return self._cursor.description
|
|
162
171
|
|
|
163
172
|
@property
|
|
@@ -176,11 +185,40 @@ class AsyncAdapt_dbapi_cursor:
|
|
|
176
185
|
def lastrowid(self) -> int:
|
|
177
186
|
return self._cursor.lastrowid
|
|
178
187
|
|
|
188
|
+
async def _async_soft_close(self) -> None:
|
|
189
|
+
"""close the cursor but keep the results pending, and memoize the
|
|
190
|
+
description.
|
|
191
|
+
|
|
192
|
+
.. versionadded:: 2.0.44
|
|
193
|
+
|
|
194
|
+
"""
|
|
195
|
+
|
|
196
|
+
if not self._awaitable_cursor_close or self.server_side:
|
|
197
|
+
return
|
|
198
|
+
|
|
199
|
+
self._soft_closed_memoized = self._soft_closed_memoized.union(
|
|
200
|
+
{
|
|
201
|
+
"description": self._cursor.description,
|
|
202
|
+
}
|
|
203
|
+
)
|
|
204
|
+
await self._cursor.close()
|
|
205
|
+
|
|
179
206
|
def close(self) -> None:
|
|
180
|
-
# note we aren't actually closing the cursor here,
|
|
181
|
-
# we are just letting GC do it. see notes in aiomysql dialect
|
|
182
207
|
self._rows.clear()
|
|
183
208
|
|
|
209
|
+
# updated as of 2.0.44
|
|
210
|
+
# try to "close" the cursor based on what we know about the driver
|
|
211
|
+
# and if we are able to. otherwise, hope that the asyncio
|
|
212
|
+
# extension called _async_soft_close() if the cursor is going into
|
|
213
|
+
# a sync context
|
|
214
|
+
if self._cursor is None or bool(self._soft_closed_memoized):
|
|
215
|
+
return
|
|
216
|
+
|
|
217
|
+
if not self._awaitable_cursor_close:
|
|
218
|
+
self._cursor.close() # type: ignore[unused-coroutine]
|
|
219
|
+
elif in_greenlet():
|
|
220
|
+
self.await_(self._cursor.close())
|
|
221
|
+
|
|
184
222
|
def execute(
|
|
185
223
|
self,
|
|
186
224
|
operation: Any,
|
|
@@ -349,3 +387,43 @@ class AsyncAdaptFallback_dbapi_connection(AsyncAdapt_dbapi_connection):
|
|
|
349
387
|
__slots__ = ()
|
|
350
388
|
|
|
351
389
|
await_ = staticmethod(await_fallback)
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
class AsyncAdapt_terminate:
|
|
393
|
+
"""Mixin for a AsyncAdapt_dbapi_connection to add terminate support."""
|
|
394
|
+
|
|
395
|
+
__slots__ = ()
|
|
396
|
+
|
|
397
|
+
def terminate(self) -> None:
|
|
398
|
+
if in_greenlet():
|
|
399
|
+
# in a greenlet; this is the connection was invalidated case.
|
|
400
|
+
try:
|
|
401
|
+
# try to gracefully close; see #10717
|
|
402
|
+
self.await_(asyncio.shield(self._terminate_graceful_close())) # type: ignore[attr-defined] # noqa: E501
|
|
403
|
+
except self._terminate_handled_exceptions() as e:
|
|
404
|
+
# in the case where we are recycling an old connection
|
|
405
|
+
# that may have already been disconnected, close() will
|
|
406
|
+
# fail. In this case, terminate
|
|
407
|
+
# the connection without any further waiting.
|
|
408
|
+
# see issue #8419
|
|
409
|
+
self._terminate_force_close()
|
|
410
|
+
if isinstance(e, asyncio.CancelledError):
|
|
411
|
+
# re-raise CancelledError if we were cancelled
|
|
412
|
+
raise
|
|
413
|
+
else:
|
|
414
|
+
# not in a greenlet; this is the gc cleanup case
|
|
415
|
+
self._terminate_force_close()
|
|
416
|
+
|
|
417
|
+
def _terminate_handled_exceptions(self) -> Tuple[Type[BaseException], ...]:
|
|
418
|
+
"""Returns the exceptions that should be handled when
|
|
419
|
+
calling _graceful_close.
|
|
420
|
+
"""
|
|
421
|
+
return (asyncio.TimeoutError, asyncio.CancelledError, OSError)
|
|
422
|
+
|
|
423
|
+
async def _terminate_graceful_close(self) -> None:
|
|
424
|
+
"""Try to close connection gracefully"""
|
|
425
|
+
raise NotImplementedError
|
|
426
|
+
|
|
427
|
+
def _terminate_force_close(self) -> None:
|
|
428
|
+
"""Terminate the connection"""
|
|
429
|
+
raise NotImplementedError
|
|
@@ -3503,6 +3503,9 @@ join sys.schemas as sch on
|
|
|
3503
3503
|
where
|
|
3504
3504
|
tab.name = :tabname
|
|
3505
3505
|
and sch.name = :schname
|
|
3506
|
+
order by
|
|
3507
|
+
ind_col.index_id,
|
|
3508
|
+
ind_col.key_ordinal
|
|
3506
3509
|
"""
|
|
3507
3510
|
)
|
|
3508
3511
|
.bindparams(
|
|
@@ -3998,6 +4001,8 @@ index_info AS (
|
|
|
3998
4001
|
index_info.index_schema = fk_info.unique_constraint_schema
|
|
3999
4002
|
AND index_info.index_name = fk_info.unique_constraint_name
|
|
4000
4003
|
AND index_info.ordinal_position = fk_info.ordinal_position
|
|
4004
|
+
AND NOT (index_info.table_schema = fk_info.table_schema
|
|
4005
|
+
AND index_info.table_name = fk_info.table_name)
|
|
4001
4006
|
|
|
4002
4007
|
ORDER BY fk_info.constraint_schema, fk_info.constraint_name,
|
|
4003
4008
|
fk_info.ordinal_position
|
|
@@ -45,6 +45,7 @@ from ...connectors.asyncio import AsyncAdapt_dbapi_connection
|
|
|
45
45
|
from ...connectors.asyncio import AsyncAdapt_dbapi_cursor
|
|
46
46
|
from ...connectors.asyncio import AsyncAdapt_dbapi_module
|
|
47
47
|
from ...connectors.asyncio import AsyncAdapt_dbapi_ss_cursor
|
|
48
|
+
from ...connectors.asyncio import AsyncAdapt_terminate
|
|
48
49
|
from ...util.concurrency import await_fallback
|
|
49
50
|
from ...util.concurrency import await_only
|
|
50
51
|
|
|
@@ -82,7 +83,9 @@ class AsyncAdapt_aiomysql_ss_cursor(
|
|
|
82
83
|
)
|
|
83
84
|
|
|
84
85
|
|
|
85
|
-
class AsyncAdapt_aiomysql_connection(
|
|
86
|
+
class AsyncAdapt_aiomysql_connection(
|
|
87
|
+
AsyncAdapt_terminate, AsyncAdapt_dbapi_connection
|
|
88
|
+
):
|
|
86
89
|
__slots__ = ()
|
|
87
90
|
|
|
88
91
|
_cursor_cls = AsyncAdapt_aiomysql_cursor
|
|
@@ -101,13 +104,16 @@ class AsyncAdapt_aiomysql_connection(AsyncAdapt_dbapi_connection):
|
|
|
101
104
|
def get_autocommit(self) -> bool:
|
|
102
105
|
return self._connection.get_autocommit() # type: ignore
|
|
103
106
|
|
|
104
|
-
def terminate(self) -> None:
|
|
105
|
-
# it's not awaitable.
|
|
106
|
-
self._connection.close()
|
|
107
|
-
|
|
108
107
|
def close(self) -> None:
|
|
109
108
|
self.await_(self._connection.ensure_closed())
|
|
110
109
|
|
|
110
|
+
async def _terminate_graceful_close(self) -> None:
|
|
111
|
+
await self._connection.ensure_closed()
|
|
112
|
+
|
|
113
|
+
def _terminate_force_close(self) -> None:
|
|
114
|
+
# it's not awaitable.
|
|
115
|
+
self._connection.close()
|
|
116
|
+
|
|
111
117
|
|
|
112
118
|
class AsyncAdaptFallback_aiomysql_connection(AsyncAdapt_aiomysql_connection):
|
|
113
119
|
__slots__ = ()
|
|
@@ -42,6 +42,7 @@ from ...connectors.asyncio import AsyncAdapt_dbapi_connection
|
|
|
42
42
|
from ...connectors.asyncio import AsyncAdapt_dbapi_cursor
|
|
43
43
|
from ...connectors.asyncio import AsyncAdapt_dbapi_module
|
|
44
44
|
from ...connectors.asyncio import AsyncAdapt_dbapi_ss_cursor
|
|
45
|
+
from ...connectors.asyncio import AsyncAdapt_terminate
|
|
45
46
|
from ...util.concurrency import await_fallback
|
|
46
47
|
from ...util.concurrency import await_only
|
|
47
48
|
|
|
@@ -73,7 +74,9 @@ class AsyncAdapt_asyncmy_ss_cursor(
|
|
|
73
74
|
)
|
|
74
75
|
|
|
75
76
|
|
|
76
|
-
class AsyncAdapt_asyncmy_connection(
|
|
77
|
+
class AsyncAdapt_asyncmy_connection(
|
|
78
|
+
AsyncAdapt_terminate, AsyncAdapt_dbapi_connection
|
|
79
|
+
):
|
|
77
80
|
__slots__ = ()
|
|
78
81
|
|
|
79
82
|
_cursor_cls = AsyncAdapt_asyncmy_cursor
|
|
@@ -107,13 +110,16 @@ class AsyncAdapt_asyncmy_connection(AsyncAdapt_dbapi_connection):
|
|
|
107
110
|
def get_autocommit(self) -> bool:
|
|
108
111
|
return self._connection.get_autocommit() # type: ignore
|
|
109
112
|
|
|
110
|
-
def terminate(self) -> None:
|
|
111
|
-
# it's not awaitable.
|
|
112
|
-
self._connection.close()
|
|
113
|
-
|
|
114
113
|
def close(self) -> None:
|
|
115
114
|
self.await_(self._connection.ensure_closed())
|
|
116
115
|
|
|
116
|
+
async def _terminate_graceful_close(self) -> None:
|
|
117
|
+
await self._connection.ensure_closed()
|
|
118
|
+
|
|
119
|
+
def _terminate_force_close(self) -> None:
|
|
120
|
+
# it's not awaitable.
|
|
121
|
+
self._connection.close()
|
|
122
|
+
|
|
117
123
|
|
|
118
124
|
class AsyncAdaptFallback_asyncmy_connection(AsyncAdapt_asyncmy_connection):
|
|
119
125
|
__slots__ = ()
|
|
@@ -736,6 +736,8 @@ class OracleDialect_oracledb(_cx_oracle.OracleDialect_cx_oracle):
|
|
|
736
736
|
|
|
737
737
|
class AsyncAdapt_oracledb_cursor(AsyncAdapt_dbapi_cursor):
|
|
738
738
|
_cursor: AsyncCursor
|
|
739
|
+
_awaitable_cursor_close: bool = False
|
|
740
|
+
|
|
739
741
|
__slots__ = ()
|
|
740
742
|
|
|
741
743
|
@property
|
|
@@ -749,10 +751,6 @@ class AsyncAdapt_oracledb_cursor(AsyncAdapt_dbapi_cursor):
|
|
|
749
751
|
def var(self, *args, **kwargs):
|
|
750
752
|
return self._cursor.var(*args, **kwargs)
|
|
751
753
|
|
|
752
|
-
def close(self):
|
|
753
|
-
self._rows.clear()
|
|
754
|
-
self._cursor.close()
|
|
755
|
-
|
|
756
754
|
def setinputsizes(self, *args: Any, **kwargs: Any) -> Any:
|
|
757
755
|
return self._cursor.setinputsizes(*args, **kwargs)
|
|
758
756
|
|
|
@@ -44,6 +44,7 @@ if TYPE_CHECKING:
|
|
|
44
44
|
|
|
45
45
|
|
|
46
46
|
_T = TypeVar("_T", bound=typing_Any)
|
|
47
|
+
_CT = TypeVar("_CT", bound=typing_Any)
|
|
47
48
|
|
|
48
49
|
|
|
49
50
|
def Any(
|
|
@@ -357,7 +358,7 @@ class ARRAY(sqltypes.ARRAY[_T]):
|
|
|
357
358
|
self.dimensions = dimensions
|
|
358
359
|
self.zero_indexes = zero_indexes
|
|
359
360
|
|
|
360
|
-
class Comparator(sqltypes.ARRAY.Comparator[
|
|
361
|
+
class Comparator(sqltypes.ARRAY.Comparator[_CT]):
|
|
361
362
|
"""Define comparison operations for :class:`_types.ARRAY`.
|
|
362
363
|
|
|
363
364
|
Note that these operations are in addition to those provided
|
|
@@ -464,7 +465,7 @@ class ARRAY(sqltypes.ARRAY[_T]):
|
|
|
464
465
|
super_rp = process
|
|
465
466
|
pattern = re.compile(r"^{(.*)}$")
|
|
466
467
|
|
|
467
|
-
def handle_raw_string(value: str) ->
|
|
468
|
+
def handle_raw_string(value: str) -> Sequence[Optional[str]]:
|
|
468
469
|
inner = pattern.match(value).group(1) # type: ignore[union-attr] # noqa: E501
|
|
469
470
|
return _split_enum_values(inner)
|
|
470
471
|
|
|
@@ -485,10 +486,13 @@ class ARRAY(sqltypes.ARRAY[_T]):
|
|
|
485
486
|
return process
|
|
486
487
|
|
|
487
488
|
|
|
488
|
-
def _split_enum_values(array_string: str) ->
|
|
489
|
+
def _split_enum_values(array_string: str) -> Sequence[Optional[str]]:
|
|
489
490
|
if '"' not in array_string:
|
|
490
491
|
# no escape char is present so it can just split on the comma
|
|
491
|
-
return
|
|
492
|
+
return [
|
|
493
|
+
r if r != "NULL" else None
|
|
494
|
+
for r in (array_string.split(",") if array_string else [])
|
|
495
|
+
]
|
|
492
496
|
|
|
493
497
|
# handles quoted strings from:
|
|
494
498
|
# r'abc,"quoted","also\\\\quoted", "quoted, comma", "esc \" quot", qpr'
|
|
@@ -505,5 +509,11 @@ def _split_enum_values(array_string: str) -> list[str]:
|
|
|
505
509
|
elif in_quotes:
|
|
506
510
|
result.append(tok.replace("_$ESC_QUOTE$_", '"'))
|
|
507
511
|
else:
|
|
508
|
-
|
|
512
|
+
# interpret NULL (without quotes!) as None
|
|
513
|
+
result.extend(
|
|
514
|
+
[
|
|
515
|
+
r if r != "NULL" else None
|
|
516
|
+
for r in re.findall(r"([^\s,]+),?", tok)
|
|
517
|
+
]
|
|
518
|
+
)
|
|
509
519
|
return result
|
|
@@ -205,6 +205,7 @@ from .types import CITEXT
|
|
|
205
205
|
from ... import exc
|
|
206
206
|
from ... import pool
|
|
207
207
|
from ... import util
|
|
208
|
+
from ...connectors.asyncio import AsyncAdapt_terminate
|
|
208
209
|
from ...engine import AdaptedConnection
|
|
209
210
|
from ...engine import processors
|
|
210
211
|
from ...sql import sqltypes
|
|
@@ -490,6 +491,7 @@ class AsyncAdapt_asyncpg_cursor:
|
|
|
490
491
|
)
|
|
491
492
|
|
|
492
493
|
server_side = False
|
|
494
|
+
_awaitable_cursor_close: bool = False
|
|
493
495
|
|
|
494
496
|
def __init__(self, adapt_connection):
|
|
495
497
|
self._adapt_connection = adapt_connection
|
|
@@ -501,6 +503,9 @@ class AsyncAdapt_asyncpg_cursor:
|
|
|
501
503
|
self.rowcount = -1
|
|
502
504
|
self._invalidate_schema_cache_asof = 0
|
|
503
505
|
|
|
506
|
+
async def _async_soft_close(self) -> None:
|
|
507
|
+
return
|
|
508
|
+
|
|
504
509
|
def close(self):
|
|
505
510
|
self._rows.clear()
|
|
506
511
|
|
|
@@ -691,7 +696,7 @@ class AsyncAdapt_asyncpg_ss_cursor(AsyncAdapt_asyncpg_cursor):
|
|
|
691
696
|
)
|
|
692
697
|
|
|
693
698
|
|
|
694
|
-
class AsyncAdapt_asyncpg_connection(AdaptedConnection):
|
|
699
|
+
class AsyncAdapt_asyncpg_connection(AsyncAdapt_terminate, AdaptedConnection):
|
|
695
700
|
__slots__ = (
|
|
696
701
|
"dbapi",
|
|
697
702
|
"isolation_level",
|
|
@@ -897,32 +902,18 @@ class AsyncAdapt_asyncpg_connection(AdaptedConnection):
|
|
|
897
902
|
|
|
898
903
|
self.await_(self._connection.close())
|
|
899
904
|
|
|
900
|
-
def
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
self.dbapi.asyncpg.PostgresError,
|
|
913
|
-
) as e:
|
|
914
|
-
# in the case where we are recycling an old connection
|
|
915
|
-
# that may have already been disconnected, close() will
|
|
916
|
-
# fail with the above timeout. in this case, terminate
|
|
917
|
-
# the connection without any further waiting.
|
|
918
|
-
# see issue #8419
|
|
919
|
-
self._connection.terminate()
|
|
920
|
-
if isinstance(e, asyncio.CancelledError):
|
|
921
|
-
# re-raise CancelledError if we were cancelled
|
|
922
|
-
raise
|
|
923
|
-
else:
|
|
924
|
-
# not in a greenlet; this is the gc cleanup case
|
|
925
|
-
self._connection.terminate()
|
|
905
|
+
def _terminate_handled_exceptions(self):
|
|
906
|
+
return super()._terminate_handled_exceptions() + (
|
|
907
|
+
self.dbapi.asyncpg.PostgresError,
|
|
908
|
+
)
|
|
909
|
+
|
|
910
|
+
async def _terminate_graceful_close(self) -> None:
|
|
911
|
+
# timeout added in asyncpg 0.14.0 December 2017
|
|
912
|
+
await self._connection.close(timeout=2)
|
|
913
|
+
self._started = False
|
|
914
|
+
|
|
915
|
+
def _terminate_force_close(self) -> None:
|
|
916
|
+
self._connection.terminate()
|
|
926
917
|
self._started = False
|
|
927
918
|
|
|
928
919
|
@staticmethod
|
|
@@ -279,6 +279,34 @@ class JSONB(JSON):
|
|
|
279
279
|
|
|
280
280
|
:class:`_types.JSON`
|
|
281
281
|
|
|
282
|
+
.. warning::
|
|
283
|
+
|
|
284
|
+
**For applications that have indexes against JSONB subscript
|
|
285
|
+
expressions**
|
|
286
|
+
|
|
287
|
+
SQLAlchemy 2.0.42 made a change in how the subscript operation for
|
|
288
|
+
:class:`.JSONB` is rendered, from ``-> 'element'`` to ``['element']``,
|
|
289
|
+
for PostgreSQL versions greater than 14. This change caused an
|
|
290
|
+
unintended side effect for indexes that were created against
|
|
291
|
+
expressions that use subscript notation, e.g.
|
|
292
|
+
``Index("ix_entity_json_ab_text", data["a"]["b"].astext)``. If these
|
|
293
|
+
indexes were generated with the older syntax e.g. ``((entity.data ->
|
|
294
|
+
'a') ->> 'b')``, they will not be used by the PostgreSQL query planner
|
|
295
|
+
when a query is made using SQLAlchemy 2.0.42 or higher on PostgreSQL
|
|
296
|
+
versions 14 or higher. This occurs because the new text will resemble
|
|
297
|
+
``(entity.data['a'] ->> 'b')`` which will fail to produce the exact
|
|
298
|
+
textual syntax match required by the PostgreSQL query planner.
|
|
299
|
+
Therefore, for users upgrading to SQLAlchemy 2.0.42 or higher, existing
|
|
300
|
+
indexes that were created against :class:`.JSONB` expressions that use
|
|
301
|
+
subscripting would need to be dropped and re-created in order for them
|
|
302
|
+
to work with the new query syntax, e.g. an expression like
|
|
303
|
+
``((entity.data -> 'a') ->> 'b')`` would become ``(entity.data['a'] ->>
|
|
304
|
+
'b')``.
|
|
305
|
+
|
|
306
|
+
.. seealso::
|
|
307
|
+
|
|
308
|
+
:ticket:`12868` - discussion of this issue
|
|
309
|
+
|
|
282
310
|
"""
|
|
283
311
|
|
|
284
312
|
__visit_name__ = "JSONB"
|
|
@@ -585,6 +585,9 @@ class AsyncAdapt_psycopg_cursor:
|
|
|
585
585
|
def arraysize(self, value):
|
|
586
586
|
self._cursor.arraysize = value
|
|
587
587
|
|
|
588
|
+
async def _async_soft_close(self) -> None:
|
|
589
|
+
return
|
|
590
|
+
|
|
588
591
|
def close(self):
|
|
589
592
|
self._rows.clear()
|
|
590
593
|
# Normal cursor just call _close() in a non-sync way.
|
|
@@ -141,6 +141,9 @@ class AsyncAdapt_aiosqlite_cursor:
|
|
|
141
141
|
self.description: Optional[_DBAPICursorDescription] = None
|
|
142
142
|
self._rows: Deque[Any] = deque()
|
|
143
143
|
|
|
144
|
+
async def _async_soft_close(self) -> None:
|
|
145
|
+
return
|
|
146
|
+
|
|
144
147
|
def close(self) -> None:
|
|
145
148
|
self._rows.clear()
|
|
146
149
|
|
|
@@ -992,7 +992,10 @@ from __future__ import annotations
|
|
|
992
992
|
import datetime
|
|
993
993
|
import numbers
|
|
994
994
|
import re
|
|
995
|
+
from typing import Any
|
|
996
|
+
from typing import Callable
|
|
995
997
|
from typing import Optional
|
|
998
|
+
from typing import TYPE_CHECKING
|
|
996
999
|
|
|
997
1000
|
from .json import JSON
|
|
998
1001
|
from .json import JSONIndexType
|
|
@@ -1025,6 +1028,13 @@ from ...types import TEXT # noqa
|
|
|
1025
1028
|
from ...types import TIMESTAMP # noqa
|
|
1026
1029
|
from ...types import VARCHAR # noqa
|
|
1027
1030
|
|
|
1031
|
+
if TYPE_CHECKING:
|
|
1032
|
+
from ...engine.interfaces import DBAPIConnection
|
|
1033
|
+
from ...engine.interfaces import Dialect
|
|
1034
|
+
from ...engine.interfaces import IsolationLevel
|
|
1035
|
+
from ...sql.type_api import _BindProcessorType
|
|
1036
|
+
from ...sql.type_api import _ResultProcessorType
|
|
1037
|
+
|
|
1028
1038
|
|
|
1029
1039
|
class _SQliteJson(JSON):
|
|
1030
1040
|
def result_processor(self, dialect, coltype):
|
|
@@ -1163,7 +1173,9 @@ class DATETIME(_DateTimeMixin, sqltypes.DateTime):
|
|
|
1163
1173
|
"%(hour)02d:%(minute)02d:%(second)02d"
|
|
1164
1174
|
)
|
|
1165
1175
|
|
|
1166
|
-
def bind_processor(
|
|
1176
|
+
def bind_processor(
|
|
1177
|
+
self, dialect: Dialect
|
|
1178
|
+
) -> Optional[_BindProcessorType[Any]]:
|
|
1167
1179
|
datetime_datetime = datetime.datetime
|
|
1168
1180
|
datetime_date = datetime.date
|
|
1169
1181
|
format_ = self._storage_format
|
|
@@ -1199,7 +1211,9 @@ class DATETIME(_DateTimeMixin, sqltypes.DateTime):
|
|
|
1199
1211
|
|
|
1200
1212
|
return process
|
|
1201
1213
|
|
|
1202
|
-
def result_processor(
|
|
1214
|
+
def result_processor(
|
|
1215
|
+
self, dialect: Dialect, coltype: object
|
|
1216
|
+
) -> Optional[_ResultProcessorType[Any]]:
|
|
1203
1217
|
if self._reg:
|
|
1204
1218
|
return processors.str_to_datetime_processor_factory(
|
|
1205
1219
|
self._reg, datetime.datetime
|
|
@@ -1254,7 +1268,9 @@ class DATE(_DateTimeMixin, sqltypes.Date):
|
|
|
1254
1268
|
|
|
1255
1269
|
_storage_format = "%(year)04d-%(month)02d-%(day)02d"
|
|
1256
1270
|
|
|
1257
|
-
def bind_processor(
|
|
1271
|
+
def bind_processor(
|
|
1272
|
+
self, dialect: Dialect
|
|
1273
|
+
) -> Optional[_BindProcessorType[Any]]:
|
|
1258
1274
|
datetime_date = datetime.date
|
|
1259
1275
|
format_ = self._storage_format
|
|
1260
1276
|
|
|
@@ -1275,7 +1291,9 @@ class DATE(_DateTimeMixin, sqltypes.Date):
|
|
|
1275
1291
|
|
|
1276
1292
|
return process
|
|
1277
1293
|
|
|
1278
|
-
def result_processor(
|
|
1294
|
+
def result_processor(
|
|
1295
|
+
self, dialect: Dialect, coltype: object
|
|
1296
|
+
) -> Optional[_ResultProcessorType[Any]]:
|
|
1279
1297
|
if self._reg:
|
|
1280
1298
|
return processors.str_to_datetime_processor_factory(
|
|
1281
1299
|
self._reg, datetime.date
|
|
@@ -2125,13 +2143,13 @@ class SQLiteDialect(default.DefaultDialect):
|
|
|
2125
2143
|
)
|
|
2126
2144
|
def __init__(
|
|
2127
2145
|
self,
|
|
2128
|
-
native_datetime=False,
|
|
2129
|
-
json_serializer=None,
|
|
2130
|
-
json_deserializer=None,
|
|
2131
|
-
_json_serializer=None,
|
|
2132
|
-
_json_deserializer=None,
|
|
2133
|
-
**kwargs,
|
|
2134
|
-
):
|
|
2146
|
+
native_datetime: bool = False,
|
|
2147
|
+
json_serializer: Optional[Callable[..., Any]] = None,
|
|
2148
|
+
json_deserializer: Optional[Callable[..., Any]] = None,
|
|
2149
|
+
_json_serializer: Optional[Callable[..., Any]] = None,
|
|
2150
|
+
_json_deserializer: Optional[Callable[..., Any]] = None,
|
|
2151
|
+
**kwargs: Any,
|
|
2152
|
+
) -> None:
|
|
2135
2153
|
default.DefaultDialect.__init__(self, **kwargs)
|
|
2136
2154
|
|
|
2137
2155
|
if _json_serializer:
|
|
@@ -2199,7 +2217,9 @@ class SQLiteDialect(default.DefaultDialect):
|
|
|
2199
2217
|
def get_isolation_level_values(self, dbapi_connection):
|
|
2200
2218
|
return list(self._isolation_lookup)
|
|
2201
2219
|
|
|
2202
|
-
def set_isolation_level(
|
|
2220
|
+
def set_isolation_level(
|
|
2221
|
+
self, dbapi_connection: DBAPIConnection, level: IsolationLevel
|
|
2222
|
+
) -> None:
|
|
2203
2223
|
isolation_level = self._isolation_lookup[level]
|
|
2204
2224
|
|
|
2205
2225
|
cursor = dbapi_connection.cursor()
|
|
@@ -2387,7 +2407,10 @@ class SQLiteDialect(default.DefaultDialect):
|
|
|
2387
2407
|
)
|
|
2388
2408
|
# remove create table
|
|
2389
2409
|
match = re.match(
|
|
2390
|
-
|
|
2410
|
+
(
|
|
2411
|
+
r"create table .*?\((.*)\)"
|
|
2412
|
+
r"(?:\s*,?\s*(?:WITHOUT\s+ROWID|STRICT))*$"
|
|
2413
|
+
),
|
|
2391
2414
|
tablesql.strip(),
|
|
2392
2415
|
re.DOTALL | re.IGNORECASE,
|
|
2393
2416
|
)
|