]> git.ipfire.org Git - thirdparty/sqlalchemy/alembic.git/commitdiff
qualify sqlite batch add column for dynamic defaults
authorMike Bayer <mike_mp@zzzcomputing.com>
Sat, 7 Aug 2021 15:48:19 +0000 (11:48 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Tue, 10 Aug 2021 16:23:13 +0000 (12:23 -0400)
Batch "auto" mode will now select for "recreate" if the ``add_column()``
operation is used on SQLite, and the column itself meets the criteria for
SQLite where ADD COLUMN is not allowed, in this case a functional or
parenthesized SQL expression or a ``Computed`` (i.e. generated) column.

Change-Id: Ie948b4b8ad8dc698b458403831e47bac4ad45b8a
Fixes: #883
alembic/ddl/sqlite.py
docs/build/unreleased/883.rst [new file with mode: 0644]
tests/test_batch.py

index 8b78966a113f0669a2746a97f8bb64fb592816de..cb790ea7b524e6b13f9e83ac8c614f8716fc2d4c 100644 (file)
@@ -2,6 +2,8 @@ import re
 
 from sqlalchemy import cast
 from sqlalchemy import JSON
+from sqlalchemy import schema
+from sqlalchemy import sql
 
 from .impl import DefaultImpl
 from .. import util
@@ -25,7 +27,19 @@ class SQLiteImpl(DefaultImpl):
 
         """
         for op in batch_op.batch:
-            if op[0] not in ("add_column", "create_index", "drop_index"):
+            if op[0] == "add_column":
+                col = op[1][1]
+                if isinstance(
+                    col.server_default, schema.DefaultClause
+                ) and isinstance(col.server_default.arg, sql.ClauseElement):
+                    return True
+                elif (
+                    isinstance(col.server_default, util.sqla_compat.Computed)
+                    and col.server_default.persisted
+                ):
+                    return True
+                return False
+            elif op[0] not in ("create_index", "drop_index"):
                 return True
         else:
             return False
diff --git a/docs/build/unreleased/883.rst b/docs/build/unreleased/883.rst
new file mode 100644 (file)
index 0000000..ecf38bc
--- /dev/null
@@ -0,0 +1,8 @@
+.. change::
+    :tags: bug, sqlite, batch
+    :tickets: 883
+
+    Batch "auto" mode will now select for "recreate" if the ``add_column()``
+    operation is used on SQLite, and the column itself meets the criteria for
+    SQLite where ADD COLUMN is not allowed, in this case a functional or
+    parenthesized SQL expression or a ``Computed`` (i.e. generated) column.
index 9e0491d7aaaf4f9679487d4c7c7c5f9a88c2f066..91baef752bea808f9a75b41b0be99c86df56943d 100644 (file)
@@ -26,6 +26,7 @@ from sqlalchemy.schema import CreateTable
 from sqlalchemy.sql import column
 from sqlalchemy.sql import text
 
+from alembic import testing
 from alembic.ddl import sqlite
 from alembic.operations import Operations
 from alembic.operations.batch import ApplyBatchImpl
@@ -40,8 +41,16 @@ from alembic.testing import TestBase
 from alembic.testing.fixtures import op_fixture
 from alembic.util import exc as alembic_exc
 from alembic.util.sqla_compat import _select
+from alembic.util.sqla_compat import has_computed
+from alembic.util.sqla_compat import has_identity
 from alembic.util.sqla_compat import sqla_14
 
+if has_computed:
+    from alembic.util.sqla_compat import Computed
+
+if has_identity:
+    from alembic.util.sqla_compat import Identity
+
 
 class BatchApplyTest(TestBase):
     def setUp(self):
@@ -1931,6 +1940,77 @@ class BatchRoundTripTest(TestBase):
             ["id", "data", "x", "data2"],
         )
 
+    def test_add_column_auto_server_default_calculated(self):
+        """test #883"""
+        with self.op.batch_alter_table("foo") as batch_op:
+            batch_op.add_column(
+                Column(
+                    "data2",
+                    DateTime(),
+                    server_default=self._datetime_server_default_fixture(),
+                )
+            )
+
+        self._assert_data(
+            [
+                {"id": 1, "data": "d1", "x": 5, "data2": mock.ANY},
+                {"id": 2, "data": "22", "x": 6, "data2": mock.ANY},
+                {"id": 3, "data": "8.5", "x": 7, "data2": mock.ANY},
+                {"id": 4, "data": "9.46", "x": 8, "data2": mock.ANY},
+                {"id": 5, "data": "d5", "x": 9, "data2": mock.ANY},
+            ]
+        )
+        eq_(
+            [col["name"] for col in inspect(config.db).get_columns("foo")],
+            ["id", "data", "x", "data2"],
+        )
+
+    @testing.combinations((True,), (False,))
+    @testing.exclusions.only_on("sqlite")
+    def test_add_column_auto_generated(self, persisted):
+        """test #883"""
+        with self.op.batch_alter_table("foo") as batch_op:
+            batch_op.add_column(
+                Column(
+                    "data2", Integer, Computed("1 + 1", persisted=persisted)
+                )
+            )
+
+        self._assert_data(
+            [
+                {"id": 1, "data": "d1", "x": 5, "data2": 2},
+                {"id": 2, "data": "22", "x": 6, "data2": 2},
+                {"id": 3, "data": "8.5", "x": 7, "data2": 2},
+                {"id": 4, "data": "9.46", "x": 8, "data2": 2},
+                {"id": 5, "data": "d5", "x": 9, "data2": 2},
+            ]
+        )
+        eq_(
+            [col["name"] for col in inspect(config.db).get_columns("foo")],
+            ["id", "data", "x", "data2"],
+        )
+
+    @config.requirements.identity_columns
+    def test_add_column_auto_identity(self):
+        """test #883"""
+
+        self._no_pk_fixture()
+
+        with self.op.batch_alter_table("nopk") as batch_op:
+            batch_op.add_column(Column("id", Integer, Identity()))
+
+        self._assert_data(
+            [
+                {"a": 1, "b": 2, "c": 3, "id": 1},
+                {"a": 2, "b": 4, "c": 5, "id": 2},
+            ],
+            tablename="nopk",
+        )
+        eq_(
+            [col["name"] for col in inspect(config.db).get_columns("foo")],
+            ["id", "data", "x"],
+        )
+
     def test_add_column_insert_before_recreate(self):
         with self.op.batch_alter_table("foo", recreate="always") as batch_op:
             batch_op.add_column(