ЁЯПл The SchoolтА║ЁЯЧДя╕П DatabasesтА║ЁЯПЧя╕П рдзрдбрд╛ 08 тАФ Migrations: рд╢рд╛рд│рд╛ рдЪрд╛рд▓реВ рдЕрд╕рддрд╛рдирд╛рдЪ рдЦреЛрд▓реАрдЪреЗ рдиреВрддрдиреАрдХрд░рдг
ЁЯЦ╝я╕П See the drawing + lab ЁЯПа Course home ЁЯМ┐ Branch on GitHub тЬПя╕П View source
ЁЯЦ╝я╕П рдЖрдХреГрддреА рдЖрдгрд┐ labThe drawing + lab рдкреВрд░реНрдг рдкрд╛рдирд╛рд╡рд░ рдЙрдШрдбрд╛ тЖЧOpen full page тЖЧ

ЁЯПЧя╕П рдзрдбрд╛ 08 тАФ Migrations: рд╢рд╛рд│рд╛ рдЪрд╛рд▓реВ рдЕрд╕рддрд╛рдирд╛рдЪ рдЦреЛрд▓реАрдЪреЗ рдиреВрддрдиреАрдХрд░рдг

ЁЯУН рддреБрдореНрд╣реА рдЗрдереЗ рдЖрд╣рд╛рдд: 18 рдкреИрдХреА рдзрдбрд╛ 08 ┬╖ рдорд╛рдЧреЗ: lesson-07-concurrency ┬╖ рдкреБрдвреЗ: lesson-09-backups-recovery


ЁЯУж рдпрд╛ рдмреНрд░рдБрдЪрдордзреНрдпреЗ рдХрд╛рдп рдЖрд╣реЗ

рдзрдбреЗ 01тАУ07, рдЖрдгрд┐ рд╢рд╛рд│рд╛ рдмрдВрдж рди рдХрд░рддрд╛ рдЦреЛрд▓реАрдЪрд╛ рдЖрдХрд╛рд░ рдХрд╕рд╛ рдмрджрд▓рддреЛ: versioned migration scripts, logbook (schema_migrations), рдЖрдгрд┐ zero-downtime рдмрджрд▓рд╛рд╕рд╛рдареА expand тЖТ migrate тЖТ contract рдкрджреНрдзрдд. рдЦрд▒реНрдпрд╛ files:

ЁЯзТ 5 рд╡рд░реНрд╖рд╛рдВрдЪреНрдпрд╛ рдореБрд▓рд╛рд▓рд╛ рд╕рдордЬрд╛рд╡рд▓реНрдпрд╛рд╕рд╛рд░рдЦреЗ

рд╢рд╛рд│реЗрд▓рд╛ students рдиреЛрдВрджрд╡рд╣реАрдд рдПрдХ рдирд╡рд╛ column рд╣рд╡рд╛ рдЖрд╣реЗ: house (red, blueтАж). рдЦреЛрд▓реА рдЪрд╛рд▓реВ рдЖрд╣реЗ; clerks рдЖрддреНрддрд╛ рддреНрдпрд╛рдд рд▓рд┐рд╣реАрдд рдЖрд╣реЗрдд. рддреБрдореНрд╣реА рдЖрдард╡рдбрд╛рднрд░ рд╢рд╛рд│рд╛ рдмрдВрдж рдХрд░реВ рд╢рдХрдд рдирд╛рд╣реА.

рдореНрд╣рдгреВрди рдиреВрддрдиреАрдХрд░рдг рдХреНрд░рдорд╛рдВрдХ рдЕрд╕рд▓реЗрд▓реНрдпрд╛ work orders ЁЯПЧя╕П рдореНрд╣рдгреВрди рдХреЗрд▓реЗ рдЬрд╛рддреЗ, git рдордзреНрдпреЗ рдареЗрд╡рд▓реЗрд▓реЗ, рдХреНрд░рдорд╛рдиреЗ, рдкреНрд░рддреНрдпреЗрдХреА рдПрдХрджрд╛рдЪ рд▓рд╛рдЧреВ рдХреЗрд▓реЗрд▓реЗ, рдЖрдгрд┐ рдПрдХрд╛ logbook рдордзреНрдпреЗ рдиреЛрдВрджрд╡рд▓реЗрд▓реЗ тАФ schema_migrations тАФ рдореНрд╣рдгрдЬреЗ рдЦреЛрд▓реАрдЪреА рдХреЛрдгрддреАрд╣реА рдкреНрд░рдд рд╕рд╛рдВрдЧреВ рд╢рдХрддреЗ "рдорд╛рдЭреНрдпрд╛рд╡рд░ work orders 001 рддреЗ 003 рдЭрд╛рд▓реНрдпрд╛ рдЖрд╣реЗрдд." рдЯреАрдо рджреЛрдирджрд╛ рдЪрд╛рд▓рд╡рд╛, рджреБрд╕рд▒реНрдпрд╛рдВрджрд╛ рддреА рдХрд╛рд╣реАрдЪ рдХрд░рдд рдирд╛рд╣реА.

рдЖрдгрд┐ rename рдХрдзреАрдЪ рдПрдХрд╛ рдкрд╛рдпрд░реАрдд рд╣реЛрдд рдирд╛рд╣реА. clerks рд▓рд┐рд╣реАрдд рдЕрд╕рддрд╛рдирд╛рдЪ class рдЪреЗ рдирд╛рд╡ section рдХрд░рд╛рдпрдЪреЗ рдЕрд╕реЗрд▓ рддрд░:

  1. Expand тАФ class рдЪреНрдпрд╛ рд╢реЗрдЬрд╛рд░реА section рдЬреЛрдбрд╛ (рдЬреБрдирд╛ code рддреНрдпрд╛рдХрдбреЗ рджреБрд░реНрд▓рдХреНрд╖ рдХрд░рддреЛ).
  2. Migrate тАФ рджреЛрдиреНрд╣реАрдд рд▓рд┐рд╣рд┐рдгрд╛рд░рд╛ code ship рдХрд░рд╛; рдЬреБрдиреНрдпрд╛ rows backfill рдХрд░рд╛.
  3. section рд╡рд╛рдЪрдгрд╛рд░рд╛ code ship рдХрд░рд╛.
  4. Contract тАФ class рдХрд╛рдвреВрди рдЯрд╛рдХрд╛.

рдкреНрд░рддреНрдпреЗрдХ рдкрд╛рдпрд░реАрдд рд╢рд╛рд│рд╛ рдЪрд╛рд▓реВ рд░рд╛рд╣рддреЗ; рдкреНрд░рддреНрдпреЗрдХ рдкрд╛рдпрд░реАрдЪреА рдкреНрд░рддреАрд╡рд░ рд░рдВрдЧреАрдд рддрд╛рд▓реАрдо рдХрд░рддрд╛ рдпреЗрддреЗ.

ЁЯЧ║я╕П рдЖрдХреГрддреА

flowchart LR
    g["ЁЯУЬ db/migrations/<br/>001_init.sql ┬╖ 002_add_house.sql ┬╖ 003_indexтАжsql"]
    m["ЁЯПЧя╕П migrate.py<br/>apply pending, in order, once"]
    log["ЁЯз╛ schema_migrations<br/>name ┬╖ applied_at"]
    room["ЁЯЧДя╕П school.db<br/>now has students.house"]
    g -->|"1"| m -->|"2 each in a transaction"| room
    m -->|"3 record"| log
    ec["expand тЖТ migrate тЖТ contract:<br/>add new ┬╖ write both ┬╖ backfill ┬╖ read new ┬╖ drop old"]
    room -.->|"4 for renames"| ec

тЭУ рдХрд╛рдп

ЁЯдФ рдХрд╛

рдХрд╛рд░рдг "рдХреЛрдгреАрддрд░реА рд╢реБрдХреНрд░рд╡рд╛рд░реА production рд╡рд░ рд╣рд╛рддрд╛рдиреЗ ALTER TABLE рдЪрд╛рд▓рд╡рд▓реЗ" рд╣рд╛рдЪ рддреЛ рдорд╛рд░реНрдЧ рдЖрд╣реЗ рдЬреНрдпрд╛рдиреЗ schemas рдЕрдЬреНрдЮрд╛рдд рд╣реЛрддрд╛рдд рдЖрдгрд┐ rollbacks рдлрдХреНрдд рдЖрдард╡рдгреАрдд рд░рд╛рд╣рддрд╛рдд. git рдордзреАрд▓ migrations рдЦреЛрд▓реАрдЪрд╛ рдЖрдХрд╛рд░ review рдХрд░рддрд╛ рдпреЗрдгрд╛рд░рд╛, рдкреБрдиреНрд╣рд╛ рдХрд░рддрд╛ рдпреЗрдгрд╛рд░рд╛ рдЖрдгрд┐ рддрд╛рд▓реАрдо рдХрд░рддрд╛ рдпреЗрдгрд╛рд░рд╛ рдмрдирд╡рддрд╛рдд тАФ рдЖрдгрд┐ rename outage рдмрдиреВ рдирдпреЗ рдпрд╛рдЪрд╛ expand/contract рд╣рд╛ рдПрдХрдореЗрд╡ рдорд╛рд░реНрдЧ рдЖрд╣реЗ.

ЁЯФз рдХрд╕реЗ (рдпрд╛ repo рдордзреНрдпреЗ)

db/migrate.py logbook рдмрдирд╡рддреЗ, db/migrations/*.sql рдЪреА рдпрд╛рджреА рдХрд░рддреЗ, рд▓рд╛рдЧреВ рди рдЭрд╛рд▓реЗрд▓реА рдкреНрд░рддреНрдпреЗрдХ file рдПрдХрд╛ transaction рдордзреНрдпреЗ рд▓рд╛рдЧреВ рдХрд░рддреЗ рдЖрдгрд┐ рддрд┐рдЪреА рдиреЛрдВрдж рдХрд░рддреЗ. 002_add_house рд╣реА expand рдкрд╛рдпрд░реА рдЖрд╣реЗ; 003 рдирд╡реНрдпрд╛ рдкреНрд░рд╢реНрдирд╛рд▓рд╛ рд▓рд╛рдЧрдгрд╛рд░рд╛ index рдЬреЛрдбрддреЗ.

ЁЯзк рдХрд░реВрди рдкрд╛рд╣рд╛

python3 db/demo.py >/dev/null           # a fresh room (schema.sql already includes 001's tables)
python3 db/migrate.py status            # тП│ all three pending
python3 db/migrate.py                   # applied 001, 002, 003
python3 db/migrate.py                   # nothing to do тАФ idempotent
python3 -c "import sqlite3; c=sqlite3.connect('db/school.db'); print(c.execute('SELECT name, house FROM students LIMIT 2').fetchall()); print(c.execute('SELECT name FROM schema_migrations').fetchall())"
# your migration: expand тЖТ contract for a rename, in two files
printf -- '-- expand: new column next to the old one\nALTER TABLE homework ADD COLUMN heading TEXT;\nUPDATE homework SET heading = title;\n' > db/migrations/004_homework_heading_expand.sql
python3 db/migrate.py && python3 -c "import sqlite3; print(sqlite3.connect('db/school.db').execute('SELECT title, heading FROM homework').fetchall())"
# (005 тАФ the contract step that drops `title` тАФ only after every reader uses `heading`; write it, don't run it yet)

тЬЕ рддрдкрд╛рд╕рд╛ тАФ рддреБрдореНрд╣рд╛рд▓рд╛ рдХрд╛рдп рджрд┐рд╕рд╛рдпрд▓рд╛ рд╣рд╡реЗ

status рдЖрдзреА рддреАрди тП│ рджрд╛рдЦрд╡рддреЛ, рдордЧ рддреАрди тЬЕ; рджреБрд╕рд░рд╛ run рдлрдХреНрдд up to date рдЫрд╛рдкрддреЛ; рдкреНрд░рддреНрдпреЗрдХ рд╡рд┐рджреНрдпрд╛рд░реНрдерд┐рдиреАрд▓рд╛ house = 'red' рдЖрд╣реЗ; рддреБрдордЪреА 004 рдПрдХрджрд╛рдЪ рд▓рд╛рдЧреВ рд╣реЛрддреЗ рдЖрдгрд┐ heading рд╣реЗ title рдЪреЗ рдкреНрд░рддрд┐рдмрд┐рдВрдм рдЕрд╕рддреЗ. logbook рдордзреНрдпреЗ рдЪрд╛рд░ рдирд╛рд╡реЗ рджрд┐рд╕рддрд╛рдд.

ЁЯПБ рддреБрдореНрд╣реА рдЖрддреНрддрд╛рдЪ рдХрд╛рдп рд╕рд┐рджреНрдз рдХреЗрд▓реЗ

рдЦреЛрд▓реАрдиреЗ рдкреБрдиреНрд╣рд╛ рди рдмрд╛рдВрдзрддрд╛ рдЪрд╛рд░ рд╡реЗрд│рд╛ рдЖрдХрд╛рд░ рдмрджрд▓рд▓рд╛, рдкреНрд░рддреНрдпреЗрдХ рдмрджрд▓рд╛рдЪреА рдиреЛрдВрдж рдЭрд╛рд▓реА, рдПрдХрд╣реА рджреЛрдирджрд╛ рд▓рд╛рдЧреВ рдЭрд╛рд▓рд╛ рдирд╛рд╣реА тАФ рдЖрдгрд┐ рддреБрдореНрд╣реА рдХрдзреАрдЪ рдХреЛрдгрддреНрдпрд╛рд╣реА clerk рд▓рд╛ рди рдЕрдбрд╡рдгрд╛рд▒реНрдпрд╛ rename рдЪрд╛ expand рдЕрд░реНрдзрд╛ рднрд╛рдЧ рд▓рд┐рд╣рд┐рд▓рд╛.

тЪая╕П рдиреЗрд╣рдореАрдЪреНрдпрд╛ рдЪреБрдХрд╛

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд рд╣реЗ рдХрд╛ рдорд╣рддреНрддреНрд╡рд╛рдЪреЗ: zero-downtime deploys рд╣реА Kubernetes рдЪреА рд╢рд┐рд╕реНрдд рд╣реЛрдгреНрдпрд╛рдЖрдзреА schema рдЪреА рд╢рд┐рд╕реНрдд рдЖрд╣реЗ. рд░реЛрдЬ ship рдХрд░рдгрд╛рд░реА рдкреНрд░рддреНрдпреЗрдХ team CI рдордзреНрдпреЗ migrations рдЪрд╛рд▓рд╡рддреЗ, default рдореНрд╣рдгреВрди expand/contract рд╡рд╛рдкрд░рддреЗ, рдЖрдгрд┐ рд╣рд╛рддрд╛рдиреЗ рдЪрд╛рд▓рд╡рд▓реЗрд▓рд╛ ALTER рдореНрд╣рдгрдЬреЗ incident рдорд╛рдирддреЗ.

тПня╕П рдкреБрдвреЗ

рдиреВрддрдиреАрдХрд░рдг рдЪреБрдХреВ рд╢рдХрддреЗ; рд╢реБрдХреНрд░рд╡рд╛рд░рдЪреНрдпрд╛ рджреБрдкрд╛рд░реАрд╣реА. Backups рдЖрдгрд┐ recovery тАФ RPO, RTO, рдЖрдгрд┐ рддреБрдореНрд╣реА рдЦрд░реЛрдЦрд░ рдХрд░рддрд╛ рддреА restore рдЪреА рд░рдВрдЧреАрдд рддрд╛рд▓реАрдо.

git checkout lesson-09-backups-recovery

ЁЯПЧя╕П Lesson 08 тАФ Migrations: renovating the room while school is open

ЁЯУН You are here: Lesson 08 of 18 ┬╖ Previous: lesson-07-concurrency ┬╖ Next: lesson-09-backups-recovery


ЁЯУж What's in this branch

Lessons 01тАУ07, plus how the shape of the room changes without closing the school: versioned migration scripts, the logbook (schema_migrations), and the expand тЖТ migrate тЖТ contract pattern for zero-downtime change. Real files:

ЁЯзТ Explain like I'm 5

The school wants a new column in the students register: house (red, blueтАж). The room is open; clerks are writing in it right now. You cannot close the school for a week.

So renovations are done as numbered work orders ЁЯПЧя╕П, kept in git, applied in order, once each, and recorded in a logbook тАФ schema_migrations тАФ so any copy of the room can say "I have had work orders 001 to 003." Run the crew twice and it does nothing the second time.

And a rename is never one step. To rename class to section while clerks keep writing:

  1. Expand тАФ add section next to class (old code ignores it).
  2. Migrate тАФ ship code that writes both; backfill the old rows.
  3. Ship code that reads section.
  4. Contract тАФ drop class.

Every step keeps school open; every step can be rehearsed on a copy.

ЁЯЧ║я╕П Diagram

flowchart LR
    g["ЁЯУЬ db/migrations/<br/>001_init.sql ┬╖ 002_add_house.sql ┬╖ 003_indexтАжsql"]
    m["ЁЯПЧя╕П migrate.py<br/>apply pending, in order, once"]
    log["ЁЯз╛ schema_migrations<br/>name ┬╖ applied_at"]
    room["ЁЯЧДя╕П school.db<br/>now has students.house"]
    g -->|"1"| m -->|"2 each in a transaction"| room
    m -->|"3 record"| log
    ec["expand тЖТ migrate тЖТ contract:<br/>add new ┬╖ write both ┬╖ backfill ┬╖ read new ┬╖ drop old"]
    room -.->|"4 for renames"| ec

тЭУ What

ЁЯдФ Why

Because "someone ran ALTER TABLE on production by hand on a Friday" is how schemas become unknowable and rollbacks become memory. Migrations in git make the shape of the room reviewable, repeatable and rehearsable тАФ and expand/contract is the only way a rename does not become an outage.

ЁЯФз How (in this repo)

db/migrate.py creates the logbook, lists db/migrations/*.sql, applies each unapplied file inside a transaction and records it. 002_add_house is an expand step; 003 adds an index a new question needed.

ЁЯзк Try it

python3 db/demo.py >/dev/null           # a fresh room (schema.sql already includes 001's tables)
python3 db/migrate.py status            # тП│ all three pending
python3 db/migrate.py                   # applied 001, 002, 003
python3 db/migrate.py                   # nothing to do тАФ idempotent
python3 -c "import sqlite3; c=sqlite3.connect('db/school.db'); print(c.execute('SELECT name, house FROM students LIMIT 2').fetchall()); print(c.execute('SELECT name FROM schema_migrations').fetchall())"
# your migration: expand тЖТ contract for a rename, in two files
printf -- '-- expand: new column next to the old one\nALTER TABLE homework ADD COLUMN heading TEXT;\nUPDATE homework SET heading = title;\n' > db/migrations/004_homework_heading_expand.sql
python3 db/migrate.py && python3 -c "import sqlite3; print(sqlite3.connect('db/school.db').execute('SELECT title, heading FROM homework').fetchall())"
# (005 тАФ the contract step that drops `title` тАФ only after every reader uses `heading`; write it, don't run it yet)

тЬЕ Verify тАФ what you should see

status shows three тП│, then three тЬЕ; the second run prints only up to date; every student has house = 'red'; your 004 applies once and heading mirrors title. The logbook lists four names.

ЁЯПБ What you just proved

The room changed shape four times without being rebuilt, each change recorded, none applied twice тАФ and you wrote the expand half of a rename that never blocks a clerk.

тЪая╕П Common mistakes

ЁЯПн Why this matters in production: zero-downtime deploys are a schema discipline before they are a Kubernetes one. Every team that ships daily runs migrations in CI, expand/contract by default, and treats a hand-run ALTER as an incident.

тПня╕П Next

Renovations can go wrong; so can Friday afternoons. Backups and recovery тАФ RPO, RTO, and the restore drill you actually run.

git checkout lesson-09-backups-recovery
тЖР PreviousconcurrencyNext тЖТbackups recovery

This page is the lesson's README from the lesson-08-migrations branch, shown here so the whole School stays on one site. Code files open on GitHub at the same branch.