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 --autogeneratecallsCALL db.indexes()andCALL db.constraints()to read the live schema, then 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).
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")
Important
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:
# 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 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¶
- class runic.migrate.manifest.SchemaManifest(range_indexes: 'list[RangeIndex]' = <factory>, fulltext_indexes: 'list[FulltextIndex]' = <factory>, vector_indexes: 'list[VectorIndex]' = <factory>, constraints: 'list[UniqueConstraint | MandatoryConstraint]' = <factory>)[source]¶
Bases:
object- Parameters:
range_indexes (list[RangeIndex])
fulltext_indexes (list[FulltextIndex])
vector_indexes (list[VectorIndex])
constraints (list[UniqueConstraint | MandatoryConstraint])
- class runic.migrate.manifest.RangeIndex(label: 'str', prop: 'str', rel: 'bool' = False)[source]¶
Bases:
object
- class runic.migrate.manifest.FulltextIndex(label: 'str', props: 'list[str] | tuple[str, ...]', language: 'str | None' = None, stopwords: 'list[str] | tuple[str, ...] | None' = None) 'None'[source]¶
Bases:
object
- class runic.migrate.manifest.VectorIndex(label: 'str', prop: 'str', dimension: 'int', similarity: 'str', m: 'int' = 16, ef_construction: 'int' = 200, ef_runtime: 'int' = 10)[source]¶
Bases:
object