{"record":{"id":"3620ecdfc4730c76","repo":"sqlalchemy/alembic","slug":"no-support-for-alter-of-constraints-in-sqlite-dial","errorCode":null,"errorMessage":"No support for ALTER of constraints in SQLite dialect. Please refer to the batch mode feature which allows for SQLite migrations using a copy-and-move strategy.","messagePattern":"No support for ALTER of constraints in SQLite dialect\\. Please refer to the batch mode feature which allows for SQLite migrations using a copy-and-move strategy\\.","errorType":"exception","errorClass":"NotImplementedError","httpStatus":null,"severity":"error","filePath":"alembic/ddl/sqlite.py","lineNumber":78,"sourceCode":"                if isinstance(\n                    col.server_default, schema.DefaultClause\n                ) and isinstance(col.server_default.arg, sql.ClauseElement):\n                    return True\n                elif (\n                    isinstance(col.server_default, Computed)\n                    and col.server_default.persisted\n                ):\n                    return True\n            elif op[0] not in (\"create_index\", \"drop_index\"):\n                return True\n        else:\n            return False\n\n    def add_constraint(self, const: Constraint, **kw: Any):\n        # attempt to distinguish between an\n        # auto-gen constraint and an explicit one\n        if const._create_rule is None:\n            raise NotImplementedError(\n                \"No support for ALTER of constraints in SQLite dialect. \"\n                \"Please refer to the batch mode feature which allows for \"\n                \"SQLite migrations using a copy-and-move strategy.\"\n            )\n        elif const._create_rule(self):\n            util.warn(\n                \"Skipping unsupported ALTER for \"\n                \"creation of implicit constraint. \"\n                \"Please refer to the batch mode feature which allows for \"\n                \"SQLite migrations using a copy-and-move strategy.\"\n            )\n\n    def drop_constraint(self, const: Constraint, **kw: Any):\n        if const._create_rule is None:\n            raise NotImplementedError(\n                \"No support for ALTER of constraints in SQLite dialect. \"\n                \"Please refer to the batch mode feature which allows for \"\n                \"SQLite migrations using a copy-and-move strategy.\"","sourceCodeStart":60,"sourceCodeEnd":96,"githubUrl":"https://github.com/sqlalchemy/alembic/blob/5551b5d35f985c99cb8f1af2b3c526b050e4c059/alembic/ddl/sqlite.py#L60-L96","documentation":"SQLiteImpl.add_constraint raises NotImplementedError because SQLite does not support ALTER TABLE ADD CONSTRAINT for foreign keys, unique constraints, checks, or primary keys after table creation. Alembic directs you to batch mode, which recreates the table with the new constraint using a copy-and-move strategy.","triggerScenarios":"Calling op.create_foreign_key(), op.create_unique_constraint(), op.create_check_constraint(), or op.create_primary_key() against a SQLite database outside of a batch_alter_table() context.","commonSituations":"Running migrations developed against PostgreSQL/MySQL on a SQLite test database; CI using in-memory SQLite where migrations assume full ALTER support.","solutions":["Wrap the operation in batch mode: with op.batch_alter_table('t') as batch_op: batch_op.create_foreign_key(...).","Set recreate='always' in batch_alter_table when the constraint change is not auto-detected as needing a rebuild.","Design test SQLite schemas to match constraints at table-creation time to avoid needing ALTER."],"exampleFix":"# before (fails on SQLite)\nop.create_foreign_key('fk_a_b', 'a', 'b', ['b_id'], ['id'])\n\n# after\nwith op.batch_alter_table('a', schema=None) as batch_op:\n    batch_op.create_foreign_key('fk_a_b', 'b', ['b_id'], ['id'])","handlingStrategy":"validation","validationCode":"def add_constraint_sqlite_safe(op, table_name, fn):\n    with op.batch_alter_table(table_name) as batch_op:\n        fn(batch_op)","typeGuard":"from sqlalchemy.dialects import sqlite\n\ndef dialect_needs_batch(dialect) -> bool:\n    return dialect.name == 'sqlite'","tryCatchPattern":null,"preventionTips":["Always use batch_alter_table for constraint changes on SQLite.","Run your migration suite against SQLite in CI to detect non-batched constraint ops.","Set recreate='always' when the auto heuristic does not detect the rebuild need."],"tags":["sqlite","add-constraint","batch-mode","ddl","dialect"],"backgroundTag":null,"analyzedSha":"5551b5d35f985c99cb8f1af2b3c526b050e4c059","analyzedAt":"2026-08-11T01:38:46.612Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}