Limitations¶
runic is a focused, deliberately scoped tool. This page documents what it does not do and the reasoning behind each constraint.
Autogenerate cannot detect renames¶
What this means: If you rename a property (email_address →
email) or a node label (User → Person), autogenerate will
generate a drop for the old name and a create for the new one.
Applying this migration as-is would delete the index on the old property and
create a new index on a non-existent property — the actual data is unchanged.
Why: FalkorDB’s CALL db.indexes() and CALL db.constraints() return
the current names; there is no change-log or rename-event API. Detecting a
rename requires comparing two names and inferring intent, which is inherently
ambiguous.
What to do: Write rename operations manually using op.rename_property
and op.relabel_nodes. Do not rely on autogenerate for rename migrations.
Autogenerate covers schema objects only¶
What this means: Autogenerate diffs indexes and constraints. It does not compare node labels that exist in the graph, property names used on existing nodes, relationship types, or any actual data.
Why: FalkorDB does not expose a declarative schema for node data. A
Person node with an email property can coexist with a Person node
without one; there is no “schema” to diff against.
What to do: Data migrations (seeding reference data, normalising values,
backfilling properties) must be written manually using op.run_cypher,
op.seed, op.rename_property, and op.relabel_nodes.
No property-type diffing¶
What this means: FalkorDB does not expose property type information via
CALL db.indexes(). Autogenerate cannot tell that a property was
previously indexed as a string and is now being used as an integer.
Why: The introspection API returns only the index type (RANGE,
FULLTEXT, VECTOR) and the property name. No type metadata is
available.
No index-options drift detection¶
What this means: Autogenerate detects the presence or absence of a vector
index, but does not detect changes to its HNSW parameters (dimension,
similarity, m, ef_construction, ef_runtime).
Why: Comparing floating-point options reliably against live schema data
is fragile. FalkorDB does not currently expose ALTER INDEX semantics, so
a parameter change requires a drop + create anyway.
What to do: If you need to change vector index parameters, write a
migration with op.drop_vector_index + op.create_vector_index manually.
FalkorDB is the only supported backend¶
What this means: runic ships one adapter — FalkorDBAdapter — and the
create_adapter("falkordb", ...) factory only knows about FalkorDB. Neo4j,
Amazon Neptune, AgeDB, and other graph databases are not supported out of the
box.
Architecture note: The runtime now flows through a clean
GraphAdapter protocol, so a second adapter (e.g.
Neo4j) can be added without touching the migration core. The main differences
to implement are constraint syntax (Cypher instead of GRAPH.CONSTRAINT),
fulltext index procedures, vector index syntax, and snapshot semantics (no
GRAPH.COPY equivalent). The introspection layer (CALL db.indexes() /
CALL db.constraints() column parsing) is still FalkorDB-specific and would
need an adapter-level override.
Current limitation: Only FalkorDB is tested and supported. Do not point runic at any other graph database.
Version state lives inside the graph¶
What this means: The current revision is stored as a
_FalkorMigrateVersion node inside the graph being migrated. If the graph
is deleted, the version pointer is lost.
Consequences:
Deleting and recreating a graph loses version tracking. Use
runic stampto re-attach the version pointer after recreating.Copying a graph via
GRAPH.COPYalso copies the version node. A copied graph already knows its revision.There is no separate version table, file, or external metadata store.
No async client support¶
What this means: runic uses the synchronous falkordb client (blocking
I/O). It does not integrate with asyncio or any async FalkorDB client.
Why: The core upgrade/downgrade loop calls methods on the graph
synchronously. Adding async support would require either an
AsyncGraphOperations alternative or running the migration loop in a thread
pool, neither of which is in scope.
What to do: Call runic from a subprocess or thread if you need to run migrations from an async application:
import subprocess
result = subprocess.run(["runic", "upgrade"], check=True)
No parallel migration execution¶
What this means: runic applies revisions one at a time, in topological order. There is no mechanism to run independent branches in parallel.
Why: Parallel execution introduces ordering hazards (two branches creating conflicting indexes, for example). The correctness guarantees of a topological sort require sequential execution.
No automatic rollback on partial failure (without snapshot)¶
What this means: If a migration script raises an exception mid-way (e.g., after creating one index but before creating a second), the database is left in a partially applied state. The version node is not updated (it remains at the prior revision), but the partial changes are not automatically undone.
The exception: Revisions with snapshot = True take a full graph copy
before running. On failure the snapshot is restored, leaving the graph in
its pre-migration state.
Why: FalkorDB does not support transactions that span schema changes (DDL is not transactional). Automatic rollback of arbitrary Cypher is not possible.
Recommendation: Use snapshot = True for high-risk migrations on
production data. For most schema-only changes (adding an index), partial
failure is recoverable by re-running the migration after fixing the script.
No --autogenerate without target_manifest¶
What this means: runic revision --autogenerate and runic check
both require target_manifest to be set in env.py via
context.configure(..., target_manifest=...). Without it they exit with
an error.
No runic.ini or TOML configuration file¶
What this means: runic has no .ini, pyproject.toml section, or
YAML config file. All configuration happens in env.py (a Python script).
Why: The pure-Python config keeps secrets out of committed config files
and removes a second config surface. Environment variables loaded by the
application’s own config system (dotenv, Pydantic Settings, etc.) are
accessible from env.py without any runic-specific glue.
No built-in online migration / zero-downtime helpers¶
What this means: runic does not provide primitives for online schema changes that must be applied in multiple steps to avoid locking (e.g., backfill-then-index patterns for large graphs under live traffic).
What to do: Model multi-step migrations as multiple sequential revisions.
Use op.run_cypher with LIMIT $batch (as op.rename_property does)
for large data changes that must proceed incrementally.