Writing Your First Migration

This tutorial walks through the anatomy of a runic revision script and shows how to write robust upgrade and downgrade functions.

Revision file anatomy

When you run runic revision -m "some message", runic generates a Python file in runic/versions/:

"""add person email index

Revision ID: 3f9a12c1ab4e
Revises: None
Create Date: 2026-05-30T09:00:00+00:00
"""
from datetime import UTC, datetime

message = "add person email index"
create_date = datetime.fromisoformat("2026-05-30T09:00:00+00:00")

revision = "3f9a12c1ab4e"
down_revision = None        # None = root of the chain
branch_labels = []
depends_on = []
irreversible = False
snapshot = False


def upgrade(op) -> None:
    pass


def downgrade(op) -> None:
    pass

The module-level variables are the revision’s metadata:

revision

Unique 12-character hex ID, auto-generated by runic revision.

down_revision

The revision ID this one builds on top of. None means this is the first revision. For merge revisions it is a tuple of two IDs.

branch_labels

Optional list of symbolic names for this branch (e.g. ["feature-x"]).

depends_on

Additional revisions that must be applied before this one, across independent branches.

irreversible

Set to True to prevent runic downgrade from running downgrade(op) on this revision without --force. Use it for changes that delete data permanently.

snapshot

Set to True to tell runic to take a full graph snapshot (via GRAPH.COPY) before applying upgrade(op). On failure the snapshot is restored automatically.

The upgrade and downgrade functions

Both functions receive a single argument op — a GraphOperations instance — that exposes all supported schema operations.

def upgrade(op) -> None:
    # UNIQUE constraints auto-create their backing range index — no
    # separate create_range_index call needed.
    op.create_constraint("UNIQUE", "NODE", "Person", ["email"])

def downgrade(op) -> None:
    op.drop_constraint("UNIQUE", "NODE", "Person", ["email"])
    op.drop_range_index("Person", "email")

Tip

Always write downgrade when you write upgrade. Even if you never expect to roll back, having a working downgrade lets you use runic test for round-trip validation.

Order matters for constraints

FalkorDB requires a range index to exist before a UNIQUE constraint can be created on the same property. When you call op.create_constraint("UNIQUE", ...) runic creates the range index for you automatically; you do not need to call create_range_index separately. The reverse also applies: call drop_constraint before drop_range_index in downgrade.

Chaining revisions

Each new revision that runic revision generates sets down_revision to the current head automatically:

# After applying 3f9a12c1ab4e, create the next revision:
$ runic revision -m "add email fulltext index"
Created revision: runic/versions/7b3d9e2f_add_email_fulltext_index.py

The new file will contain:

revision = "7b3d9e2f"
down_revision = "3f9a12c1ab4e"   # points back to the previous revision

This creates a linear chain:

None ← 3f9a12c1ab4e ← 7b3d9e2f  (head)

Marking a revision irreversible

For destructive changes (dropping a label, deleting nodes), set irreversible = True:

revision = "e1a2b3c4"
down_revision = "7b3d9e2f"
irreversible = True

def upgrade(op) -> None:
    # Drop all legacy nodes; this cannot be undone
    op.run_cypher("MATCH (n:LegacyUser) DETACH DELETE n")

def downgrade(op) -> None:
    pass   # cannot recreate deleted data

Attempting to downgrade past this revision without --force raises IrreversibleMigrationError.

Enabling snapshots

For risky migrations on production data, set snapshot = True:

snapshot = True

def upgrade(op) -> None:
    op.relabel_nodes("User", "Person")

runic will call GRAPH.COPY before running upgrade(op). If the upgrade raises an exception the snapshot is restored automatically.

Warning

Snapshots copy the entire graph and can be expensive for large graphs. Remove the snapshot copy after a successful migration by re-running upgrade on a fresh database (the snapshot graph is automatically deleted on both success and failure of the round-trip test).

See also