Managing NornicDB Constraints¶
NornicDB supports both Neo4j-compatible primitive constraints and several NornicDB-specific schema extensions. This page is the canonical guide for creating, inspecting, and operating those constraints, including block-style constraint contracts defined with REQUIRE { ... }.
Use this guide when you need to:
- apply NornicDB-only constraint families such as domain, temporal, cardinality, or endpoint-policy rules
- group multiple related rules into one named schema contract
- understand what is enforced at constraint creation time versus write time
- inspect primitive constraints separately from block-contract metadata
Choose the right constraint form¶
Use a primitive constraint when one rule is enough:
Use a block-style contract when several rules belong together and should be managed as one named schema object:
CREATE CONSTRAINT person_contract
FOR (n:Person)
REQUIRE {
n.id IS UNIQUE
n.name IS NOT NULL
n.age IS :: INTEGER
n.status IN ['active', 'inactive']
(n.tenant, n.externalId) IS NODE KEY
}
Primitive rules continue to show up in SHOW CONSTRAINTS. Contract metadata is listed separately with SHOW CONSTRAINT CONTRACTS.
NornicDB-specific constraint families¶
In addition to standard uniqueness, existence, node key, relationship key, and property type constraints, NornicDB adds several schema features that are specific to this engine.
Domain and enum constraints¶
Restrict a property to a fixed allowed set.
CREATE CONSTRAINT person_status_domain
FOR (n:Person) REQUIRE n.status IN ['active', 'inactive', 'suspended']
CREATE CONSTRAINT works_at_role_domain
FOR ()-[r:WORKS_AT]-() REQUIRE r.role IN ['engineer', 'manager', 'director']
Temporal no-overlap constraints¶
Prevent overlapping validity windows for the same logical key.
CREATE CONSTRAINT fact_version_no_overlap
FOR (n:FactVersion)
REQUIRE (n.fact_key, n.valid_from, n.valid_to) IS TEMPORAL NO OVERLAP
Cardinality constraints¶
Limit outgoing or incoming relationship count per node. Direction is encoded in the FOR clause.
CREATE CONSTRAINT employee_primary_employer_max_one
FOR ()-[r:PRIMARY_EMPLOYER]->() REQUIRE MAX COUNT 1
CREATE CONSTRAINT company_ceo_max_one
FOR ()<-[r:CEO]-() REQUIRE MAX COUNT 1
Endpoint policy constraints¶
Control which label pairs a relationship type may connect.
CREATE CONSTRAINT works_at_allowed
FOR (:Person)-[r:WORKS_AT]->(:Company) REQUIRE ALLOWED
CREATE CONSTRAINT no_direct_mutation_entity
FOR (:MutationEvent)-[r:AFFECTS]->(:Entity) REQUIRE DISALLOWED
ALLOWED rules form a whitelist for a relationship type once any allowed rule exists. DISALLOWED rules are a blacklist and take precedence.
Managing block-style constraint contracts¶
Block-style contracts let you group multiple checks under a single CREATE CONSTRAINT statement using REQUIRE { ... }.
Each block entry must be either:
- a primitive entry that maps onto the existing schema engine
- a boolean predicate that must evaluate to true for the targeted node or relationship
Node contract example¶
CREATE CONSTRAINT person_contract
FOR (n:Person)
REQUIRE {
n.id IS UNIQUE
n.name IS NOT NULL
n.age IS :: INTEGER
n.status IS :: STRING
n.status IN ['active', 'inactive']
(n.tenant, n.externalId) IS NODE KEY
COUNT { (n)-[:PRIMARY_EMPLOYER]->(:Company) } <= 1
NOT EXISTS { (n)-[:FORBIDDEN_REL]->() }
}
Relationship contract example¶
CREATE CONSTRAINT works_at_contract
FOR ()-[r:WORKS_AT]-()
REQUIRE {
r.id IS UNIQUE
r.startedAt IS NOT NULL
r.role IS :: STRING
(r.tenant, r.externalId) IS RELATIONSHIP KEY
startNode(r) <> endNode(r)
startNode(r).tenant = endNode(r).tenant
r.status IN ['active', 'inactive']
r.hoursPerWeek > 0
}
How contract entries are enforced¶
Inside a block, primitive entries compile into the existing primitive constraint system when the semantics already match. Examples include:
n.id IS UNIQUEn.name IS NOT NULLn.age IS :: INTEGER(n.tenant, n.externalId) IS NODE KEY(n.key, n.valid_from, n.valid_to) IS TEMPORAL NO OVERLAPr.id IS UNIQUEr.startedAt IS NOT NULLr.role IS :: STRING(r.tenant, r.externalId) IS RELATIONSHIP KEY
Boolean predicates remain runtime contract entries. Examples include:
n.status IN ['active', 'inactive']COUNT { (n)-[:PRIMARY_EMPLOYER]->(:Company) } <= 1NOT EXISTS { (n)-[:FORBIDDEN_REL]->() }startNode(r) <> endNode(r)startNode(r).tenant = endNode(r).tenantr.hoursPerWeek > 0
This split matters operationally:
- compiled entries reuse the current primitive storage and enforcement path
- runtime entries are evaluated only for affected contracts on writes
- compiled entries are not enforced twice
What happens when you create a contract¶
Contract creation is all-or-nothing.
When you run CREATE CONSTRAINT ... REQUIRE { ... }, NornicDB:
- parses the block
- compiles block entries that map to existing primitives
- validates every entry against current data
- stores contract metadata only if every entry is valid and the current graph already satisfies the whole contract
If any existing node or relationship violates any entry, creation fails and no partial contract is stored.
Example failure:
CREATE (:Person {
id: 'p-1',
name: 'Ada',
age: 34,
status: 'paused',
tenant: 't1',
externalId: 'e1'
})
CREATE CONSTRAINT person_contract
FOR (n:Person)
REQUIRE {
n.id IS UNIQUE
n.name IS NOT NULL
n.age IS :: INTEGER
n.status IN ['active', 'inactive']
(n.tenant, n.externalId) IS NODE KEY
}
Expected error:
constraint contract person_contract violated: predicate `n.status IN ['active', 'inactive']` evaluated to false
What happens on writes¶
After a contract exists, NornicDB enforces it on the same write paths that can change validity:
- node create
- node property updates
- node label changes
- relationship create
- relationship property updates
- node or relationship deletes when the predicate depends on edge presence
- transaction commit for batched writes
Evaluation order is:
- primitive constraints
- residual boolean predicates from block contracts
NornicDB evaluates only affected contracts rather than rescanning the whole graph. In practice that means node-targeted contracts for matching labels and relationship-targeted contracts for matching types, plus endpoint-adjacent checks when a predicate depends on connected entities.
Inspect constraints and contracts¶
Use SHOW CONSTRAINTS to inspect actual primitive constraints, including primitives compiled out of a block contract.
Use SHOW CONSTRAINT CONTRACTS to inspect the contract object itself.
The contract listing includes:
nametargetEntityTypetargetLabelOrTypeentryCountcompiledEntryCountruntimeEntryCountdefinition
Example row:
name | targetEntityType | targetLabelOrType | entryCount | compiledEntryCount | runtimeEntryCount | definition
person_contract | NODE | Person | 8 | 6 | 2 | CREATE CONSTRAINT person_contract ...
Use the two listings together:
SHOW CONSTRAINTSanswers which primitive schema rules are activeSHOW CONSTRAINT CONTRACTSanswers which higher-level contract definition produced them and how much of the contract remains runtime-only
Operational guidance¶
Prefer explicit names¶
Always name NornicDB-specific constraints and contracts. Named schema objects are easier to inspect, compare across environments, and drop or recreate during migrations.
Use IF NOT EXISTS for idempotent rollout¶
CREATE CONSTRAINT person_status_domain IF NOT EXISTS
FOR (n:Person) REQUIRE n.status IN ['active', 'inactive']
Use this for deployment scripts and bootstrap flows when the intended definition is stable.
Validate data before rollout¶
Because creation fails on existing violations, it is usually worth running a targeted audit query before applying a new contract.
MATCH (n:Person)
WHERE n.status IS NOT NULL AND NOT n.status IN ['active', 'inactive']
RETURN n.id, n.status
This is especially important for runtime predicates such as cardinality-style checks embedded in a block.
Treat contract edits as schema migrations¶
Changing a block definition changes one named schema contract, not just one primitive row. Plan updates the same way you would plan any schema migration:
- clean or backfill existing data first
- apply the new contract definition
- verify both
SHOW CONSTRAINTSandSHOW CONSTRAINT CONTRACTS
Keep primitives separate when grouping adds no value¶
Do not use a block contract just because it is available. A single uniqueness or existence rule is clearer as a normal primitive constraint. Use a contract when the grouped rules describe one business invariant.
Unsupported forms and scope limits¶
The following are not supported in block contracts:
- nested relationship mini-constraints inside
REQUIRE { ... } - automatic migration of existing primitive constraints into block contracts
- synthetic contract rows inside
SHOW CONSTRAINTS
SHOW CONSTRAINTS remains the primitive schema listing. SHOW CONSTRAINT CONTRACTS is the contract-specific introspection surface.
Recommended rollout pattern¶
- Create or clean the target data model.
- Run audit queries for expected enum, endpoint, or cardinality violations.
- Create primitive constraints or one named block contract.
- Verify
SHOW CONSTRAINTSfor compiled primitives. - Verify
SHOW CONSTRAINT CONTRACTSfor the contract metadata. - Exercise one valid write and one invalid write before promoting the migration.
Related guides¶
- For general Cypher syntax, see Cypher Queries.
- For property type and value examples, see Property Data Types.
- For a domain-model walkthrough that includes constraint examples, see Canonical Graph Ledger.
- For feature parity and Neo4j comparison, see Cypher Compatibility.