The problem is similar to this one: How to have a one-to-many relationship with a privileged child?
The "at most one per group" part of the constraint can be solved with a partial index:
CREATE UNIQUE INDEX is_FavoriteChild
ON x (yid)
WHERE is_principal ;
Another way to solve the problem is by removing the is_principal
column and add a 3rd table. This doesn't solve the "exactly one" either:
CREATE TABLE x (
xid INTEGER NOT NULL PRIMARY KEY,
yid INTEGER NOT NULL REFERENCES y (yid),
UNIQUE (yid, xid)
);
CREATE TABLE x_principal (
xid INTEGER NOT NULL,
yid INTEGER NOT NULL PRIMARY KEY,
FOREIGN KEY (yid, xid) REFERENCES x (yid, xid)
);
If you want to enforce the "exactly one" restriction using DDL alone, it can be done in Postgres with deferrable constraints (I don't think this is an option in SQLite).
For more details and options, you can see the excellent answer in the SO question by @Erwin: Complex foreign key constraint in SQLAlchemy.
(Editing the answer for the additional detail that not all values of y.yid
have to appear in table x
. One table is added):
--- This table will hold all values of yid that appear in table x
CREATE TABLE y_x (
yid INTEGER NOT NULL PRIMARY KEY REFERENCES y (yid),
--- **no other columns**
principal_xid INTEGER NOT NULL
);
CREATE TABLE x (
xid INTEGER NOT NULL PRIMARY KEY,
yid INTEGER NOT NULL REFERENCES y_x (yid)
DEFERRABLE INITIALLY DEFERRED,
UNIQUE (yid, xid)
);
ALTER TABLE y_x
ADD CONSTRAINT y_principal_x_fk
FOREIGN KEY (yid, principal_xid)
REFERENCES x (yid, xid)
DEFERRABLE INITIALLY DEFERRED;
You don't need triggers or PL/pgSQL at all.
You don't even need DEFERRABLE
constraints.
And you don't need to store any information redundantly.
Include the ID of the active email in the users
table, resulting in mutual references. One might think we need a DEFERRABLE
constraint to solve the chicken-and-egg problem of inserting a user and his active email, but using data-modifying CTEs we don't even need that.
This enforces exactly one active email per user at all times:
CREATE TABLE users (
user_id serial PRIMARY KEY
, username text NOT NULL
, email_id int NOT NULL -- FK to active email, constraint added below
);
CREATE TABLE email (
email_id serial PRIMARY KEY
, user_id int NOT NULL REFERENCES users ON DELETE CASCADE ON UPDATE CASCADE
, email text NOT NULL
, CONSTRAINT email_fk_uni UNIQUE(user_id, email_id) -- for FK constraint below
);
ALTER TABLE users ADD CONSTRAINT active_email_fkey
FOREIGN KEY (user_id, email_id) REFERENCES email(user_id, email_id);
Remove the NOT NULL
constraint from users.email_id
to make it "at most one active email". (You can still store multiple emails per user, but none of them is "active".)
You can make active_email_fkey
DEFERRABLE
to allow more leeway (insert user and email in separate commands of the same transaction), but that's not necessary.
I put user_id
first in the UNIQUE
constraint email_fk_uni
to optimize index coverage. Details:
Optional view:
CREATE VIEW user_with_active_email AS
SELECT * FROM users JOIN email USING (user_id, email_id);
Here's how you insert new users with an active email (as required):
WITH new_data(username, email) AS (
VALUES
('usr1', 'abc@d.com') -- new users with *1* active email
, ('usr2', 'def3@d.com')
, ('usr3', 'ghi1@d.com')
)
, u AS (
INSERT INTO users(username, email_id)
SELECT n.username, nextval('email_email_id_seq'::regclass)
FROM new_data n
RETURNING *
)
INSERT INTO email(email_id, user_id, email)
SELECT u.email_id, u.user_id, n.email
FROM u
JOIN new_data n USING (username);
The specific difficulty is that we have neither user_id
nor email_id
to begin with. Both are serial numbers provided from the respective SEQUENCE
. It can't be solved with a single RETURNING
clause (another chicken-and-egg problem). The solution is nextval()
as explained in detail in the linked answer below.
If you don't know the name of the attached sequence for the serial
column email.email_id
you can replace:
nextval('email_email_id_seq'::regclass)
with
nextval(pg_get_serial_sequence('email', 'email_id'))
Here's how you add a new "active" email:
WITH e AS (
INSERT INTO email (user_id, email)
VALUES (3, 'new_active@d.com')
RETURNING *
)
UPDATE users u
SET email_id = e.email_id
FROM e
WHERE u.user_id = e.user_id;
SQL Fiddle.
You might encapsulate the SQL commands in server-side functions if some simple-minded ORM isn't smart enough to cope with this.
Closely related, with ample explanation:
Also related:
About DEFERRABLE
constraints:
About nextval()
and pg_get_serial_sequence()
:
Best Answer
Variant 1
Since all you need is a single column with
standard = true
, set standard to NULL in all other rows. Then a plainUNIQUE
constraint works, since NULL values do not violate it:DEFAULT
is an optional reminder that the first row entered should become the default. It's not enforcing anything. While you cannot set more than one row tostandard = true
, you can still set all rows NULL. There is no clean way to prevent this with only constraints in a single table.CHECK
constraints do not consider other rows (without dirty tricks).Related:
Restrict two specific column values from existing at the same time
Add constraint to make column unique per group of rows
To update:
To allow a command like (where the constraint is only satisfied at the end of the statement):
.. the
UNIQUE
constraint would have to beDEFERRABLE
. See:dbfiddle here
Variant 2
Have a second table with a single row like:
Create this as superuser:
Now there is always a single row pointing to the standard (in this simple case also representing the standard rate directly). Only a superuser could break it. You might disallow that, too, with a trigger
BEFORE DELETE
.dbfiddle here
Related:
You might add a
VIEW
to see the same as in variant 1:In queries where all you want is the standard rate, use (only)
taxrate_standard.taxrate
directly.You later added:
A poor man's implementation of variant 2 would be to just add a row to
products
(or any similar table) pointing to the standard tax rate; a dummy product you might call "Standard tax rate" - if your setup allows it.The FK constraints enforces referential integrity. To complete it, enforce
tax_rate_id IS NOT NULL
for the row (if that's not the case for the column in general). And disallow its deletion. Both could be put into triggers. No extra table, but less elegant and not as reliable.