Postgres insert trigger fills id - postgresql

I have a BEFORE trigger which should fill record's root ID which, of course, would point to rootmost entry. I.e:
id | parent_id | root_id
-------------------------
a | null | a
a.1 | a | a
a.1.1 | a.1 | a
b | null | b
If entry's parent_id is null, it would point to record itself.
Question is - inside BEFORE INSERT trigger, if parent_id is null, can I or should I fetch next sequence value, fill id and root_id in order to avoid filling root_id in AFTER trigger?

According to your own definition:
if entry's parent_id is null, it would point to record itself
then you have to do:
if new.parent_id is null then
new.root_id = new.id ;
else
WITH RECURSIVE p (parent_id, level) AS
(
-- Base case
SELECT
parent_id, 0 as level
FROM
t
WHERE
t.id = new.id
UNION ALL
SELECT
t.parent_id, level + 1
FROM
t JOIN p ON t.id = p.parent_id
WHERE
t.parent_id IS NOT NULL
)
SELECT
parent_id
INTO
new.root_id
FROM
p
ORDER BY
level DESC
LIMIT
1 ;
end if ;
RETURN new ;

Related

Aggregate result of sub query to comma separated values

How to get get the result of subquery as comma separated values.
I have three tables , location and stock_location_type and location_label.
I am joining location and stock_location_type and based on the result of SLT.inventory_location_cd , I am querying another table location_label.
To do that I am writing following query.
select L.stock_catalogue_id, SLT.inventory_location_cd,
case
when nventory_location_cd = 'base location' then (select related_location_id from location_label where base_location_id = location_id)
when nventory_location_cd != 'base location' then (select base_location_id from location_label where related_location_id = location_id)
end as "Current Location",
* from location L
join stock_location_type SLT on L.stock_location_type_id = SLT.stock_location_type_id;
These subquueries returns multiple rows.
I tried using string_agg and casting related_location_id and base_location_id (as they are UUIDs). But then it complains about group by.
If I use group by then it errors out , 'multiple rows returned by subquery'.
What am I missing?
I recreated your set of tables with
A location_label table
create table location_label(location_id int, location_label varchar);
insert into location_label values(1, 'Home');
insert into location_label values(2, 'Office');
insert into location_label values(3, 'Garage');
insert into location_label values(4, 'Bar');
A stock_location_type table
create table stock_location_type (stock_location_type_id int, inventory_location_cd varchar);
insert into stock_location_type values(1, 'base location');
insert into stock_location_type values(2, 'Not base location');
A location table
create table location (stock_catalogue_id int, base_location_id int, related_location_id int, stock_location_type_id int);
insert into location values(1,1,2,1);
insert into location values(1,2,1,2);
insert into location values(1,3,3,1);
insert into location values(2,4,3,1);
insert into location values(2,3,1,1);
insert into location values(2,2,4,2);
If I understand your statement correctly you are trying to join location and location_label tables based on the inventory_location_cd column using either base_location_id or location_id.
If this is what you're trying to achieve, the following query should do it. By moving the join condition in the proper place
select L.stock_catalogue_id,
SLT.inventory_location_cd,
location_id "Current Location Id",
location_label "Current Location Name"
from location L join stock_location_type SLT
on L.stock_location_type_id = SLT.stock_location_type_id
left outer join location_label
on (
case when
inventory_location_cd = 'base location'
then base_location_id
else related_location_id
end) = location_id
;
result is
stock_catalogue_id | inventory_location_cd | Current Location Id | Current Location Name
--------------------+-----------------------+---------------------+-----------------------
1 | base location | 1 | Home
1 | Not base location | 1 | Home
1 | base location | 3 | Garage
2 | base location | 3 | Garage
2 | base location | 4 | Bar
2 | Not base location | 4 | Bar
(6 rows)
if you need to aggregate it up by stock_catalogue_id and inventory_location_cd, that can be achieved with
select L.stock_catalogue_id,
SLT.inventory_location_cd,
string_agg(location_id::text, ',') "Current Location Id",
string_agg(location_label::text, ',') "Current Location Name"
from location L join stock_location_type SLT
on L.stock_location_type_id = SLT.stock_location_type_id
left outer join location_label
on (case when inventory_location_cd = 'base location' then base_location_id else related_location_id end) = location_id
group by L.stock_catalogue_id,
SLT.inventory_location_cd;
with the result being
stock_catalogue_id | inventory_location_cd | Current Location Id | Current Location Name
--------------------+-----------------------+---------------------+-----------------------
1 | base location | 1,3 | Home,Garage
1 | Not base location | 1 | Home
2 | base location | 3,4 | Garage,Bar
2 | Not base location | 4 | Bar
(4 rows)
You can use the string_agg function to aggregate the values in a comma-separated string. So your sub-queries needs to be rewritten to
select string_agg(related_location_id, ', ') from location_label where base_location_id = location_id
and
select string_agg(base_location_id, ', ') from location_label where related_location_id = location_id

PostgreSQL recursive parent/child query

I'm having some trouble working out the PostgreSQL documentation for recursive queries, and wonder if anyone might be able to offer a suggestion for the following.
Here's the data:
Table "public.subjects"
Column | Type | Collation | Nullable | Default
-------------------+-----------------------------+-----------+----------+--------------------------------------
id | bigint | | not null | nextval('subjects_id_seq'::regclass)
name | character varying | | |
Table "public.subject_associations"
Column | Type | Collation | Nullable | Default
------------+-----------------------------+-----------+----------+--------------------------------------------------
id | bigint | | not null | nextval('subject_associations_id_seq'::regclass)
parent_id | integer | | |
child_id | integer | | |
Here, a "subject" may have many parents and many children. Of course, at the top level a subject has no parents and at the bottom no children. For example:
parent_id | child_id
------------+------------
2 | 3
1 | 4
1 | 3
4 | 8
4 | 5
5 | 6
6 | 7
What I'm looking for is starting with a child_id to get all the ancestors, and with a parent_id, all the descendants. Therefore:
parent_id 1 -> children 3, 4, 5, 6, 7, 8
parent_id 2 -> children 3
child_id 3 -> parents 1, 2
child_id 4 -> parents 1
child_id 7 -> parents 6, 5, 4, 1
Though there seem to be a lot of examples of similar things about I'm having trouble making sense of them, so any suggestions I can try out would be welcome.
To get all children for subject 1, you can use
WITH RECURSIVE c AS (
SELECT 1 AS id
UNION ALL
SELECT sa.child_id
FROM subject_associations AS sa
JOIN c ON c.id = sa. parent_id
)
SELECT id FROM c;
CREATE OR REPLACE FUNCTION func_finddescendants(start_id integer)
RETURNS SETOF subject_associations
AS $$
DECLARE
BEGIN
RETURN QUERY
WITH RECURSIVE t
AS
(
SELECT *
FROM subject_associations sa
WHERE sa.id = start_id
UNION ALL
SELECT next.*
FROM t prev
JOIN subject_associations next ON (next.parentid = prev.id)
)
SELECT * FROM t;
END;
$$ LANGUAGE PLPGSQL;
Try this
--- Table
-- DROP SEQUENCE public.data_id_seq;
CREATE SEQUENCE "data_id_seq"
INCREMENT 1
MINVALUE 1
MAXVALUE 9223372036854775807
START 1
CACHE 1;
ALTER TABLE public.data_id_seq
OWNER TO postgres;
CREATE TABLE public.data
(
id integer NOT NULL DEFAULT nextval('data_id_seq'::regclass),
name character varying(50) NOT NULL,
label character varying(50) NOT NULL,
parent_id integer NOT NULL,
CONSTRAINT data_pkey PRIMARY KEY (id),
CONSTRAINT data_name_parent_id_unique UNIQUE (name, parent_id)
)
WITH (
OIDS=FALSE
);
INSERT INTO public.data(id, name, label, parent_id) VALUES (1,'animal','Animal',0);
INSERT INTO public.data(id, name, label, parent_id) VALUES (5,'birds','Birds',1);
INSERT INTO public.data(id, name, label, parent_id) VALUES (6,'fish','Fish',1);
INSERT INTO public.data(id, name, label, parent_id) VALUES (7,'parrot','Parrot',5);
INSERT INTO public.data(id, name, label, parent_id) VALUES (8,'barb','Barb',6);
--- Function
CREATE OR REPLACE FUNCTION public.get_all_children_of_parent(use_parent integer) RETURNS integer[] AS
$BODY$
DECLARE
process_parents INT4[] := ARRAY[ use_parent ];
children INT4[] := '{}';
new_children INT4[];
BEGIN
WHILE ( array_upper( process_parents, 1 ) IS NOT NULL ) LOOP
new_children := ARRAY( SELECT id FROM data WHERE parent_id = ANY( process_parents ) AND id <> ALL( children ) );
children := children || new_children;
process_parents := new_children;
END LOOP;
RETURN children;
END;
$BODY$
LANGUAGE plpgsql VOLATILE COST 100;
ALTER FUNCTION public.get_all_children_of_parent(integer) OWNER TO postgres
--- Test
SELECT * FROM data WHERE id = any(get_all_children_of_parent(1))
SELECT * FROM data WHERE id = any(get_all_children_of_parent(5))
SELECT * FROM data WHERE id = any(get_all_children_of_parent(6))

Select row position in filtered and ordered row list PostgreSQL

I got this query,
SELECT s.pos
FROM (SELECT t.guild_id, t.user_id
ROW_NUMBER() OVER(ORDER BY t.reputation DESC) AS pos
FROM users t) s
WHERE (s.guild_id, s.user_id) = ($2, $3)
that gets a user's "rank" in a guild, but I want to filter the results by entries that are in an array of t.user_id values (like {'1', '64', '83'}) and have this affect the resulting pos value. I found FILTER and WITHIN GROUP, but I'm not sure how to fit one of those into this query. How would I do that?
Here's the full table if that helps at all:
Table "public.users"
Column | Type | Collation | Nullable | Default
------------+-----------------------+-----------+----------+---------
guild_id | character varying(20) | | not null |
user_id | character varying(20) | | not null |
reputation | real | | not null | 0
Indexes:
"users_pkey" PRIMARY KEY, btree (guild_id, user_id)
Why not select on those first?
WITH UsersWeCareAbout AS (
SELECT * FROM users u WHERE u.user_id = ANY(subgroup_array)
), RepUsers AS (
SELECT t.guild_id, t.user_id, ROW_NUMBER() OVER(ORDER BY t.reputation DESC) AS pos
FROM UsersWeCareAbout t
) SELECT s.pos FROM RepUsers s WHERE (s.guild_id, s.user_id) = ($2, $3)
(untested if only because I didn't really have enough context to test with)

PostgreSQL: duplicate key value violates unique constraint on UPDATE command

When doing an UPDATE query, we got the following error message:
ERROR: duplicate key value violates unique constraint "tableA_pkey"
DETAIL: Key (id)=(47470) already exists.
However, our UPDATE query does not affect the primary key. Here is a simplified version:
UPDATE tableA AS a
SET
items = (
SELECT array_to_string(
array(
SELECT b.value
FROM tableB b
WHERE b.a_id = b.id
GROUP BY b.name
),
','
)
)
WHERE
a.end_at BETWEEN now() AND now() - interval '1 day';
We ensured the primary key sequence was already synced:
\d tableA_id_seq
Which produces:
Column | Type | Value
---------------+---------+--------------------------
sequence_name | name | tableA_id_seq
last_value | bigint | 50364
start_value | bigint | 1
increment_by | bigint | 1
max_value | bigint | 9223372036854775807
min_value | bigint | 1
cache_value | bigint | 1
log_cnt | bigint | 0
is_cycled | boolean | f
is_called | boolean | t
Looking for maximum table index:
select max(id) from tableA;
We got a lower value:
max
-------
50363
(1 row)
Have you any idea on why such a behavior? If we exclude the problematic id, it works.
Another strange point is that replacing the previous UPDATE by:
UPDATE tableA AS a
SET
items = (
SELECT array_to_string(
array(
SELECT b.value
FROM tableB b
WHERE b.a_id = b.id
GROUP BY b.name
),
','
)
)
WHERE a.id = 47470;
It works well. Are we missing something?
EDIT: triggers
I have no user-defined triggers on this table:
SELECT t.tgname, c.relname
FROM pg_trigger t
JOIN pg_class c ON t.tgrelid = c.oid
WHERE
c.relname = 'tableA'
AND
t.tgisinternal = false
;
Which returns no row.
Note: I am using psql (PostgreSQL) 9.3.4 version.
Not really sure what was the cause. However, deleting the two (non vital) records corresponding to already existing ids (?) solved the issue.

CTE Hierachy descending but picking up child nodes not parents from ancestor

Explanation
OK, the title might be a bit much :)
I'll paste the scripts at the end.
Imagine the following n-ary tree
.
|
---1 **(25)**
|
-----1.1 **(13)**
| |
| ----1.1.1 (1)
| |
| ----1.1.2 **(7)**
| | |
| | ----1.1.2.1 (4)
| | |
| | ----1.1.2.2 (3)
| |
| ----1.1.3 (5)
|
-----1.2 (2)
|
|
-----1.3 (10)
And so on, where the root branch "." can also have a 2,3,n branch and that branch would also have its own arbitrary tree form with n-branches possible from any give node. The values in brackets at the end of each node are the values at the node so to speak. Think of them as accounts with sub-accounts with the parent accounting being the sum of the child-accounts.
What I'm trying to do with CTE is to retrieve all the [sub] accounts directly beneath a parent. So for providing 1.1 as the search point, it'll retrieve that whole branch of the tree. But, if I try to be smart and sum the returned values, I will be adding (for this specific example) 1.1.2 twice, once through the summation of its sub accounts, the second by the summation of the value it itself contains.
How would I go about something like this?
Thanks a zillion :)
Here are the scripts:
Scripts
Table
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[Account](
[ID] [nvarchar](50) NOT NULL,
[ParentID] [nvarchar](50) NULL,
[Value] [float] NOT NULL,
[HasChild] [bit] NOT NULL,
CONSTRAINT [PK_Account] PRIMARY KEY CLUSTERED
(
[ID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
ALTER TABLE [dbo].[Account] WITH CHECK ADD CONSTRAINT [FK_Account_Account] FOREIGN KEY([ParentID])
REFERENCES [dbo].[Account] ([ID])
GO
ALTER TABLE [dbo].[Account] CHECK CONSTRAINT [FK_Account_Account]
GO
ALTER TABLE [dbo].[Account] ADD CONSTRAINT [DF_Account_HasChild] DEFAULT ((0)) FOR [HasChild]
GO
CTE Script
WITH
DescendToChild([ID],ParentID,Value)
AS
(
--base case
SELECT [ID],ParentID,Value FROM Account
Where ParentID = '1.1'
UNION ALL
----recursive step
SELECT
A.[ID],A.ParentID,A.Value FROM Account as A
INNER JOIN DescendToChild D on A.ParentID = D.ID
)
select * from DescendToChild;
Here's a solution based on your sample data. It works by only summing up those nodes with no children:
DECLARE #tree TABLE
(id INT
,parentid INT
,nodeName VARCHAR(10)
,VALUE INT
)
INSERT #tree (id,parentid,nodeName,VALUE)
VALUES
(1,NULL,'.',NULL),
(2,1,'1',25),
(3,2,'1.1',13),
(4,2,'1.2',2),
(5,2,'1.3',10),
(6,3,'1.1.1',1),
(7,3,'1.1.2',7),
(8,3,'1.1.3',5),
(9,7,'1.1.2.1',4),
(10,7,'1.1.2.2',3)
;WITH recCTE
AS
(
SELECT id, parentid, nodeName, value,
CASE WHEN EXISTS (SELECT 1 FROM #tree AS t1 WHERE t1.parentid = t.id) THEN 1 ELSE 0 END AS hasChildren
FROM #tree AS t
WHERE nodeName = '1.1'
UNION ALL
SELECT t.id, t.parentid, t.nodeName, t.value,
CASE WHEN EXISTS (SELECT 1 FROM #tree AS t1 WHERE t1.parentid = t.id) THEN 1 ELSE 0 END AS hasChildren
FROM #tree AS t
JOIN recCTE AS r
ON r.id = t.parentid
)
SELECT SUM(VALUE)
FROM recCTE
WHERE hasChildren = 0
OPTION (MAXRECURSION 0)
http://social.msdn.microsoft.com/Forums/en-US/transactsql/thread/959fe835-e43d-4995-882c-910f3aa0ff68/