Skip to content

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: Schema introspection APIs return the current state of indexes and constraints. There is no rename-event or change-log API in any supported backend. 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 present in the graph, property names used on existing nodes, relationship types, or any actual data.

Why: Graph databases do 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: Autogenerate detects the presence or absence of an index but cannot detect that an indexed property changed type (e.g. stringinteger).

Why: The schema introspection queries used by runic return only the index type (RANGE, FULLTEXT, VECTOR) and the property name. No property-type metadata is available.

What to do: If you need to change the type of an indexed property, write a migration that drops the old index, migrates the data, and recreates the index.

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: A parameter change always requires a drop + create regardless of backend — there is no ALTER INDEX path. Comparing floating-point options reliably against live schema data is fragile and would not unlock a simpler migration path anyway.

What to do: Write vector index parameter changes as explicit migrations using op.drop_vector_index + op.create_vector_index.

OGM schema validation vs. autogenerate introspection

These are two separate concerns that use different code paths:

OGM schema validation (IndexManager / SchemaManager) uses get_existing_specs() to read the live schema at startup and diff/sync it against your model declarations. This is implemented for:

  • FalkorDB — via CALL db.indexes() / CALL db.constraints()
  • Neo4j — via SHOW INDEXES / SHOW CONSTRAINTS
  • Memgraph — via SHOW INDEX INFO / SHOW CONSTRAINT INFO (RANGE indexes and UNIQUE / MANDATORY constraints only — FULLTEXT and VECTOR indexes are not exposed by these commands)

ArcadeDB and Apache AGE return an empty set from get_existing_specs(); every declared spec is treated as missing.

Migrate autogenerate (runic revision --autogenerate and runic check) uses read_live_schema() to generate a diff against your SchemaManifest. This is implemented only for FalkorDB. All other adapters return an empty live schema from read_live_schema(). If you run --autogenerate against a non-FalkorDB backend, runic sees the entire manifest as "new" and generates a create-all script — it does not produce a meaningful diff.

What to do: For Neo4j and Memgraph, use SchemaManager.validate_schema and sync_schema at startup for live introspection. Write migration scripts manually rather than relying on autogenerate. Autogenerate's revision creation still works as a scaffolding tool — review and trim the generated creates on first run if your schema already exists.

Version state lives inside the graph

What this means: The current revision is stored as a 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.
  • On FalkorDB, 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: All migration adapters use synchronous (blocking) I/O. runic does not integrate with asyncio or any async graph client.

Why: The core upgrade/downgrade loop calls adapter methods 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:

python
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

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 (FalkorDB only): 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. This mechanism is not available on other backends — an adapter advertises its capability via supports_snapshots(). On a backend that does not support snapshots, a snapshot = True revision logs a warning and runs without a snapshot rather than failing.

Why: DDL operations are not transactional across the adapters runic supports. Automatic rollback of arbitrary Cypher is not possible.

DDL failures abort the migration (fail-fast): A failed schema operation (create/drop index or constraint) raises and stops the migration on every backend, so an incomplete schema is never silently reported as applied. Note that the Bolt adapters differ in idempotency: Neo4j DDL uses IF NOT EXISTS / IF EXISTS, but Memgraph and ArcadeDB index DDL do not. Re-running a migration over an already-applied schema can therefore abort on Memgraph/ArcadeDB; prefer snapshot = True (FalkorDB) or idempotent, focused revisions on those backends.

Recommendation: Use snapshot = True (FalkorDB) for high-risk migrations on production data. On other backends, prefer small, focused revisions that are easy to reason about if they need to be re-run after a failure.

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.

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