]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Add ``insert_default`` to ``Column``.
authorFederico Caselli <cfederico87@gmail.com>
Thu, 9 May 2024 20:21:11 +0000 (22:21 +0200)
committerFederico Caselli <cfederico87@gmail.com>
Thu, 9 May 2024 20:21:46 +0000 (22:21 +0200)
Added :paramref:`_schema.Column.insert_default` as an alias of
:paramref:`_schema.Column.default` for compatibility with
func:`_orm.mapped_column`.

Fixes: #11374
Change-Id: I5509b6cbac7b37ac37430a88442b1319cc9c1024
(cherry picked from commit c87572b60cbcb869c41a7b4283a11c5c14ef048c)

doc/build/changelog/unreleased_20/11374.rst [new file with mode: 0644]
doc/build/faq/ormconfiguration.rst
doc/build/orm/dataclasses.rst
lib/sqlalchemy/orm/_orm_constructors.py
lib/sqlalchemy/sql/schema.py
test/sql/test_metadata.py

diff --git a/doc/build/changelog/unreleased_20/11374.rst b/doc/build/changelog/unreleased_20/11374.rst
new file mode 100644 (file)
index 0000000..d52da2e
--- /dev/null
@@ -0,0 +1,7 @@
+.. change::
+    :tags: schema, usecase
+    :tickets: 11374
+
+    Added :paramref:`_schema.Column.insert_default` as an alias of
+    :paramref:`_schema.Column.default` for compatibility with
+    :func:`_orm.mapped_column`.
index 90d74d23ee9becc0c46b592ba1cdba535578210e..bfcf117ae09ff0619f4e59c191373a2f911e2f0e 100644 (file)
@@ -349,3 +349,94 @@ loads directly to primary key values just loaded.
 .. seealso::
 
     :ref:`subquery_eager_loading`
+
+.. _defaults_default_factory_insert_default:
+
+What are ``default``, ``default_factory`` and ``insert_default`` and what should I use?
+---------------------------------------------------------------------------------------
+
+There's a bit of a clash in SQLAlchemy's API here due to the addition of PEP-681
+dataclass transforms, which is strict about its naming conventions. PEP-681 comes
+into play if you are using :class:`_orm.MappedAsDataclass` as shown in :ref:`orm_declarative_native_dataclasses`.
+If you are not using MappedAsDataclass, then it does not apply.
+
+Part One - Classic SQLAlchemy that is not using dataclasses
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+When **not** using :class:`_orm.MappedAsDataclass`, as has been the case for many years
+in SQLAlchemy, the :func:`_orm.mapped_column` (and :class:`_schema.Column`)
+construct supports a parameter :paramref:`_orm.mapped_column.default`.
+This indicates a Python-side default (as opposed to a server side default that
+would be part of your database's schema definition) that will take place when
+an ``INSERT`` statement is emitted. This default can be **any** of a static Python value
+like a string, **or** a Python callable function, **or** a SQLAlchemy SQL construct.
+Full documentation for :paramref:`_orm.mapped_column.default` is at
+:ref:`defaults_client_invoked_sql`.
+
+When using :paramref:`_orm.mapped_column.default` with an ORM mapping that is **not**
+using :class:`_orm.MappedAsDataclass`, this default value /callable **does not show
+up on your object when you first construct it**. It only takes place when SQLAlchemy
+works up an ``INSERT`` statement for your object.
+
+A very important thing to note is that when using :func:`_orm.mapped_column`
+(and :class:`_schema.Column`), the classic :paramref:`_orm.mapped_column.default`
+parameter is also available under a new name, called
+:paramref:`_orm.mapped_column.insert_default`. If you build a
+:func:`_orm.mapped_column` and you are **not** using :class:`_orm.MappedAsDataclass`, the
+:paramref:`_orm.mapped_column.default` and :paramref:`_orm.mapped_column.insert_default`
+parameters are **synonymous**.
+
+Part Two - Using Dataclasses support with MappedAsDataclass
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+When you **are** using :class:`_orm.MappedAsDataclass`, that is, the specific form
+of mapping used at :ref:`orm_declarative_native_dataclasses`, the meaning of the
+:paramref:`_orm.mapped_column.default` keyword changes. We recognize that it's not
+ideal that this name changes its behavior, however there was no alternative as
+PEP-681 requires :paramref:`_orm.mapped_column.default` to take on this meaning.
+
+When dataclasses are used, the :paramref:`_orm.mapped_column.default` parameter must
+be used the way it's described at
+`Python Dataclasses <https://docs.python.org/3/library/dataclasses.html>`_ - it refers
+to a constant value like a string or a number, and **is applied to your object
+immediately when constructed**. It is also at the moment also applied to the
+:paramref:`_orm.mapped_column.default` parameter of :class:`_schema.Column` where
+it would be used in an ``INSERT`` statement automatically even if not present
+on the object. If you instead want to use a callable for your dataclass,
+which will be applied to the object when constructed, you would use
+:paramref:`_orm.mapped_column.default_factory`.
+
+To get access to the ``INSERT``-only behavior of :paramref:`_orm.mapped_column.default`
+that is described in part one above, you would use the
+:paramref:`_orm.mapped_column.insert_default` parameter instead.
+:paramref:`_orm.mapped_column.insert_default` when dataclasses are used continues
+to be a direct route to the Core-level "default" process where the parameter can
+be a static value or callable.
+
+.. list-table:: Summary Chart
+   :header-rows: 1
+
+   * - Construct
+     - Works with dataclasses?
+     - Works without dataclasses?
+     - Accepts scalar?
+     - Accepts callable?
+     - Populates object immediately?
+   * - :paramref:`_orm.mapped_column.default`
+     - ✔
+     - ✔
+     - ✔
+     - Only if no dataclasses
+     - Only if dataclasses
+   * - :paramref:`_orm.mapped_column.insert_default`
+     - ✔
+     - ✔
+     - ✔
+     - ✔
+     - ✖
+   * - :paramref:`_orm.mapped_column.default_factory`
+     - ✔
+     - ✖
+     - ✖
+     - ✔
+     - Only if dataclasses
index e737597cf1486e45ed410b35c0de3fb25f2bc84a..910d6a21c555c9fd35044009331556918a1bb512 100644 (file)
@@ -18,7 +18,7 @@ attrs_ third party integration library.
 .. _orm_declarative_native_dataclasses:
 
 Declarative Dataclass Mapping
--------------------------------
+-----------------------------
 
 SQLAlchemy :ref:`Annotated Declarative Table <orm_declarative_mapped_column>`
 mappings may be augmented with an additional
index 3403c39e29fc8d5d0cfb6358d8d5589154923d77..38ea2b2f25fd995f448e713de7ff510bf4c8e168 100644 (file)
@@ -255,12 +255,28 @@ def mapped_column(
         be used instead**.  This is necessary to disambiguate the callable from
         being interpreted as a dataclass level default.
 
+     .. seealso::
+
+        :ref:`defaults_default_factory_insert_default`
+
+        :paramref:`_orm.mapped_column.insert_default`
+
+        :paramref:`_orm.mapped_column.default_factory`
+
     :param insert_default: Passed directly to the
      :paramref:`_schema.Column.default` parameter; will supersede the value
      of :paramref:`_orm.mapped_column.default` when present, however
      :paramref:`_orm.mapped_column.default` will always apply to the
      constructor default for a dataclasses mapping.
 
+     .. seealso::
+
+        :ref:`defaults_default_factory_insert_default`
+
+        :paramref:`_orm.mapped_column.default`
+
+        :paramref:`_orm.mapped_column.default_factory`
+
     :param sort_order: An integer that indicates how this mapped column
      should be sorted compared to the others when the ORM is creating a
      :class:`_schema.Table`. Among mapped columns that have the same
@@ -295,6 +311,15 @@ def mapped_column(
      specifies a default-value generation function that will take place
      as part of the ``__init__()``
      method as generated by the dataclass process.
+
+     .. seealso::
+
+        :ref:`defaults_default_factory_insert_default`
+
+        :paramref:`_orm.mapped_column.default`
+
+        :paramref:`_orm.mapped_column.insert_default`
+
     :param compare: Specific to
      :ref:`orm_declarative_native_dataclasses`, indicates if this field
      should be included in comparison operations when generating the
index aa359fdbbd7956ba3c754db94b9afff18c6eab16..eda4a97cc2de9f5396811e4834e9ec0e6835db98 100644 (file)
@@ -60,6 +60,7 @@ from . import roles
 from . import type_api
 from . import visitors
 from .base import _DefaultDescriptionTuple
+from .base import _NoArg
 from .base import _NoneName
 from .base import _SentinelColumnCharacterization
 from .base import _SentinelDefaultCharacterization
@@ -1514,7 +1515,8 @@ class Column(DialectKWArgs, SchemaItem, ColumnClause[_T]):
         name: Optional[str] = None,
         type_: Optional[_TypeEngineArgument[_T]] = None,
         autoincrement: _AutoIncrementType = "auto",
-        default: Optional[Any] = None,
+        default: Optional[Any] = _NoArg.NO_ARG,
+        insert_default: Optional[Any] = _NoArg.NO_ARG,
         doc: Optional[str] = None,
         key: Optional[str] = None,
         index: Optional[bool] = None,
@@ -1751,6 +1753,11 @@ class Column(DialectKWArgs, SchemaItem, ColumnClause[_T]):
 
                 :ref:`metadata_defaults_toplevel`
 
+        :param insert_default: An alias of :paramref:`.Column.default`
+            for compatibility with :func:`_orm.mapped_column`.
+
+            .. versionadded: 2.0.31
+
         :param doc: optional String that can be used by the ORM or similar
             to document attributes on the Python side.   This attribute does
             **not** render SQL comments; use the
@@ -2104,12 +2111,19 @@ class Column(DialectKWArgs, SchemaItem, ColumnClause[_T]):
             # otherwise, add DDL-related events
             self._set_type(self.type)
 
-        if default is not None:
-            if not isinstance(default, (ColumnDefault, Sequence)):
-                default = ColumnDefault(default)
+        if insert_default is not _NoArg.NO_ARG:
+            resolved_default = insert_default
+        elif default is not _NoArg.NO_ARG:
+            resolved_default = default
+        else:
+            resolved_default = None
+
+        if resolved_default is not None:
+            if not isinstance(resolved_default, (ColumnDefault, Sequence)):
+                resolved_default = ColumnDefault(resolved_default)
 
-            self.default = default
-            l_args.append(default)
+            self.default = resolved_default
+            l_args.append(resolved_default)
         else:
             self.default = None
 
index a54a5fcc8d59da42d81ae1c27ebc97cba47d2941..97c2f08645824f79be3bc82e36320a84b35285da 100644 (file)
@@ -751,13 +751,25 @@ class MetaDataTest(fixtures.TestBase, ComparesTables):
                     comment="foo",
                 ),
                 "Column('foo', Integer(), table=None, primary_key=True, "
-                "nullable=False, onupdate=%s, default=%s, server_default=%s, "
-                "comment='foo')"
-                % (
-                    ColumnDefault(1),
-                    ColumnDefault(42),
-                    DefaultClause("42"),
+                f"nullable=False, onupdate={ColumnDefault(1)}, default="
+                f"{ColumnDefault(42)}, server_default={DefaultClause('42')}, "
+                "comment='foo')",
+            ),
+            (
+                Column(
+                    "foo",
+                    Integer,
+                    primary_key=True,
+                    nullable=False,
+                    onupdate=1,
+                    insert_default=42,
+                    server_default="42",
+                    comment="foo",
                 ),
+                "Column('foo', Integer(), table=None, primary_key=True, "
+                f"nullable=False, onupdate={ColumnDefault(1)}, default="
+                f"{ColumnDefault(42)}, server_default={DefaultClause('42')}, "
+                "comment='foo')",
             ),
             (
                 Table("bar", MetaData(), Column("x", String)),
@@ -4691,6 +4703,16 @@ class ColumnDefaultsTest(fixtures.TestBase):
         assert c.onupdate.arg == target
         assert c.onupdate.column is c
 
+    def test_column_insert_default(self):
+        c = self._fixture(insert_default="y")
+        assert c.default.arg == "y"
+
+    def test_column_insert_default_predecende_on_default(self):
+        c = self._fixture(insert_default="x", default="y")
+        assert c.default.arg == "x"
+        c = self._fixture(default="y", insert_default="x")
+        assert c.default.arg == "x"
+
 
 class ColumnOptionsTest(fixtures.TestBase):
     def test_default_generators(self):