Constraints
patinaDB supports uniqueness, existence (NOT NULL), node-key, and
property-type (IS :: <TYPE>) constraints on nodes, plus existence,
property-type, uniqueness, and relationship-key constraints on
relationships, with Neo4j-5 (and legacy Neo4j-4) DDL — all four Neo4j
constraint kinds, on both nodes and relationships. Constraints are enforced at
write time, replicate across a cluster, and are carried in backups and Raft
snapshots.
Creating constraints
CREATE CONSTRAINT [name] [IF NOT EXISTS]
FOR (n:Label) REQUIRE n.prop IS UNIQUE
Node kinds:
| Kind | DDL | Enforces |
|---|---|---|
| Uniqueness | REQUIRE n.prop IS UNIQUE | No two :Label nodes share a non-null value for prop. |
| Existence | REQUIRE n.prop IS NOT NULL | Every :Label node has prop set (non-null). |
| Node key | REQUIRE (n.p1, n.p2) IS NODE KEY | The tuple is present on every :Label node and unique (composite unique + existence). |
| Property type | REQUIRE n.prop IS :: INTEGER | When present, prop’s value is of the declared type (does not require presence). |
Relationship kinds (FOR ()-[r:TYPE]-(), any direction):
| Kind | DDL | Enforces |
|---|---|---|
| Existence | REQUIRE r.prop IS NOT NULL | Every :TYPE relationship has prop set (non-null). |
| Property type | REQUIRE r.prop IS :: INTEGER | When present, prop’s value is of the declared type. |
| Uniqueness | REQUIRE r.prop IS UNIQUE | No two :TYPE relationships share a non-null value for prop. |
| Relationship key | REQUIRE (r.p1, r.p2) IS [REL|RELATIONSHIP] KEY | The tuple is present on every :TYPE relationship and unique. |
Relationship uniqueness/key is index-backed by the existing edge-property value
index (O(log N + matches)), not a full scan of the relationship type.
The legacy Neo4j-4 forms are also accepted:
CREATE CONSTRAINT … ON (n:Label) ASSERT n.prop IS UNIQUE and the shorthand
ON :Label(prop). The property-type predicate also accepts the IS TYPED <TYPE>
synonym of IS :: <TYPE>.
Property types
IS :: <TYPE> (and IS TYPED <TYPE>) accepts: BOOLEAN, STRING, INTEGER,
FLOAT, POINT, DATE, LOCAL TIME, ZONED TIME, LOCAL DATETIME,
ZONED DATETIME, DURATION, and LIST (BOOL/INT are accepted as aliases).
A missing or null property is allowed — a type constraint restricts a
present value’s type only; combine it with an existence constraint if you also
need the property present. Element-type refinement (LIST<INTEGER NOT NULL>) is
not yet supported (bare LIST only).
CREATE CONSTRAINT person_email FOR (p:Person) REQUIRE p.email IS UNIQUE
-- status: 'Constraint created on :Person(email) IS UNIQUE'
CREATE CONSTRAINT emp_key FOR (e:Employee) REQUIRE (e.dept, e.num) IS NODE KEY
-- status: 'Constraint created on :Employee(dept, num) IS NODE KEY'
The optional name identifies the constraint for DROP and SHOW. IF NOT EXISTS makes creation idempotent.
Creating over existing data
CREATE CONSTRAINT first validates the current graph. If the existing data
already violates the constraint (a duplicate value, or a missing required
property), the statement fails and no constraint is registered — fix the data and
re-run.
Enforcement semantics
Enforcement runs at write time on CREATE, MERGE, and SET / REMOVE —
including a SET / REMOVE inside a FOREACH body, which is enforced
identically to a top-level write:
CREATE (:Person {email: 'ada@x.io', name: 'Ada'})
CREATE (:Person {email: 'ada@x.io', name: 'Ada2'})
-- error: Unique constraint violation on :Person(email): value already exists
- UNIQUE is NULL-exempt. A null (or absent) value is not constrained —
many nodes (or relationships) may omit
prop. Only two concrete equal values collide. (Use a node-key/relationship-key or a separate existence constraint if you also need the property present.) 1and1.0are the same value. Uniqueness treats an integer and a mathematically-equal float as identical (matching Neo4j), so underUNIQUE(k)a storedInteger 1collides with aCREATE/SETof1.0, and vice-versa. This holds for node UNIQUE, relationship UNIQUE, and every column of a composite NODE KEY / REL KEY, on both the single-writer path and concurrent transactions. The fold is exact — a float merges with an integer only when it is finite, whole, and round-trips bit-for-bit — so two genuinely different values never collide, and NULL stays exempt as above. See Cypher → numeric equivalence for the full rule. (Very large exact-integer float values — roughly≥ 2^59, e.g.1e18— are still enforced correctly, but fall back to a scan instead of an index seek, so those checks can be slower.)- Existence and node-key/relationship-key reject a missing/null required
property — including a
CREATE/MERGEthat omits it, aSET n.prop = null, or aREMOVE n.prop. This holds for both node existence and relationship existence (checked at edge create,SET r.prop = null,SET r =/+=, andREMOVE r.prop). - Property-type rejects a present wrong-typed value on
CREATE/MERGE/SET(node or relationship). A missing/null value is allowed. - The uniqueness check is an
O(log N + matches)seek of the label-scoped value index (node) or the edge-property value index (relationship), so it is cheap even on a large label/type. - Node-key uniqueness is served by an automatically registered backing compound index over the key tuple — you do not create it separately. Relationship-key uniqueness is served the same way over the edge-property value index (no separate index to create).
Inspecting and dropping
SHOW CONSTRAINTS
constraints: ['person_email: :Person(email) IS UNIQUE', 'emp_key: :Employee(dept, num) IS NODE KEY', 'person_name: :Person(name) IS NOT NULL']
Relationship constraints look the same, scoped by type instead of label, e.g.
REQUIRE (r.a, r.b) IS REL KEY FOR ()-[r:WORKS_AT]-().
DROP CONSTRAINT person_email [IF EXISTS]
Replication and durability
- Cluster replication is automatic. Constraint DDL replicates through the
Raft log and re-runs deterministically on every node (the same
IndexDdlpath that index DDL uses). - Raft snapshots carry the defs. A follower that bootstraps purely from a streamed snapshot (after the DDL log is purged) still enforces every constraint.
- Portable backups carry the defs too.
GET /mgmt/snapshot/POST /mgmt/restoreround-trip UNIQUE, existence, and node-key defs (alongside vector indexes, tags, and RBAC users) — restore re-issues them asCREATE CONSTRAINT … IF NOT EXISTS. See the REST API.
Not supported
LIST<T>element-type refinement — only bareLISTis accepted.- Bare composite UNIQUE without existence — use
IS NODE KEY/IS [REL|RELATIONSHIP] KEYfor a multi-property key (which also requires presence).
See Limitations for the full schema-feature scope, and the Data Model for how properties and the value index work.