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_addressemail) or a node label (UserPerson), 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 stamp to re-attach the version pointer after recreating.

  • Copying a graph via GRAPH.COPY also 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.