some sqlite schema alterations need foreign key constraints to be disabled enforce it here for the duration of the schema edition the backend mostly works without this function and there are use cases for compiling python without the sqlite3 libraries e g security hardening manual emulation of sqlite parameter quoting bytes are only allowed for blob fields encoded as string literals containing hexadecimal data and preceded by a single x character shortcut to transform a model from oldmodel into newmodel this follows the correct procedure to perform nonrename or column addition operations based on sqlite s documentation https www sqlite orglangaltertable htmlcaution the essential steps are 1 create a table with the updated definition called newappmodel 2 copy the data from the existing appmodel table to the new table 3 drop the appmodel table 4 rename the newappmodel table to appmodel 5 restore any index of the previous appmodel table selfreferential fields must be recreated rather than copied from the old model to ensure their remotefield fieldname doesn t refer to an altered field work out the new fields dict mapping since mapping might mix column names and default values its values must be already quoted this maps field names not columns for things like uniquetogether if any of the new or altered fields is introducing a new pk remove the old one do not remove the old primary key when an altered field that introduces a primary key is the same field add in any created fields choose a default and insert it into the copy map add in any altered fields remove any deleted fields remove any implicit m2m tables work inside a new app registry work out the new value of uniquetogether taking renames into account provide isolated instances of the fields to the new model body so that the existing model s internals aren t interfered with when the dummy model is constructed construct a new model with the new fields to allow self referential primary key to resolve to this model won t ever be materialized as a table and solely exists for foreign key reference resolution purposes this wouldn t be required if the schema editor was operating on model states instead of rendered models construct a model with a renamed table name create a new table with the updated schema copy data from the old table into the new table delete the old table to make way for the new rename the new table to take way for the old run deferred sql on correct table fix any pkremoved field delete the table and only that remove all deferred statements referencing the deleted table create a field on a model from django db models expressions import value specialcase implicit m2m tables if field manytomany and field remotefield through meta autocreated self createmodelfield remotefield through elif primary keys and unique fields are not supported in alter table add column field primarykey or field unique or not field null fields with default values cannot by handled by alter table add column statement because drop default is not supported in alter table or self effectivedefaultfield is not none fields with nonconstant defaults cannot by handled by alter table add column statement or field dbdefault is not notprovided and not isinstancefield dbdefault value self remaketablemodel createfieldfield else super addfieldmodel field def removefieldself model field m2m fields are a special case if field manytomany for implicit m2m tables delete the autocreated table if field remotefield through meta autocreated self deletemodelfield remotefield through for explicit through m2m fields do nothing elif self connection features canaltertabledropcolumn primary keys unique fields indexed fields and foreign keys are not supported in alter table drop column and not field primarykey and not field unique and not field dbindex and not field remotefield and field dbconstraint super removefieldmodel field for everything else remake else it might not actually have a column behind it if field dbparametersconnectionself connectiontype is none return self remaketablemodel deletefieldfield def alterfield self model oldfield newfield oldtype newtype olddbparams newdbparams strictfalse use alter table rename column if only the column name changed and there aren t any constraints alter by remaking table rebuild tables with fks pointing to this field ignore selfrelationship since the table was already rebuilt ignore selfrelationship since the table was already rebuilt alter m2ms to repoint their to endpoints if oldfield remotefield through meta dbtable newfield remotefield through meta dbtable the field name didn t change but some options did so we have to propagate this altering self remaketable oldfield remotefield through alterfields the field that points to the target model is needed so that table can be remade with the new m2m field this is m2mreversefieldname oldfield remotefield through meta getfield oldfield m2mreversefieldname newfield remotefield through meta getfield newfield m2mreversefieldname the field that points to the model itself is needed so that table can be remade with the new self field this is m2mfieldname oldfield remotefield through meta getfield oldfield m2mfieldname newfield remotefield through meta getfield newfield m2mfieldname return make a new through table self createmodelnewfield remotefield through copy the data across self execute insert into s s select s from s self quotenamenewfield remotefield through meta dbtable join id newfield m2mcolumnname newfield m2mreversename join id oldfield m2mcolumnname oldfield m2mreversename self quotenameoldfield remotefield through meta dbtable delete the old through table self deletemodeloldfield remotefield through def addconstraintself model constraint if isinstanceconstraint uniqueconstraint and constraint condition or constraint containsexpressions or constraint include or constraint deferrable super addconstraintmodel constraint else self remaketablemodel def removeconstraintself model constraint if isinstanceconstraint uniqueconstraint and constraint condition or constraint containsexpressions or constraint include or constraint deferrable super removeconstraintmodel constraint else self remaketablemodel def collatesqlself collation return collate collation some sqlite schema alterations need foreign key constraints to be disabled enforce it here for the duration of the schema edition the backend mostly works without this function and there are use cases for compiling python without the sqlite3 libraries e g security hardening manual emulation of sqlite parameter quoting bytes are only allowed for blob fields encoded as string literals containing hexadecimal data and preceded by a single x character shortcut to transform a model from old_model into new_model this follows the correct procedure to perform non rename or column addition operations based on sqlite s documentation https www sqlite org lang_altertable html caution the essential steps are 1 create a table with the updated definition called new__app_model 2 copy the data from the existing app_model table to the new table 3 drop the app_model table 4 rename the new__app_model table to app_model 5 restore any index of the previous app_model table self referential fields must be recreated rather than copied from the old model to ensure their remote_field field_name doesn t refer to an altered field work out the new fields dict mapping since mapping might mix column names and default values its values must be already quoted this maps field names not columns for things like unique_together if any of the new or altered fields is introducing a new pk remove the old one do not remove the old primary key when an altered field that introduces a primary key is the same field add in any created fields choose a default and insert it into the copy map add in any altered fields remove any deleted fields remove any implicit m2m tables work inside a new app registry work out the new value of unique_together taking renames into account provide isolated instances of the fields to the new model body so that the existing model s internals aren t interfered with when the dummy model is constructed construct a new model with the new fields to allow self referential primary key to resolve to this model won t ever be materialized as a table and solely exists for foreign key reference resolution purposes this wouldn t be required if the schema editor was operating on model states instead of rendered models construct a model with a renamed table name create a new table with the updated schema copy data from the old table into the new table delete the old table to make way for the new rename the new table to take way for the old run deferred sql on correct table fix any pk removed field delete the table and only that remove all deferred statements referencing the deleted table create a field on a model special case implicit m2m tables primary keys and unique fields are not supported in alter table add column fields with default values cannot by handled by alter table add column statement because drop default is not supported in alter table fields with non constant defaults cannot by handled by alter table add column statement remove a field from a model usually involves deleting a column but for m2ms may involve deleting a table m2m fields are a special case for implicit m2m tables delete the auto created table for explicit through m2m fields do nothing primary keys unique fields indexed fields and foreign keys are not supported in alter table drop column for everything else remake it might not actually have a column behind it perform a physical non manytomany field update use alter table rename column if only the column name changed and there aren t any constraints alter by remaking table rebuild tables with fks pointing to this field ignore self relationship since the table was already rebuilt ignore self relationship since the table was already rebuilt alter m2ms to repoint their to endpoints the field name didn t change but some options did so we have to propagate this altering the field that points to the target model is needed so that table can be remade with the new m2m field this is m2m_reverse_field_name the field that points to the model itself is needed so that table can be remade with the new self field this is m2m_field_name make a new through table copy the data across delete the old through table
import copy
from decimal import Decimal

from django.apps.registry import Apps
from django.db import NotSupportedError
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
from django.db.backends.ddl_references import Statement
from django.db.backends.utils import strip_quotes
from django.db.models import NOT_PROVIDED, UniqueConstraint


class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
    sql_delete_table = "DROP TABLE %(table)s"
    sql_create_fk = None
    sql_create_inline_fk = (
        "REFERENCES %(to_table)s (%(to_column)s) DEFERRABLE INITIALLY DEFERRED"
    )
    sql_create_column_inline_fk = sql_create_inline_fk
    sql_delete_column = "ALTER TABLE %(table)s DROP COLUMN %(column)s"
    sql_create_unique = "CREATE UNIQUE INDEX %(name)s ON %(table)s (%(columns)s)"
    sql_delete_unique = "DROP INDEX %(name)s"
    sql_alter_table_comment = None
    sql_alter_column_comment = None

    def __enter__(self):
        
        
        if not self.connection.disable_constraint_checking():
            raise NotSupportedError(
                "SQLite schema editor cannot be used while foreign key "
                "constraint checks are enabled. Make sure to disable them "
                "before entering a transaction.atomic() context because "
                "SQLite does not support disabling them in the middle of "
                "a multi-statement transaction."
            )
        return super().__enter__()

    def __exit__(self, exc_type, exc_value, traceback):
        self.connection.check_constraints()
        super().__exit__(exc_type, exc_value, traceback)
        self.connection.enable_constraint_checking()

    def quote_value(self, value):
        
        
        
        try:
            import sqlite3

            value = sqlite3.adapt(value)
        except ImportError:
            pass
        except sqlite3.ProgrammingError:
            pass
        
        if isinstance(value, bool):
            return str(int(value))
        elif isinstance(value, (Decimal, float, int)):
            return str(value)
        elif isinstance(value, str):
            return "'%s'" % value.replace("'", "''")
        elif value is None:
            return "NULL"
        elif isinstance(value, (bytes, bytearray, memoryview)):
            
            
            
            return "X'%s'" % value.hex()
        else:
            raise ValueError(
                "Cannot quote parameter value %r of type %s" % (value, type(value))
            )

    def prepare_default(self, value):
        return self.quote_value(value)

    def _remake_table(
        self, model, create_field=None, delete_field=None, alter_fields=None
    ):
        

        
        
        
        def is_self_referential(f):
            return f.is_relation and f.remote_field.model is model

        
        body = {
            f.name: f.clone() if is_self_referential(f) else f
            for f in model._meta.local_concrete_fields
        }
        
        
        mapping = {
            f.column: self.quote_name(f.column)
            for f in model._meta.local_concrete_fields
            if f.generated is False
        }
        
        rename_mapping = {}
        
        
        restore_pk_field = None
        alter_fields = alter_fields or []
        if getattr(create_field, "primary_key", False) or any(
            getattr(new_field, "primary_key", False) for _, new_field in alter_fields
        ):
            for name, field in list(body.items()):
                if field.primary_key and not any(
                    
                    
                    name == new_field.name
                    for _, new_field in alter_fields
                ):
                    field.primary_key = False
                    restore_pk_field = field
                    if field.auto_created:
                        del body[name]
                        del mapping[field.column]
        
        if create_field:
            body[create_field.name] = create_field
            
            if (
                create_field.db_default is NOT_PROVIDED
                and not (create_field.many_to_many or create_field.generated)
                and create_field.concrete
            ):
                mapping[create_field.column] = self.prepare_default(
                    self.effective_default(create_field)
                )
        
        for alter_field in alter_fields:
            old_field, new_field = alter_field
            body.pop(old_field.name, None)
            mapping.pop(old_field.column, None)
            body[new_field.name] = new_field
            if old_field.null and not new_field.null:
                if new_field.db_default is NOT_PROVIDED:
                    default = self.prepare_default(self.effective_default(new_field))
                else:
                    default, _ = self.db_default_sql(new_field)
                case_sql = "coalesce(%(col)s, %(default)s)" % {
                    "col": self.quote_name(old_field.column),
                    "default": default,
                }
                mapping[new_field.column] = case_sql
            else:
                mapping[new_field.column] = self.quote_name(old_field.column)
            rename_mapping[old_field.name] = new_field.name
        
        if delete_field:
            del body[delete_field.name]
            mapping.pop(delete_field.column, None)
            
            if (
                delete_field.many_to_many
                and delete_field.remote_field.through._meta.auto_created
            ):
                return self.delete_model(delete_field.remote_field.through)
        
        apps = Apps()

        
        
        unique_together = [
            [rename_mapping.get(n, n) for n in unique]
            for unique in model._meta.unique_together
        ]

        indexes = model._meta.indexes
        if delete_field:
            indexes = [
                index for index in indexes if delete_field.name not in index.fields
            ]

        constraints = list(model._meta.constraints)

        
        
        
        body_copy = copy.deepcopy(body)

        
        
        
        
        
        meta_contents = {
            "app_label": model._meta.app_label,
            "db_table": model._meta.db_table,
            "unique_together": unique_together,
            "indexes": indexes,
            "constraints": constraints,
            "apps": apps,
        }
        meta = type("Meta", (), meta_contents)
        body_copy["Meta"] = meta
        body_copy["__module__"] = model.__module__
        type(model._meta.object_name, model.__bases__, body_copy)

        
        body_copy = copy.deepcopy(body)
        meta_contents = {
            "app_label": model._meta.app_label,
            "db_table": "new__%s" % strip_quotes(model._meta.db_table),
            "unique_together": unique_together,
            "indexes": indexes,
            "constraints": constraints,
            "apps": apps,
        }
        meta = type("Meta", (), meta_contents)
        body_copy["Meta"] = meta
        body_copy["__module__"] = model.__module__
        new_model = type("New%s" % model._meta.object_name, model.__bases__, body_copy)

        
        self.create_model(new_model)

        
        self.execute(
            "INSERT INTO %s (%s) SELECT %s FROM %s"
            % (
                self.quote_name(new_model._meta.db_table),
                ", ".join(self.quote_name(x) for x in mapping),
                ", ".join(mapping.values()),
                self.quote_name(model._meta.db_table),
            )
        )

        
        self.delete_model(model, handle_autom2m=False)

        
        self.alter_db_table(
            new_model,
            new_model._meta.db_table,
            model._meta.db_table,
        )

        
        for sql in self.deferred_sql:
            self.execute(sql)
        self.deferred_sql = []
        
        if restore_pk_field:
            restore_pk_field.primary_key = True

    def delete_model(self, model, handle_autom2m=True):
        if handle_autom2m:
            super().delete_model(model)
        else:
            
            self.execute(
                self.sql_delete_table
                % {
                    "table": self.quote_name(model._meta.db_table),
                }
            )
            
            for sql in list(self.deferred_sql):
                if isinstance(sql, Statement) and sql.references_table(
                    model._meta.db_table
                ):
                    self.deferred_sql.remove(sql)

    def add_field(self, model, field):
        
        from django.db.models.expressions import Value

        
        if field.many_to_many and field.remote_field.through._meta.auto_created:
            self.create_model(field.remote_field.through)
        elif (
            
            
            field.primary_key
            or field.unique
            or not field.null
            
            
            
            or self.effective_default(field) is not None
            
            
            or (
                field.db_default is not NOT_PROVIDED
                and not isinstance(field.db_default, Value)
            )
        ):
            self._remake_table(model, create_field=field)
        else:
            super().add_field(model, field)

    def remove_field(self, model, field):
        
        
        if field.many_to_many:
            
            if field.remote_field.through._meta.auto_created:
                self.delete_model(field.remote_field.through)
            
        elif (
            self.connection.features.can_alter_table_drop_column
            
            
            and not field.primary_key
            and not field.unique
            and not field.db_index
            and not (field.remote_field and field.db_constraint)
        ):
            super().remove_field(model, field)
        
        else:
            
            if field.db_parameters(connection=self.connection)["type"] is None:
                return
            self._remake_table(model, delete_field=field)

    def _alter_field(
        self,
        model,
        old_field,
        new_field,
        old_type,
        new_type,
        old_db_params,
        new_db_params,
        strict=False,
    ):
        
        
        
        if (
            old_field.column != new_field.column
            and self.column_sql(model, old_field) == self.column_sql(model, new_field)
            and not (
                old_field.remote_field
                and old_field.db_constraint
                or new_field.remote_field
                and new_field.db_constraint
            )
        ):
            return self.execute(
                self._rename_field_sql(
                    model._meta.db_table, old_field, new_field, new_type
                )
            )
        
        self._remake_table(model, alter_fields=[(old_field, new_field)])
        
        old_collation = old_db_params.get("collation")
        new_collation = new_db_params.get("collation")
        if new_field.unique and (
            old_type != new_type or old_collation != new_collation
        ):
            related_models = set()
            opts = new_field.model._meta
            for remote_field in opts.related_objects:
                
                if remote_field.related_model == model:
                    continue
                if not remote_field.many_to_many:
                    if remote_field.field_name == new_field.name:
                        related_models.add(remote_field.related_model)
                elif new_field.primary_key and remote_field.through._meta.auto_created:
                    related_models.add(remote_field.through)
            if new_field.primary_key:
                for many_to_many in opts.many_to_many:
                    
                    if many_to_many.related_model == model:
                        continue
                    if many_to_many.remote_field.through._meta.auto_created:
                        related_models.add(many_to_many.remote_field.through)
            for related_model in related_models:
                self._remake_table(related_model)

    def _alter_many_to_many(self, model, old_field, new_field, strict):
        
        if (
            old_field.remote_field.through._meta.db_table
            == new_field.remote_field.through._meta.db_table
        ):
            
            
            self._remake_table(
                old_field.remote_field.through,
                alter_fields=[
                    (
                        
                        
                        
                        old_field.remote_field.through._meta.get_field(
                            old_field.m2m_reverse_field_name()
                        ),
                        new_field.remote_field.through._meta.get_field(
                            new_field.m2m_reverse_field_name()
                        ),
                    ),
                    (
                        
                        
                        
                        old_field.remote_field.through._meta.get_field(
                            old_field.m2m_field_name()
                        ),
                        new_field.remote_field.through._meta.get_field(
                            new_field.m2m_field_name()
                        ),
                    ),
                ],
            )
            return

        
        self.create_model(new_field.remote_field.through)
        
        self.execute(
            "INSERT INTO %s (%s) SELECT %s FROM %s"
            % (
                self.quote_name(new_field.remote_field.through._meta.db_table),
                ", ".join(
                    [
                        "id",
                        new_field.m2m_column_name(),
                        new_field.m2m_reverse_name(),
                    ]
                ),
                ", ".join(
                    [
                        "id",
                        old_field.m2m_column_name(),
                        old_field.m2m_reverse_name(),
                    ]
                ),
                self.quote_name(old_field.remote_field.through._meta.db_table),
            )
        )
        
        self.delete_model(old_field.remote_field.through)

    def add_constraint(self, model, constraint):
        if isinstance(constraint, UniqueConstraint) and (
            constraint.condition
            or constraint.contains_expressions
            or constraint.include
            or constraint.deferrable
        ):
            super().add_constraint(model, constraint)
        else:
            self._remake_table(model)

    def remove_constraint(self, model, constraint):
        if isinstance(constraint, UniqueConstraint) and (
            constraint.condition
            or constraint.contains_expressions
            or constraint.include
            or constraint.deferrable
        ):
            super().remove_constraint(model, constraint)
        else:
            self._remake_table(model)

    def _collate_sql(self, collation):
        return "COLLATE " + collation