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.
Files changed (49) hide show
  1. sqlalchemy/__init__.py +1 -1
  2. sqlalchemy/connectors/pyodbc.py +13 -4
  3. sqlalchemy/dialects/mssql/base.py +1 -1
  4. sqlalchemy/dialects/postgresql/_psycopg_common.py +1 -1
  5. sqlalchemy/dialects/postgresql/base.py +88 -13
  6. sqlalchemy/dialects/postgresql/hstore.py +2 -2
  7. sqlalchemy/dialects/postgresql/json.py +3 -0
  8. sqlalchemy/dialects/sqlite/aiosqlite.py +30 -0
  9. sqlalchemy/dialects/sqlite/base.py +10 -28
  10. sqlalchemy/dialects/sqlite/pysqlcipher.py +0 -1
  11. sqlalchemy/dialects/sqlite/pysqlite.py +89 -12
  12. sqlalchemy/engine/cursor.py +6 -0
  13. sqlalchemy/engine/result.py +27 -0
  14. sqlalchemy/orm/bulk_persistence.py +16 -0
  15. sqlalchemy/orm/decl_base.py +14 -0
  16. sqlalchemy/orm/path_registry.py +55 -13
  17. sqlalchemy/orm/properties.py +2 -2
  18. sqlalchemy/orm/relationships.py +2 -2
  19. sqlalchemy/orm/scoping.py +2 -3
  20. sqlalchemy/orm/session.py +30 -27
  21. sqlalchemy/orm/strategies.py +1 -1
  22. sqlalchemy/orm/strategy_options.py +11 -6
  23. sqlalchemy/orm/util.py +4 -1
  24. sqlalchemy/sql/_elements_constructors.py +12 -0
  25. sqlalchemy/sql/compiler.py +10 -0
  26. sqlalchemy/sql/elements.py +9 -5
  27. sqlalchemy/sql/lambdas.py +1 -1
  28. sqlalchemy/sql/schema.py +16 -17
  29. sqlalchemy/sql/selectable.py +12 -26
  30. sqlalchemy/sql/sqltypes.py +9 -5
  31. sqlalchemy/testing/__init__.py +2 -0
  32. sqlalchemy/testing/config.py +7 -0
  33. sqlalchemy/testing/fixtures/base.py +10 -5
  34. sqlalchemy/testing/fixtures/mypy.py +5 -3
  35. sqlalchemy/testing/fixtures/orm.py +2 -2
  36. sqlalchemy/testing/fixtures/sql.py +2 -2
  37. sqlalchemy/testing/plugin/plugin_base.py +4 -0
  38. sqlalchemy/testing/plugin/pytestplugin.py +30 -0
  39. sqlalchemy/testing/suite/test_reflection.py +88 -0
  40. sqlalchemy/testing/util.py +41 -0
  41. sqlalchemy/util/__init__.py +2 -0
  42. sqlalchemy/util/compat.py +1 -1
  43. sqlalchemy/util/langhelpers.py +93 -0
  44. sqlalchemy/util/typing.py +2 -0
  45. {sqlalchemy-2.0.50.dist-info → sqlalchemy-2.0.52.dist-info}/METADATA +4 -2
  46. {sqlalchemy-2.0.50.dist-info → sqlalchemy-2.0.52.dist-info}/RECORD +49 -49
  47. {sqlalchemy-2.0.50.dist-info → sqlalchemy-2.0.52.dist-info}/WHEEL +1 -1
  48. {sqlalchemy-2.0.50.dist-info → sqlalchemy-2.0.52.dist-info}/licenses/LICENSE +0 -0
  49. {sqlalchemy-2.0.50.dist-info → sqlalchemy-2.0.52.dist-info}/top_level.txt +0 -0
sqlalchemy/__init__.py CHANGED
@@ -269,7 +269,7 @@ from .types import Uuid as Uuid
269
269
  from .types import VARBINARY as VARBINARY
270
270
  from .types import VARCHAR as VARCHAR
271
271
 
272
- __version__ = "2.0.50"
272
+ __version__ = "2.0.52"
273
273
 
274
274
 
275
275
  def __go(lcls: Any) -> None:
@@ -77,10 +77,16 @@ class PyODBCConnector(Connector):
77
77
  else:
78
78
 
79
79
  def check_quote(token: str) -> str:
80
- if ";" in str(token) or str(token).startswith("{"):
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("DRIVER={%s}" % driver)
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(["%s=%s" % (k, v) for k, v in keys.items()])
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
 
@@ -1585,7 +1585,7 @@ class MSUUid(sqltypes.Uuid):
1585
1585
  if self.native_uuid:
1586
1586
 
1587
1587
  def process(value):
1588
- return f"""'{str(value).replace("''", "'")}'"""
1588
+ return f"""'{str(value).replace("'", "''")}'"""
1589
1589
 
1590
1590
  return process
1591
1591
  else:
@@ -222,4 +222,4 @@ class _PGDialect_common_psycopg(PGDialect):
222
222
  )
223
223
 
224
224
  def do_recover_twophase(self, connection):
225
- return [row[1] for row in connection.connection.tpc_recover()]
225
+ return [str(row) for row in connection.connection.tpc_recover()]
@@ -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('%s')" % self.preparer.format_sequence(seq)
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('%s')"
3145
- % self.identifier_preparer.format_sequence(seq)
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
- if effective_schema is not None:
3183
- exc = 'select nextval(\'"%s"."%s"\')' % (
3184
- effective_schema,
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
- else:
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'").bindparams(
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 = re.compile(
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> (\\ . | [^"])* )" # Quoted 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> (\\ . | [^"])* )" # Quoted 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+)-(\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_ ]+?[\t ]+UNIQUE"
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 by counting balanced parens
2869
- # Must track string context to ignore parens inside string literals
2870
- start = match.end() # Position after 'CHECK ('
2871
- paren_count = 1
2872
- in_single_quote = False
2873
- in_double_quote = False
2874
-
2875
- for pos, char in enumerate(table_data[start:], start):
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
- To use a ``:memory:`` database in a multithreaded scenario, the same
268
- connection object must be shared among threads, since the database exists
269
- only within the scope of that connection. The
270
- :class:`.StaticPool` implementation will maintain a single connection
271
- globally, and the ``check_same_thread`` flag can be passed to Pysqlite
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
- Note that using a ``:memory:`` database in multiple threads requires a recent
283
- version of SQLite.
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
  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -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(
@@ -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(
@@ -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