SQLAlchemy 2.1.3__py3-none-any.whl → 2.1.5__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 (39) hide show
  1. sqlalchemy/__init__.py +1 -1
  2. sqlalchemy/dialects/mssql/base.py +7 -0
  3. sqlalchemy/dialects/oracle/base.py +4 -1
  4. sqlalchemy/dialects/postgresql/asyncpg.py +6 -2
  5. sqlalchemy/dialects/postgresql/base.py +12 -2
  6. sqlalchemy/dialects/postgresql/named_types.py +16 -0
  7. sqlalchemy/engine/_result_cy.py +37 -36
  8. sqlalchemy/engine/base.py +40 -30
  9. sqlalchemy/engine/characteristics.py +1 -1
  10. sqlalchemy/engine/create.py +20 -10
  11. sqlalchemy/engine/default.py +9 -6
  12. sqlalchemy/ext/mutable.py +11 -0
  13. sqlalchemy/orm/__init__.py +2 -0
  14. sqlalchemy/orm/_orm_constructors.py +134 -0
  15. sqlalchemy/orm/base.py +54 -0
  16. sqlalchemy/orm/bulk_persistence.py +5 -3
  17. sqlalchemy/orm/decl_api.py +22 -5
  18. sqlalchemy/orm/decl_base.py +110 -24
  19. sqlalchemy/orm/mapper.py +56 -2
  20. sqlalchemy/orm/properties.py +75 -1
  21. sqlalchemy/orm/util.py +9 -2
  22. sqlalchemy/pool/base.py +47 -29
  23. sqlalchemy/sql/compiler.py +15 -2
  24. sqlalchemy/sql/dml.py +8 -0
  25. sqlalchemy/sql/schema.py +2 -0
  26. sqlalchemy/sql/sqltypes.py +1 -0
  27. sqlalchemy/testing/cancellation.py +77 -6
  28. sqlalchemy/testing/plugin/pytestplugin.py +28 -1
  29. sqlalchemy/testing/profiling.py +23 -0
  30. sqlalchemy/testing/requirements.py +5 -0
  31. sqlalchemy/testing/suite/test_select.py +6 -0
  32. sqlalchemy/testing/suite/test_types.py +75 -0
  33. sqlalchemy/util/queue.py +11 -0
  34. {sqlalchemy-2.1.3.dist-info → sqlalchemy-2.1.5.dist-info}/METADATA +1 -1
  35. {sqlalchemy-2.1.3.dist-info → sqlalchemy-2.1.5.dist-info}/RECORD +39 -39
  36. {sqlalchemy-2.1.3.dist-info → sqlalchemy-2.1.5.dist-info}/WHEEL +0 -0
  37. {sqlalchemy-2.1.3.dist-info → sqlalchemy-2.1.5.dist-info}/licenses/AUTHORS +0 -0
  38. {sqlalchemy-2.1.3.dist-info → sqlalchemy-2.1.5.dist-info}/licenses/LICENSE +0 -0
  39. {sqlalchemy-2.1.3.dist-info → sqlalchemy-2.1.5.dist-info}/top_level.txt +0 -0
sqlalchemy/__init__.py CHANGED
@@ -285,7 +285,7 @@ from .types import Uuid as Uuid
285
285
  from .types import VARBINARY as VARBINARY
286
286
  from .types import VARCHAR as VARCHAR
287
287
 
288
- __version__ = "2.1.3"
288
+ __version__ = "2.1.5"
289
289
 
290
290
 
291
291
  def __go(lcls: Any) -> None:
@@ -2098,6 +2098,13 @@ class MSSQLCompiler(compiler.SQLCompiler):
2098
2098
  self.process(binary.right, **kw),
2099
2099
  )
2100
2100
 
2101
+ def visit_truediv_binary(self, binary, operator, **kw):
2102
+ return (
2103
+ self.process(binary.left, **kw)
2104
+ + " / "
2105
+ + f"({self.process(binary.right, **kw)} + 0.0)"
2106
+ )
2107
+
2101
2108
  def visit_true(self, expr, **kw):
2102
2109
  return "1"
2103
2110
 
@@ -2279,7 +2279,10 @@ class OracleDialect(default._BackendsMultiReflection, default.DefaultDialect):
2279
2279
  return self.get_isolation_level(dbapi_conn)
2280
2280
  except NotImplementedError:
2281
2281
  raise
2282
- except:
2282
+ except Exception:
2283
+ # Exception only; a bare except here also caught
2284
+ # asyncio.CancelledError and KeyboardInterrupt, returning the
2285
+ # default in place of propagating them
2283
2286
  return "READ COMMITTED"
2284
2287
 
2285
2288
  def _execute_reflection(
@@ -886,14 +886,18 @@ class AsyncAdapt_asyncpg_connection(
886
886
 
887
887
  assert self._transaction is None
888
888
  try:
889
- self._transaction = self._connection.transaction(
889
+ transaction = self._connection.transaction(
890
890
  isolation=self.isolation_level,
891
891
  readonly=self.readonly,
892
892
  deferrable=self.deferrable,
893
893
  )
894
- await self._transaction.start()
894
+ await transaction.start()
895
895
  except Exception as error:
896
896
  self._handle_exception(error)
897
+ else:
898
+ # only track the transaction once BEGIN has succeeded; a
899
+ # transaction that failed to start has nothing to roll back
900
+ self._transaction = transaction
897
901
 
898
902
  async def _call_and_discard(self, fn: Callable[[], Awaitable[Any]]):
899
903
  try:
@@ -5070,8 +5070,9 @@ class PGDialect(default._BackendsMultiReflection, default.DefaultDialect):
5070
5070
 
5071
5071
  # https://www.postgresql.org/docs/current/static/sql-createtable.html
5072
5072
  return re.compile(
5073
- r"FOREIGN KEY \((.*?)\) "
5074
- rf"REFERENCES (?:({qtoken})\.)?({qtoken})\(((?:{qtoken}(?: *, *)?)+)\)" # noqa: E501
5073
+ rf"FOREIGN KEY \((.*?)(?:, PERIOD ({qtoken}))?\) "
5074
+ rf"REFERENCES (?:({qtoken})\.)?({qtoken})"
5075
+ rf"\(((?:{qtoken}(?: *, *)?)+)(?:, PERIOD ({qtoken}))?\)"
5075
5076
  r"[\s]?(MATCH (FULL|PARTIAL|SIMPLE)+)?"
5076
5077
  r"[\s]?(?:ON (UPDATE|DELETE) "
5077
5078
  r"(CASCADE|RESTRICT|NO ACTION|"
@@ -5089,9 +5090,11 @@ class PGDialect(default._BackendsMultiReflection, default.DefaultDialect):
5089
5090
 
5090
5091
  (
5091
5092
  constrained_columns,
5093
+ constrained_period_column,
5092
5094
  referred_schema,
5093
5095
  referred_table,
5094
5096
  referred_columns,
5097
+ referred_period_column,
5095
5098
  _,
5096
5099
  match,
5097
5100
  upddelkey1,
@@ -5103,6 +5106,13 @@ class PGDialect(default._BackendsMultiReflection, default.DefaultDialect):
5103
5106
  initially,
5104
5107
  ) = m
5105
5108
 
5109
+ # TODO: the PERIOD designation of a temporal foreign key
5110
+ # (PostgreSQL 18+) is not yet reflected
5111
+ if constrained_period_column:
5112
+ constrained_columns += f", {constrained_period_column}"
5113
+ if referred_period_column:
5114
+ referred_columns += f", {referred_period_column}"
5115
+
5106
5116
  onupdate = (
5107
5117
  upddelval1
5108
5118
  if upddelkey1 == "UPDATE"
@@ -339,6 +339,9 @@ class ENUM(NamedType, type_api.NativeForEmulated, sqltypes.Enum):
339
339
  kw.setdefault("_create_events", False)
340
340
  kw.setdefault("values_callable", impl.values_callable)
341
341
  kw.setdefault("omit_aliases", impl._omit_aliases)
342
+ kw.setdefault("sort_key_function", impl._sort_key_function)
343
+ kw.setdefault("create_constraint", impl.create_constraint)
344
+ kw.setdefault("length", impl.length)
342
345
  kw.setdefault("_adapted_from", impl)
343
346
 
344
347
  return cls(**kw)
@@ -514,6 +517,19 @@ class DOMAIN(NamedType, sqltypes.SchemaType):
514
517
  def __test_init__(cls):
515
518
  return cls("name", sqltypes.Integer)
516
519
 
520
+ def adapt(self, cls, **kw):
521
+ # the DOMAIN-specific arguments are keyword-only, which are not
522
+ # picked up by util.constructor_copy(); pass them explicitly so
523
+ # that copy(), to_metadata() and dialect adaptation retain them
524
+ kw.setdefault("collation", self.collation)
525
+ kw.setdefault("collation_schema", self.collation_schema)
526
+ kw.setdefault("default", self.default)
527
+ kw.setdefault("constraint_name", self.constraint_name)
528
+ kw.setdefault("not_null", self.not_null)
529
+ kw.setdefault("check", self.check)
530
+ kw.setdefault("create_type", self.create_type)
531
+ return super().adapt(cls, **kw)
532
+
517
533
 
518
534
  class CreateEnumType(schema._CreateDropBase):
519
535
  __visit_name__ = "create_enum_type"
@@ -91,6 +91,37 @@ class _NoRow(Enum):
91
91
  _NO_ROW = _NoRow._NO_ROW
92
92
 
93
93
 
94
+ def _list_map(
95
+ fn: Callable[[Any], _T], /
96
+ ) -> Callable[[Sequence[Any]], list[_T]]:
97
+ """Return a callable that applies ``fn`` to each element of a sequence,
98
+ returning a list.
99
+
100
+ This function serves to prevent a reference cycle between ``fn`` and
101
+ the closure scope of the function that defines it, when compiled with
102
+ Cython.
103
+
104
+ """
105
+ if cython.compiled:
106
+
107
+ def list_map(rows: Sequence[Any], /) -> list[_T]:
108
+ size: cython.Py_hash_t = len(rows)
109
+ i: cython.Py_ssize_t
110
+ result: list = PyList_New(size)
111
+ for i in range(size):
112
+ row: object = fn(rows[i])
113
+ Py_INCREF(row)
114
+ PyList_SET_ITEM(result, i, row)
115
+ return result
116
+
117
+ else:
118
+
119
+ def list_map(rows: Sequence[Any], /) -> list[_T]:
120
+ return [fn(row) for row in rows]
121
+
122
+ return list_map
123
+
124
+
94
125
  class BaseResultInternal(Generic[_R]):
95
126
  __slots__ = ()
96
127
 
@@ -245,22 +276,7 @@ class BaseResultInternal(Generic[_R]):
245
276
  row = log_row(row)
246
277
  return row
247
278
 
248
- if cython.compiled:
249
-
250
- def many_rows(rows: Sequence[Any], /) -> list[Any]:
251
- size: cython.Py_hash_t = len(rows)
252
- i: cython.Py_ssize_t
253
- result: list = PyList_New(size)
254
- for i in range(size):
255
- row: object = single_row(rows[i])
256
- Py_INCREF(row)
257
- PyList_SET_ITEM(result, i, row)
258
- return result
259
-
260
- else:
261
-
262
- def many_rows(rows: Sequence[Any], /) -> list[Any]:
263
- return [single_row(row) for row in rows]
279
+ many_rows = _list_map(single_row)
264
280
 
265
281
  if flag == _FLAG_SCALAR_TO_TUPLE or has_log_row:
266
282
  # scalar-source rows have no tuple form; row logging requires
@@ -279,27 +295,12 @@ class BaseResultInternal(Generic[_R]):
279
295
  input_row = tuple(input_row)
280
296
  return input_row
281
297
 
282
- if cython.compiled:
283
-
284
- def interim_rows(rows: Sequence[Any], /) -> Sequence[Any]:
285
- size: cython.Py_hash_t = len(rows)
286
- i: cython.Py_ssize_t
287
- result: list = PyList_New(size)
288
- for i in range(size):
289
- row: object = single_interim_row(rows[i])
290
- Py_INCREF(row)
291
- PyList_SET_ITEM(result, i, row)
292
- return result
293
-
294
- else:
295
-
296
- def interim_rows(rows: Sequence[Any], /) -> Sequence[Any]:
297
- return [single_interim_row(row) for row in rows]
298
+ interim_rows = _list_map(single_interim_row)
298
299
 
299
300
  return single_row, many_rows, interim_rows # type: ignore[return-value] # noqa: E501
300
301
 
301
302
  @HasMemoized_ro_memoized_attribute
302
- def _iterator_getter(self) -> Callable[[], Iterator[_R]]:
303
+ def _iterator_getter(self) -> Callable[[Self], Iterator[_R]]:
303
304
  make_row = self._row_getter[0]
304
305
 
305
306
  post_creational_filter = self._post_creational_filter
@@ -308,7 +309,7 @@ class BaseResultInternal(Generic[_R]):
308
309
  uniques: set
309
310
  uniques, strategy = self._unique_strategy
310
311
 
311
- def iterrows() -> Iterator[_R]:
312
+ def iterrows(self: Self) -> Iterator[_R]:
312
313
  for raw_row in self._fetchiter_impl():
313
314
  row = (
314
315
  make_row(raw_row) if make_row is not None else raw_row
@@ -323,7 +324,7 @@ class BaseResultInternal(Generic[_R]):
323
324
 
324
325
  else:
325
326
 
326
- def iterrows() -> Iterator[_R]:
327
+ def iterrows(self: Self) -> Iterator[_R]:
327
328
  for raw_row in self._fetchiter_impl():
328
329
  row = (
329
330
  make_row(raw_row) if make_row is not None else raw_row
@@ -617,7 +618,7 @@ class BaseResultInternal(Generic[_R]):
617
618
  return row # type: ignore[return-value]
618
619
 
619
620
  def _iter_impl(self) -> Iterator[_R]:
620
- return self._iterator_getter()
621
+ return self._iterator_getter(self)
621
622
 
622
623
  def _next_impl(self) -> _R:
623
624
  row = self._onerow_getter(self)
sqlalchemy/engine/base.py CHANGED
@@ -758,12 +758,19 @@ class Connection(ConnectionEventsTarget, inspection.Inspectable["Inspector"]):
758
758
  if self.closed:
759
759
  raise exc.ResourceClosedError("This Connection is closed")
760
760
 
761
- if self._still_open_and_dbapi_connection_is_valid:
762
- pool_proxied_connection = self._dbapi_connection
763
- assert pool_proxied_connection is not None
764
- pool_proxied_connection.invalidate(exception)
765
-
766
- self._dbapi_connection = None
761
+ try:
762
+ if self._still_open_and_dbapi_connection_is_valid:
763
+ pool_proxied_connection = self._dbapi_connection
764
+ assert pool_proxied_connection is not None
765
+ pool_proxied_connection.invalidate(exception)
766
+ finally:
767
+ # this is always a hard invalidation, which closes the DBAPI
768
+ # connection; for an asyncio driver that close is an awaited
769
+ # terminate() which a cancellation can interrupt. this
770
+ # Connection is invalidated regardless, so that nothing
771
+ # further, such as a rollback, is emitted on the DBAPI
772
+ # connection
773
+ self._dbapi_connection = None
767
774
 
768
775
  def detach(self) -> None:
769
776
  """Detach the underlying DB-API connection from its connection pool.
@@ -1257,30 +1264,33 @@ class Connection(ConnectionEventsTarget, inspection.Inspectable["Inspector"]):
1257
1264
 
1258
1265
  """
1259
1266
 
1260
- if self._transaction:
1261
- self._transaction.close()
1262
- skip_reset = True
1263
- else:
1264
- skip_reset = False
1265
-
1266
- if self._dbapi_connection is not None:
1267
- conn = self._dbapi_connection
1268
-
1269
- # as we just closed the transaction, close the connection
1270
- # pool connection without doing an additional reset
1271
- if skip_reset:
1272
- cast("_ConnectionFairy", conn)._close_special(
1273
- transaction_reset=True
1274
- )
1275
- else:
1276
- conn.close()
1277
-
1278
- # There is a slight chance that conn.close() may have
1279
- # triggered an invalidation here in which case
1280
- # _dbapi_connection would already be None, however usually
1281
- # it will be non-None here and in a "closed" state.
1282
- self._dbapi_connection = None
1283
- self.__can_reconnect = False
1267
+ # if the rollback here raises, skip_reset stays False and the pool
1268
+ # does its own reset of the connection, invalidating it if that
1269
+ # fails also. see #13657
1270
+ skip_reset = False
1271
+ try:
1272
+ if self._transaction:
1273
+ self._transaction.close()
1274
+ skip_reset = True
1275
+ finally:
1276
+ if self._dbapi_connection is not None:
1277
+ conn = self._dbapi_connection
1278
+
1279
+ # as we just closed the transaction, close the connection
1280
+ # pool connection without doing an additional reset
1281
+ if skip_reset:
1282
+ cast("_ConnectionFairy", conn)._close_special(
1283
+ transaction_reset=True
1284
+ )
1285
+ else:
1286
+ conn.close()
1287
+
1288
+ # There is a slight chance that conn.close() may have
1289
+ # triggered an invalidation here in which case
1290
+ # _dbapi_connection would already be None, however usually
1291
+ # it will be non-None here and in a "closed" state.
1292
+ self._dbapi_connection = None
1293
+ self.__can_reconnect = False
1284
1294
 
1285
1295
  # special case to handle mypy issue:
1286
1296
  # https://github.com/python/mypy/issues/20651
@@ -142,7 +142,7 @@ class LoggingTokenCharacteristic(ConnectionCharacteristic):
142
142
  if value:
143
143
  conn._message_formatter = lambda msg: "[%s] %s" % (value, msg)
144
144
  else:
145
- del conn._message_formatter
145
+ conn._message_formatter = None
146
146
 
147
147
  def get_characteristic(
148
148
  self, dialect: Dialect, dbapi_conn: DBAPIConnection
@@ -747,16 +747,26 @@ def create_engine(url: Union[str, _url.URL], **kwargs: Any) -> Engine:
747
747
  )
748
748
  c._execution_options = util.EMPTY_DICT
749
749
 
750
- try:
751
- dialect.initialize(c)
752
- finally:
753
- # note that "invalidated" and "closed" are mutually
754
- # exclusive in 1.4 Connection.
755
- if not c.invalidated and not c.closed:
756
- # transaction is rolled back otherwise, tested by
757
- # test/dialect/postgresql/test_dialect.py
758
- # ::MiscBackendTest::test_initial_transaction_state
759
- dialect.do_rollback(c.connection)
750
+ dialect.initialize(c)
751
+
752
+ # roll back only once initialize() has succeeded, rather than
753
+ # in a finally: block. when initialize() raises, the DBAPI
754
+ # connection may be partway through an operation that c
755
+ # doesn't know about, such as psycopg's TypeInfo.fetch()
756
+ # interrupted by a cancellation; the rollback then fails
757
+ # and its error replaces the original one. the pool closes
758
+ # the connection in that case, so there's nothing to roll
759
+ # back.
760
+ #
761
+ # note that "invalidated" and "closed" are mutually
762
+ # exclusive in 1.4 Connection. c may be invalidated here if
763
+ # initialize() caught an error on a statement that was a
764
+ # disconnect.
765
+ if not c.invalidated and not c.closed:
766
+ # transaction is rolled back otherwise, tested by
767
+ # test/dialect/postgresql/test_dialect.py
768
+ # ::MiscBackendTest::test_initial_transaction_state
769
+ dialect.do_rollback(c.connection)
760
770
 
761
771
  # previously, the "first_connect" event was used here, which was then
762
772
  # scaled back if the "on_connect" handler were present. now,
@@ -2137,6 +2137,15 @@ class DefaultExecutionContext(ExecutionContext):
2137
2137
  ):
2138
2138
  self._setup_out_parameters(result)
2139
2139
 
2140
+ if not result._soft_closed and not result._metadata.returns_rows:
2141
+ # no results, get rowcount (which requires open cursor on
2142
+ # some drivers) and release the cursor. this applies to all
2143
+ # statements that don't return rows, including DML, text(),
2144
+ # DDL and SAVEPOINT
2145
+ if self._rowcount is None:
2146
+ self._rowcount = self.cursor.rowcount
2147
+ result._soft_close()
2148
+
2140
2149
  self._soft_closed = result._soft_closed
2141
2150
 
2142
2151
  if yp:
@@ -2272,12 +2281,6 @@ class DefaultExecutionContext(ExecutionContext):
2272
2281
  # the rows have all been fetched however.
2273
2282
  assert result._metadata.returns_rows
2274
2283
 
2275
- elif not result._metadata.returns_rows:
2276
- # no results, get rowcount
2277
- # (which requires open cursor on some drivers)
2278
- if self._rowcount is None:
2279
- self._rowcount = self.cursor.rowcount
2280
- result._soft_close()
2281
2284
  elif self.isupdate or self.isdelete:
2282
2285
  if self._rowcount is None:
2283
2286
  self._rowcount = self.cursor.rowcount
sqlalchemy/ext/mutable.py CHANGED
@@ -840,6 +840,12 @@ class MutableDict(Mutable, Dict[_KT, _VT]):
840
840
  dict.update(self, *a, **kw)
841
841
  self.changed()
842
842
 
843
+ def __ior__( # type: ignore[override,misc]
844
+ self, other: Any
845
+ ) -> MutableDict[_KT, _VT]:
846
+ self.update(other)
847
+ return self
848
+
843
849
  if TYPE_CHECKING:
844
850
 
845
851
  @overload
@@ -949,6 +955,11 @@ class MutableList(Mutable, List[_T]):
949
955
  self.extend(x)
950
956
  return self
951
957
 
958
+ def __imul__(self, n: SupportsIndex) -> MutableList[_T]:
959
+ list.__imul__(self, n)
960
+ self.changed()
961
+ return self
962
+
952
963
  def insert(self, i: SupportsIndex, x: _T) -> None:
953
964
  list.insert(self, i, x)
954
965
  self.changed()
@@ -37,6 +37,7 @@ from ._orm_constructors import outerjoin as outerjoin
37
37
  from ._orm_constructors import query_expression as query_expression
38
38
  from ._orm_constructors import relationship as relationship
39
39
  from ._orm_constructors import synonym as synonym
40
+ from ._orm_constructors import unmapped_column as unmapped_column
40
41
  from ._orm_constructors import with_loader_criteria as with_loader_criteria
41
42
  from ._orm_constructors import with_polymorphic as with_polymorphic
42
43
  from .attributes import AttributeEventToken as AttributeEventToken
@@ -51,6 +52,7 @@ from .base import NotExtension as NotExtension
51
52
  from .base import ORMDescriptor as ORMDescriptor
52
53
  from .base import PassiveFlag as PassiveFlag
53
54
  from .base import SQLORMExpression as SQLORMExpression
55
+ from .base import Unmapped as Unmapped
54
56
  from .base import WriteOnlyMapped as WriteOnlyMapped
55
57
  from .context import FromStatement as FromStatement
56
58
  from .context import QueryContext as QueryContext
@@ -29,6 +29,7 @@ from .descriptor_props import Synonym
29
29
  from .interfaces import _AttributeOptions
30
30
  from .properties import MappedColumn
31
31
  from .properties import MappedSQLExpression
32
+ from .properties import UnmappedColumn
32
33
  from .query import AliasOption
33
34
  from .relationships import _RelationshipArgumentType
34
35
  from .relationships import _RelationshipBackPopulatesArgument
@@ -395,6 +396,139 @@ def mapped_column(
395
396
  )
396
397
 
397
398
 
399
+ def unmapped_column(
400
+ __name_pos: Optional[
401
+ Union[str, _TypeEngineArgument[Any], SchemaEventTarget]
402
+ ] = None,
403
+ __type_pos: Optional[
404
+ Union[_TypeEngineArgument[Any], SchemaEventTarget]
405
+ ] = None,
406
+ /,
407
+ *args: SchemaEventTarget,
408
+ init: Literal[False] = False,
409
+ nullable: Optional[
410
+ Union[bool, Literal[SchemaConst.NULL_UNSPECIFIED]]
411
+ ] = SchemaConst.NULL_UNSPECIFIED,
412
+ primary_key: Optional[bool] = False,
413
+ autoincrement: _AutoIncrementType = "auto",
414
+ use_existing_column: bool = False,
415
+ name: Optional[str] = None,
416
+ type_: Optional[_TypeEngineArgument[Any]] = None,
417
+ index: Optional[bool] = None,
418
+ unique: Optional[bool] = None,
419
+ info: Optional[_InfoType] = None,
420
+ default: Optional[Any] = _NoArg.NO_ARG,
421
+ insert_default: Optional[Any] = _NoArg.NO_ARG,
422
+ onupdate: Optional[Any] = None,
423
+ server_default: Optional[_ServerDefaultArgument] = None,
424
+ server_onupdate: Optional[_ServerOnUpdateArgument] = None,
425
+ quote: Optional[bool] = None,
426
+ system: bool = False,
427
+ comment: Optional[str] = None,
428
+ sort_order: Union[_NoArg, int] = _NoArg.NO_ARG,
429
+ **kw: Any,
430
+ ) -> UnmappedColumn[Any]:
431
+ r"""declare a :class:`_schema.Column` that is part of the
432
+ :class:`_schema.Table` for a :ref:`Declarative Table
433
+ <orm_declarative_table>` mapping, but which is not mapped by the ORM.
434
+
435
+ :func:`_orm.unmapped_column` is used with the :class:`_orm.Unmapped`
436
+ annotation::
437
+
438
+ class User(Base):
439
+ __tablename__ = "user"
440
+
441
+ id: Mapped[int] = mapped_column(primary_key=True)
442
+ name: Mapped[str]
443
+
444
+ legacy_name: Unmapped[str | None] = unmapped_column()
445
+
446
+ The resulting :class:`_schema.Column` is added to the
447
+ :class:`_schema.Table` in the same way as one produced by
448
+ :func:`_orm.mapped_column`, deriving its type and nullability from the
449
+ annotation, so that the column is present in DDL as well as for schema
450
+ comparison tools such as Alembic autogenerate. However, the column is
451
+ excluded from the mapping, the attribute is removed from the class, and
452
+ when using :ref:`orm_declarative_native_dataclasses` it is not part of
453
+ the dataclass.
454
+
455
+ A bare ``Unmapped[]`` annotation with no right-hand side is equivalent
456
+ to ``unmapped_column()`` with no arguments; however, see the note at
457
+ :class:`_orm.Unmapped` regarding typing tools and dataclasses.
458
+
459
+ As the attribute is not part of the Python model, ORM and dataclass
460
+ oriented arguments of :func:`_orm.mapped_column`, such as
461
+ ``default_factory`` and ``deferred``, are not part of
462
+ :func:`_orm.unmapped_column`.
463
+
464
+ .. versionadded:: 2.1.5
465
+
466
+ .. seealso::
467
+
468
+ :ref:`orm_declarative_unmapped_column`
469
+
470
+ :param __name: String name to give to the :class:`_schema.Column`;
471
+ defaults to the attribute name.
472
+ :param __type: :class:`_types.TypeEngine` type or instance for the
473
+ :class:`_schema.Column`; if omitted, it is derived from the
474
+ annotation, or from a :class:`_schema.ForeignKey` if present.
475
+ :param \*args: Additional positional arguments such as
476
+ :class:`_schema.ForeignKey` and :class:`_schema.CheckConstraint`,
477
+ passed through to the :class:`_schema.Column`.
478
+ :param init: Always ``False``; this parameter is present so that
479
+ :pep:`681` typing tools omit the attribute from the ``__init__()``
480
+ method of :ref:`ORM mapped dataclasses
481
+ <orm_declarative_native_dataclasses>`.
482
+ :param nullable: whether the column is "NULL" or "NOT NULL"; if omitted,
483
+ derived from the annotation as with :func:`_orm.mapped_column`.
484
+ :param primary_key: indicates the :class:`_schema.Column` is part of the
485
+ table's primary key. As the mapper requires that its primary key
486
+ columns be mapped, the columns that the mapper should use for the
487
+ identity of the class must then be indicated using the
488
+ :paramref:`_orm.Mapper.primary_key` parameter; see
489
+ :ref:`orm_declarative_unmapped_column` for an example.
490
+ :param autoincrement: Passed directly to the
491
+ :paramref:`_schema.Column.autoincrement` parameter.
492
+ :param use_existing_column: see
493
+ :paramref:`_orm.mapped_column.use_existing_column`.
494
+ :param default: Passed directly to the :paramref:`_schema.Column.default`
495
+ parameter. As the attribute is not part of a dataclass, this is
496
+ strictly a Core column default.
497
+ :param insert_default: synonym for
498
+ :paramref:`_orm.unmapped_column.default`.
499
+ :param sort_order: see :paramref:`_orm.mapped_column.sort_order`.
500
+ :param \**kw: All remaining keyword arguments are passed through to the
501
+ constructor for the :class:`_schema.Column`, including dialect-specific
502
+ arguments.
503
+
504
+ """
505
+
506
+ return UnmappedColumn(
507
+ __name_pos,
508
+ __type_pos,
509
+ *args,
510
+ name=name,
511
+ type_=type_,
512
+ default=default,
513
+ insert_default=insert_default,
514
+ index=index,
515
+ unique=unique,
516
+ info=info,
517
+ nullable=nullable,
518
+ primary_key=primary_key,
519
+ autoincrement=autoincrement,
520
+ onupdate=onupdate,
521
+ server_default=server_default,
522
+ server_onupdate=server_onupdate,
523
+ use_existing_column=use_existing_column,
524
+ quote=quote,
525
+ comment=comment,
526
+ system=system,
527
+ sort_order=sort_order,
528
+ **kw,
529
+ )
530
+
531
+
398
532
  def orm_insert_sentinel(
399
533
  name: Optional[str] = None,
400
534
  type_: Optional[_TypeEngineArgument[Any]] = None,
sqlalchemy/orm/base.py CHANGED
@@ -881,6 +881,60 @@ class _DeclarativeMapped(Mapped[_T_co], _MappedAttribute[_T_co]):
881
881
  return NotImplemented
882
882
 
883
883
 
884
+ class Unmapped(_MappedAnnotationBase[_T_co]):
885
+ """Represent a database column that is part of the mapped
886
+ :class:`_schema.Table`, but is not mapped by the ORM.
887
+
888
+ The :class:`_orm.Unmapped` type annotation is used in an
889
+ :ref:`Annotated Declarative Table <orm_declarative_mapped_column>`
890
+ mapping, in conjunction with the :func:`_orm.unmapped_column`
891
+ construct, to indicate a column that should be present in the
892
+ :class:`_schema.Table` generated for the class, while not being part
893
+ of the Python model at all. The type and nullability of the
894
+ :class:`_schema.Column` are derived from the annotation in the same
895
+ way as for :class:`_orm.Mapped`.
896
+
897
+ E.g.::
898
+
899
+ class User(Base):
900
+ __tablename__ = "user"
901
+
902
+ id: Mapped[int] = mapped_column(primary_key=True)
903
+ name: Mapped[str]
904
+
905
+ legacy_name: Unmapped[str | None] = unmapped_column()
906
+
907
+ The ``legacy_name`` column is part of ``User.__table__``, but there is
908
+ no ``User.legacy_name`` attribute, and when using
909
+ :ref:`orm_declarative_native_dataclasses`, it is not part of the
910
+ dataclass.
911
+
912
+ A bare ``Unmapped[]`` annotation with no right-hand side is equivalent to
913
+ ``unmapped_column()`` with no arguments. When using
914
+ :ref:`orm_declarative_native_dataclasses`, the attribute is omitted from
915
+ the dataclass and its ``__init__()`` method in both cases; however,
916
+ :pep:`681` typing tools are only able to determine this when
917
+ :func:`_orm.unmapped_column` is present, and otherwise see the attribute
918
+ as a required ``__init__()`` argument, so it should always be included
919
+ in that case.
920
+
921
+ .. versionadded:: 2.1.5
922
+
923
+ .. seealso::
924
+
925
+ :ref:`orm_declarative_unmapped_column`
926
+
927
+ :func:`_orm.unmapped_column`
928
+
929
+ """
930
+
931
+ __slots__ = ()
932
+
933
+
934
+ def _is_unmapped_container(mapped_container: Optional[type]) -> bool:
935
+ return mapped_container is Unmapped
936
+
937
+
884
938
  class DynamicMapped(_MappedAnnotationBase[_T_co]):
885
939
  """Represent the ORM mapped attribute type for a "dynamic" relationship.
886
940