Skip to content

Autogenerate

runic can compare a desired schema described in Python (the SchemaManifest) with the actual schema currently in your graph and generate migration scripts for the difference — no hand-written Cypher required.


How it works

  1. You declare the target schema as a SchemaManifest object and pass it to context.configure() in env.py.
  2. runic revision --autogenerate reads the live schema via the adapter's read_live_schema() and diffs it against the manifest.
  3. It generates an upgrade/downgrade body and writes a new revision file marked # AUTOGENERATED — review before applying; cannot detect renames.
  4. Always review the generated file before applying it. Autogenerate cannot detect renames and may generate incorrect drop + create pairs in place of a rename (see limitations).

INFO

Live-schema introspection (step 2) is currently implemented only for FalkorDB, which calls CALL db.indexes() and CALL db.constraints(). All other adapters return an empty live schema; --autogenerate with those backends treats the entire manifest as new and generates a create-all script. See limitations.

Declaring a SchemaManifest

Import the manifest classes from runic:

python
from runic.migrate.manifest import (
    FulltextIndex,
    MandatoryConstraint,
    RangeIndex,
    SchemaManifest,
    UniqueConstraint,
    VectorIndex,
)

Build the manifest:

python
TARGET = SchemaManifest(
    range_indexes=[
        RangeIndex("Person", "email"),
        RangeIndex("Person", "name"),
        RangeIndex("KNOWS", "since", rel=True),
    ],
    fulltext_indexes=[
        FulltextIndex("Article", ("title", "body")),
        FulltextIndex("Review", ("text",), language="german"),
    ],
    vector_indexes=[
        VectorIndex("Document", "embedding", dimension=1536, similarity="cosine"),
    ],
    constraints=[
        UniqueConstraint("NODE", "Person", ["email"]),
        MandatoryConstraint("NODE", "Person", ["name"]),
    ],
)

Configuring env.py for autogenerate

Pass target_manifest to context.configure():

python
# runic/env.py
import os
from runic.migrate import context
from runic.migrate.adapters import create_adapter
from runic.migrate.manifest import RangeIndex, SchemaManifest, UniqueConstraint

TARGET = SchemaManifest(
    range_indexes=[RangeIndex("Person", "email")],
    constraints=[UniqueConstraint("NODE", "Person", ["email"])],
)

adapter = create_adapter(
    "falkordb",
    url=os.getenv("FALKORDB_URL", "falkor://localhost:6379"),
    graph_name=os.getenv("FALKORDB_GRAPH", "my_graph"),
)
context.configure(adapter, target_manifest=TARGET)

Generating a migration script

Run runic revision --autogenerate:

bash
$ runic revision --autogenerate -m "add person constraints"
Created revision: runic/versions/b2c3d4e5_add_person_constraints.py  [CANDIDATE  review before applying]

Open the generated file:

python
def upgrade(op) -> None:
    # AUTOGENERATED — review before applying; cannot detect renames
    op.create_range_index("Person", "email")
    op.create_constraint("UNIQUE", "NODE", "Person", ["email"])

def downgrade(op) -> None:
    # AUTOGENERATED — review before applying; cannot detect renames
    op.drop_constraint("UNIQUE", "NODE", "Person", ["email"])
    op.drop_range_index("Person", "email")

DANGER

The [CANDIDATE — review before applying] notice in the CLI output is a reminder that the generated file must be reviewed before being applied. Never apply an autogenerated migration to production without reviewing it.

Using runic check in CI

runic check exits with code 1 if the live schema has drifted from target_manifest:

bash
$ runic check
Schema up-to-date.

$ runic check
Pending schema changes (run `runic revision --autogenerate -m "..."` to generate):
  + op.create_range_index("Order", "placed_at")

Add it to your CI pipeline to fail the build when uncommitted schema changes are present (example using FalkorDB; substitute your backend's env vars):

text
# GitHub Actions example
- name: Check schema drift
  run: |
    FALKORDB_URL=${{ secrets.FALKORDB_URL }} \
    FALKORDB_GRAPH=${{ secrets.FALKORDB_GRAPH }} \
    runic check

What autogenerate detects

Autogenerate compares canonical keys for each schema object type:

Object typeKey
Range indexentity type, label, property
Fulltext indexlabel, properties (tuple)
Vector indexlabel, property
Constraintkind (UNIQUE/MANDATORY), entity, label, properties (tuple)

Drop order in the generated upgrade follows the dependency rule: constraints first, then indexes. Create order is the reverse: indexes first, constraints second (because UNIQUE constraints require a prior range index).

What autogenerate does NOT detect

See limitations for a full list. Key gaps:

  • Renames — a renamed property or label appears as drop + create, which would delete and recreate the index but would not rename data.
  • Property type changes — FalkorDB does not expose property type information via db.indexes(); runic cannot diff on type.
  • Node/relationship data — only schema objects (indexes and constraints) are compared.
  • Index options drift — changes to HNSW parameters (m, ef_construction) on an existing vector index are not detected.

Manifest classes reference

SchemaManifest

Holds all schema objects (indexes and constraints) that define the desired state of the graph schema. Pass an instance to context.configure() as target_manifest.

RangeIndex

Declares a range index on a node or relationship property.

FulltextIndex

Declares a fulltext index on one or more properties of a node label, with an optional language parameter.

VectorIndex

Declares a vector (HNSW) index on a node property, specifying dimension and similarity metric.

UniqueConstraint

Declares a uniqueness constraint on one or more properties of a node or relationship.

MandatoryConstraint

Declares a mandatory (existence) constraint on one or more properties of a node or relationship.

runic - Graph schema migrations and OGM for Cypher-based graph databases. · Impressum