Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

KindDDLEnforces
UniquenessREQUIRE n.prop IS UNIQUENo two :Label nodes share a non-null value for prop.
ExistenceREQUIRE n.prop IS NOT NULLEvery :Label node has prop set (non-null).
Node keyREQUIRE (n.p1, n.p2) IS NODE KEYThe tuple is present on every :Label node and unique (composite unique + existence).
Property typeREQUIRE n.prop IS :: INTEGERWhen present, prop’s value is of the declared type (does not require presence).

Relationship kinds (FOR ()-[r:TYPE]-(), any direction):

KindDDLEnforces
ExistenceREQUIRE r.prop IS NOT NULLEvery :TYPE relationship has prop set (non-null).
Property typeREQUIRE r.prop IS :: INTEGERWhen 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/MERGE that omits it, a SET n.prop = null, or a REMOVE n.prop. This holds for both node existence and relationship existence (checked at edge create, SET r.prop = null, and REMOVE 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 IndexDdl path 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/restore round-trip UNIQUE, existence, and node-key defs (alongside vector indexes, tags, and RBAC users) — restore re-issues them as CREATE 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 bare LIST is accepted.
  • Bare composite UNIQUE without existence — use IS NODE KEY for 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.