SQLAlchemy 2.0.50__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/_psycopg_common.py +1 -1
- sqlalchemy/dialects/postgresql/base.py +88 -13
- sqlalchemy/dialects/postgresql/hstore.py +2 -2
- sqlalchemy/dialects/postgresql/json.py +3 -0
- sqlalchemy/dialects/sqlite/aiosqlite.py +30 -0
- sqlalchemy/dialects/sqlite/base.py +10 -28
- sqlalchemy/dialects/sqlite/pysqlcipher.py +0 -1
- sqlalchemy/dialects/sqlite/pysqlite.py +89 -12
- sqlalchemy/engine/cursor.py +6 -0
- sqlalchemy/engine/result.py +27 -0
- 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/scoping.py +2 -3
- sqlalchemy/orm/session.py +30 -27
- sqlalchemy/orm/strategies.py +1 -1
- 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 +9 -5
- sqlalchemy/sql/lambdas.py +1 -1
- sqlalchemy/sql/schema.py +16 -17
- sqlalchemy/sql/selectable.py +12 -26
- sqlalchemy/sql/sqltypes.py +9 -5
- sqlalchemy/testing/__init__.py +2 -0
- sqlalchemy/testing/config.py +7 -0
- sqlalchemy/testing/fixtures/base.py +10 -5
- 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.50.dist-info → sqlalchemy-2.0.52.dist-info}/METADATA +4 -2
- {sqlalchemy-2.0.50.dist-info → sqlalchemy-2.0.52.dist-info}/RECORD +49 -49
- {sqlalchemy-2.0.50.dist-info → sqlalchemy-2.0.52.dist-info}/WHEEL +1 -1
- {sqlalchemy-2.0.50.dist-info → sqlalchemy-2.0.52.dist-info}/licenses/LICENSE +0 -0
- {sqlalchemy-2.0.50.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
|
|
|
@@ -1198,6 +1198,48 @@ constraints, enabling table inheritance hierarchies in PostgreSQL.
|
|
|
1198
1198
|
|
|
1199
1199
|
Table("some_table", metadata, ..., postgresql_inherits=("t1", "t2", ...))
|
|
1200
1200
|
|
|
1201
|
+
For schema-qualified parent table names, use :class:`.quoted_name` with
|
|
1202
|
+
``quote=False`` to prevent the dotted name from being quoted as a single
|
|
1203
|
+
identifier::
|
|
1204
|
+
|
|
1205
|
+
from sqlalchemy.sql import quoted_name
|
|
1206
|
+
|
|
1207
|
+
Table(
|
|
1208
|
+
"some_table",
|
|
1209
|
+
metadata,
|
|
1210
|
+
...,
|
|
1211
|
+
postgresql_inherits=quoted_name(
|
|
1212
|
+
"my_schema.some_supertable", quote=False
|
|
1213
|
+
),
|
|
1214
|
+
)
|
|
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
|
+
|
|
1201
1243
|
``ON COMMIT``
|
|
1202
1244
|
^^^^^^^^^^^^^
|
|
1203
1245
|
|
|
@@ -2120,7 +2162,9 @@ class PGCompiler(compiler.SQLCompiler):
|
|
|
2120
2162
|
return "string_agg%s" % self.function_argspec(fn)
|
|
2121
2163
|
|
|
2122
2164
|
def visit_sequence(self, seq, **kw):
|
|
2123
|
-
return "nextval(
|
|
2165
|
+
return "nextval(%s)" % self.preparer._format_sequence_string_literal(
|
|
2166
|
+
seq
|
|
2167
|
+
)
|
|
2124
2168
|
|
|
2125
2169
|
def limit_clause(self, select, **kw):
|
|
2126
2170
|
text = ""
|
|
@@ -2953,9 +2997,43 @@ class PGTypeCompiler(compiler.GenericTypeCompiler):
|
|
|
2953
2997
|
return "JSONPATH"
|
|
2954
2998
|
|
|
2955
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
|
+
|
|
2956
3021
|
class PGIdentifierPreparer(compiler.IdentifierPreparer):
|
|
2957
3022
|
reserved_words = RESERVED_WORDS
|
|
2958
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
|
+
|
|
2959
3037
|
def _unquote_identifier(self, value):
|
|
2960
3038
|
if value[0] == self.initial_quote:
|
|
2961
3039
|
value = value[1:-1].replace(
|
|
@@ -3141,8 +3219,8 @@ class PGExecutionContext(default.DefaultExecutionContext):
|
|
|
3141
3219
|
def fire_sequence(self, seq, type_):
|
|
3142
3220
|
return self._execute_scalar(
|
|
3143
3221
|
(
|
|
3144
|
-
"select nextval(
|
|
3145
|
-
% self.identifier_preparer.
|
|
3222
|
+
"select nextval(%s)"
|
|
3223
|
+
% self.identifier_preparer._format_sequence_string_literal(seq)
|
|
3146
3224
|
),
|
|
3147
3225
|
type_,
|
|
3148
3226
|
)
|
|
@@ -3179,13 +3257,12 @@ class PGExecutionContext(default.DefaultExecutionContext):
|
|
|
3179
3257
|
else:
|
|
3180
3258
|
effective_schema = None
|
|
3181
3259
|
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
seq_name,
|
|
3260
|
+
exc = (
|
|
3261
|
+
"select nextval(%s)"
|
|
3262
|
+
% self.identifier_preparer._format_sequence_string_literal(
|
|
3263
|
+
_CompilerSequence(seq_name, effective_schema)
|
|
3186
3264
|
)
|
|
3187
|
-
|
|
3188
|
-
exc = "select nextval('\"%s\"')" % (seq_name,)
|
|
3265
|
+
)
|
|
3189
3266
|
|
|
3190
3267
|
return self._execute_scalar(exc, column.type)
|
|
3191
3268
|
|
|
@@ -3502,7 +3579,7 @@ class PGDialect(default.DefaultDialect):
|
|
|
3502
3579
|
|
|
3503
3580
|
def do_prepare_twophase(self, connection, xid):
|
|
3504
3581
|
connection.execute(
|
|
3505
|
-
sql.text("PREPARE TRANSACTION :xid
|
|
3582
|
+
sql.text("PREPARE TRANSACTION :xid").bindparams(
|
|
3506
3583
|
sql.bindparam("xid", xid, literal_execute=True)
|
|
3507
3584
|
)
|
|
3508
3585
|
)
|
|
@@ -5214,9 +5291,7 @@ class PGDialect(default.DefaultDialect):
|
|
|
5214
5291
|
util.warn("Could not parse CHECK constraint text: %r" % src)
|
|
5215
5292
|
sqltext = ""
|
|
5216
5293
|
else:
|
|
5217
|
-
sqltext =
|
|
5218
|
-
r"^[\s\n]*\((.+)\)[\s\n]*$", flags=re.DOTALL
|
|
5219
|
-
).sub(r"\1", m.group(1))
|
|
5294
|
+
sqltext = util.strip_outer_parens(m.group(1))
|
|
5220
5295
|
entry = {
|
|
5221
5296
|
"name": check_name,
|
|
5222
5297
|
"sqltext": sqltext,
|
|
@@ -302,12 +302,12 @@ class _HStoreMatrixFunction(sqlfunc.GenericFunction):
|
|
|
302
302
|
HSTORE_PAIR_RE = re.compile(
|
|
303
303
|
r"""
|
|
304
304
|
(
|
|
305
|
-
"(?P<key> (\\ . | [^"])* )"
|
|
305
|
+
"(?P<key> (\\ . | [^"\\])* )" # Quoted key
|
|
306
306
|
)
|
|
307
307
|
[ ]* => [ ]* # Pair operator, optional adjoining whitespace
|
|
308
308
|
(
|
|
309
309
|
(?P<value_null> NULL ) # NULL value
|
|
310
|
-
| "(?P<value> (\\ . | [^"])* )"
|
|
310
|
+
| "(?P<value> (\\ . | [^"\\])* )" # Quoted value
|
|
311
311
|
)
|
|
312
312
|
""",
|
|
313
313
|
re.VERBOSE,
|
|
@@ -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:
|
|
@@ -144,7 +144,6 @@ class SQLiteDialect_pysqlcipher(SQLiteDialect_pysqlite):
|
|
|
144
144
|
cursor.execute(f"pragma key={passphrase}")
|
|
145
145
|
for prag, value in query_pragmas.items():
|
|
146
146
|
cursor.execute(f"pragma {prag}={value}")
|
|
147
|
-
print(query_pragmas)
|
|
148
147
|
cursor.close()
|
|
149
148
|
|
|
150
149
|
if super_on_connect:
|
|
@@ -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
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
sqlalchemy/engine/cursor.py
CHANGED
|
@@ -183,9 +183,15 @@ class CursorResultMetaData(ResultMetaData):
|
|
|
183
183
|
return key in self._keymap
|
|
184
184
|
|
|
185
185
|
def _for_freeze(self) -> ResultMetaData:
|
|
186
|
+
ambiguous = {
|
|
187
|
+
rec[MD_LOOKUP_KEY]
|
|
188
|
+
for rec in self._keymap.values()
|
|
189
|
+
if rec[MD_INDEX] is None
|
|
190
|
+
}
|
|
186
191
|
return SimpleResultMetaData(
|
|
187
192
|
self._keys,
|
|
188
193
|
extra=[self._keymap[key][MD_OBJECTS] for key in self._keys],
|
|
194
|
+
_ambiguous_keys=frozenset(ambiguous) if ambiguous else None,
|
|
189
195
|
)
|
|
190
196
|
|
|
191
197
|
def _make_new_metadata(
|
sqlalchemy/engine/result.py
CHANGED
|
@@ -267,6 +267,7 @@ class SimpleResultMetaData(ResultMetaData):
|
|
|
267
267
|
"_translated_indexes",
|
|
268
268
|
"_unique_filters",
|
|
269
269
|
"_key_to_index",
|
|
270
|
+
"_ambiguous_keys",
|
|
270
271
|
)
|
|
271
272
|
|
|
272
273
|
_keys: Sequence[str]
|
|
@@ -279,6 +280,7 @@ class SimpleResultMetaData(ResultMetaData):
|
|
|
279
280
|
_tuplefilter: Optional[_TupleGetterType] = None,
|
|
280
281
|
_translated_indexes: Optional[Sequence[int]] = None,
|
|
281
282
|
_unique_filters: Optional[Sequence[Callable[[Any], Any]]] = None,
|
|
283
|
+
_ambiguous_keys: Optional[frozenset[str]] = None,
|
|
282
284
|
):
|
|
283
285
|
self._keys = list(keys)
|
|
284
286
|
self._tuplefilter = _tuplefilter
|
|
@@ -300,8 +302,15 @@ class SimpleResultMetaData(ResultMetaData):
|
|
|
300
302
|
|
|
301
303
|
self._keymap = {key: rec for keys, rec in recs_names for key in keys}
|
|
302
304
|
|
|
305
|
+
if _ambiguous_keys:
|
|
306
|
+
for name in _ambiguous_keys.intersection(self._keymap):
|
|
307
|
+
rec = self._keymap[name]
|
|
308
|
+
self._keymap[name] = (None,) + rec[1:]
|
|
309
|
+
|
|
303
310
|
self._processors = _processors
|
|
304
311
|
|
|
312
|
+
self._ambiguous_keys = _ambiguous_keys
|
|
313
|
+
|
|
305
314
|
self._key_to_index = self._make_key_to_index(self._keymap, 0)
|
|
306
315
|
|
|
307
316
|
def _has_key(self, key: object) -> bool:
|
|
@@ -318,12 +327,14 @@ class SimpleResultMetaData(ResultMetaData):
|
|
|
318
327
|
self._keys,
|
|
319
328
|
extra=[self._keymap[key][2] for key in self._keys],
|
|
320
329
|
_unique_filters=unique_filters,
|
|
330
|
+
_ambiguous_keys=self._ambiguous_keys,
|
|
321
331
|
)
|
|
322
332
|
|
|
323
333
|
def __getstate__(self) -> Dict[str, Any]:
|
|
324
334
|
return {
|
|
325
335
|
"_keys": self._keys,
|
|
326
336
|
"_translated_indexes": self._translated_indexes,
|
|
337
|
+
"_ambiguous_keys": self._ambiguous_keys,
|
|
327
338
|
}
|
|
328
339
|
|
|
329
340
|
def __setstate__(self, state: Dict[str, Any]) -> None:
|
|
@@ -336,6 +347,7 @@ class SimpleResultMetaData(ResultMetaData):
|
|
|
336
347
|
state["_keys"],
|
|
337
348
|
_translated_indexes=_translated_indexes,
|
|
338
349
|
_tuplefilter=_tuplefilter,
|
|
350
|
+
_ambiguous_keys=state.get("_ambiguous_keys"),
|
|
339
351
|
)
|
|
340
352
|
|
|
341
353
|
def _index_for_key(self, key: Any, raiseerr: bool = True) -> int:
|
|
@@ -346,9 +358,21 @@ class SimpleResultMetaData(ResultMetaData):
|
|
|
346
358
|
except KeyError as ke:
|
|
347
359
|
rec = self._key_fallback(key, ke, raiseerr)
|
|
348
360
|
|
|
361
|
+
if rec[0] is None:
|
|
362
|
+
self._raise_for_ambiguous_column_name(rec)
|
|
349
363
|
return rec[0] # type: ignore[no-any-return]
|
|
350
364
|
|
|
365
|
+
def _raise_for_ambiguous_column_name(
|
|
366
|
+
self, rec: _KeyMapRecType
|
|
367
|
+
) -> NoReturn:
|
|
368
|
+
raise exc.InvalidRequestError(
|
|
369
|
+
"Ambiguous column name '%s' in "
|
|
370
|
+
"result set column descriptions" % rec[1]
|
|
371
|
+
)
|
|
372
|
+
|
|
351
373
|
def _indexes_for_keys(self, keys: Sequence[Any]) -> Sequence[int]:
|
|
374
|
+
# only used by the ORM with Column objects; does not need
|
|
375
|
+
# ambiguous column name support
|
|
352
376
|
return [self._keymap[key][0] for key in keys]
|
|
353
377
|
|
|
354
378
|
def _metadata_for_keys(
|
|
@@ -363,6 +387,9 @@ class SimpleResultMetaData(ResultMetaData):
|
|
|
363
387
|
except KeyError as ke:
|
|
364
388
|
rec = self._key_fallback(key, ke, True)
|
|
365
389
|
|
|
390
|
+
if rec[0] is None:
|
|
391
|
+
self._raise_for_ambiguous_column_name(rec)
|
|
392
|
+
|
|
366
393
|
yield rec
|
|
367
394
|
|
|
368
395
|
def _reduce(self, keys: Sequence[Any]) -> ResultMetaData:
|
|
@@ -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
|