Chapter 17 · Schema Evolution, Migrations, Testing, and Release Compatibility

ALTER TABLE Capabilities and SQLite’s Schema-Rewrite Model

Learn exactly which schema changes SQLite can perform directly, why its schema-as-text design matters, and when a controlled table rebuild is the safer migration primitive.

Beginner120–145 minutesALTER support + rebuild/verification labSQLite 3.53.4 baselineSQLite 3.53.4 baseline · ALTER COLUMN NOT NULL is 3.53.0+Last reviewed: August 2026

Learning outcomes

A schema migration is a program that changes the structure and meaning of persistent data. SQLite makes many common changes easy, but it does not pretend that every table redesign can be expressed as one ALTER TABLE clause. The reliable mental model is: understand the currently supported direct operations, understand SQLite’s schema-as-text design, and use a deliberate rebuild when the desired change exceeds those operations.

01

Identify current direct ALTER TABLE capabilities and their version boundaries.

02

Explain why SQLite stores schema definitions as SQL text in sqlite_schema and reparses them after alteration.

03

Apply ADD COLUMN and understand its restrictions against existing rows.

04

Recognize DROP COLUMN dependency failures before attempting a release migration.

05

Use legacy_alter_table only as a compatibility switch for old rename behavior, not as a normal modern setting.

06

Perform a safe create-copy-drop-rename table rebuild and verify dependent objects afterward.

What ALTER TABLE means in current SQLite

SQLite’s direct schema-change surface has expanded over time. Current SQLite 3.53.4 supports table rename, column rename, column addition, column drop, and—new in 3.53.0—setting or dropping a column’s NOT NULL constraint. Older tutorials that say SQLite can only rename tables or add columns are historical, not current.

OperationCurrent behaviorImportant boundary
ALTER TABLE t RENAME TO nRenames the table; modern SQLite updates references in triggers/views and foreign keys.Do not enable legacy rename behavior unless an old application deliberately depends on it.
ALTER TABLE t RENAME COLUMN a TO bUpdates the table definition and dependent indexes/triggers/views when the rewrite is unambiguous.Supported since 3.25.0; fails rather than silently creating semantic ambiguity.
ALTER TABLE t ADD COLUMN ...Appends a column to the schema. Simple additions normally do not rewrite every row.No PRIMARY KEY/UNIQUE; restricted defaults; STORED generated columns cannot be added this way.
ALTER TABLE t DROP COLUMN cRewrites table content to remove that column.Supported since 3.35.0; fails when the column participates in keys, indexes, FKs, CHECKs, generated expressions, triggers, or views.
ALTER TABLE t ALTER c SET/DROP NOT NULLCurrent 3.53.0+ support for changing NOT NULL.Do not use this syntax if your application minimum is older than 3.53.0; rebuild instead.
Version policy comes before migration syntax

A migration file must run on the oldest SQLite engine your release supports. “My development laptop has 3.53.4” is not enough reason to use 3.53-only syntax in a mobile, OS-bundled, or older embedded deployment.

Why SQLite schema changes feel different

SQLite stores the original CREATE TABLE, CREATE INDEX, CREATE VIEW, and CREATE TRIGGER SQL in sqlite_schema. A direct ALTER operation rewrites that SQL text and reparses the schema. This design keeps the file format compact and portable, but it means a schema change must leave every dependent definition valid.

sql · inspect schema text before changing it
SELECT type, name, tbl_name, sqlFROM sqlite_schemaWHERE tbl_name IN ('device', 'maintenance_note')   OR name LIKE 'idx_%'ORDER BY type, name;PRAGMA table_xinfo('maintenance_note');PRAGMA index_list('maintenance_note');PRAGMA foreign_key_list('maintenance_note');

This inspection is not ceremony. It tells you which objects the migration must preserve or intentionally replace. A table definition is only one part of the database contract.

Simple alteration: add a controlled column

Suppose FieldNotes wants to record where each maintenance note originated. Existing rows need a valid value, so this is a good direct ADD COLUMN case.

sql · direct ADD COLUMN lab
DROP TABLE IF EXISTS note_alter_demo;CREATE TABLE note_alter_demo(    note_id     INTEGER PRIMARY KEY,    note_text   TEXT NOT NULL);INSERT INTO note_alter_demo(note_text)VALUES ('Inspect coupling'), ('Check bearing temperature');ALTER TABLE note_alter_demoADD COLUMN source_system TEXT NOT NULL DEFAULT 'manual'    CHECK (source_system IN ('manual','sensor','import'));SELECT note_id, note_text, source_systemFROM note_alter_demoORDER BY note_id;PRAGMA table_xinfo('note_alter_demo');

Expected state: the two preexisting rows read as source_system='manual'. SQLite also validates an added CHECK constraint against existing rows in current releases. By contrast, ADD COLUMN x TEXT UNIQUE is not allowed, and a non-NULL added column requires a non-NULL default when old rows already exist.

DROP COLUMN is not “delete this name from CREATE TABLE”

Dropping a column can fail for reasons that are healthy: SQLite refuses to leave schema objects pointing at something that no longer exists. Treat those failures as dependency information, not as an invitation to disable checking.

sql · dependency makes DROP COLUMN fail
DROP TABLE IF EXISTS drop_demo;CREATE TABLE drop_demo(    id INTEGER PRIMARY KEY,    code TEXT NOT NULL,    note TEXT);CREATE INDEX idx_drop_demo_code ON drop_demo(code);-- Current SQLite rejects this while the index depends on code.ALTER TABLE drop_demo DROP COLUMN code;-- A real migration would decide whether the index itself should disappear,-- then remove/recreate dependent objects deliberately.
Do not use writable_schema as a shortcut

SQLite documents direct sqlite_schema editing as a specialized, dangerous procedure. This course uses ordinary ALTER operations or the general rebuild procedure. A migration should not depend on hand-editing schema text to “make the error go away.”

Rename compatibility and legacy_alter_table

Modern rename behavior was improved in SQLite 3.25.0/3.26.0 so references inside triggers, views, and foreign keys are normally updated. PRAGMA legacy_alter_table=ON restores older rename behavior for applications that depended on it. That makes it a compatibility escape hatch—not a recommended default.

sql · observe rather than blindly change compatibility state
SELECT sqlite_version();PRAGMA legacy_alter_table;PRAGMA foreign_keys;-- Modern application policy:-- leave legacy_alter_table OFF unless supporting a known old behavior contract.

A change ALTER cannot express: rebuild the table

Imagine a legacy notes table where severity was free text. The new release wants a strict value set, a normalized legacy spelling, a composite uniqueness rule, and an audit trigger. That is a table redesign, so rebuilding is clearer than trying to force one ALTER statement to do everything.

sql · deliberate table-rebuild migration
PRAGMA foreign_keys = OFF;BEGIN IMMEDIATE;CREATE TABLE new_note_rebuild_demo(    note_id      INTEGER PRIMARY KEY,    device_id    INTEGER NOT NULL,    occurred_at  TEXT NOT NULL,    severity     TEXT NOT NULL                 CHECK(severity IN ('info','warning','critical')),    note_text    TEXT NOT NULL CHECK(trim(note_text) <> ''),    UNIQUE(device_id, occurred_at, note_text));INSERT INTO new_note_rebuild_demo       (note_id, device_id, occurred_at, severity, note_text)SELECT note_id,       device_id,       occurred_at,       CASE severity         WHEN 'warn' THEN 'warning'         ELSE severity       END,       trim(note_text)FROM note_rebuild_demo;DROP TABLE note_rebuild_demo;ALTER TABLE new_note_rebuild_demo RENAME TO note_rebuild_demo;CREATE INDEX idx_note_rebuild_device_timeON note_rebuild_demo(device_id, occurred_at);CREATE TRIGGER trg_note_rebuild_nonblankBEFORE INSERT ON note_rebuild_demoWHEN trim(NEW.note_text) = ''BEGIN    SELECT RAISE(ABORT, 'blank note');END;PRAGMA foreign_key_check;COMMIT;PRAGMA foreign_keys = ON;

The critical order is create new → copy/transform → drop old → rename new. SQLite explicitly warns against starting by renaming the old table, because modern rename propagation can modify views, triggers, and foreign-key references in ways that make the later steps wrong.

Verify the database contract, not only the row count

sql · post-migration verification
SELECT type, name, sqlFROM sqlite_schemaWHERE tbl_name='note_rebuild_demo' OR name LIKE 'idx_note_rebuild_%'ORDER BY type, name;PRAGMA table_xinfo('note_rebuild_demo');PRAGMA index_list('note_rebuild_demo');PRAGMA foreign_key_check;PRAGMA integrity_check;-- Constraint rejection is part of verification too.INSERT INTO note_rebuild_demo(device_id, occurred_at, severity, note_text)VALUES (1, '2026-08-12T10:00:00Z', 'impossible', 'should fail');

A successful migration should prove structure, data, dependent objects, referential integrity, and expected failure behavior. “The ALTER command returned success” is only one observation.

Checkpoint

Choose direct ALTER or rebuild

For each requested change, decide whether a direct operation is sufficient for the declared runtime.

  1. Add an optional text column with no special default.
  2. Add a UNIQUE business key to an existing populated table.
  3. Rename a column on modern SQLite where dependent views are unambiguous.
  4. Drop a column used by an index and a view.
  5. Change a column type while translating legacy values.
  6. Set NOT NULL when your supported minimum is SQLite 3.45.0.
Review the answers

Direct ADD COLUMN fits the first. A new UNIQUE business key and a type/data transformation generally need a rebuild. Modern RENAME COLUMN fits the third. The indexed/view-referenced drop needs dependency redesign, commonly a rebuild. Because ALTER COLUMN SET NOT NULL is 3.53.0+, a 3.45.0 minimum must use a compatible rebuild instead.

Bridge to versioned migrations

Knowing how to change one schema safely is not enough. Applications need an ordered history: version 1 → 2 → 3, each paired with release code, with exactly one process owning migration execution. Lesson 2 turns these SQL operations into a migration protocol.

Authoritative references

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only assets compatible with the Ethereum/ERC-20 network. Do not send TRC-20/TRON assets.