Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion alembic/ddl/sqlite.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,9 +182,12 @@ def cast_for_batch_migrate(
existing_transfer: Dict[str, Union[TypeEngine, Cast]],
new_type: TypeEngine,
) -> None:
# the JSON check uses the type affinity rather than isinstance() so
# that a TypeDecorator which augments JSON is also detected; CASTing
# to JSON in SQLite yields 0 and would destroy the data
if (
existing.type._type_affinity is not new_type._type_affinity
and not isinstance(new_type, JSON)
and new_type._type_affinity is not JSON
):
existing_transfer["expr"] = cast(
existing_transfer["expr"], new_type
Expand Down
12 changes: 12 additions & 0 deletions docs/build/unreleased/1120.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.. change::
:tags: bug, batch migrations
:tickets: 1120

Fixed data loss in SQLite batch migrations when altering a column to a
:class:`~sqlalchemy.types.TypeDecorator` that augments
:class:`~sqlalchemy.types.JSON`. The data transfer step suppresses the
``CAST`` for :class:`~sqlalchemy.types.JSON`, as CASTing to ``JSON`` in
SQLite yields ``0``, however the check used ``isinstance()`` and therefore
did not match a ``TypeDecorator`` wrapping ``JSON``; every value in the
column was replaced with ``0``. The check now compares type affinity, so
augmented types are detected as well.
28 changes: 28 additions & 0 deletions tests/test_batch.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from sqlalchemy import String
from sqlalchemy import Table
from sqlalchemy import Text
from sqlalchemy import TypeDecorator
from sqlalchemy import UniqueConstraint
from sqlalchemy.dialects import sqlite as sqlite_dialect
from sqlalchemy.schema import CreateIndex
Expand Down Expand Up @@ -1154,6 +1155,33 @@ def test_change_type(self):
"ALTER TABLE _alembic_tmp_foo RENAME TO foo",
)

def test_change_type_json_typedecorator(self):
"""a TypeDecorator that augments JSON must not be CAST either.

CASTing to JSON in SQLite yields 0, so emitting the CAST here
would destroy the column's data.

"""

class CustomJson(TypeDecorator):
impl = JSON
cache_ok = True

context = self._fixture()
self.table.append_column(Column("toj", Text))
with self.op.batch_alter_table(
"foo", copy_from=self.table
) as batch_op:
batch_op.alter_column("toj", type_=CustomJson)
context.assert_(
"CREATE TABLE _alembic_tmp_foo (id INTEGER NOT NULL, "
"data VARCHAR(50), x INTEGER, toj JSON, PRIMARY KEY (id))",
"INSERT INTO _alembic_tmp_foo (id, data, x, toj) "
"SELECT foo.id, foo.data, foo.x, foo.toj FROM foo",
"DROP TABLE foo",
"ALTER TABLE _alembic_tmp_foo RENAME TO foo",
)

def test_change_type_from_schematype(self):
context = self._fixture()
self.table.append_column(
Expand Down