]> git.ipfire.org Git - thirdparty/sqlalchemy/alembic.git/commitdiff
revert primary_key=True change and replace with inline_primary_key
authorMike Bayer <mike_mp@zzzcomputing.com>
Tue, 10 Feb 2026 13:50:31 +0000 (08:50 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Tue, 10 Feb 2026 14:58:34 +0000 (09:58 -0500)
Reverted the behavior of :meth:`.Operations.add_column` that would
automatically render the "PRIMARY KEY" keyword inline when a
:class:`.Column` with ``primary_key=True`` is added. The automatic
behavior, added in version 1.18.2, is now opt-in via the new
:paramref:`.Operations.add_column.inline_primary_key` parameter. This
change restores the ability to render a PostgreSQL SERIAL column, which is
required to be ``primary_key=True``, while not impacting the ability to
render a separate primary key constraint. This also provides consistency
with the :paramref:`.Operations.add_column.inline_references` parameter and
gives users explicit control over SQL generation.

Fixes: #1232
Change-Id: Ia9333c1768caa5d99b0527d59fcc797af2c3249c

alembic/ddl/base.py
alembic/ddl/impl.py
alembic/op.pyi
alembic/operations/base.py
alembic/operations/ops.py
alembic/operations/toimpl.py
docs/build/changelog.rst
docs/build/unreleased/1232_amendment.rst [new file with mode: 0644]
tests/test_batch.py
tests/test_op.py
tests/test_postgresql.py

index caab9098463bb25792be3e96c09d2462b794847a..30a3a15a39b0011679a041f12c0e2ea313d517e7 100644 (file)
@@ -156,11 +156,13 @@ class AddColumn(AlterTable):
         schema: Optional[Union[quoted_name, str]] = None,
         if_not_exists: Optional[bool] = None,
         inline_references: Optional[bool] = None,
+        inline_primary_key: Optional[bool] = None,
     ) -> None:
         super().__init__(name, schema=schema)
         self.column = column
         self.if_not_exists = if_not_exists
         self.inline_references = inline_references
+        self.inline_primary_key = inline_primary_key
 
 
 class DropColumn(AlterTable):
@@ -203,6 +205,7 @@ def visit_add_column(element: AddColumn, compiler: DDLCompiler, **kw) -> str:
             element.column,
             if_not_exists=element.if_not_exists,
             inline_references=element.inline_references,
+            inline_primary_key=element.inline_primary_key,
             **kw,
         ),
     )
@@ -355,6 +358,7 @@ def add_column(
     column: Column[Any],
     if_not_exists: Optional[bool] = None,
     inline_references: Optional[bool] = None,
+    inline_primary_key: Optional[bool] = None,
     **kw,
 ) -> str:
     text = "ADD COLUMN %s%s" % (
@@ -362,7 +366,7 @@ def add_column(
         compiler.get_column_specification(column, **kw),
     )
 
-    if column.primary_key:
+    if inline_primary_key and column.primary_key:
         text += " PRIMARY KEY"
 
     # Handle inline REFERENCES if requested
index a6153106af16bc618a804ffda305faa4b1007518..964cd1f30b99c8b44b4dac37acc50e9dcfad9781 100644 (file)
@@ -387,6 +387,7 @@ class DefaultImpl(metaclass=ImplMeta):
         schema: Optional[Union[str, quoted_name]] = None,
         if_not_exists: Optional[bool] = None,
         inline_references: Optional[bool] = None,
+        inline_primary_key: Optional[bool] = None,
     ) -> None:
         self._exec(
             base.AddColumn(
@@ -395,6 +396,7 @@ class DefaultImpl(metaclass=ImplMeta):
                 schema=schema,
                 if_not_exists=if_not_exists,
                 inline_references=inline_references,
+                inline_primary_key=inline_primary_key,
             )
         )
 
index 40a480cc6f68be6f3a19ea9b8a01b906fb5965f7..449798dcfa4a60224b9acd47d50cc5b9110f2634 100644 (file)
@@ -65,6 +65,7 @@ def add_column(
     schema: Optional[str] = None,
     if_not_exists: Optional[bool] = None,
     inline_references: Optional[bool] = None,
+    inline_primary_key: Optional[bool] = None,
 ) -> None:
     """Issue an "add column" instruction using the current
     migration context.
@@ -163,6 +164,13 @@ def add_column(
 
      .. versionadded:: 1.18.2
 
+    :param inline_primary_key: If True, renders the PRIMARY KEY constraint
+     inline within the ADD COLUMN directive, rather than rendering it
+     separately. This is a purely syntactic option and should only be
+     used for single-column primary keys.
+
+     .. versionadded:: 1.18.4
+
     """
 
 def alter_column(
index 218ea190607dba7ed8f23b1e4435014a060c531c..4eb251463e64a8507fd0a1313a90e9e88d6dcceb 100644 (file)
@@ -631,6 +631,7 @@ class Operations(AbstractOperations):
             schema: Optional[str] = None,
             if_not_exists: Optional[bool] = None,
             inline_references: Optional[bool] = None,
+            inline_primary_key: Optional[bool] = None,
         ) -> None:
             """Issue an "add column" instruction using the current
             migration context.
@@ -729,6 +730,13 @@ class Operations(AbstractOperations):
 
              .. versionadded:: 1.18.2
 
+            :param inline_primary_key: If True, renders the PRIMARY KEY constraint
+             inline within the ADD COLUMN directive, rather than rendering it
+             separately. This is a purely syntactic option and should only be
+             used for single-column primary keys.
+
+             .. versionadded:: 1.18.4
+
             """  # noqa: E501
             ...
 
@@ -1691,6 +1699,7 @@ class BatchOperations(AbstractOperations):
             insert_after: Optional[str] = None,
             if_not_exists: Optional[bool] = None,
             inline_references: Optional[bool] = None,
+            inline_primary_key: Optional[bool] = None,
         ) -> None:
             """Issue an "add column" instruction using the current
             batch migration context.
index 575af2a9e8170e9049098f2e65870559e7a70da4..a999311d40105c604a38d287bc329d2d961edd69 100644 (file)
@@ -2054,12 +2054,14 @@ class AddColumnOp(AlterTableOp):
         schema: Optional[str] = None,
         if_not_exists: Optional[bool] = None,
         inline_references: Optional[bool] = None,
+        inline_primary_key: Optional[bool] = None,
         **kw: Any,
     ) -> None:
         super().__init__(table_name, schema=schema)
         self.column = column
         self.if_not_exists = if_not_exists
         self.inline_references = inline_references
+        self.inline_primary_key = inline_primary_key
         self.kw = kw
 
     def reverse(self) -> DropColumnOp:
@@ -2100,6 +2102,7 @@ class AddColumnOp(AlterTableOp):
         schema: Optional[str] = None,
         if_not_exists: Optional[bool] = None,
         inline_references: Optional[bool] = None,
+        inline_primary_key: Optional[bool] = None,
     ) -> None:
         """Issue an "add column" instruction using the current
         migration context.
@@ -2198,6 +2201,13 @@ class AddColumnOp(AlterTableOp):
 
          .. versionadded:: 1.18.2
 
+        :param inline_primary_key: If True, renders the PRIMARY KEY constraint
+         inline within the ADD COLUMN directive, rather than rendering it
+         separately. This is a purely syntactic option and should only be
+         used for single-column primary keys.
+
+         .. versionadded:: 1.18.4
+
         """
 
         op = cls(
@@ -2206,6 +2216,7 @@ class AddColumnOp(AlterTableOp):
             schema=schema,
             if_not_exists=if_not_exists,
             inline_references=inline_references,
+            inline_primary_key=inline_primary_key,
         )
         return operations.invoke(op)
 
@@ -2219,6 +2230,7 @@ class AddColumnOp(AlterTableOp):
         insert_after: Optional[str] = None,
         if_not_exists: Optional[bool] = None,
         inline_references: Optional[bool] = None,
+        inline_primary_key: Optional[bool] = None,
     ) -> None:
         """Issue an "add column" instruction using the current
         batch migration context.
@@ -2241,6 +2253,7 @@ class AddColumnOp(AlterTableOp):
             schema=operations.impl.schema,
             if_not_exists=if_not_exists,
             inline_references=inline_references,
+            inline_primary_key=inline_primary_key,
             **kw,
         )
         return operations.invoke(op)
index b9b4b7117cc7311002bae00aaa13b9b332531501..85b9d8a324f5e3e0867c5c0a2309008d5e5b9eba 100644 (file)
@@ -173,6 +173,7 @@ def add_column(operations: "Operations", operation: "ops.AddColumnOp") -> None:
     schema = operation.schema
     kw = operation.kw
     inline_references = operation.inline_references
+    inline_primary_key = operation.inline_primary_key
 
     if column.table is not None:
         column = _copy(column)
@@ -184,6 +185,7 @@ def add_column(operations: "Operations", operation: "ops.AddColumnOp") -> None:
         schema=schema,
         if_not_exists=operation.if_not_exists,
         inline_references=inline_references,
+        inline_primary_key=inline_primary_key,
         **kw,
     )
 
index 37434f47208f0310c9860403d4ad58f1d2d02323..5cad83f652a39bf0d5aca4c497ef43d8a5947000 100644 (file)
@@ -65,6 +65,13 @@ Changelog
         status with any existing primary key constraint or particular backend
         limitations on adding columns to the primary key.
 
+        .. note::
+
+            As of version 1.18.4, this behavior has been amended to be opt-in
+            via the new ``inline_primary_key`` parameter to
+            :meth:`.Operations.add_column`, rather than occurring automatically
+            when ``primary_key=True`` is set on the :class:`.Column` object.
+
     .. change::
         :tags: bug, typing
         :tickets: 1669
diff --git a/docs/build/unreleased/1232_amendment.rst b/docs/build/unreleased/1232_amendment.rst
new file mode 100644 (file)
index 0000000..c83644e
--- /dev/null
@@ -0,0 +1,24 @@
+.. change::
+    :tags: bug, operations
+    :tickets: 1232
+
+    Reverted the behavior of :meth:`.Operations.add_column` that would
+    automatically render the "PRIMARY KEY" keyword inline when a
+    :class:`.Column` with ``primary_key=True`` is added. The automatic
+    behavior, added in version 1.18.2, is now opt-in via the new
+    :paramref:`.Operations.add_column.inline_primary_key` parameter. This
+    change restores the ability to render a PostgreSQL SERIAL column, which is
+    required to be ``primary_key=True``, while not impacting the ability to
+    render a separate primary key constraint. This also provides consistency
+    with the :paramref:`.Operations.add_column.inline_references` parameter and
+    gives users explicit control over SQL generation.
+
+    To render PRIMARY KEY inline, use the
+    :paramref:`.Operations.add_column.inline_primary_key` parameter set to
+    ``True``::
+
+        op.add_column(
+            "my_table",
+            Column("id", Integer, primary_key=True),
+            inline_primary_key=True
+        )
index f56e2d335c8d1d26490ee91ec852da3afd5c53f5..fa582829c76476b7c328fd44c524b2be5450cb57 100644 (file)
@@ -940,6 +940,7 @@ class BatchAPITest(TestBase):
                 schema=None,
                 if_not_exists=None,
                 inline_references=None,
+                inline_primary_key=None,
             )
             in batch.impl.operations.impl.mock_calls
         )
@@ -1716,11 +1717,21 @@ class BatchRoundTripTest(TestBase):
         pk_const = inspect(self.conn).get_pk_constraint("foo")
         eq_(pk_const["constrained_columns"], [])
 
-    def test_drop_pk_col_readd_pk_col(self):
+    @testing.variation(
+        "use_inline_pk",
+        [
+            True,
+            (False, exclusions.fails_on(["postgresql", "mysql", "mariadb"])),
+        ],
+    )
+    def test_drop_pk_col_readd_pk_col(self, use_inline_pk):
         # drop a column, add it back with primary_key=True, should remain
         with self.op.batch_alter_table("foo") as batch_op:
             batch_op.drop_column("id")
-            batch_op.add_column(Column("id", Integer, primary_key=True))
+            batch_op.add_column(
+                Column("id", Integer, primary_key=True),
+                inline_primary_key=bool(use_inline_pk),
+            )
 
         pk_const = inspect(self.conn).get_pk_constraint("foo")
         eq_(pk_const["constrained_columns"], ["id"])
@@ -2295,9 +2306,6 @@ class BatchRoundTripMySQLTest(BatchRoundTripTest):
     def _datetime_server_default_fixture(self):
         return func.current_timestamp()
 
-    def test_drop_pk_col_readd_pk_col(self):
-        super().test_drop_pk_col_readd_pk_col()
-
     @exclusions.fails()
     def test_drop_pk_col_readd_col_also_pk_const(self):
         super().test_drop_pk_col_readd_col_also_pk_const()
@@ -2351,9 +2359,6 @@ class BatchRoundTripPostgresqlTest(BatchRoundTripTest):
     def _datetime_server_default_fixture(self):
         return func.current_timestamp()
 
-    def test_drop_pk_col_readd_pk_col(self):
-        super().test_drop_pk_col_readd_pk_col()
-
     @exclusions.fails()
     def test_drop_pk_col_readd_col_also_pk_const(self):
         super().test_drop_pk_col_readd_col_also_pk_const()
index 73f8108575bbd4dfd1743db2070f4407ff70398d..704e2e919321ce89dbd59c60ebca3bd05206656a 100644 (file)
@@ -24,6 +24,7 @@ from sqlalchemy.sql import text
 from sqlalchemy.sql.schema import quoted_name
 
 from alembic import op
+from alembic import testing
 from alembic.operations import MigrateOperation
 from alembic.operations import Operations
 from alembic.operations import ops
@@ -71,7 +72,33 @@ class OpTest(TestBase):
             'ALTER TABLE "some.schema".somename ADD COLUMN colname VARCHAR'
         )
 
-    def test_add_column_primary_key(self):
+    @testing.variation("use_inline", [True, False, "none"])
+    def test_add_column_primary_key(self, use_inline):
+        context = op_fixture("postgresql")
+
+        if use_inline.none:
+            op.add_column(
+                "somename",
+                Column("colname", String, primary_key=True),
+            )
+        else:
+            op.add_column(
+                "somename",
+                Column("colname", String, primary_key=True),
+                inline_primary_key=bool(use_inline),
+            )
+
+        if use_inline.use_inline:
+            context.assert_(
+                "ALTER TABLE somename ADD COLUMN colname "
+                "VARCHAR NOT NULL PRIMARY KEY"
+            )
+        else:
+            context.assert_(
+                "ALTER TABLE somename ADD COLUMN colname " "VARCHAR NOT NULL"
+            )
+
+    def test_add_column_primary_key_not_inline_by_default(self):
         context = op_fixture("postgresql")
         op.add_column(
             "somename",
@@ -79,10 +106,19 @@ class OpTest(TestBase):
         )
 
         context.assert_(
-            "ALTER TABLE somename ADD COLUMN colname "
-            "VARCHAR NOT NULL PRIMARY KEY"
+            "ALTER TABLE somename ADD COLUMN colname VARCHAR NOT NULL"
         )
 
+    def test_add_column_inline_primary_key_no_pk_flag(self):
+        context = op_fixture("postgresql")
+        op.add_column(
+            "somename",
+            Column("colname", String),
+            inline_primary_key=True,
+        )
+
+        context.assert_("ALTER TABLE somename ADD COLUMN colname VARCHAR")
+
     def test_rename_table_schema_hard_quoting(self):
         context = op_fixture("postgresql")
         op.rename_table(
index fb20d170c73f7636d8e9546bb2c65f4a96c7749a..d0304651dc1b36c4600b0388205f1eb3d5d3dac1 100644 (file)
@@ -187,7 +187,11 @@ class PostgresqlOpTest(TestBase):
 
     def test_col_w_pk_is_serial(self):
         context = op_fixture("postgresql")
-        op.add_column("some_table", Column("q", Integer, primary_key=True))
+        op.add_column(
+            "some_table",
+            Column("q", Integer, primary_key=True),
+            inline_primary_key=True,
+        )
         context.assert_(
             "ALTER TABLE some_table ADD COLUMN q SERIAL NOT NULL PRIMARY KEY"
         )