SQLAlchemy 2.0.51__py3-none-any.whl → 2.0.52__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/pyodbc.py +13 -4
- sqlalchemy/dialects/mssql/base.py +1 -1
- sqlalchemy/dialects/postgresql/base.py +72 -12
- sqlalchemy/dialects/postgresql/json.py +3 -0
- sqlalchemy/dialects/sqlite/aiosqlite.py +30 -0
- sqlalchemy/dialects/sqlite/base.py +10 -28
- sqlalchemy/dialects/sqlite/pysqlite.py +89 -12
- sqlalchemy/orm/bulk_persistence.py +16 -0
- sqlalchemy/orm/decl_base.py +14 -0
- sqlalchemy/orm/path_registry.py +55 -13
- sqlalchemy/orm/properties.py +2 -2
- sqlalchemy/orm/relationships.py +2 -2
- sqlalchemy/orm/session.py +25 -23
- sqlalchemy/orm/strategy_options.py +11 -6
- sqlalchemy/orm/util.py +4 -1
- sqlalchemy/sql/_elements_constructors.py +12 -0
- sqlalchemy/sql/compiler.py +10 -0
- sqlalchemy/sql/elements.py +2 -2
- sqlalchemy/sql/schema.py +16 -17
- sqlalchemy/sql/sqltypes.py +9 -5
- sqlalchemy/testing/__init__.py +2 -0
- sqlalchemy/testing/config.py +7 -0
- sqlalchemy/testing/fixtures/mypy.py +5 -3
- sqlalchemy/testing/fixtures/orm.py +2 -2
- sqlalchemy/testing/fixtures/sql.py +2 -2
- sqlalchemy/testing/plugin/plugin_base.py +4 -0
- sqlalchemy/testing/plugin/pytestplugin.py +30 -0
- sqlalchemy/testing/suite/test_reflection.py +88 -0
- sqlalchemy/testing/util.py +41 -0
- sqlalchemy/util/__init__.py +2 -0
- sqlalchemy/util/compat.py +1 -1
- sqlalchemy/util/langhelpers.py +93 -0
- sqlalchemy/util/typing.py +2 -0
- {sqlalchemy-2.0.51.dist-info → sqlalchemy-2.0.52.dist-info}/METADATA +4 -2
- {sqlalchemy-2.0.51.dist-info → sqlalchemy-2.0.52.dist-info}/RECORD +39 -39
- {sqlalchemy-2.0.51.dist-info → sqlalchemy-2.0.52.dist-info}/WHEEL +1 -1
- {sqlalchemy-2.0.51.dist-info → sqlalchemy-2.0.52.dist-info}/licenses/LICENSE +0 -0
- {sqlalchemy-2.0.51.dist-info → sqlalchemy-2.0.52.dist-info}/top_level.txt +0 -0
sqlalchemy/__init__.py
CHANGED
sqlalchemy/connectors/pyodbc.py
CHANGED
|
@@ -77,10 +77,16 @@ class PyODBCConnector(Connector):
|
|
|
77
77
|
else:
|
|
78
78
|
|
|
79
79
|
def check_quote(token: str) -> str:
|
|
80
|
-
if
|
|
80
|
+
if (
|
|
81
|
+
";" in str(token)
|
|
82
|
+
or "}" in str(token)
|
|
83
|
+
or str(token).startswith("{")
|
|
84
|
+
):
|
|
81
85
|
token = "{%s}" % token.replace("}", "}}")
|
|
82
86
|
return token
|
|
83
87
|
|
|
88
|
+
driver = keys.pop("driver", self.pyodbc_driver_name)
|
|
89
|
+
|
|
84
90
|
keys = {k: check_quote(v) for k, v in keys.items()}
|
|
85
91
|
|
|
86
92
|
dsn_connection = "dsn" in keys or (
|
|
@@ -96,7 +102,6 @@ class PyODBCConnector(Connector):
|
|
|
96
102
|
port = ",%d" % int(keys.pop("port"))
|
|
97
103
|
|
|
98
104
|
connectors = []
|
|
99
|
-
driver = keys.pop("driver", self.pyodbc_driver_name)
|
|
100
105
|
if driver is None and keys:
|
|
101
106
|
# note if keys is empty, this is a totally blank URL
|
|
102
107
|
util.warn(
|
|
@@ -105,7 +110,9 @@ class PyODBCConnector(Connector):
|
|
|
105
110
|
"DSN-less connections"
|
|
106
111
|
)
|
|
107
112
|
else:
|
|
108
|
-
connectors.append(
|
|
113
|
+
connectors.append(
|
|
114
|
+
"DRIVER={%s}" % str(driver).replace("}", "}}")
|
|
115
|
+
)
|
|
109
116
|
|
|
110
117
|
connectors.extend(
|
|
111
118
|
[
|
|
@@ -136,7 +143,9 @@ class PyODBCConnector(Connector):
|
|
|
136
143
|
"AutoTranslate=%s" % keys.pop("odbc_autotranslate")
|
|
137
144
|
)
|
|
138
145
|
|
|
139
|
-
connectors.extend(
|
|
146
|
+
connectors.extend(
|
|
147
|
+
["%s=%s" % (check_quote(k), v) for k, v in keys.items()]
|
|
148
|
+
)
|
|
140
149
|
|
|
141
150
|
return ((";".join(connectors),), connect_args)
|
|
142
151
|
|
|
@@ -1213,6 +1213,33 @@ identifier::
|
|
|
1213
1213
|
),
|
|
1214
1214
|
)
|
|
1215
1215
|
|
|
1216
|
+
SQLAlchemy does not automatically copy the columns from the inherited tables
|
|
1217
|
+
mentioned in the ``postgresql_inherits`` argument into the new
|
|
1218
|
+
:class:`_schema.Table`. To populate the new table columns reflection may be
|
|
1219
|
+
used, or a function similar to the following one::
|
|
1220
|
+
|
|
1221
|
+
def get_parent_columns(tbl: sa.Table) -> list[sa.Column]:
|
|
1222
|
+
return [
|
|
1223
|
+
sa.Column(
|
|
1224
|
+
c.name,
|
|
1225
|
+
c.type,
|
|
1226
|
+
key=c.key,
|
|
1227
|
+
nullable=c.nullable,
|
|
1228
|
+
# Set system=true to omit from the CREATE TABLE statement
|
|
1229
|
+
system=True,
|
|
1230
|
+
)
|
|
1231
|
+
for c in tbl.columns
|
|
1232
|
+
]
|
|
1233
|
+
|
|
1234
|
+
|
|
1235
|
+
documents = Table(
|
|
1236
|
+
"some_table",
|
|
1237
|
+
metadata,
|
|
1238
|
+
*get_parent_columns(some_supertable),
|
|
1239
|
+
# ... # add other columns here normally if needed
|
|
1240
|
+
postgresql_inherits="some_supertable",
|
|
1241
|
+
)
|
|
1242
|
+
|
|
1216
1243
|
``ON COMMIT``
|
|
1217
1244
|
^^^^^^^^^^^^^
|
|
1218
1245
|
|
|
@@ -2135,7 +2162,9 @@ class PGCompiler(compiler.SQLCompiler):
|
|
|
2135
2162
|
return "string_agg%s" % self.function_argspec(fn)
|
|
2136
2163
|
|
|
2137
2164
|
def visit_sequence(self, seq, **kw):
|
|
2138
|
-
return "nextval(
|
|
2165
|
+
return "nextval(%s)" % self.preparer._format_sequence_string_literal(
|
|
2166
|
+
seq
|
|
2167
|
+
)
|
|
2139
2168
|
|
|
2140
2169
|
def limit_clause(self, select, **kw):
|
|
2141
2170
|
text = ""
|
|
@@ -2968,9 +2997,43 @@ class PGTypeCompiler(compiler.GenericTypeCompiler):
|
|
|
2968
2997
|
return "JSONPATH"
|
|
2969
2998
|
|
|
2970
2999
|
|
|
3000
|
+
class _CompilerSequence:
|
|
3001
|
+
"""Minimal stand-in for :class:`.Sequence`.
|
|
3002
|
+
|
|
3003
|
+
Used for the implicit sequence behind a SERIAL column, where no
|
|
3004
|
+
:class:`.Sequence` object exists but a name still has to be rendered
|
|
3005
|
+
through :meth:`.IdentifierPreparer.format_sequence`.
|
|
3006
|
+
|
|
3007
|
+
"""
|
|
3008
|
+
|
|
3009
|
+
__slots__ = ("name", "schema")
|
|
3010
|
+
|
|
3011
|
+
# the schema handed to us has already been resolved against the
|
|
3012
|
+
# schema translate map, so don't let format_sequence() translate it
|
|
3013
|
+
# a second time
|
|
3014
|
+
_use_schema_map = False
|
|
3015
|
+
|
|
3016
|
+
def __init__(self, name, schema=None):
|
|
3017
|
+
self.name = name
|
|
3018
|
+
self.schema = schema
|
|
3019
|
+
|
|
3020
|
+
|
|
2971
3021
|
class PGIdentifierPreparer(compiler.IdentifierPreparer):
|
|
2972
3022
|
reserved_words = RESERVED_WORDS
|
|
2973
3023
|
|
|
3024
|
+
def _format_sequence_string_literal(self, sequence):
|
|
3025
|
+
"""Render a sequence name as a SQL string literal.
|
|
3026
|
+
|
|
3027
|
+
``nextval()`` and friends take the sequence as a string rather than
|
|
3028
|
+
as an identifier, so the quoted identifier produced by
|
|
3029
|
+
:meth:`.format_sequence` has to be escaped a second time for the
|
|
3030
|
+
enclosing literal.
|
|
3031
|
+
|
|
3032
|
+
"""
|
|
3033
|
+
return "'%s'" % self.format_sequence(
|
|
3034
|
+
sequence, use_schema=True
|
|
3035
|
+
).replace("'", "''")
|
|
3036
|
+
|
|
2974
3037
|
def _unquote_identifier(self, value):
|
|
2975
3038
|
if value[0] == self.initial_quote:
|
|
2976
3039
|
value = value[1:-1].replace(
|
|
@@ -3156,8 +3219,8 @@ class PGExecutionContext(default.DefaultExecutionContext):
|
|
|
3156
3219
|
def fire_sequence(self, seq, type_):
|
|
3157
3220
|
return self._execute_scalar(
|
|
3158
3221
|
(
|
|
3159
|
-
"select nextval(
|
|
3160
|
-
% self.identifier_preparer.
|
|
3222
|
+
"select nextval(%s)"
|
|
3223
|
+
% self.identifier_preparer._format_sequence_string_literal(seq)
|
|
3161
3224
|
),
|
|
3162
3225
|
type_,
|
|
3163
3226
|
)
|
|
@@ -3194,13 +3257,12 @@ class PGExecutionContext(default.DefaultExecutionContext):
|
|
|
3194
3257
|
else:
|
|
3195
3258
|
effective_schema = None
|
|
3196
3259
|
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
seq_name,
|
|
3260
|
+
exc = (
|
|
3261
|
+
"select nextval(%s)"
|
|
3262
|
+
% self.identifier_preparer._format_sequence_string_literal(
|
|
3263
|
+
_CompilerSequence(seq_name, effective_schema)
|
|
3201
3264
|
)
|
|
3202
|
-
|
|
3203
|
-
exc = "select nextval('\"%s\"')" % (seq_name,)
|
|
3265
|
+
)
|
|
3204
3266
|
|
|
3205
3267
|
return self._execute_scalar(exc, column.type)
|
|
3206
3268
|
|
|
@@ -5229,9 +5291,7 @@ class PGDialect(default.DefaultDialect):
|
|
|
5229
5291
|
util.warn("Could not parse CHECK constraint text: %r" % src)
|
|
5230
5292
|
sqltext = ""
|
|
5231
5293
|
else:
|
|
5232
|
-
sqltext =
|
|
5233
|
-
r"^[\s\n]*\((.+)\)[\s\n]*$", flags=re.DOTALL
|
|
5234
|
-
).sub(r"\1", m.group(1))
|
|
5294
|
+
sqltext = util.strip_outer_parens(m.group(1))
|
|
5235
5295
|
entry = {
|
|
5236
5296
|
"name": check_name,
|
|
5237
5297
|
"sqltext": sqltext,
|
|
@@ -49,6 +49,9 @@ class JSONPathType(sqltypes.JSON.JSONPathType):
|
|
|
49
49
|
if isinstance(value, str):
|
|
50
50
|
# If it's already a string assume that it's in json path
|
|
51
51
|
# format. This allows using cast with json paths literals
|
|
52
|
+
# Still need to process through super_proc for proper escaping
|
|
53
|
+
if super_proc:
|
|
54
|
+
value = super_proc(value)
|
|
52
55
|
return value
|
|
53
56
|
elif value:
|
|
54
57
|
# If it's already a string assume that it's in json path
|
|
@@ -76,6 +76,36 @@ based on the kind of SQLite database that's requested:
|
|
|
76
76
|
may be used by specifying it via the
|
|
77
77
|
:paramref:`_sa.create_engine.poolclass` parameter.
|
|
78
78
|
|
|
79
|
+
.. _aiosqlite_memory:
|
|
80
|
+
|
|
81
|
+
Using a Memory Database with Multiple Coroutines
|
|
82
|
+
-------------------------------------------------
|
|
83
|
+
|
|
84
|
+
The default :class:`.StaticPool` used for ``:memory:`` databases forces all
|
|
85
|
+
coroutines to share a single DBAPI connection. Because SQLite maintains only
|
|
86
|
+
one transaction state per connection, concurrent coroutines can interfere
|
|
87
|
+
with each other — a ``ROLLBACK`` in one coroutine will also discard
|
|
88
|
+
uncommitted work from any other coroutine using the same engine.
|
|
89
|
+
|
|
90
|
+
For async workloads where multiple :class:`.AsyncSession` or
|
|
91
|
+
:class:`.AsyncConnection` objects may be active simultaneously, use SQLite's
|
|
92
|
+
shared-cache URI mode instead. This gives each checkout its own DBAPI
|
|
93
|
+
connection with independent transaction state while still sharing one
|
|
94
|
+
in-memory database::
|
|
95
|
+
|
|
96
|
+
engine = create_async_engine(
|
|
97
|
+
"sqlite+aiosqlite:///file::memory:?cache=shared&uri=true"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
Because this URL form is treated as a file-based database by the dialect,
|
|
101
|
+
:class:`.AsyncAdaptedQueuePool` is used automatically and no additional
|
|
102
|
+
configuration is needed.
|
|
103
|
+
|
|
104
|
+
See the pysqlite documentation at
|
|
105
|
+
:ref:`pysqlite_uri_shared_cache` for full details on shared-cache memory
|
|
106
|
+
databases, including how to use named databases to maintain multiple
|
|
107
|
+
independent in-memory databases within the same process.
|
|
108
|
+
|
|
79
109
|
""" # noqa
|
|
80
110
|
|
|
81
111
|
from __future__ import annotations
|
|
@@ -1128,7 +1128,7 @@ class DATETIME(_DateTimeMixin, sqltypes.DateTime):
|
|
|
1128
1128
|
storage_format=(
|
|
1129
1129
|
"%(year)04d/%(month)02d/%(day)02d %(hour)02d:%(minute)02d:%(second)02d"
|
|
1130
1130
|
),
|
|
1131
|
-
regexp=r"(\d+)/(\d+)/(\d+) (\d+)
|
|
1131
|
+
regexp=r"(\d+)/(\d+)/(\d+) (\d+):(\d+):(\d+)",
|
|
1132
1132
|
)
|
|
1133
1133
|
|
|
1134
1134
|
:param truncate_microseconds: when ``True`` microseconds will be truncated
|
|
@@ -2774,7 +2774,7 @@ class SQLiteDialect(default.DefaultDialect):
|
|
|
2774
2774
|
)
|
|
2775
2775
|
INLINE_UNIQUE_PATTERN = (
|
|
2776
2776
|
r'(?:(".+?")|(?:[\[`])?([a-z0-9_]+)(?:[\]`])?)[\t ]'
|
|
2777
|
-
r"+[a-z0-9_ ]
|
|
2777
|
+
r"+[a-z0-9_]+(?:[\t ]+[a-z0-9_]+)*?[\t ]+UNIQUE"
|
|
2778
2778
|
)
|
|
2779
2779
|
|
|
2780
2780
|
for match in re.finditer(UNIQUE_PATTERN, table_data, re.I):
|
|
@@ -2865,32 +2865,14 @@ class SQLiteDialect(default.DefaultDialect):
|
|
|
2865
2865
|
flags=re.DOTALL,
|
|
2866
2866
|
)
|
|
2867
2867
|
|
|
2868
|
-
# Find the matching closing parenthesis
|
|
2869
|
-
#
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
# Track string literal context
|
|
2877
|
-
if char == "'" and not in_double_quote:
|
|
2878
|
-
in_single_quote = not in_single_quote
|
|
2879
|
-
elif char == '"' and not in_single_quote:
|
|
2880
|
-
in_double_quote = not in_double_quote
|
|
2881
|
-
# Only count parens when not inside a string literal
|
|
2882
|
-
elif not in_single_quote and not in_double_quote:
|
|
2883
|
-
if char == "(":
|
|
2884
|
-
paren_count += 1
|
|
2885
|
-
elif char == ")":
|
|
2886
|
-
paren_count -= 1
|
|
2887
|
-
if paren_count == 0:
|
|
2888
|
-
# Successfully found matching closing parenthesis
|
|
2889
|
-
sqltext = table_data[start:pos].strip()
|
|
2890
|
-
cks.append(
|
|
2891
|
-
{"sqltext": sqltext, "name": constraint_name}
|
|
2892
|
-
)
|
|
2893
|
-
break
|
|
2868
|
+
# Find the matching closing parenthesis with quote-aware paren
|
|
2869
|
+
# counting. ``match.end() - 1`` is the position of the ``(``
|
|
2870
|
+
# that opened the CHECK clause; ``match.end()`` is the first
|
|
2871
|
+
# character of the constraint body.
|
|
2872
|
+
close = util.find_matching_paren(table_data, match.end() - 1)
|
|
2873
|
+
if close is not None:
|
|
2874
|
+
sqltext = table_data[match.end() : close].strip()
|
|
2875
|
+
cks.append({"sqltext": sqltext, "name": constraint_name})
|
|
2894
2876
|
|
|
2895
2877
|
cks.sort(key=lambda d: d["name"] or "~") # sort None as last
|
|
2896
2878
|
if cks:
|
|
@@ -209,8 +209,8 @@ processing.
|
|
|
209
209
|
|
|
210
210
|
.. _pysqlite_threading_pooling:
|
|
211
211
|
|
|
212
|
-
Threading/Pooling Behavior
|
|
213
|
-
|
|
212
|
+
Concurrency/Threading/Pooling Behavior
|
|
213
|
+
--------------------------------------
|
|
214
214
|
|
|
215
215
|
The ``sqlite3`` DBAPI by default prohibits the use of a particular connection
|
|
216
216
|
in a thread which is not the one in which it was created. As SQLite has
|
|
@@ -261,15 +261,82 @@ connection reuse implemented by :class:`.QueuePool`. However, it still
|
|
|
261
261
|
may be beneficial to use this class if the application is experiencing
|
|
262
262
|
issues with files being locked.
|
|
263
263
|
|
|
264
|
-
Using a Memory Database in Multiple Threads
|
|
265
|
-
|
|
264
|
+
Using a Memory Database in Multiple Threads or Coroutines
|
|
265
|
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
266
|
+
|
|
267
|
+
A ``:memory:`` SQLite database exists only within the scope of a single
|
|
268
|
+
DBAPI connection. It is not possible for two separate ``sqlite3``
|
|
269
|
+
connection objects to access the same ``:memory:`` database unless SQLite's
|
|
270
|
+
shared-cache feature is enabled. Without shared cache, each connection
|
|
271
|
+
creates its own independent in-memory database.
|
|
272
|
+
|
|
273
|
+
This means a ``:memory:`` database is **not suitable** for use with
|
|
274
|
+
multiple concurrent threads or coroutines unless either:
|
|
275
|
+
|
|
276
|
+
* All workers are fully serialized (mutexed) against each other
|
|
277
|
+
such that only one is using the database at a time, or
|
|
278
|
+
* SQLite's shared-cache URI feature is used to allow multiple
|
|
279
|
+
independent connections to access the same in-memory database.
|
|
280
|
+
|
|
281
|
+
The same considerations apply when using the :ref:`aiosqlite <aiosqlite>`
|
|
282
|
+
dialect, which wraps ``pysqlite`` connections in an async interface —
|
|
283
|
+
without shared cache, each connection still creates its own independent
|
|
284
|
+
in-memory database.
|
|
285
|
+
|
|
286
|
+
The recommended approach for multithreaded or async in-memory
|
|
287
|
+
use is the shared-cache URI feature, described below.
|
|
288
|
+
|
|
289
|
+
.. _pysqlite_uri_shared_cache:
|
|
290
|
+
|
|
291
|
+
Using a Shared-Cache Memory Database
|
|
292
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
293
|
+
|
|
294
|
+
SQLite's
|
|
295
|
+
`URI shared-cache <https://www.sqlite.org/sharedcache.html>`_
|
|
296
|
+
mode allows multiple independent DBAPI connections, each with
|
|
297
|
+
their own transaction state, to access the same in-memory
|
|
298
|
+
database. This is enabled by using a ``file:`` URI with
|
|
299
|
+
``cache=shared`` and passing ``uri=true`` in the query
|
|
300
|
+
string::
|
|
301
|
+
|
|
302
|
+
engine = create_engine("sqlite:///file::memory:?cache=shared&uri=true")
|
|
303
|
+
|
|
304
|
+
For async use with aiosqlite, use
|
|
305
|
+
:func:`_asyncio.create_async_engine`::
|
|
306
|
+
|
|
307
|
+
engine = create_async_engine(
|
|
308
|
+
"sqlite+aiosqlite:///file::memory:?cache=shared&uri=true"
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
Because this URL form is treated as a file-based database by the
|
|
312
|
+
dialect, :class:`.QueuePool` is used automatically and
|
|
313
|
+
``check_same_thread`` defaults to ``False``, so no additional pool
|
|
314
|
+
or connect_args configuration is needed. Each checkout from the
|
|
315
|
+
pool is a distinct DBAPI connection with its own transaction state,
|
|
316
|
+
and the in-memory database persists as long as at least one
|
|
317
|
+
connection remains open.
|
|
318
|
+
|
|
319
|
+
The shared-cache database is scoped by the filename component of
|
|
320
|
+
the URI. ``file::memory:`` (empty name) is process-global — all
|
|
321
|
+
engines in the process that use this URI share the same database.
|
|
322
|
+
To maintain multiple independent in-memory databases within the
|
|
323
|
+
same process, supply a distinct name for each::
|
|
324
|
+
|
|
325
|
+
engine_a = create_engine(
|
|
326
|
+
"sqlite:///file:db_a?mode=memory&cache=shared&uri=true"
|
|
327
|
+
)
|
|
328
|
+
engine_b = create_engine(
|
|
329
|
+
"sqlite:///file:db_b?mode=memory&cache=shared&uri=true"
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
Using StaticPool for Single-Connection Memory Databases
|
|
333
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
266
334
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
as ``False``::
|
|
335
|
+
An older approach for sharing a ``:memory:`` database among threads
|
|
336
|
+
is to force all checkouts to return the same DBAPI connection using
|
|
337
|
+
:class:`.StaticPool`. **This approach does not support any form of
|
|
338
|
+
concurrency** and is only useful when all access to the engine is
|
|
339
|
+
fully serialized, such as in single-threaded test suites::
|
|
273
340
|
|
|
274
341
|
from sqlalchemy.pool import StaticPool
|
|
275
342
|
|
|
@@ -279,8 +346,18 @@ as ``False``::
|
|
|
279
346
|
poolclass=StaticPool,
|
|
280
347
|
)
|
|
281
348
|
|
|
282
|
-
|
|
283
|
-
|
|
349
|
+
.. warning::
|
|
350
|
+
|
|
351
|
+
Because :class:`.StaticPool` maintains a single DBAPI connection,
|
|
352
|
+
all :class:`.Session` or :class:`.Connection` objects that use
|
|
353
|
+
this engine share that one underlying connection and its single
|
|
354
|
+
SQLite transaction state. A ``ROLLBACK`` issued by one session
|
|
355
|
+
(e.g. during error handling) will also roll back uncommitted work
|
|
356
|
+
from any other session, and concurrent ``COMMIT`` / ``ROLLBACK``
|
|
357
|
+
calls can interfere with each other unpredictably. This approach
|
|
358
|
+
is only appropriate when access to the engine is fully serialized,
|
|
359
|
+
such as in single-threaded test suites. For concurrent workloads,
|
|
360
|
+
use the shared-cache URI approach described above.
|
|
284
361
|
|
|
285
362
|
Using Temporary Tables with SQLite
|
|
286
363
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
@@ -785,6 +785,22 @@ class BulkUDCompileState(ORMDMLState):
|
|
|
785
785
|
}
|
|
786
786
|
)
|
|
787
787
|
|
|
788
|
+
# disable result-level adapt_to_context. ORM UPDATE/DELETE with
|
|
789
|
+
# RETURNING uses a "two level" statement: the invoked ORM statement
|
|
790
|
+
# holds the user's returning() columns while the cached Core
|
|
791
|
+
# statement returns primary key + supplemental columns, so the two
|
|
792
|
+
# are not positionally aligned. adapt_to_context() would remap
|
|
793
|
+
# result keys positionally between them and mislabel columns on a
|
|
794
|
+
# cache hit; the ORM instead interprets rows via its own
|
|
795
|
+
# from_statement context, so the Core adaptation would interfere
|
|
796
|
+
# here.
|
|
797
|
+
if not execution_options:
|
|
798
|
+
execution_options = context._orm_load_exec_options
|
|
799
|
+
else:
|
|
800
|
+
execution_options = execution_options.union(
|
|
801
|
+
context._orm_load_exec_options
|
|
802
|
+
)
|
|
803
|
+
|
|
788
804
|
return (
|
|
789
805
|
statement,
|
|
790
806
|
util.immutabledict(execution_options).union(
|
sqlalchemy/orm/decl_base.py
CHANGED
|
@@ -1240,6 +1240,20 @@ class _ClassScanMapperConfig(_MapperConfig):
|
|
|
1240
1240
|
"documentation for additional information.",
|
|
1241
1241
|
code="dcte",
|
|
1242
1242
|
) from ex
|
|
1243
|
+
else:
|
|
1244
|
+
# as of Python 3.15, dataclasses no longer renders the
|
|
1245
|
+
# auto-generated class docstring immediately; it instead installs
|
|
1246
|
+
# a descriptor that renders the ``__init__`` signature the first
|
|
1247
|
+
# time ``__doc__`` is accessed (see
|
|
1248
|
+
# ``dataclasses._AutoDocstring``). As the ``finally:`` block
|
|
1249
|
+
# below puts the class' original annotations back, that deferred
|
|
1250
|
+
# render would no longer see the dataclass-oriented annotations
|
|
1251
|
+
# applied above, and would omit them from the docstring entirely.
|
|
1252
|
+
# Read the attribute now, while those annotations are still in
|
|
1253
|
+
# place, so the docstring we generate is the same on every Python
|
|
1254
|
+
# version.
|
|
1255
|
+
klass.__doc__
|
|
1256
|
+
|
|
1243
1257
|
finally:
|
|
1244
1258
|
# restore original annotations outside of the dataclasses
|
|
1245
1259
|
# process; for mixins and __abstract__ superclasses, SQLAlchemy
|
sqlalchemy/orm/path_registry.py
CHANGED
|
@@ -17,6 +17,7 @@ from typing import cast
|
|
|
17
17
|
from typing import Dict
|
|
18
18
|
from typing import Iterator
|
|
19
19
|
from typing import List
|
|
20
|
+
from typing import Mapping
|
|
20
21
|
from typing import Optional
|
|
21
22
|
from typing import overload
|
|
22
23
|
from typing import Sequence
|
|
@@ -30,6 +31,8 @@ from .. import exc
|
|
|
30
31
|
from .. import util
|
|
31
32
|
from ..sql import visitors
|
|
32
33
|
from ..sql.cache_key import HasCacheKey
|
|
34
|
+
from ..util.typing import Final
|
|
35
|
+
from ..util.typing import Literal
|
|
33
36
|
|
|
34
37
|
if TYPE_CHECKING:
|
|
35
38
|
from ._typing import _InternalEntityType
|
|
@@ -82,6 +85,40 @@ def _unreduce_path(path: _SerializedPath) -> PathRegistry:
|
|
|
82
85
|
_WILDCARD_TOKEN: _LiteralStar = "*"
|
|
83
86
|
_DEFAULT_TOKEN = "_sa_default"
|
|
84
87
|
|
|
88
|
+
_RELATIONSHIP_TOKEN: Final[Literal["relationship"]] = "relationship"
|
|
89
|
+
_COLUMN_TOKEN: Final[Literal["column"]] = "column"
|
|
90
|
+
|
|
91
|
+
_UNPREFIXED_TOKENS = frozenset([_WILDCARD_TOKEN, _DEFAULT_TOKEN])
|
|
92
|
+
"""the wildcard strings accepted from the user in a loader option.
|
|
93
|
+
|
|
94
|
+
these are prefixed with the target property's ``strategy_wildcard_key`` to
|
|
95
|
+
form the tokens in :data:`._PATH_TOKENS`, and are not themselves valid as an
|
|
96
|
+
element of a path.
|
|
97
|
+
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
_PATH_TOKENS = frozenset(
|
|
101
|
+
f"{wildcard_key}:{suffix}"
|
|
102
|
+
for wildcard_key in (_RELATIONSHIP_TOKEN, _COLUMN_TOKEN)
|
|
103
|
+
for suffix in (_WILDCARD_TOKEN, _DEFAULT_TOKEN)
|
|
104
|
+
)
|
|
105
|
+
"""the complete set of tokens which may appear within a path.
|
|
106
|
+
|
|
107
|
+
:attr:`.PathToken._intern` is populated from this collection at module import
|
|
108
|
+
time, so that a token is present in every process, including one which has
|
|
109
|
+
not yet run any query.
|
|
110
|
+
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
_ACCEPTED_TOKENS = _UNPREFIXED_TOKENS | _PATH_TOKENS
|
|
114
|
+
"""every string a loader option may accept in place of an attribute name.
|
|
115
|
+
|
|
116
|
+
this is the union of the bare wildcards the user writes and the prefixed
|
|
117
|
+
tokens which the loader option internals hand back to themselves; only the
|
|
118
|
+
latter may appear in a path.
|
|
119
|
+
|
|
120
|
+
"""
|
|
121
|
+
|
|
85
122
|
|
|
86
123
|
class PathRegistry(HasCacheKey):
|
|
87
124
|
"""Represent query load paths and registry functions.
|
|
@@ -356,12 +393,12 @@ class CreatesToken(PathRegistry):
|
|
|
356
393
|
is_root: bool
|
|
357
394
|
|
|
358
395
|
def token(self, token: _StrPathToken) -> TokenRegistry:
|
|
359
|
-
if token.
|
|
396
|
+
if token not in PathToken._intern:
|
|
397
|
+
raise exc.ArgumentError(f"invalid token: {token}")
|
|
398
|
+
elif token.endswith(f":{_WILDCARD_TOKEN}"):
|
|
360
399
|
return TokenRegistry(self, token)
|
|
361
|
-
elif token.endswith(f":{_DEFAULT_TOKEN}"):
|
|
362
|
-
return TokenRegistry(self.root, token)
|
|
363
400
|
else:
|
|
364
|
-
|
|
401
|
+
return TokenRegistry(self.root, token)
|
|
365
402
|
|
|
366
403
|
|
|
367
404
|
class RootRegistry(CreatesToken):
|
|
@@ -408,7 +445,17 @@ PathRegistry.root = RootRegistry()
|
|
|
408
445
|
class PathToken(orm_base.InspectionAttr, HasCacheKey, str):
|
|
409
446
|
"""cacheable string token"""
|
|
410
447
|
|
|
411
|
-
_intern:
|
|
448
|
+
_intern: Mapping[str, PathToken]
|
|
449
|
+
"""the :class:`.PathToken` for each of :data:`._PATH_TOKENS`.
|
|
450
|
+
|
|
451
|
+
this collection is fully populated below at module import time and is
|
|
452
|
+
never added to afterwards; it's typed as :class:`.Mapping` so that a
|
|
453
|
+
mutation is flagged by type checkers.
|
|
454
|
+
:meth:`.PathRegistry._deserialize_path` relies on it being complete,
|
|
455
|
+
distinguishing a token from a mapped attribute key by testing
|
|
456
|
+
membership here.
|
|
457
|
+
|
|
458
|
+
"""
|
|
412
459
|
|
|
413
460
|
def _gen_cache_key(
|
|
414
461
|
self, anon_map: anon_map, bindparams: List[BindParameter[Any]]
|
|
@@ -419,13 +466,8 @@ class PathToken(orm_base.InspectionAttr, HasCacheKey, str):
|
|
|
419
466
|
def _path_for_compare(self) -> Optional[_PathRepresentation]:
|
|
420
467
|
return None
|
|
421
468
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
if strvalue in cls._intern:
|
|
425
|
-
return cls._intern[strvalue]
|
|
426
|
-
else:
|
|
427
|
-
cls._intern[strvalue] = result = PathToken(strvalue)
|
|
428
|
-
return result
|
|
469
|
+
|
|
470
|
+
PathToken._intern = {token: PathToken(token) for token in _PATH_TOKENS}
|
|
429
471
|
|
|
430
472
|
|
|
431
473
|
class TokenRegistry(PathRegistry):
|
|
@@ -437,7 +479,7 @@ class TokenRegistry(PathRegistry):
|
|
|
437
479
|
parent: CreatesToken
|
|
438
480
|
|
|
439
481
|
def __init__(self, parent: CreatesToken, token: _StrPathToken):
|
|
440
|
-
token = PathToken.
|
|
482
|
+
token = PathToken._intern[token]
|
|
441
483
|
|
|
442
484
|
self.token = token
|
|
443
485
|
self.parent = parent
|
sqlalchemy/orm/properties.py
CHANGED
|
@@ -29,7 +29,6 @@ from typing import Union
|
|
|
29
29
|
|
|
30
30
|
from . import attributes
|
|
31
31
|
from . import exc as orm_exc
|
|
32
|
-
from . import strategy_options
|
|
33
32
|
from .base import _DeclarativeMapped
|
|
34
33
|
from .base import class_mapper
|
|
35
34
|
from .descriptor_props import CompositeProperty
|
|
@@ -42,6 +41,7 @@ from .interfaces import _MapsColumns
|
|
|
42
41
|
from .interfaces import MapperProperty
|
|
43
42
|
from .interfaces import PropComparator
|
|
44
43
|
from .interfaces import StrategizedProperty
|
|
44
|
+
from .path_registry import _COLUMN_TOKEN
|
|
45
45
|
from .relationships import RelationshipProperty
|
|
46
46
|
from .util import de_stringify_annotation
|
|
47
47
|
from .. import exc as sa_exc
|
|
@@ -108,7 +108,7 @@ class ColumnProperty(
|
|
|
108
108
|
|
|
109
109
|
"""
|
|
110
110
|
|
|
111
|
-
strategy_wildcard_key =
|
|
111
|
+
strategy_wildcard_key = _COLUMN_TOKEN
|
|
112
112
|
inherit_cache = True
|
|
113
113
|
""":meta private:"""
|
|
114
114
|
|
sqlalchemy/orm/relationships.py
CHANGED
|
@@ -45,7 +45,6 @@ from typing import Union
|
|
|
45
45
|
import weakref
|
|
46
46
|
|
|
47
47
|
from . import attributes
|
|
48
|
-
from . import strategy_options
|
|
49
48
|
from ._typing import insp_is_aliased_class
|
|
50
49
|
from ._typing import is_has_collection_adapter
|
|
51
50
|
from .base import _DeclarativeMapped
|
|
@@ -64,6 +63,7 @@ from .interfaces import ONETOMANY
|
|
|
64
63
|
from .interfaces import PropComparator
|
|
65
64
|
from .interfaces import RelationshipDirection
|
|
66
65
|
from .interfaces import StrategizedProperty
|
|
66
|
+
from .path_registry import _RELATIONSHIP_TOKEN
|
|
67
67
|
from .util import _orm_annotate
|
|
68
68
|
from .util import _orm_deannotate
|
|
69
69
|
from .util import CascadeOptions
|
|
@@ -322,7 +322,7 @@ class RelationshipProperty(
|
|
|
322
322
|
|
|
323
323
|
"""
|
|
324
324
|
|
|
325
|
-
strategy_wildcard_key =
|
|
325
|
+
strategy_wildcard_key = _RELATIONSHIP_TOKEN
|
|
326
326
|
inherit_cache = True
|
|
327
327
|
""":meta private:"""
|
|
328
328
|
|