Schema management¶
runic.migrate provides two utilities for keeping graph indexes and constraints
in sync with your model declarations:
IndexManager— creates indexes for one model class at a time; low overhead, surgical control.SchemaManager— validates, diffs, and syncs indexes across a list of models.
Both accept a migrate adapter from create_adapter()
and are typically called once at application startup, not per request.
See also
examples/orm/05_schema_management.pyRunnable example covering
IndexManager,SchemaManagervalidate/diff/sync, and multi-backend adapter creation.
Quick start¶
from runic.migrate import SchemaManager
from runic.migrate.adapters import create_adapter
adapter = create_adapter(
"neo4j", # or "falkordb", "memgraph", "arcadedb", "age"
host="localhost",
database="mydb",
password="secret",
)
schema = SchemaManager(adapter)
schema.sync_schema([Person, Trip]) # create all missing indexes
result = schema.validate_schema([Person, Trip])
print(schema.get_schema_diff([Person, Trip]))
For FalkorDB, the raw FalkorDB.Graph handle (db.select_graph("myapp"))
is also accepted for backward compatibility; the adapter path is preferred.
Declaring indexes on models¶
Index hints live on Field() parameters:
from runic.ogm import Field, Node
class Person(Node, labels=["Person"]):
id: str = Field(primary_key=True)
email: str = Field(unique=True) # UNIQUE constraint
bio: str = Field(index_type="FULLTEXT") # fulltext index
embedding: list[float] = Field(index_type="VECTOR") # vector index
class Trip(Node, labels=["Trip"]):
id: str = Field(primary_key=True)
title: str = Field(index_type="FULLTEXT")
start_date: str = Field(index=True) # RANGE index
Parameter |
Effect |
|---|---|
|
Creates a RANGE index (equality and range queries). |
|
Creates a UNIQUE constraint. On FalkorDB a backing RANGE index is also created automatically. |
|
Creates a fulltext index. Multiple fields with the same label are batched
into a single |
|
Creates a vector index. Backends that require a dimension at creation time
(Neo4j, Memgraph) need the index pre-created with the correct dimension via
a migration op — |
IndexManager¶
IndexManager creates indexes for one
model class at a time.
from runic.migrate import IndexManager
from runic.migrate.adapters import create_adapter
adapter = create_adapter("neo4j", host="localhost", database="neo4j", password="secret")
manager = IndexManager(adapter)
manager.create_indexes(Person) # create declared indexes; skip if already present
manager.ensure_indexes(Trip) # alias — preferred name for startup code
Method |
Description |
|---|---|
|
Create all declared indexes on cls. FULLTEXT specs for the same label
are batched into one call. When |
|
Alias for |
|
Issue the adapter call for a single |
|
Drop the index or constraint described by spec. |
Note
if_not_exists=False forces all create calls even for existing indexes.
Most adapters raise an error or log a warning for duplicate creates unless
they support IF NOT EXISTS semantics (Neo4j, Memgraph do; FalkorDB does not).
SchemaManager¶
SchemaManager adds validate, diff,
and sync operations on top of IndexManager.
from runic.migrate import SchemaManager
from runic.migrate.adapters import create_adapter
import logging
log = logging.getLogger(__name__)
adapter = create_adapter("memgraph", host="localhost", database="memgraph")
schema = SchemaManager(adapter)
MODELS = [Person, Trip]
result = schema.validate_schema(MODELS)
if not result.is_valid:
log.warning("Missing: %s", result.missing_indexes)
log.warning("Extra: %s", result.extra_indexes)
schema.sync_schema(MODELS) # create missing; leave extras
schema.sync_schema(MODELS, drop_extra=True) # also drop unrecognised indexes
log.info("%s", schema.get_schema_diff(MODELS))
Method |
Description |
|---|---|
|
Diff declared vs existing. Returns a |
|
Create entity types (ArcadeDB) and all missing indexes. When
|
|
Human-readable diff string. Lines are prefixed |
|
Full diagnostics — returns a |
|
Issues |
ValidationResult fields¶
Field |
Description |
|---|---|
|
|
|
Specs declared on a model but not yet created in the graph. |
|
Specs present in the graph but not declared on any model. |
|
Non-fatal messages collected during introspection (e.g. connection failures). |
When to use IndexManager vs SchemaManager vs migration ops¶
Tool |
Context |
When to reach for it |
|---|---|---|
|
Application startup or scripts |
You want to ensure a single model’s indexes exist. Fast, per-class, no diffing overhead. |
|
Startup, CI health checks, or ops scripts |
You want to diff, validate, or sync the whole schema across multiple
models. |
Migration ops ( |
Production deployments |
You need a versioned, replayable audit trail. Every change is tracked in the graph and can be rolled back. Use for production schema changes where history and rollback matter. |
Rule of thumb: use SchemaManager at startup for development and test
environments; use migration ops for any change you’d want to review in a PR and
be able to roll back in production.
Cross-backend behaviour¶
Backend |
Pass |
Notes |
|---|---|---|
FalkorDB |
|
Full introspection via |
Neo4j |
|
Introspection via |
Memgraph |
|
Introspection via |
ArcadeDB |
|
No introspection (HTTP management API required — not implemented).
Every declared spec is treated as missing; |
Apache AGE |
|
No introspection. All DDL calls log a warning and do nothing — AGE does not support Cypher-level DDL. Manage indexes via PostgreSQL DDL. |
Note
runic.migrate manages both schema utilities and versioned migrations.
For a tracked, replayable record of schema changes see runic.migrate.
API reference¶
- class runic.migrate.schema.IndexManager(adapter_or_graph)[source]
Bases:
objectCreates and manages graph indexes and constraints from entity Field declarations.
Accepts any object satisfying the
IndexAdapterprotocol — a migrate adapter (Neo4j, Memgraph, FalkorDB, ArcadeDB, AGE) or a raw FalkorDB graph handle (auto-wrapped inFalkorDBIndexAdapterfor backward compat).Fulltext batching — Neo4j and Memgraph use a single named fulltext index per label covering all search fields.
create_indexes()collapses all FULLTEXT specs for the same label into onecreate_fulltext_index(label, prop1, prop2, ...)call.Example:
from runic.migrate import IndexManager, create_adapter adapter = create_adapter( "neo4j", host="localhost", database="neo4j", password="secret" ) manager = IndexManager(adapter) manager.create_indexes(Person) manager.ensure_indexes(Trip)
- Parameters:
adapter_or_graph (Any)
- create_indexes(entity_class, *, if_not_exists=True)[source]
Create all indexes and constraints declared on entity_class.
FULLTEXT specs sharing a label are batched into a single
create_fulltext_index(label, *props)call.When if_not_exists is
True(default), existing non-FULLTEXT specs are skipped. FULLTEXT creation is always attempted — adapters must handle idempotency.
- ensure_indexes(entity_class)[source]
Create missing indexes for entity_class; skip those that already exist.
- Parameters:
entity_class (type)
- Return type:
None
- class runic.migrate.schema.SchemaManager(adapter_or_graph)[source]
Bases:
objectValidates and synchronizes graph indexes against entity Field declarations.
Accepts any object satisfying the
IndexAdapterprotocol (a migrate adapter or a raw FalkorDB graph handle for backward compat).Example:
from runic.migrate import SchemaManager, create_adapter adapter = create_adapter( "neo4j", host="localhost", database="neo4j", password="secret" ) schema = SchemaManager(adapter) result = schema.validate_schema([Person, KnowsEdge]) schema.sync_schema([Person, KnowsEdge])
- Parameters:
adapter_or_graph (Any)
- ensure_entity_types(entity_classes)[source]
Create vertex/edge types for entity_classes on adapters that require them.
No-op for schemaless backends. Issues
CREATE VERTEX TYPE/CREATE EDGE TYPEDDL for ArcadeDB.
- validate_schema(entity_classes)[source]
Compare declared indexes against the live graph state.
Returns a
ValidationResultdescribing missing and extra indexes.is_validisTrueonly when both sets are empty and no errors occurred.- Parameters:
- Return type:
- sync_schema(entity_classes, *, drop_extra=False)[source]
Create entity types and missing indexes; drop extras when drop_extra is
True.Calls
ensure_entity_typesfirst (required for ArcadeDB empty collections), then delegates toIndexManager.create_indexes()per class.
- get_schema_diff(entity_classes)[source]
Return a human-readable diff of declared vs existing indexes.
Lines are prefixed with
MISSINGorEXTRA; returns a single “in sync” message when no differences exist.