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:
revisionUnique 12-character hex ID, auto-generated by
runic revision.down_revisionThe revision ID this one builds on top of.
Nonemeans this is the first revision. For merge revisions it is a tuple of two IDs.branch_labelsOptional list of symbolic names for this branch (e.g.
["feature-x"]).depends_onAdditional revisions that must be applied before this one, across independent branches.
irreversibleSet to
Trueto preventrunic downgradefrom runningdowngrade(op)on this revision without--force. Use it for changes that delete data permanently.snapshotSet to
Trueto tell runic to take a full graph snapshot (viaGRAPH.COPY) before applyingupgrade(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¶
Upgrading and Downgrading — applying, reverting, and relative targets
Operations Reference — full
op.*API