postgres constraint based on SELECT condition - postgresql

ᕼello! I think I have a somewhat tricky postgres situation:
parents have children. children have an age, and a flag that they are the appreciated.
The rule: a parent can't appreciate two children of the same age!
My question is: how to enforce this rule?
Current schema:
CREATE TABLE parent (
id SERIAL PRIMARY KEY,
name VARCHAR(50) NOT NULL
);
CREATE TABLE child (
id SERIAL PRIMARY KEY,
parent INTEGER REFERENCES parent(id) NOT NULL,
name VARCHAR(50) NOT NULL,
age INTEGER NOT NULL,
appreciated BOOLEAN NOT NULL
);
Put some values in:
INSERT INTO parent(name) VALUES
('bob'), -- assume bob's id = 0
('mary'); -- assume mary's id = 1
INSERT INTO child(parent, name, age, appreciated) VALUES
(0, 'child1', 10, FALSE), -- Bob has children 1, 2, 3
(0, 'child2', 10, FALSE),
(0, 'child3', 15, FALSE),
(1, 'child4', 20, FALSE), -- Mary has children 4, 5, 6
(1, 'child5', 20, FALSE),
(1, 'child6', 10, FALSE);
All fine so far. No child is appreciated, which is always valid.
Mary is allowed to appreciate child6:
UPDATE child SET appreciated=TRUE WHERE name='child6';
Bob is allowed to appreciate child2. child2 is the same age as child6 (who is already appreciated), but child6 is not Bob's child.
UPDATE child SET appreciated=TRUE WHERE name='child2';
Bob now cannot appreciate child1. This child1 is the same age as child2, and child2 is already appreciated.
UPDATE child SET appreciated=TRUE WHERE name='child2'; -- This needs to FAIL!
How do I enforce such a constraint? I'm open to all kinds of solutions, but modifying the general schema is not an option.
Thanks in advance!

How about a UNIQUE partial index, like so:
CREATE UNIQUE INDEX ON child(parent,age) WHERE appreciated;
So every pair of parent,age has to be unique, but only when appreciated children are considered.

You might want to use a trigger that activates BEFORE the insert/update and that fails if the constraint you create is not satisfied.
I suppose it should be like
create trigger <trigger_name>
before insert or update on <table_name>
for each row
declare
dummy number;
begin
select count(*)
into dummy
from <table_name>
where (appreciated=TRUE and :new.child = child and :new.age = age);
if dummy > 0 then
raise_application_error(-20001,'Too many appreciated children');
end if;
end;
Some documentation

The simplest thing I would think to do is add a flag grateful(?) == false to the parent model and when child.appreciated == true { parent.grateful == true }
Check the value of parent.grateful in the function that acts on child.appreciated.
If parent.grateful == true
return "sorry this parent has already shown their appreciation."
LOL this is an interesting concept though. Good Luck. :)

Related

T-SQL How to exclude a child record when it's also it's own parent record?

I have a scenario like the following:
create table #Example (
id int
, overall_id int
, parent_id int
, child_id int
);
insert into #Example values
(1, 25963, 491575090, 491575090)
,(2, 25963, 547952026, 491575090)
,(3, 25963, 547952026, 230085039)
,(4, 25963, 547952026, 547952026);
select e.*
from #Example as e;
drop table #Example;
I want to exclude the record with id "2" because that is it's own parent record (see id "1").
I do not want to exclude 3, because the child record is not it's own parent record. And I don't want to exclude 1 and 4 because those are their own parent records.
One problem is that in my business scenario, I have no corresponding "ID" field, that is something I provided in this example so that I could refer to each row uniquely.
Any help on techniques to exclude record 2 would be greatly appreciated!
I still don't understand the question, but the expected result falls out of:
select *
from #Example as E
where not exists (
select 42
from #Example as IE
where
-- There is a row that is self parenting?!
IE.parent_id = IE.child_id and
-- The row under consideration is related in a child/parent way?
IE.child_id = E.child_id and
-- It isn't the same row as we're considering.
IE.id <> E.id );
See dbfiddle.

How to show chain element by order

I have goal to create query which return me item ids regarding position in chain.
I have chain logic, each element has right and left fk and index.
Chain can contains elements which can added like append and like prepend approach, regarding this id from table not help to build current chain dependencies.
This is db structure
create table public.chain_data
(
id integer not null
constraint chain_data_pkey
primary key,
unique_identifiers_id integer not null
constraint fk_388447e52a0b191e
references public.unique_identifiers
on delete cascade,
chain_data_name varchar(255) not null,
carriage boolean default false,
left_id integer not null
constraint fk_388447e5e26cce02
references public.chain_data,
right_id integer
constraint fk_388447e554976835
references public.chain_data
);
alter table public.chain_data
owner to "universal-counter";
create index idx_388447e52a0b191e
on public.chain_data (unique_identifiers_id);
create unique index left_right_uniq_idx
on public.chain_data (right_id, left_id);
create unique index carriage_uniq_index
on public.chain_data (unique_identifiers_id, carriage)
where (carriage <> false);
and data example. this chain began from id = 10 and then was prepend new items(rows) in start of chain. Each element has left and right dependencies. So inserts:
INSERT INTO public.chain_data (id, unique_identifiers_id, chain_data_name, carriage, left_id, right_id)
VALUES
(10, 8, 'dddd_2', true, 22, null),
(22, 8, 'shuba', false, 23, 10),
(24, 8, 'viktor', false, null, 23),
(23, 8, 'ivan', false, 24, 22);
Regarding this query should to return ids like this
24, 23, 22, 10
because element with id = 24 present on start chain then by left and right dependencies obviously 23, 22 and 10 id= 10 is last element in chain
demo:db<>fiddle
You can use a recursive CTE for that:
WITH RECURSIVE chain AS (
SELECT id, right_id -- 1
FROM chain_data
WHERE left_id IS NULL
UNION
SELECT cd.id, cd.right_id -- 2
FROM chain_data cd
JOIN chain c ON c.right_id = cd.id
)
SELECT
string_agg(id::text, ', ') -- 3
FROM
chain
Initial part of the recursion: The record with the NULL value
The recursion part: Join the current table on the previous step using the previous right_id as current id
Afterwards you can aggregate all fetched records with the string_agg() aggregation to return your string list.

MySQL 5.6 SELECT UNION Query

The SELECT UNION query below functions as needed.
We want to display different columns from the same table and same entry as separate rows.
I know there has to be a better / cleaner way.
Please advise.
SELECT task1 AS Job FROM prevent WHERE task1 != "" AND eq = ? AND id = ? AND pmTask LIKE ?
UNION
SELECT task2 AS Job FROM prevent WHERE task2 != "" AND eq = ? AND id = ? AND pmTask LIKE ?
UNION
SELECT task3 AS Job FROM prevent WHERE task3 != "" AND eq = ? AND id = ? AND pmTask LIKE ?
Here is a snapshot of the db entry displayed below.
Your query is the actual best way of doing it.
In MySQL 8.x you could use a CTE to simplify the code, but since you are using 5.6, I don't see a better option.
To be brutally honest, your database model is not that great. It will support a fixed/maximum number of tasks per job since their are included as columns of the table rather than as 1:n relationship with another table. This design will generate a lot more work, every time you need to store and retrieve data from it. Unfortunately, unless you change your model, there's little you can do to improve those queries.
EDIT AS OF Jan 29, 2020:
From your description, jobs and tasks seem to be two different entities. If that's the case, instead of using a single table to store both of them it's advisable to use two.
For example:
create table prevent (
id int primary key not null,
eq int,
eqName varchar(50),
timeFrame int,
pmTask varchar(50)
);
create table task (
prevent_id int primary key not null,
task_number int,
name varchar(100),
constraint fk_task_job1 foreign key (job_id) references prevent (id)
);
insert into prevent (id, eq, eqName, timeFrame, pmTask)
values (910, 910, 'Boiler', 90, 'Boiler Quarterly Check');
insert into task (prevent_id, task_number, name) values (910, 1, 'job work tasks');
insert into task (prevent_id, task_number, name) values (910, 2, 'check oil levels');
insert into task (prevent_id, task_number, name) values (910, 3, 'belt tension');
Then your query would be:
select
t.task_number,
t.name
from prevent j
join task t on t.prevent_id = j.id
where t.name <> ''
and j.id = ?
and j.pmTask like ?
order by t.task_number

Postgres select all data via relation

If parents have children, and children have books they've read, how do I know all the books read by all the children of a parent?
Basic setup:
CREATE TABLE parent(
id SERIAL PRIMARY KEY,
name VARCHAR(50) NOT NULL
);
CREATE TABLE child(
id SERIAL PRIMARY KEY,
parent INTEGER REFERENCES parent(id) NOT NULL,
name VARCHAR(50) NOT NULL
);
CREATE TABLE book(
id SERIAL PRIMARY KEY,
readBy INTEGER REFERENCES child(id) NOT NULL,
name VARCHAR(50) NOT NULL
);
Insert some data:
INSERT INTO parent (name) VALUES
('Bob'),
('Mary');
INSERT INTO child (parent, name) VALUES
(1, 'Stu'), -- Bob has children Stu and Jane
(1, 'Jane'),
(2, 'Greg'), -- Mary has children Greg and Bella
(2, 'Bella');
INSERT INTO book (readBy, name) VALUES
(1, 'Crime & Punishment'), -- Stu has read C&P and Goodnight Moon
(1, 'Goodnight Moon'),
(2, 'The Singularity'), -- Jane has read The Singularity and 1fish2fish
(2, 'One Fish Two Fish'),
(3, 'Narnia'); -- Greg has read Narnia (Bella read nothing)
How do I formulate a SELECT query involving "Bob" as a parameter and get all the books read by his kids?:
( 'Crime & Punishment', 'Goodnight Moon', 'The Singularity', 'One Fish Two Fish' )
The same query, except involving "Mary" should give only the one book read by "Greg", who is her only child who has read anything:
( 'Narnia' )
Thanks in advance for any help! :)
Disclaimer: I'm sure this question must have been asked before but I wasn't able to find it :(
You can chain joins
select book.name
from book
join child on book.readby=child.id
join parent on child.parent=parent.id
where parent.name='Bob';
Or if you want the results as an array/list
This is unusual, usually results as rows like above are more useful but you appear to be asking for a single-line result.
select array_agg(book.name)
from book
join child on book.readby=child.id
join parent on child.parent=parent.id
where parent.name='Bob';
Note: the results could appear in any order.

Constraint on sum from rows

I've got a table in PostgreSQL 9.4:
user_votes (
user_id int,
portfolio_id int,
car_id int
vote int
)
Is it possible to put a constraint on the table so a user max can have 99 point to vote with in each portfolio?
This means that a user can have multiple rows consisting of the same user_id and portfolio_id, but different car_id and vote. The sum on votes should never exceed 99, but it can be placed among different cars.
So doing:
INSERT INTO user_vores (user_id, portfolio_id, car_id, vote) VALUES
(1, 1, 1, 20),
(1, 1, 7, 40),
(1, 1, 9, 25)
would all be allowed, but when trying to add something that exceeds 99 votes should fail, like another row:
INSERT INTO user_vores (user_id, portfolio_id, car_id, vote) VALUES
(1, 1, 21, 40)
Unfortunately no, if you tried to create such a constraint you will see this error message:
ERROR: aggregate functions are not allowed in check constraints
But the wonderfull thing about postgresql is that there is always more than one way to skin a cat. You can use a BEFORE trigger to check that the data you are trying to insert fullfills our requirements.
Row-level triggers fired BEFORE can return null to signal the trigger
manager to skip the rest of the operation for this row (i.e.,
subsequent triggers are not fired, and the INSERT/UPDATE/DELETE does
not occur for this row). If a nonnull value is returned then the
operation proceeds with that row value.
Inside your trigger you would count the number of votes
SELECT COUNT(*) into vote_count FROM user_votes WHERE user_id = NEW.user_id
Now if vote_count is 99 you return NULL and the data will not be inserted.