Constraints
patinaDB supports uniqueness, existence (NOT NULL), node-key, and
property-type (IS :: <TYPE>) constraints on nodes, plus existence and
property-type constraints on relationships, with Neo4j-5 (and legacy
Neo4j-4) DDL. 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. |
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).
$ patinadb ./cdb.db query \
"CREATE CONSTRAINT person_email FOR (p:Person) REQUIRE p.email IS UNIQUE"
status: 'Constraint created on :Person(email) IS UNIQUE'
$ patinadb ./cdb.db query \
"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:
$ patinadb ./cdb.db query "CREATE (:Person {email: 'ada@x.io', name: 'Ada'})"
$ patinadb ./cdb.db query "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 may omit
prop. Only two concrete equal values collide. (Use a node-key or a separate existence constraint if you also need the property present.) - Existence and node-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, 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, so it is cheap even on a large label. - Node-key uniqueness is served by an automatically registered backing compound index over the key tuple — you do not create it separately.
Inspecting and dropping
$ patinadb ./cdb.db query "SHOW CONSTRAINTS"
constraints: ['person_email: :Person(email) IS UNIQUE', 'emp_key: :Employee(dept, num) IS NODE KEY', 'person_name: :Person(name) IS NOT NULL']
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
- Relationship key / uniqueness (
FOR ()-[r:TYPE]-() REQUIRE r.id IS UNIQUE/IS KEY) — rejected with a clear “not supported yet” error (needs relationship-property indexes). LIST<T>element-type refinement — only bareLISTis accepted.- Bare composite UNIQUE without existence — use
IS NODE 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.