Typorithm · medium

Migrations preserve state and existing rows

pythondjangoindependent-practice
Saved on this device
Full lesson · 4 learning steps Open any exercise for focused practice
1

Step 1 of 4 · intro card

Migrations preserve state and existing rows

0%

Concept and contract

Distinguish editing a model, generating a migration and applying a reviewed schema change to existing data.

Editing a model definition does not itself update an existing database. A normal project uses makemigrations to describe a model-state change and migrate to apply the reviewed operation graph.

This fixture exposes the distinction with Django’s migration operation objects in an owned in-memory database. It creates an initial historical model, saves a row and then applies an additive change. The low-level fixture is not a production migration command recipe.

Mechanism and worked example

ProjectState supplies the historical model registry used at each step.

ProjectState supplies the historical model registry used at each step. The first migration creates a title-only table, while the second adds country and supplies a value for its existing row.

The code reads through the new historical model and confirms the title survived. It deletes only the fixture table at the end so later cases begin independently; no existing application database or user table is targeted.

Complete worked example

python

from bootstrap_lab import django_setup
django_setup(__name__)

from django.db import connection, migrations, models
from django.db.migrations.state import ProjectState

def migrate_saved_row(country):
    if country not in {"US", "IL", "UA"}:
        raise ValueError("supported default required")
    first = migrations.Migration("initial", "framework_lab")
    first.operations = [migrations.CreateModel(name="MigrationRecord", fields=[("id", models.AutoField(primary_key=True)), ("title", models.CharField(max_length=80))])]
    second = migrations.Migration("country", "framework_lab")
    second.operations = [migrations.AddField(model_name="migrationrecord", name="country", field=models.CharField(max_length=2, default=country), preserve_default=False)]
    with connection.schema_editor() as editor:
        before = first.apply(ProjectState(), editor)
        old_model = before.apps.get_model("framework_lab", "MigrationRecord")
        old_model.objects.create(title="Saved")
        after = second.apply(before, editor)
        model = after.apps.get_model("framework_lab", "MigrationRecord")
        row = model.objects.get()
        result = {"title": row.title, "country": row.country}
        editor.delete_model(model)
    return result
Reasoning
ProjectState supplies the historical model registry used at each step. The first migration creates a title-only table, while the second adds country and supplies a value for its existing row.

Counterexamples and common mistakes

The counterexample hardcodes a different backfill default instead of using the declared input.

The counterexample hardcodes a different backfill default instead of using the declared input. The migration can complete successfully while existing data receives the wrong meaning.

Recreating an empty table and testing only new inserts would miss preservation failures entirely. Do not import a current live model into a historical data migration and assume its shape will remain correct years later. Review both forward behavior and any claimed reverse operation.

Counterexample: a runnable framework mistake

python

from bootstrap_lab import django_setup
django_setup(__name__)

from django.db import connection, migrations, models
from django.db.migrations.state import ProjectState

def migrate_saved_row(country):
    if country not in {"US", "IL", "UA"}:
        raise ValueError("supported default required")
    first = migrations.Migration("initial", "framework_lab")
    first.operations = [migrations.CreateModel(name="MigrationRecord", fields=[("id", models.AutoField(primary_key=True)), ("title", models.CharField(max_length=80))])]
    second = migrations.Migration("country", "framework_lab")
    second.operations = [migrations.AddField(model_name="migrationrecord", name="country", field=models.CharField(max_length=2, default="US"), preserve_default=False)]
    with connection.schema_editor() as editor:
        before = first.apply(ProjectState(), editor)
        old_model = before.apps.get_model("framework_lab", "MigrationRecord")
        old_model.objects.create(title="Saved")
        after = second.apply(before, editor)
        model = after.apps.get_model("framework_lab", "MigrationRecord")
        row = model.objects.get()
        result = {"title": row.title, "country": row.country}
        editor.delete_model(model)
    return result
Reasoning
The counterexample hardcodes a different backfill default instead of using the declared input. The migration can complete successfully while existing data receives the wrong meaning.

Independent practice: one bounded framework change

Add a published boolean field with a false backfill default in the second migration and include the observed value in the result.

Add a published boolean field with a false backfill default in the second migration and include the observed value in the result. Preserve the saved title and requested country.

Start with an existing row rather than creating only a new post-migration object. This adds one explicit schema transition without asking you to plan a live zero-downtime migration or infer locking behavior from the disposable SQLite example.

Step 1: Add a false published field through the second migration. Step 2: Preserve the historical row and its country backfill.

Practice starter: solve before reading the solution

python

from bootstrap_lab import django_setup
django_setup(__name__)

from django.db import connection, migrations, models
from django.db.migrations.state import ProjectState

def migrate_saved_row(country):
    # Remove this placeholder, then adapt the retained baseline:
    # Add a false published field through the second migration.
    raise NotImplementedError
    if country not in {"US", "IL", "UA"}:
        raise ValueError("supported default required")
    first = migrations.Migration("initial", "framework_lab")
    first.operations = [migrations.CreateModel(name="MigrationRecord", fields=[("id", models.AutoField(primary_key=True)), ("title", models.CharField(max_length=80))])]
    second = migrations.Migration("country", "framework_lab")
    second.operations = [migrations.AddField(model_name="migrationrecord", name="country", field=models.CharField(max_length=2, default=country), preserve_default=False)]
    with connection.schema_editor() as editor:
        before = first.apply(ProjectState(), editor)
        old_model = before.apps.get_model("framework_lab", "MigrationRecord")
        old_model.objects.create(title="Saved")
        after = second.apply(before, editor)
        model = after.apps.get_model("framework_lab", "MigrationRecord")
        row = model.objects.get()
        result = {"title": row.title, "country": row.country}
        editor.delete_model(model)
    return result
Reasoning
Add a false published field through the second migration. Preserve the historical row and its country backfill.
  • Hint 1: Append an AddField operation, then inspect the new historical model.
  • Hint 2: Do not replace the migration test with a fresh empty schema.

Solution, tests and trade-offs

The cases inspect actual data after applying both operations.

The cases inspect actual data after applying both operations. They distinguish different requested country defaults and reject an unsupported input before creating the fixture table.

Practice checks the additional boolean and preservation of both original fields. These results prove the controlled state transition, not that adding a field has the same operational cost on every backend or on a large table under concurrent production traffic.

Practice solution and acceptance checks

python

from bootstrap_lab import django_setup
django_setup(__name__)

from django.db import connection, migrations, models
from django.db.migrations.state import ProjectState

def migrate_saved_row(country):
    if country not in {"US", "IL", "UA"}:
        raise ValueError("supported default required")
    first = migrations.Migration("initial", "framework_lab")
    first.operations = [migrations.CreateModel(name="MigrationRecord", fields=[("id", models.AutoField(primary_key=True)), ("title", models.CharField(max_length=80))])]
    second = migrations.Migration("country", "framework_lab")
    second.operations = [migrations.AddField(model_name="migrationrecord", name="country", field=models.CharField(max_length=2, default=country), preserve_default=False)]
    second.operations.append(migrations.AddField(model_name="migrationrecord", name="published", field=models.BooleanField(default=False), preserve_default=False))
    with connection.schema_editor() as editor:
        before = first.apply(ProjectState(), editor)
        old_model = before.apps.get_model("framework_lab", "MigrationRecord")
        old_model.objects.create(title="Saved")
        after = second.apply(before, editor)
        model = after.apps.get_model("framework_lab", "MigrationRecord")
        row = model.objects.get()
        result = {"title": row.title, "country": row.country, "published": row.published}
        editor.delete_model(model)
    return result
Reasoning
The cases inspect actual data after applying both operations. They distinguish different requested country defaults and reject an unsupported input before creating the fixture table.

Interview reasoning and next step

Explain what belongs in a migration file and why its historical state matters.

Explain what belongs in a migration file and why its historical state matters. A deployment may need expand-and-contract steps, staged backfills and compatibility between old and new application versions.

Migration review also includes data meaning, not just whether SQL executes. The following authentication and authorization lessons build on stable stored identity while keeping credentials, session state and ownership checks separate from schema evolution.

Optional stretch — after the core task works: Plan a reversible data migration and state which transformations cannot recover original information.

Computer practice: https://applydjinn.com/typorithm/python-frameworks-lab-20261105.zip. Install the pinned dependencies in an isolated environment, run verify.py --track django, then edit the retained exercise baseline and check it with --practice dj-migrations. The lab uses owned in-memory databases, local framework clients and tiny CPU tensors. It never reads the working ApplyDjinn database, contacts production feeds or downloads models.

Distinguish editing a model, generating a migration and applying a reviewed schema change to existing data.