CREATE CONSTRAINT#
Requires a property of a label to be unique, or requires every element of the label to satisfy a condition.
Synopsis#
CREATE CONSTRAINT [ constraint_name ] ON label_name ASSERT condition
bold keywords · italic placeholders you supply · [ ] optional · ... repeatable. See Syntax conventions.
Description#
There are two forms, and they become two different things on disk.
ASSERT property IS UNIQUE becomes an exclusion constraint over the property read:
EXCLUDE USING btree ((properties.'title') WITH =). A write whose value already exists is
rejected as a conflict.
ASSERT condition becomes a check constraint over the same kind of read:
CHECK ((properties.'age')::integer > 9). A write whose row fails the condition is
rejected.
The name is optional in both forms. Left out, one is derived: <label>_unique_constraint
for the unique form and <label>_properties_check for the check form. A numeric suffix is
added if that name is taken. A name you supply has to be free in the graph’s schema, because
the unique form creates an index of that name.
Other rules:
The label is looked up in the graph named by
graph_path; its name cannot be qualified.Both forms work on a vertex label and on an edge label.
CREATE OR REPLACE CONSTRAINTis parsed and then refused withCREATE OR REPLACE CONSTRAINT is not supported. To change a constraint, drop it and create it again.A constraint is created over the rows already in the label, so an existing row that violates it makes the statement fail.
A check constraint is a SQL check constraint, so a missing property satisfies it. The condition evaluates to unknown for an absent property, and a check constraint accepts unknown. Use the constraint to restrict values, not to require a key.
How a graph is stored covers the on-disk mapping in one screen.
Parameters#
- constraint_name
Name for the constraint. Optional; a name is derived from the label and the form when you leave it out. For the unique form it also names the index, so it must not collide with an existing relation in the graph.
- label_name
Label the constraint applies to, in the graph named by
graph_path.- property
The property that must be unique, written the way Cypher writes it:
title, orname.firstfor a key inside a nested map.- condition
A Cypher condition over the label’s properties. It is evaluated once per written element.
Examples#
The examples build their own graph, constrain, rather than using
the example dataset, and drop it at the end.
CREATE GRAPH constrain;
SET graph_path = constrain;
CREATE VLABEL film;
CREATE ELABEL rated;
Require a property to be unique#
CREATE CONSTRAINT film_title_uniq ON film ASSERT title IS UNIQUE;
CREATE CONSTRAINT
That produced an exclusion constraint over the property read:
\d constrain.film
Table "constrain.film"
Column | Type | Collation | Nullable | Default
------------+---------+-----------+----------+---------------------------------------------------------------------------------------------
id | graphid | | not null | graphid(graph_labid('constrain.film'::cstring), nextval('constrain.film_id_seq'::regclass))
properties | jsonb | | not null | jsonb_build_object()
Indexes:
"film_pkey" PRIMARY KEY, btree (id)
"film_title_uniq" EXCLUDE USING btree ((properties.'title') WITH =)
Inherits: constrain.ag_vertex
The first write goes in:
CREATE (:film {title: 'Arrival', year: 2016});
UPDATE 1
The second is rejected, and the message names the constraint and the value:
CREATE (:film {title: 'Arrival', year: 2021});
ERROR: conflicting key value violates exclusion constraint "film_title_uniq"
DETAIL: Key ((properties.'title'))=("Arrival") conflicts with existing key ((properties.'title'))=("Arrival").
Leave the constraint unnamed#
CREATE CONSTRAINT ON film ASSERT year IS UNIQUE;
\dGv shows both constraints in the terms they were written in, and the derived name the
second one was given:
\dGv constrain.film
List of labels
Graph | Name | Type | Owner
-----------+------+--------+-------
constrain | film | vertex | agens
(1 row)
Vertex label "constrain.film"
--
Constraints:
"film_title_uniq" ASSERT (title) IS UNIQUE
"film_unique_constraint" ASSERT (year) IS UNIQUE
Inherits: constrain.ag_vertex
Restrict what a property may hold#
CREATE VLABEL teen;
CREATE CONSTRAINT teen_age ON teen ASSERT (10 <= age::int AND age::int <= 19);
\d constrain.teen
Table "constrain.teen"
Column | Type | Collation | Nullable | Default
------------+---------+-----------+----------+---------------------------------------------------------------------------------------------
id | graphid | | not null | graphid(graph_labid('constrain.teen'::cstring), nextval('constrain.teen_id_seq'::regclass))
properties | jsonb | | not null | jsonb_build_object()
Indexes:
"teen_pkey" PRIMARY KEY, btree (id)
Check constraints:
"teen_age" CHECK (10 <= ((properties.'age')::integer) AND ((properties.'age')::integer) <= 19)
Inherits: constrain.ag_vertex
A row inside the range is written:
CREATE (:teen {name: 'Kim', age: 15});
UPDATE 1
One outside it is not:
CREATE (:teen {name: 'Lee', age: 20});
ERROR: new row for relation "teen" violates check constraint "teen_age"
DETAIL: Failing row contains (5.2, {"age": 20, "name": "Lee"}).
A vertex with no age at all is accepted, because the condition is unknown rather than
false:
CREATE (:teen {name: 'Bo'});
MATCH (t:teen) RETURN t.name AS name, t.age AS age ORDER BY name;
name | age
-------+-----
"Bo" |
"Kim" | 15
(2 rows)
Compare without a cast#
Writing the comparison against the raw property compares jsonb values instead of
integers:
CREATE CONSTRAINT ON teen ASSERT age > 9;
\dGv constrain.teen
List of labels
Graph | Name | Type | Owner
-----------+------+--------+-------
constrain | teen | vertex | agens
(1 row)
Vertex label "constrain.teen"
--
Constraints:
"teen_age" ASSERT (10 <= ((age)::integer) AND ((age)::integer) <= 19)
"teen_properties_check" ASSERT ((age) > cypher_to_jsonb(9))
Inherits: constrain.ag_vertex
Both forms reject {age: 'twelve'}, but they report it differently. The cast form raises
invalid input syntax for type integer: "twelve". The uncast form reports a violated check
constraint. Cast when you want the property’s type enforced as well as its range.
Constrain an edge label#
CREATE CONSTRAINT rated_pair ON rated ASSERT source IS UNIQUE;
\d constrain.rated
Table "constrain.rated"
Column | Type | Collation | Nullable | Default
------------+---------+-----------+----------+-----------------------------------------------------------------------------------------------
id | graphid | | not null | graphid(graph_labid('constrain.rated'::cstring), nextval('constrain.rated_id_seq'::regclass))
start | graphid | | not null |
end | graphid | | not null |
properties | jsonb | | not null | jsonb_build_object()
Indexes:
"rated_end_idx" btree ("end", start)
"rated_id_idx" brin (id)
"rated_pair" EXCLUDE USING btree ((properties.'source') WITH =)
"rated_start_idx" btree (start, "end")
Inherits: constrain.ag_edge
The constraint sits beside the label’s three ordinary indexes.
Add a constraint to a label that already has rows#
The constraint is checked against what is there, so it cannot be created over rows that break it. The two forms report that differently:
CREATE VLABEL rerun;
CREATE (:rerun {title: 'Arrival'}), (:rerun {title: 'Arrival'});
CREATE CONSTRAINT rerun_t ON rerun ASSERT title IS UNIQUE;
ERROR: could not create exclusion constraint "rerun_t"
DETAIL: Key ((properties.'title'))=("Arrival") conflicts with key ((properties.'title'))=("Arrival").
CREATE VLABEL adult;
CREATE (:adult {age: 40});
CREATE CONSTRAINT adult_a ON adult ASSERT (age::int < 20);
ERROR: check constraint "adult_a" of relation "adult" is violated by some row
Refusals worth recognizing#
There is no replace:
CREATE OR REPLACE CONSTRAINT film_title_uniq ON film ASSERT title IS UNIQUE;
ERROR: CREATE OR REPLACE CONSTRAINT is not supported
A unique constraint’s name is an index name, so it collides with one:
CREATE CONSTRAINT film_title_uniq ON film ASSERT year IS UNIQUE;
ERROR: relation "film_title_uniq" already exists
And the label has to exist. Cypher does not create one here the way CREATE (:film) does:
CREATE CONSTRAINT c ON nosuch ASSERT a IS UNIQUE;
ERROR: label "nosuch" does not exist
Finally, drop the graph the examples built:
DROP GRAPH constrain CASCADE;
Notes#
A unique constraint is an exclusion constraint, not a unique index, so it is not
interchangeable with CREATE UNIQUE PROPERTY INDEX. Both reject a duplicate. The error
differs: conflicting key value violates exclusion constraint against
duplicate key value violates unique constraint. What \dGv reports differs. And so does
the statement that removes it: a constraint is dropped by
DROP CONSTRAINT, a unique index by
DROP PROPERTY INDEX.
The two forms differ over
label inheritance, and the difference catches people
out. A check constraint is inherited: a child label created after it carries the constraint
and is checked by it. A unique constraint is not. A child label gets no exclusion
constraint, so the same value can be written once through the parent and once through the
child. MATCH on the parent then returns both.
Note
To make a property unique across a hierarchy, create the constraint on each label in it. There is nothing that makes one value unique across several labels; a constraint, like the index behind it, covers one table.
Compatibility#
ISO/IEC 39075 (GQL)#
GQL has no constraint DDL. It constrains a graph with a graph type
(<create graph type statement>, §12.6), which declares which labels and properties a
graph’s elements may have. A graph type has no per-property uniqueness rule and no check
condition. And a graph is only constrained at all if it references a graph type.
AgensGraph’s graphs are open, so CREATE CONSTRAINT is its own. Graph types are tracked as
planned in the GQL conformance matrix.
PostgreSQL#
The check form corresponds to ALTER TABLE ... ADD CONSTRAINT ... CHECK, and the unique
form to ALTER TABLE ... ADD CONSTRAINT ... EXCLUDE USING btree (... WITH =). It does not
correspond to UNIQUE, which cannot be written over an expression. The condition is a
Cypher expression rather than a SQL one, so age means the property, not a column.
NOT NULL, PRIMARY KEY, FOREIGN KEY, DEFERRABLE and NOT VALID, which
ADD CONSTRAINT offers, have no spelling here.