Docs

Migrations

Evlet never changes a site's database by itself. When a model changes, you add a migration: a short Python file that says what to change. Evlet can write most of it for you.

#When you need one

You need a migration when a change affects the database table:

  • a new model or a new child table;
  • a new field, a renamed field or a removed field;
  • a field that changes type, length, choices or required.

You do not need one for labels, list settings, layouts or permissions. Run evlet migrate once anyway, so the site records the reviewed source change.

When a site is first created, installing its apps creates their tables directly. Every change after that goes through migrations. Until you add one, a site with a changed model refuses to start and names the difference.

#The workflow

Terminal
evlet migrate acme --plan
evlet makemigration shop --instance acme --name product_names
evlet migrate acme
  1. evlet migrate acme --plan shows what differs and suggests a migration. It writes nothing.
  2. evlet makemigration writes that suggestion to the app's migrations/ folder, as m0001_<name>.py, m0002_<name>.py and so on. It never overwrites a file.
  3. Read the file. Fill in anything marked ... or #.
  4. Stop the server, then run evlet migrate acme. It backs up the database to data/backups/, then applies the migration.

All pending migrations run in one transaction. If any step fails, the database is left as it was.

#Frozen models

A migration that adds a model holds a frozen copy of it: a class with the fields as they are at that moment. Here, the timesheet from Calculations and derive() and the receipt from Documents and lifecycle are added to a site that already has data:

Python
"""0001_timesheets: written by evlet makemigration. Review it, then keep it unchanged once applied."""

from evlet import AddTable, Child, CreateModel, Currency, Decimal, Integer, Migration, Model, Text


class Receipt(Model):
    number = Text(required=True, unique=True, max_length=40)
    amount = Currency(currency='AED', required=True)
    remarks = Text()


class Timesheet(Model):
    employee = Text(required=True, max_length=120)
    project_count = Integer()


class Entry(Child):
    project = Text(required=True, max_length=80)
    hours = Decimal(scale=2, default=0)


class ProjectTotal(Child):
    project = Text(max_length=80)
    hours = Decimal(scale=2)


migration = Migration(
    '0001_timesheets',
    CreateModel(Receipt),
    CreateModel(Timesheet),
    AddTable('timesheet', 'entries', Entry),
    AddTable('timesheet', 'projects', ProjectTotal),
)

Why a copy? Your live model will keep changing. If the migration imported it, an old migration would change meaning every time you edit the model. The frozen class keeps the migration exact.

  • CreateModel(Frozen) adds a model's table.
  • AddTable(parent, name, FrozenRow) adds a child table to an existing model. Evlet adds the link to the parent and the row order itself.

Never edit a migration once a site has applied it. Evlet stores a checksum of each applied migration and refuses a history that was changed, reordered or shortened.

#Add and change fields

This migration adds a phone number to customers and lets product names grow from 120 to 200 characters:

Python
"""0002_product_names: written by evlet makemigration. Review it, then keep it unchanged once applied."""

from evlet import AddField, AlterField, Migration, Text


migration = Migration(
    '0002_product_names',
    AddField('customer', 'phone', Text(max_length=30)),
    AlterField('product', 'product_name', Text(required=True, unique=True, max_length=200)),
)

AddField(model, name, field) adds a column. A required new field needs a value for the rows that exist: pass backfill=....

AlterField(model, name, field) changes a field, but only in ways every stored value survives:

FromTo
IntegerDecimal, Currency or Percent
Decimala decimal kind with the same or a wider scale
TextText or LongText with the same or a longer max_length
SelectSelect with the same choices plus new ones

It can also change minimum, maximum and required. To make a field required on a table with rows, give the empty rows a value with fill=:

Python
AlterField('contact', 'phone', Text(required=True), fill='unknown')

Each stored value is checked against the new field inside the migration. A value that does not fit names its record, and nothing is changed.

#Other operations

OperationWhat it does
RenameField(model, old, new)Renames a field and keeps its values.
AddIndex(model, field)Adds an index. unique=True makes it a uniqueness rule.
RetireField(model, name)Takes a field out of the model. Its column and values stay.
DropField(model, name)Deletes a retired field's column and values. It needs evlet migrate --allow-destructive, and runs after a backup.
RunData(function)Runs your own Python to move or fill values.
Recompute(...)Works out stored calculated values again.

Removing a model, narrowing a field and undoing a migration are not supported. Evlet refuses them rather than guess what to do with your data.

#Upgrading Evlet itself

A new Evlet release can bring its own framework migrations. After you upgrade Evlet, a site refuses to serve until you run evlet migrate for it. That run applies the framework's migrations along with any of your own.