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/``: .. code-block:: python """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 :class:`~runic.migrate.operations.GraphOperations` instance — that exposes all supported schema operations. .. code-block:: python 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: .. code-block:: bash # 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: .. code-block:: python revision = "7b3d9e2f" down_revision = "3f9a12c1ab4e" # points back to the previous revision This creates a linear chain: .. code-block:: text None ← 3f9a12c1ab4e ← 7b3d9e2f (head) Marking a revision irreversible --------------------------------- For destructive changes (dropping a label, deleting nodes), set ``irreversible = True``: .. code-block:: python 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 :class:`~runic.migrate.context.IrreversibleMigrationError`. Enabling snapshots ------------------- For risky migrations on production data, set ``snapshot = True``: .. code-block:: python 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 -------- * :doc:`upgrade_downgrade` — applying, reverting, and relative targets * :doc:`../operations_reference` — full ``op.*`` API