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
- You declare the target schema as a
SchemaManifestobject and pass it tocontext.configure()inenv.py. runic revision --autogeneratereads the live schema via the adapter'sread_live_schema()and diffs it against the manifest.- It generates an
upgrade/downgradebody and writes a new revision file marked# AUTOGENERATED — review before applying; cannot detect renames. - Always review the generated file before applying it. Autogenerate cannot detect renames and may generate incorrect
drop+createpairs 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:
from runic.migrate.manifest import (
FulltextIndex,
MandatoryConstraint,
RangeIndex,
SchemaManifest,
UniqueConstraint,
VectorIndex,
)Build the manifest:
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():
# 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:
$ runic revision --autogenerate -m "add person constraints"
Created revision: runic/versions/b2c3d4e5_add_person_constraints.py [CANDIDATE — review before applying]Open the generated file:
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:
$ 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):
# GitHub Actions example
- name: Check schema drift
run: |
FALKORDB_URL=${{ secrets.FALKORDB_URL }} \
FALKORDB_GRAPH=${{ secrets.FALKORDB_GRAPH }} \
runic checkWhat autogenerate detects
Autogenerate compares canonical keys for each schema object type:
| Object type | Key |
|---|---|
| Range index | entity type, label, property |
| Fulltext index | label, properties (tuple) |
| Vector index | label, property |
| Constraint | kind (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.