Schema Management (Indexes & Constraints)
Drivine manages range indexes, uniqueness constraints, vector indexes and full-text indexes on Neo4j, Memgraph,
FalkorDB and Oracle. ensure is idempotent and drift-aware, and the engines’ differences (DDL, introspection,
FalkorDB’s constraint commands) are handled for you. Declare nothing, and pay nothing.
Imperative
Section titled “Imperative”await gom.indexes.ensure(vectorIndex('Proposition', 'embedding', 1536));await gom.indexes.ensure(rangeIndex('Proposition', 'contextId'));await gom.indexes.ensure(rangeIndex('Message', ['sessionId', 'createdAt'])); // compositeawait gom.indexes.ensure(fullTextIndex('Chunk', 'text'));await gom.indexes.ensure(fullTextIndex('Entity', ['name', 'description']));await gom.constraints.ensure(uniquenessConstraint('ChatSession', 'sessionId'));Schema statements run outside any transaction. ensure returns a result whose status is:
| Status | Meaning |
|---|---|
CREATED |
Nothing existed, and the item was made. |
ALREADY_MATCHING |
A matching item exists; nothing changed. |
DRIFT |
An item exists with a different shape, such as other vector dimensions. Nothing changed: recreate(spec) replaces it. |
VIOLATION |
Constraints only: the data breaks it. A sample of the values held more than once is included. |
RECREATED |
From recreate(spec): the old item dropped and the new one made. |
Pinning a vector index. Only the dimensions and similarity are always sent; the rest is the engine’s choice, and engine defaults change between versions. Pin the HNSW shape portably, and anything engine-specific as an engine option:
@VectorIndex({ similarity: 'COSINE', hnswM: 32, hnswEfConstruction: 200 }) embedding?: number[];
vectorIndex('Proposition', 'embedding', 1536, 'COSINE', { engineOptions: [neo4jVectorOptions({ quantizationEnabled: false }), falkorDbVectorOptions({ efRuntime: 20 })]});An unpinned parameter is never reported as drift; a pinned one the engine contradicts is.
Declarative: Decorators
Section titled “Declarative: Decorators”@NodeFragment({ labels: ['Proposition'] })export class PropositionNode { @NodeId() @RangeIndex() @Unique() id: string; @RangeIndex() contextId: string; @VectorIndex({ similarity: 'COSINE' }) embedding?: number[]; @FullTextIndex() body: string; // also what loadMatching searches}
// composite declarations go on the class@NodeFragment({ labels: ['Message'] })@RangeIndex({ properties: ['sessionId', 'createdAt'] })@Unique({ properties: ['sessionId', 'sequence'] })@FullTextIndex({ properties: ['subject', 'body'] })export class MessageNode { /* … */ }Declarative: Catalogs, Enforced at Startup
Section titled “Declarative: Catalogs, Enforced at Startup”Give the module a schema, and it is enforced before the application starts, indexes before constraints:
DrivineModule.withOptions({ connectionProviders: [DatabaseRegistry.buildOrResolveFromEnv()], schema: { catalogs: [ // vector dimensions come from your embedding model, at run time SchemaCatalog.fromFragments(() => embeddings.dimensions, PropositionNode, MessageNode), SchemaCatalog.of(rangeIndex('Session', 'lastActivityAt'), uniquenessConstraint('Session', 'id')) ] }});Catalogs merge, identical declarations count once, and conflicting ones for the same kind, label and properties fail
startup. A catalog applies to every database that supports schema, unless you narrow it:
.forDefaultDatabase(), .forDatabase('users') or .forDatabases('a', 'b'). A named target is strict: an unknown
database, or one without schema support, fails startup.
| Option | Meaning |
|---|---|
mode |
FAIL_FAST (the default) throws on drift or a violation; WARN logs it and carries on. |
recreateOnDrift |
Destructive: replace an item whose shape drifted. |
recreateOnStartup |
Destructive: drop and make every declared item on every startup. |
violationSampleSize |
How many values held more than once a violation reports; 0 skips the check. Defaults to 10. |
enabled |
false skips the startup run; SchemaManager can still be injected. |
SchemaManager is injectable, to change the schema as the application runs: enforce() is safe to call again, and
recreateAll() drops and remakes every declared item.
Version-Triggered Rebuild
Section titled “Version-Triggered Rebuild”Drift detection sees only what introspection shows. Swapping an embedding model for another with the same dimensions leaves stale vectors behind. Tag the catalog with a version, and a new version recreates its items once:
SchemaCatalog.of(vectorIndex('Chunk', 'embedding', embeddings.dimensions)).withVersion(embeddings.modelId);The applied version and the items are recorded in a _DrivineSchema node for each catalog, in drivine4j’s shape. A
first version is adopted without recreating anything. An item recorded but no longer declared is reported, not dropped,
and an item two catalogs declare is never recreated by one of them alone.
Recreating a vector index rebuilds it from the stored embeddings: it doesn’t re-embed. After a model swap, re-embed the nodes, then enforce.
Per-Engine Notes
Section titled “Per-Engine Notes”| Neo4j | Memgraph | FalkorDB | Oracle | |
|---|---|---|---|---|
| Vector | CREATE VECTOR INDEX |
WITH CONFIG, uSearch metrics |
OPTIONS, unnamed |
A VECTOR table beside the nodes, kept by a trigger, with a neighbour-partition vector index |
| Full-text | CREATE FULLTEXT INDEX |
CREATE TEXT INDEX |
per property, unnamed | A text table beside the nodes, with an Oracle Text index |
| Range | named, composite | label-property | per label | an index over the JSON property |
| Uniqueness | REQUIRE … IS UNIQUE |
ASSERT … IS UNIQUE |
GRAPH.CONSTRAINT, with its backing index, polled while it builds |
a unique index for the label’s nodes |
Neptune and Postgres have no schema management.

