SQLAlchemy 2.1.1__py3-none-any.whl → 2.1.2__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/dialects/mssql/base.py +21 -0
- sqlalchemy/dialects/mssql/pyodbc.py +6 -9
- sqlalchemy/dialects/postgresql/base.py +18 -6
- sqlalchemy/engine/_row_cy.py +1 -1
- sqlalchemy/engine/default.py +12 -0
- sqlalchemy/engine/interfaces.py +19 -1
- sqlalchemy/ext/asyncio/base.py +31 -0
- sqlalchemy/ext/asyncio/engine.py +3 -5
- sqlalchemy/ext/asyncio/session.py +3 -5
- sqlalchemy/ext/automap.py +8 -8
- sqlalchemy/orm/relationships.py +29 -12
- sqlalchemy/orm/strategies.py +3 -1
- sqlalchemy/pool/base.py +22 -6
- sqlalchemy/sql/_elements_constructors.py +26 -0
- sqlalchemy/sql/compiler.py +58 -17
- sqlalchemy/sql/elements.py +32 -3
- sqlalchemy/sql/selectable.py +4 -2
- sqlalchemy/sql/sqltypes.py +10 -4
- sqlalchemy/sql/type_api.py +53 -24
- sqlalchemy/testing/cancellation.py +203 -73
- sqlalchemy/testing/requirements.py +16 -2
- sqlalchemy/testing/suite/test_types.py +129 -3
- sqlalchemy/util/langhelpers.py +1 -1
- {sqlalchemy-2.1.1.dist-info → sqlalchemy-2.1.2.dist-info}/METADATA +2 -2
- {sqlalchemy-2.1.1.dist-info → sqlalchemy-2.1.2.dist-info}/RECORD +30 -30
- {sqlalchemy-2.1.1.dist-info → sqlalchemy-2.1.2.dist-info}/WHEEL +0 -0
- {sqlalchemy-2.1.1.dist-info → sqlalchemy-2.1.2.dist-info}/licenses/AUTHORS +0 -0
- {sqlalchemy-2.1.1.dist-info → sqlalchemy-2.1.2.dist-info}/licenses/LICENSE +0 -0
- {sqlalchemy-2.1.1.dist-info → sqlalchemy-2.1.2.dist-info}/top_level.txt +0 -0
sqlalchemy/__init__.py
CHANGED
|
@@ -3315,6 +3315,27 @@ class MSDialect(default._BackendsMultiReflection, default.DefaultDialect):
|
|
|
3315
3315
|
finally:
|
|
3316
3316
|
cursor.close()
|
|
3317
3317
|
|
|
3318
|
+
def min_year_for_type(self, typeobj):
|
|
3319
|
+
typeobj = sqltypes.to_instance(typeobj)
|
|
3320
|
+
|
|
3321
|
+
if isinstance(typeobj, SMALLDATETIME):
|
|
3322
|
+
return 1900
|
|
3323
|
+
elif isinstance(typeobj, (DATETIME2, DATETIMEOFFSET)):
|
|
3324
|
+
return datetime.MINYEAR
|
|
3325
|
+
elif isinstance(typeobj, sqltypes.DateTime):
|
|
3326
|
+
# DateTime renders DATETIMEOFFSET with timezone=True,
|
|
3327
|
+
# else DATETIME
|
|
3328
|
+
return datetime.MINYEAR if typeobj.timezone else 1753
|
|
3329
|
+
elif (
|
|
3330
|
+
isinstance(typeobj, sqltypes.Date)
|
|
3331
|
+
and not isinstance(typeobj, sqltypes.DATE)
|
|
3332
|
+
and self.server_version_info < MS_2008_VERSION
|
|
3333
|
+
):
|
|
3334
|
+
# Date renders DATETIME before SQL Server 2008
|
|
3335
|
+
return 1753
|
|
3336
|
+
else:
|
|
3337
|
+
return datetime.MINYEAR
|
|
3338
|
+
|
|
3318
3339
|
def initialize(self, connection):
|
|
3319
3340
|
super().initialize(connection)
|
|
3320
3341
|
self._setup_version_attributes()
|
|
@@ -489,15 +489,12 @@ class _ODBCDateTimeBindProcessor:
|
|
|
489
489
|
else:
|
|
490
490
|
# for DATETIMEOFFSET or DateTime(timezone=True)
|
|
491
491
|
#
|
|
492
|
-
# Convert to string format required by T-SQL
|
|
493
|
-
|
|
494
|
-
# offset
|
|
495
|
-
#
|
|
496
|
-
#
|
|
497
|
-
|
|
498
|
-
r"([\+\-]\d{2})([\d\.]+)$", r"\1:\2", dto_string
|
|
499
|
-
)
|
|
500
|
-
return dto_string
|
|
492
|
+
# Convert to string format required by T-SQL. isoformat()
|
|
493
|
+
# zero-pads the year, which strftime("%Y") does not do on
|
|
494
|
+
# all platforms, and renders the offset with a colon,
|
|
495
|
+
# e.g. -07:00. backend currently rejects seconds /
|
|
496
|
+
# fractional seconds in the offset
|
|
497
|
+
return value.isoformat(sep=" ", timespec="microseconds")
|
|
501
498
|
|
|
502
499
|
return process
|
|
503
500
|
|
|
@@ -4846,8 +4846,12 @@ class PGDialect(default._BackendsMultiReflection, default.DefaultDialect):
|
|
|
4846
4846
|
)
|
|
4847
4847
|
.outerjoin(
|
|
4848
4848
|
pg_catalog.pg_description,
|
|
4849
|
-
|
|
4850
|
-
|
|
4849
|
+
sql.and_(
|
|
4850
|
+
pg_catalog.pg_description.c.objoid
|
|
4851
|
+
== pg_catalog.pg_constraint.c.oid,
|
|
4852
|
+
pg_catalog.pg_description.c.classoid
|
|
4853
|
+
== sql.func.cast("pg_catalog.pg_constraint", REGCLASS),
|
|
4854
|
+
),
|
|
4851
4855
|
)
|
|
4852
4856
|
.where(
|
|
4853
4857
|
pg_catalog.pg_constraint.c.contype == bindparam("contype"),
|
|
@@ -5039,8 +5043,12 @@ class PGDialect(default._BackendsMultiReflection, default.DefaultDialect):
|
|
|
5039
5043
|
)
|
|
5040
5044
|
.outerjoin(
|
|
5041
5045
|
pg_catalog.pg_description,
|
|
5042
|
-
|
|
5043
|
-
|
|
5046
|
+
sql.and_(
|
|
5047
|
+
pg_catalog.pg_description.c.objoid
|
|
5048
|
+
== pg_catalog.pg_constraint.c.oid,
|
|
5049
|
+
pg_catalog.pg_description.c.classoid
|
|
5050
|
+
== sql.func.cast("pg_catalog.pg_constraint", REGCLASS),
|
|
5051
|
+
),
|
|
5044
5052
|
)
|
|
5045
5053
|
.order_by(
|
|
5046
5054
|
pg_catalog.pg_class.c.relname,
|
|
@@ -5632,8 +5640,12 @@ class PGDialect(default._BackendsMultiReflection, default.DefaultDialect):
|
|
|
5632
5640
|
)
|
|
5633
5641
|
.outerjoin(
|
|
5634
5642
|
pg_catalog.pg_description,
|
|
5635
|
-
|
|
5636
|
-
|
|
5643
|
+
sql.and_(
|
|
5644
|
+
pg_catalog.pg_description.c.objoid
|
|
5645
|
+
== pg_catalog.pg_constraint.c.oid,
|
|
5646
|
+
pg_catalog.pg_description.c.classoid
|
|
5647
|
+
== sql.func.cast("pg_catalog.pg_constraint", REGCLASS),
|
|
5648
|
+
),
|
|
5637
5649
|
)
|
|
5638
5650
|
.order_by(
|
|
5639
5651
|
pg_catalog.pg_class.c.relname,
|
sqlalchemy/engine/_row_cy.py
CHANGED
sqlalchemy/engine/default.py
CHANGED
|
@@ -16,6 +16,7 @@ as the base class for their own corresponding classes.
|
|
|
16
16
|
|
|
17
17
|
from __future__ import annotations
|
|
18
18
|
|
|
19
|
+
import datetime
|
|
19
20
|
import functools
|
|
20
21
|
import operator
|
|
21
22
|
import random
|
|
@@ -829,6 +830,17 @@ class DefaultDialect(Dialect):
|
|
|
829
830
|
"""
|
|
830
831
|
return type_api.adapt_type(typeobj, self.colspecs)
|
|
831
832
|
|
|
833
|
+
def min_year_for_type(self, typeobj: TypeEngine[Any]) -> int:
|
|
834
|
+
"""Return the earliest year the database accepts for the given
|
|
835
|
+
date or datetime type.
|
|
836
|
+
|
|
837
|
+
The default implementation returns ``datetime.MINYEAR``.
|
|
838
|
+
|
|
839
|
+
.. versionadded:: 2.0.55
|
|
840
|
+
|
|
841
|
+
"""
|
|
842
|
+
return datetime.MINYEAR
|
|
843
|
+
|
|
832
844
|
def has_index(self, connection, table_name, index_name, schema=None, **kw):
|
|
833
845
|
if not self.has_table(connection, table_name, schema=schema, **kw):
|
|
834
846
|
return False
|
sqlalchemy/engine/interfaces.py
CHANGED
|
@@ -1465,6 +1465,23 @@ class Dialect(EventTarget):
|
|
|
1465
1465
|
|
|
1466
1466
|
raise NotImplementedError()
|
|
1467
1467
|
|
|
1468
|
+
def min_year_for_type(self, typeobj: TypeEngine[Any]) -> int:
|
|
1469
|
+
"""Return the earliest year the database accepts for the given
|
|
1470
|
+
date or datetime type.
|
|
1471
|
+
|
|
1472
|
+
The type may be a generic type such as :class:`.DateTime`, in
|
|
1473
|
+
which case the limit is that of the database type the dialect
|
|
1474
|
+
renders for it. Dialects whose date or datetime types don't
|
|
1475
|
+
accept all years from ``datetime.MINYEAR`` forward override this
|
|
1476
|
+
method; e.g. SQL Server's ``DATETIME`` type, which the dialect
|
|
1477
|
+
renders for :class:`.DateTime`, accepts years from 1753 forward.
|
|
1478
|
+
|
|
1479
|
+
.. versionadded:: 2.0.55
|
|
1480
|
+
|
|
1481
|
+
"""
|
|
1482
|
+
|
|
1483
|
+
raise NotImplementedError()
|
|
1484
|
+
|
|
1468
1485
|
def initialize(self, connection: Connection) -> None:
|
|
1469
1486
|
"""Called during strategized creation of the dialect with a
|
|
1470
1487
|
connection.
|
|
@@ -3437,7 +3454,8 @@ class ExceptionContext:
|
|
|
3437
3454
|
|
|
3438
3455
|
"""
|
|
3439
3456
|
|
|
3440
|
-
|
|
3457
|
+
if not TYPE_CHECKING:
|
|
3458
|
+
__slots__ = ()
|
|
3441
3459
|
|
|
3442
3460
|
dialect: Dialect
|
|
3443
3461
|
"""The :class:`_engine.Dialect` in use.
|
sqlalchemy/ext/asyncio/base.py
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
10
|
import abc
|
|
11
|
+
import asyncio
|
|
11
12
|
import functools
|
|
12
13
|
from typing import Any
|
|
13
14
|
from typing import AsyncGenerator
|
|
@@ -15,6 +16,7 @@ from typing import AsyncIterator
|
|
|
15
16
|
from typing import Awaitable
|
|
16
17
|
from typing import Callable
|
|
17
18
|
from typing import ClassVar
|
|
19
|
+
from typing import Coroutine
|
|
18
20
|
from typing import Dict
|
|
19
21
|
from typing import Generator
|
|
20
22
|
from typing import Generic
|
|
@@ -37,6 +39,35 @@ _T_co = TypeVar("_T_co", bound=Any, covariant=True)
|
|
|
37
39
|
_PT = TypeVar("_PT", bound=Any)
|
|
38
40
|
|
|
39
41
|
|
|
42
|
+
async def _run_to_completion(coro: Coroutine[Any, Any, _T]) -> _T:
|
|
43
|
+
"""Run ``coro`` in a task of its own and wait for it to complete, even
|
|
44
|
+
if the calling task is cancelled meanwhile.
|
|
45
|
+
|
|
46
|
+
Used for cleanup, such as the ``close()`` performed by an async context
|
|
47
|
+
manager on exit, that must not be abandoned partway through. A
|
|
48
|
+
cancellation received while waiting is re-raised once ``coro`` has
|
|
49
|
+
completed, so that the caller does not return before its cleanup has
|
|
50
|
+
finished; were it to return early, the cleanup would carry on in the
|
|
51
|
+
background, where for example it could return a connection into a pool
|
|
52
|
+
that the application has meanwhile disposed of.
|
|
53
|
+
|
|
54
|
+
"""
|
|
55
|
+
task = asyncio.create_task(coro)
|
|
56
|
+
cancelled = None
|
|
57
|
+
while True:
|
|
58
|
+
try:
|
|
59
|
+
result = await asyncio.shield(task)
|
|
60
|
+
except asyncio.CancelledError as err:
|
|
61
|
+
if task.done():
|
|
62
|
+
raise
|
|
63
|
+
cancelled = err
|
|
64
|
+
else:
|
|
65
|
+
break
|
|
66
|
+
if cancelled is not None:
|
|
67
|
+
raise cancelled
|
|
68
|
+
return result
|
|
69
|
+
|
|
70
|
+
|
|
40
71
|
class ReversibleProxy(Generic[_PT]):
|
|
41
72
|
_proxy_objects: ClassVar[
|
|
42
73
|
Dict[weakref.ref[Any], weakref.ref[ReversibleProxy[Any]]]
|
sqlalchemy/ext/asyncio/engine.py
CHANGED
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
# the MIT License: https://www.opensource.org/licenses/mit-license.php
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
-
import asyncio
|
|
10
9
|
import contextlib
|
|
11
10
|
from typing import Any
|
|
12
11
|
from typing import AsyncIterator
|
|
@@ -24,6 +23,7 @@ from typing import TypeVar
|
|
|
24
23
|
from typing import Union
|
|
25
24
|
|
|
26
25
|
from . import exc as async_exc
|
|
26
|
+
from .base import _run_to_completion
|
|
27
27
|
from .base import asyncstartablecontext
|
|
28
28
|
from .base import GeneratorStartableContext
|
|
29
29
|
from .base import ProxyComparable
|
|
@@ -606,8 +606,7 @@ class AsyncConnection( # type: ignore[misc]
|
|
|
606
606
|
except GeneratorExit:
|
|
607
607
|
pass
|
|
608
608
|
else:
|
|
609
|
-
|
|
610
|
-
await asyncio.shield(task)
|
|
609
|
+
await _run_to_completion(ar.close())
|
|
611
610
|
|
|
612
611
|
@overload
|
|
613
612
|
async def execute(
|
|
@@ -913,8 +912,7 @@ class AsyncConnection( # type: ignore[misc]
|
|
|
913
912
|
return self.start().__await__()
|
|
914
913
|
|
|
915
914
|
async def __aexit__(self, type_: Any, value: Any, traceback: Any) -> None:
|
|
916
|
-
|
|
917
|
-
await asyncio.shield(task)
|
|
915
|
+
await _run_to_completion(self.close())
|
|
918
916
|
|
|
919
917
|
# START PROXY METHODS AsyncConnection
|
|
920
918
|
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
# the MIT License: https://www.opensource.org/licenses/mit-license.php
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
-
import asyncio
|
|
10
9
|
from typing import Any
|
|
11
10
|
from typing import Awaitable
|
|
12
11
|
from typing import Callable
|
|
@@ -30,6 +29,7 @@ from typing import Union
|
|
|
30
29
|
|
|
31
30
|
from . import engine
|
|
32
31
|
from . import exc as async_exc
|
|
32
|
+
from .base import _run_to_completion
|
|
33
33
|
from .base import ReversibleProxy
|
|
34
34
|
from .base import StartableContext
|
|
35
35
|
from .result import _ensure_sync_result
|
|
@@ -1375,8 +1375,7 @@ class AsyncSession(ReversibleProxy[Session]):
|
|
|
1375
1375
|
return self
|
|
1376
1376
|
|
|
1377
1377
|
async def __aexit__(self, type_: Any, value: Any, traceback: Any) -> None:
|
|
1378
|
-
|
|
1379
|
-
await asyncio.shield(task)
|
|
1378
|
+
await _run_to_completion(self.close())
|
|
1380
1379
|
|
|
1381
1380
|
def _maker_context_manager(self: _AS) -> _AsyncSessionContextManager[_AS]:
|
|
1382
1381
|
return _AsyncSessionContextManager(self)
|
|
@@ -2121,8 +2120,7 @@ class _AsyncSessionContextManager(Generic[_AS]):
|
|
|
2121
2120
|
await self.trans.__aexit__(type_, value, traceback)
|
|
2122
2121
|
await self.async_session.__aexit__(type_, value, traceback)
|
|
2123
2122
|
|
|
2124
|
-
|
|
2125
|
-
await asyncio.shield(task)
|
|
2123
|
+
await _run_to_completion(go())
|
|
2126
2124
|
|
|
2127
2125
|
|
|
2128
2126
|
class AsyncSessionTransaction(
|
sqlalchemy/ext/automap.py
CHANGED
|
@@ -1661,11 +1661,11 @@ def _m2m_relationship(
|
|
|
1661
1661
|
overlaps=overlaps,
|
|
1662
1662
|
secondary=table,
|
|
1663
1663
|
primaryjoin=and_(
|
|
1664
|
-
fk.column == fk.parent for fk in m2m_const[0].elements
|
|
1665
|
-
),
|
|
1664
|
+
*(fk.column == fk.parent for fk in m2m_const[0].elements)
|
|
1665
|
+
),
|
|
1666
1666
|
secondaryjoin=and_(
|
|
1667
|
-
fk.column == fk.parent for fk in m2m_const[1].elements
|
|
1668
|
-
),
|
|
1667
|
+
*(fk.column == fk.parent for fk in m2m_const[1].elements)
|
|
1668
|
+
),
|
|
1669
1669
|
backref=backref_obj,
|
|
1670
1670
|
collection_class=collection_class,
|
|
1671
1671
|
)
|
|
@@ -1687,11 +1687,11 @@ def _m2m_relationship(
|
|
|
1687
1687
|
overlaps=overlaps,
|
|
1688
1688
|
secondary=table,
|
|
1689
1689
|
primaryjoin=and_(
|
|
1690
|
-
fk.column == fk.parent for fk in m2m_const[1].elements
|
|
1691
|
-
),
|
|
1690
|
+
*(fk.column == fk.parent for fk in m2m_const[1].elements)
|
|
1691
|
+
),
|
|
1692
1692
|
secondaryjoin=and_(
|
|
1693
|
-
fk.column == fk.parent for fk in m2m_const[0].elements
|
|
1694
|
-
),
|
|
1693
|
+
*(fk.column == fk.parent for fk in m2m_const[0].elements)
|
|
1694
|
+
),
|
|
1695
1695
|
back_populates=relationship_name,
|
|
1696
1696
|
collection_class=collection_class,
|
|
1697
1697
|
)
|
sqlalchemy/orm/relationships.py
CHANGED
|
@@ -2620,24 +2620,41 @@ class _JoinCondition:
|
|
|
2620
2620
|
return self._has_annotation(self.primaryjoin, "remote")
|
|
2621
2621
|
|
|
2622
2622
|
@util.memoized_property
|
|
2623
|
-
def
|
|
2624
|
-
"""Return True if the "secondary" selectable
|
|
2625
|
-
|
|
2626
|
-
|
|
2623
|
+
def secondary_can_be_used_for_related_join(self) -> bool:
|
|
2624
|
+
"""Return True if the "secondary" selectable may be joined directly
|
|
2625
|
+
to the related entity in order to load related rows for a set of
|
|
2626
|
+
parent primary key values, omitting the parent table.
|
|
2627
2627
|
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2628
|
+
This is the case when the primaryjoin consists only of equality
|
|
2629
|
+
comparisons between the complete primary key of the parent and
|
|
2630
|
+
columns of the secondary. If the primaryjoin contains any other
|
|
2631
|
+
criteria, it would be lost when the parent table is omitted.
|
|
2632
|
+
|
|
2633
|
+
Used by the selectinload loader strategy for its ``omit_join``
|
|
2634
|
+
optimization.
|
|
2631
2635
|
|
|
2632
2636
|
"""
|
|
2633
2637
|
if self.secondary is None:
|
|
2634
2638
|
return False
|
|
2635
2639
|
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2640
|
+
parent_pk_cols = self.prop.parent._local_pk_cols
|
|
2641
|
+
if not parent_pk_cols.issubset(l for (l, _) in self.synchronize_pairs):
|
|
2642
|
+
return False
|
|
2643
|
+
|
|
2644
|
+
# compare against the parent primary key comparisons only; any
|
|
2645
|
+
# other criteria in the primaryjoin, including comparisons of
|
|
2646
|
+
# non-primary key parent columns to the secondary, make the
|
|
2647
|
+
# comparison fail. compare() treats AND as unordered and "==" as
|
|
2648
|
+
# commutative, so the primaryjoin may state these comparisons in
|
|
2649
|
+
# any order or orientation
|
|
2650
|
+
return self.primaryjoin.compare(
|
|
2651
|
+
sql.and_(
|
|
2652
|
+
*[
|
|
2653
|
+
l == r
|
|
2654
|
+
for (l, r) in self.synchronize_pairs
|
|
2655
|
+
if l in parent_pk_cols
|
|
2656
|
+
]
|
|
2657
|
+
)
|
|
2641
2658
|
)
|
|
2642
2659
|
|
|
2643
2660
|
def _annotate_fks(self) -> None:
|
sqlalchemy/orm/strategies.py
CHANGED
|
@@ -3029,7 +3029,9 @@ class _SelectInLoader(_PostLoader, util.MemoizedSlots):
|
|
|
3029
3029
|
self.omit_join = lazyloader.use_get
|
|
3030
3030
|
elif is_m2m and not self.parent_property._is_self_referential:
|
|
3031
3031
|
join_cond = self.parent_property._join_condition
|
|
3032
|
-
self.omit_join =
|
|
3032
|
+
self.omit_join = (
|
|
3033
|
+
join_cond.secondary_can_be_used_for_related_join
|
|
3034
|
+
)
|
|
3033
3035
|
else:
|
|
3034
3036
|
self.omit_join = self.parent._get_clause[0].compare(
|
|
3035
3037
|
lazyloader._rev_lazywhere,
|
sqlalchemy/pool/base.py
CHANGED
|
@@ -733,10 +733,21 @@ class _ConnectionRecord(ConnectionPoolEntry):
|
|
|
733
733
|
def _checkin_failed(
|
|
734
734
|
self, err: BaseException, _fairy_was_created: bool = True
|
|
735
735
|
) -> None:
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
736
|
+
try:
|
|
737
|
+
self.invalidate(e=err)
|
|
738
|
+
finally:
|
|
739
|
+
if self.dbapi_connection is not None:
|
|
740
|
+
# invalidate() was itself interrupted, such as by a second
|
|
741
|
+
# cancellation, before it could close the connection. keep
|
|
742
|
+
# the connection so that its close can be retried, but
|
|
743
|
+
# have the next checkout recycle it rather than use it
|
|
744
|
+
self._soft_invalidate_time = time.time()
|
|
745
|
+
|
|
746
|
+
# the record must be returned in any case; nothing else will
|
|
747
|
+
# ever return it, as no _ConnectionFairy refers to it
|
|
748
|
+
self.checkin(
|
|
749
|
+
_fairy_was_created=_fairy_was_created,
|
|
750
|
+
)
|
|
740
751
|
|
|
741
752
|
def checkin(self, _fairy_was_created: bool = True) -> None:
|
|
742
753
|
if self.fairy_ref is None and _fairy_was_created:
|
|
@@ -1017,10 +1028,15 @@ def _finalize_fairy(
|
|
|
1017
1028
|
pool.logger.error(
|
|
1018
1029
|
"Exception during reset or similar", exc_info=True
|
|
1019
1030
|
)
|
|
1020
|
-
if connection_record:
|
|
1021
|
-
connection_record.invalidate(e=e)
|
|
1022
1031
|
if not isinstance(e, Exception):
|
|
1032
|
+
# a cancellation or similar propagates from here, so the
|
|
1033
|
+
# checkin() below is not reached; return the record now,
|
|
1034
|
+
# rather than leaving it to garbage collection of the fairy
|
|
1035
|
+
if connection_record:
|
|
1036
|
+
connection_record._checkin_failed(e)
|
|
1023
1037
|
raise
|
|
1038
|
+
if connection_record:
|
|
1039
|
+
connection_record.invalidate(e=e)
|
|
1024
1040
|
finally:
|
|
1025
1041
|
if detach and is_gc_cleanup and dont_restore_gced:
|
|
1026
1042
|
message = (
|
|
@@ -1817,6 +1817,12 @@ def text(text: str) -> TextClause:
|
|
|
1817
1817
|
|
|
1818
1818
|
t = text(r"SELECT * FROM users WHERE name='\:username'")
|
|
1819
1819
|
|
|
1820
|
+
.. note:: A name that is immediately followed by another colon,
|
|
1821
|
+
such as ``:this:that`` or ``:value::int``, is currently not
|
|
1822
|
+
interpreted as a bound parameter and is rendered as literal text;
|
|
1823
|
+
whitespace may be used to separate the name from the colon, e.g.
|
|
1824
|
+
``:value ::int``. This behavior may change in a future release.
|
|
1825
|
+
|
|
1820
1826
|
The :class:`_expression.TextClause`
|
|
1821
1827
|
construct includes methods which can
|
|
1822
1828
|
provide information about the bound parameters as well as the column
|
|
@@ -1910,6 +1916,26 @@ def tstring(template: Template) -> TString:
|
|
|
1910
1916
|
some_json = {"foo": "bar"}
|
|
1911
1917
|
stmt = tstring(t"SELECT {literal(some_json, JSON)}")
|
|
1912
1918
|
|
|
1919
|
+
**Literal SQL Text**:
|
|
1920
|
+
|
|
1921
|
+
The plain string portions of the template are rendered exactly as
|
|
1922
|
+
given. Unlike :func:`_expression.text`, they are not scanned for
|
|
1923
|
+
bound parameter names in the ``:name`` format, and there are no escaping
|
|
1924
|
+
rules such as ``\:`` for colons; a colon in the template, such as within
|
|
1925
|
+
a quoted SQL string, is rendered unchanged. A named bound parameter
|
|
1926
|
+
whose value is passed at execution time may be embedded explicitly
|
|
1927
|
+
using :func:`_expression.bindparam`::
|
|
1928
|
+
|
|
1929
|
+
from sqlalchemy import tstring, bindparam, Integer
|
|
1930
|
+
|
|
1931
|
+
user_id = bindparam("user_id", type_=Integer)
|
|
1932
|
+
stmt = tstring(t"SELECT * FROM users WHERE id = {user_id}")
|
|
1933
|
+
result = connection.execute(stmt, {"user_id": 42})
|
|
1934
|
+
|
|
1935
|
+
.. versionchanged:: 2.1.2 The plain string portions of a
|
|
1936
|
+
:func:`_expression.tstring` are no longer scanned for bound parameter
|
|
1937
|
+
names or ``\:`` escapes, as was the case in 2.1.0 and 2.1.1.
|
|
1938
|
+
|
|
1913
1939
|
**Column Specification**:
|
|
1914
1940
|
|
|
1915
1941
|
Like :func:`_expression.text`, the :func:`_expression.tstring` construct
|
sqlalchemy/sql/compiler.py
CHANGED
|
@@ -266,8 +266,8 @@ FK_INITIALLY = re.compile(r"^(?:DEFERRED|IMMEDIATE)$", re.I)
|
|
|
266
266
|
_WINDOW_EXCLUDE_RE = re.compile(
|
|
267
267
|
r"^(?:CURRENT ROW|GROUP|TIES|NO OTHERS)$", re.I
|
|
268
268
|
)
|
|
269
|
-
BIND_PARAMS =
|
|
270
|
-
BIND_PARAMS_ESC = re.compile(r"\x5c(
|
|
269
|
+
BIND_PARAMS = elements.TextClause._bind_params_regex
|
|
270
|
+
BIND_PARAMS_ESC = re.compile(r"\x5c(:\w*)(?![:\w])", re.UNICODE)
|
|
271
271
|
|
|
272
272
|
_pyformat_template = "%%(%(name)s)s"
|
|
273
273
|
BIND_TEMPLATES = {
|
|
@@ -1422,6 +1422,17 @@ class SQLCompiler(Compiled):
|
|
|
1422
1422
|
_positional_pattern = re.compile(
|
|
1423
1423
|
f"{_pyformat_pattern.pattern}|{_post_compile_pattern.pattern}"
|
|
1424
1424
|
)
|
|
1425
|
+
|
|
1426
|
+
# variants of the above for dialects that double percent signs in
|
|
1427
|
+
# literal text; a "%%" pair is matched first so that an escaped
|
|
1428
|
+
# percent sign is never taken as the start of a bound parameter
|
|
1429
|
+
_pct_escaped_pyformat_pattern = re.compile(
|
|
1430
|
+
f"%%|{_pyformat_pattern.pattern}"
|
|
1431
|
+
)
|
|
1432
|
+
_pct_escaped_positional_pattern = re.compile(
|
|
1433
|
+
f"%%|{_positional_pattern.pattern}"
|
|
1434
|
+
)
|
|
1435
|
+
|
|
1425
1436
|
_collect_params: Final[bool]
|
|
1426
1437
|
_collected_params: util.immutabledict[str, Any]
|
|
1427
1438
|
|
|
@@ -1733,24 +1744,38 @@ class SQLCompiler(Compiled):
|
|
|
1733
1744
|
placeholder = "?"
|
|
1734
1745
|
|
|
1735
1746
|
positions = []
|
|
1747
|
+
binds = self.binds
|
|
1748
|
+
|
|
1749
|
+
if self.escaped_bind_names:
|
|
1750
|
+
reverse_escape = {v: k for k, v in self.escaped_bind_names.items()}
|
|
1751
|
+
assert len(self.escaped_bind_names) == len(reverse_escape)
|
|
1752
|
+
else:
|
|
1753
|
+
reverse_escape = {}
|
|
1754
|
+
|
|
1755
|
+
if self.preparer._double_percents:
|
|
1756
|
+
positional_pattern = self._pct_escaped_positional_pattern
|
|
1757
|
+
else:
|
|
1758
|
+
positional_pattern = self._positional_pattern
|
|
1736
1759
|
|
|
1737
1760
|
def find_position(m: re.Match[str]) -> str:
|
|
1738
1761
|
normal_bind = m.group(1)
|
|
1739
1762
|
if normal_bind:
|
|
1763
|
+
if reverse_escape.get(normal_bind, normal_bind) not in binds:
|
|
1764
|
+
# literal text that looks like a bound parameter
|
|
1765
|
+
return m.group(0)
|
|
1740
1766
|
positions.append(normal_bind)
|
|
1741
1767
|
return placeholder
|
|
1742
|
-
|
|
1768
|
+
elif m.group(2):
|
|
1743
1769
|
# this a post-compile bind
|
|
1744
1770
|
positions.append(m.group(2))
|
|
1745
1771
|
return m.group(0)
|
|
1772
|
+
else:
|
|
1773
|
+
# an escaped percent sign
|
|
1774
|
+
return m.group(0)
|
|
1746
1775
|
|
|
1747
|
-
self.string = re.sub(
|
|
1748
|
-
self._positional_pattern, find_position, self.string
|
|
1749
|
-
)
|
|
1776
|
+
self.string = re.sub(positional_pattern, find_position, self.string)
|
|
1750
1777
|
|
|
1751
|
-
if
|
|
1752
|
-
reverse_escape = {v: k for k, v in self.escaped_bind_names.items()}
|
|
1753
|
-
assert len(self.escaped_bind_names) == len(reverse_escape)
|
|
1778
|
+
if reverse_escape:
|
|
1754
1779
|
self.positiontup = [
|
|
1755
1780
|
reverse_escape.get(name, name) for name in positions
|
|
1756
1781
|
]
|
|
@@ -1761,7 +1786,7 @@ class SQLCompiler(Compiled):
|
|
|
1761
1786
|
positions = []
|
|
1762
1787
|
|
|
1763
1788
|
single_values_expr = re.sub(
|
|
1764
|
-
|
|
1789
|
+
positional_pattern,
|
|
1765
1790
|
find_position,
|
|
1766
1791
|
self._insertmanyvalues.single_values_expr,
|
|
1767
1792
|
)
|
|
@@ -1769,7 +1794,7 @@ class SQLCompiler(Compiled):
|
|
|
1769
1794
|
(
|
|
1770
1795
|
v[0],
|
|
1771
1796
|
v[1],
|
|
1772
|
-
re.sub(
|
|
1797
|
+
re.sub(positional_pattern, find_position, v[2]),
|
|
1773
1798
|
v[3],
|
|
1774
1799
|
)
|
|
1775
1800
|
for v in self._insertmanyvalues.insert_crud_params
|
|
@@ -1829,9 +1854,15 @@ class SQLCompiler(Compiled):
|
|
|
1829
1854
|
}
|
|
1830
1855
|
assert len(param_pos) == len_before
|
|
1831
1856
|
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1857
|
+
if self.preparer._double_percents:
|
|
1858
|
+
pyformat_pattern = self._pct_escaped_pyformat_pattern
|
|
1859
|
+
else:
|
|
1860
|
+
pyformat_pattern = self._pyformat_pattern
|
|
1861
|
+
|
|
1862
|
+
# any match not in param_pos is an escaped percent sign or literal
|
|
1863
|
+
# text that looks like a bound parameter
|
|
1864
|
+
self.string = pyformat_pattern.sub(
|
|
1865
|
+
lambda m: param_pos.get(m.group(1), m.group(0)), self.string
|
|
1835
1866
|
)
|
|
1836
1867
|
|
|
1837
1868
|
if self._insertmanyvalues:
|
|
@@ -2277,9 +2308,16 @@ class SQLCompiler(Compiled):
|
|
|
2277
2308
|
numeric_positiontup, self.next_numeric_pos
|
|
2278
2309
|
)
|
|
2279
2310
|
}
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2311
|
+
if self.preparer._double_percents:
|
|
2312
|
+
pyformat_pattern = self._pct_escaped_pyformat_pattern
|
|
2313
|
+
else:
|
|
2314
|
+
pyformat_pattern = self._pyformat_pattern
|
|
2315
|
+
|
|
2316
|
+
# only the expanded parameters are replaced; any other
|
|
2317
|
+
# match is an escaped percent sign or literal text that looks
|
|
2318
|
+
# like a bound parameter
|
|
2319
|
+
statement = pyformat_pattern.sub(
|
|
2320
|
+
lambda m: param_pos.get(m.group(1), m.group(0)), statement
|
|
2283
2321
|
)
|
|
2284
2322
|
new_positiontup.extend(numeric_positiontup)
|
|
2285
2323
|
|
|
@@ -2820,6 +2858,9 @@ class SQLCompiler(Compiled):
|
|
|
2820
2858
|
),
|
|
2821
2859
|
)
|
|
2822
2860
|
|
|
2861
|
+
def visit_tstring_text(self, element, **kw):
|
|
2862
|
+
return self.post_process_text(element.text)
|
|
2863
|
+
|
|
2823
2864
|
def visit_tstring(self, tstring, add_to_result_map=None, **kw):
|
|
2824
2865
|
if self._collect_params:
|
|
2825
2866
|
self._add_to_params(tstring)
|