I have a table:
CREATE EXTENSION btree_gist;
CREATE TABLE excllock (
id BIGSERIAL PRIMARY KEY,
myrange INT8RANGE NOT NULL,
key UUID NOT NULL,
isread BOOLEAN NOT NULL,
EXCLUDE USING gist (
myrange WITH &&,
key WITH =
)
);
I’d like it to behave this way:
- Multiple rows with overlapping
myrange
onkey
can be added as long as all of them areisread
. - In case a new row with
isread=FALSE
is added, one of three things should happen:- There is already at least one
isread
row present in the database, so the constraint should fail (concurrent read access already exists, cannot write). - There is already an
isread=FALSE
row present, so the constraint fails (concurrent write access already exists, cannot write). - There are no rows matching exclusion constraint: the row is added (no concurrent access, can write).
- There is already at least one
So in effect there can only be multiple isread=TRUE
or one isread=FALSE
at a time (or no records matching (key, myrange)
at all), essentially creating a read/write lock on the key-range pair.
Examples:
-- Insert multiple read locks (allowed)
INSERT INTO excllock (myrange, key, isread) VALUES ('[1,10)', '00000000-0000-0000-0000-000000000001', TRUE);
INSERT INTO excllock (myrange, key, isread) VALUES ('[5,15)', '00000000-0000-0000-0000-000000000001', TRUE);
-- Try inserting a write lock (should fail due to read locks)
INSERT INTO excllock (myrange, key, isread) VALUES ('[1,10)', '00000000-0000-0000-0000-000000000001', FALSE);
-- Insert a write lock on a free range
INSERT INTO excllock (myrange, key, isread) VALUES ('[100,1000)', '00000000-0000-0000-0000-000000000001', FALSE);
-- Try to insert a read lock inside the write range (should fail)
INSERT INTO excllock (myrange, key, isread) VALUES ('[105,900)', '00000000-0000-0000-0000-000000000001', TRUE);
I thought of using where (NOT isread)
, but it will ignore isread
rows completely.
Is there a clever way of creating this kind of constraint?
2
Answers
This can be achieved by adding an additional parameter to your exclusion constraint to represent the read/write status of the lock. I have modeled a read lock with a range encompassing only the id of that row, which is guaranteed to be unique and never overlap the id of any other row. I have then used an infinite range to represent a write lock which will overlap all other ranges regardless of whether they represent read or write locks.
Slightly more text, but it’s faster and lighter, and the whole logic is handled by one
<>
, plus an additional but simpler, partial constraint:demo at db<>fiddle
int8range
idea, the example above took about 15% less space (11MB
down from13MB
) to accommodate the table and indexes enforcing the constraints.13.01s
down from15.21s
) to complete compared to the other example.write
(not isread
) entries/locks to share a key and range. Without a test case for that, it’s enough toAnd that would pass all of the ones you showed, way faster than either method, with a smaller footprint.
With the missing test case included, both methods shown so far are logically equivalent, rejecting the exact same
26'877
entries out of60'000
generated from the same seed. That being said, what @EAW showed seems like a cooler method (also, less lines if you’re into that) and I share your enthusiasm.