Options for merging two separate complex postgres functions - sql

I have two separate plpgsql functions for my project, that I would like to merge into one, or come up with an alternative solution.
The first function uses a domain address and path, to retrieve the domain_id and webpage_id, along other things (such as whether the domain uses a liking system or voting system or neither for its posts).
And the second function uses the webpage_id from the first function to retrieve appropriate posts for that page and whether to get post likes/votes/neither.
As of now, what I'm doing is to call the following function, first. What it does is that it checks for the domain_id and webpage_id based off the domain_url and slug.
If the values don't exist, it checks whether the domain is registered, and if it is, it makes an insert into the webpages table, else it returns an error.
create or replace function get_domain_details(query_domain_url text, query_slug text) returns table(id bigint, owner_id uuid page_id bigint, post_count int, is_likes boolean, is_votes boolean
)
as $$
DECLARE new_domain_id int;
new_page_id bigint;
BEGIN
RETURN query
WITH domain_row AS (
SELECT domains.id, domains.owner_id, webpages.id as page_id, webpages.post_count, domain_settings.is_likes, domain_settings.is_votes
FROM domains
JOIN domain_settings
ON domain_settings.domain_id = domains.id
RIGHT JOIN webpages
ON webpages.domain_id = domains.id AND webpages.slug = query_slug
WHERE domains.domain_address = query_domain_url),
abc as (
INSERT INTO page_visits (domain_id, page_id)
SELECT domain_row.id, domain_row.page_id FROM domain_row
)
TABLE domain_row;
IF NOT FOUND THEN
IF EXISTS (SELECT domains.id FROM domains WHERE domain_address = query_domain_url) THEN
WITH webpage_rows AS (INSERT INTO webpages (domain_id, slug)
VALUES ((SELECT domains.id FROM domains WHERE domain_address = query_domain_url), query_slug)
RETURNING *
)
SELECT webpage_rows.id, webpage_rows.domain_id
INTO new_page_id, new_domain_id
FROM webpage_rows;
RETURN query
WITH domain_row_2 AS (
SELECT domains.id, domains.owner_id, new_page_id as page_id, 0 as post_count, domain_settings.is_likes, domain_settings.is_votes
FROM domains
JOIN domain_settings
ON domain_settings.domain_id = domains.id
WHERE domains.id = new_domain_id),
abc_2 as (
INSERT INTO page_visits (domain_id, page_id)
SELECT domain_row_2.id, domain_row_2.page_id FROM domain_row_2
)
TABLE domain_row_2;
ELSE
RAISE EXCEPTION 'This is not a valid domain!';
END IF;
END IF;
RETURN;
END;
$$ language plpgsql;
And once I've received the page_id and liking/voting system info from the database, I use that to make another request to the database from my nodejs server to retrieve the posts and its details.
If is_votes is true, it joins with the votes table, else if is_likes is true, it joins with the likes table, else it doesn't join with either.
create or replace function get_page_posts (
query_page_id bigint, is_votes boolean, is_likes boolean
)
returns table (id bigint, full_name text, avatar_url text, author_id uuid, post_text text, created_at timestamptz, comment_count int, vote_count int, like_count int
)
as $$
BEGIN
IF is_votes = true THEN
RETURN query
SELECT posts.id, users.full_name, users.avatar_url, posts.author_id, posts.post_text, posts.created_at, post_data.comment_count, post_data.vote_count, post_data.like_count
FROM posts
LEFT JOIN users
ON users.user_id = posts.author_id
LEFT JOIN post_data
ON post_data.post_id = posts.id
LEFT JOIN votes v1
ON v1.post_id = posts.id
LEFT JOIN webpages
ON webpages.id = posts.page_id
WHERE webpages.id = query_page_id
AND NOT posts.status = 'deleted'
ORDER BY posts.id DESC;
ELSIF is_likes = true THEN
RETURN query
SELECT posts.id, users.full_name, users.avatar_url, posts.author_id, posts.post_text, posts.created_at, post_data.comment_count, post_data.vote_count, post_data.like_count
FROM posts
LEFT JOIN users
ON users.user_id = posts.author_id
LEFT JOIN post_data
ON post_data.post_id = posts.id
LEFT JOIN webpages
ON webpages.id = posts.page_id
LEFT JOIN likes
ON likes.post_id = posts.id
WHERE webpages.id = query_page_id
AND NOT posts.status = 'deleted'
ORDER BY posts.id DESC;
ELSE
RETURN query
SELECT posts.id, users.full_name, users.avatar_url, posts.author_id, posts.comment_text, posts.created_at, post_data.comment_count, post_data.vote_count, post_data.like_count
FROM posts
LEFT JOIN users
ON users.user_id = posts.author_id
LEFT JOIN post_data
ON post_data.post_id = posts.id
LEFT JOIN webpages
ON webpages.id = posts.page_id
WHERE posts.parent_id IS NULL
AND webpages.id = query_page_id
AND NOT posts.status = 'deleted'
ORDER BY posts.id DESC;
END IF;
END;
$$
language plpgsql
Now, the issue that I have is that this is slowing this entire loading process down, so I'm trying to merge both functions together so that I don't have to make two separate requests to the database.
I have currently combined both of them into a new function such that it looks like this.
However, I am unsure if this is a safe and viable approach.
... as $$
BEGIN
RETURN query
WITH abc AS (
SELECT * FROM get_domain_details(query_domain_url, query_slug)
),
def AS (
SELECT * FROM get_page_posts((SELECT abc.page_id FROM abc), (SELECT abc.is_votes FROM abc), (SELECT abc.is_likes FROM abc))
)
SELECT * FROM abc UNION ALL SELECT * FROM def;
END;
$$ language plpgsql;

Related

Select query, insert into table, and return initial query in postgres

I have a rather complex plpgsql stored procedure and I need to select from multiple tables and insert as well.
This is part of what I currently have.
BEGIN
RETURN query
SELECT domains.id, webpages.id as page_id ...
FROM domains
LEFT JOIN domain_settings
ON domain_settings.domain_id = domains.id
RIGHT JOIN webpages
ON webpages.domain_id = domains.id
LEFT JOIN subscriptions
ON webpages.id = subscriptions.page_id
AND subscriptions.user_id = query_user_id
AND subscriptions.comment_id IS NULL
WHERE domains.domain_address = query_domain_url
IF NOT FOUND THEN ...
END;
$$ language plpgsql;
Now, I would like add an insert query into another table using certain values from the return query before the 'if not found then' statement:
INSERT INTO page_visits (domain_id, page_id)
SELECT id, page_id FROM ?? (return query statement)
And after the insert, I want to return the initial return query values. How do I go about doing this? I tried using WITH AS statements, but I can't seem to get it to work
A set-returning PL/pgSQL function builds the return stack while processing the function body. There is no way to access that return stack from within the same function. You could nest the function. Or use a temporary table.
But using a CTE is probably the simplest way for the cas at hand. Going out on a limb, you may be looking for something like this:
CREATE OR REPLACE FUNCTION demo(query_user_id int, query_domain_url text)
RETURNS TABLE (c1 int, c2 int)
LANGUAGE plpgsql AS
$func$
BEGIN
RETURN QUERY
WITH sel AS (
SELECT d.id, w.id as page_id ...
FROM webpages w
JOIN domains d ON d.id = w.domain_id
LEFT JOIN domain_settings ds ON ds.domain_id = d.id
LEFT JOIN subscriptions s ON s.page_id = w.id
AND s.user_id = query_user_id -- origin?
AND s.comment_id IS NULL
WHERE d.domain_address = query_domain_url -- origin?
)
, ins AS (
INSERT INTO tbl (col1, col2)
SELECT main.id, sel.page_id
FROM (SELECT 'foo') AS main(id)
LEFT JOIN sel USING (id) -- LEFT JOIN ?
)
TABLE sel;
IF NOT FOUND THEN
-- do something
END IF;
END
$func$;
Remember, if the transaction does not commit successfully, the INSERT is also rolled back.
The final TABLE sel is just short syntax for SELECT * FROM sel. See:
Is there a shortcut for SELECT * FROM?

with, works in the first query, but not in the second

i have this FUNCTION, that check if there are results in the first consult, table_one
if not are results, check in the second_table
separate each query works, but if join it, just work the first sentence but not the second one
CREATE OR REPLACE FUNCTION get_data(id INT)
RETURNS TABLE(
id INT,
created_at TIMESTAMP,
attempts INT,
status VARCHAR
)
language plpgsql
AS
$$
DECLARE
_SENT VARCHAR := 'SENT';
BEGIN
RETURN QUERY
WITH r AS (
SELECT p_i.id, a_r.created_at, a_r.attempts,
CASE a_r.status
WHEN 'PENDING' THEN _SENT
END AS status
FROM table_one p_i
LEFT JOIN (
SELECT a_r.table_one_id, max(a_r.id) id
FROM awa_req a_r
GROUP BY a_r.table_one_id
) last_md on last_md.table_one_id = p_i.id
LEFT JOIN awa_req a_r on a_r.table_one_id = last_md.table_one_id and a_r.id = last_md.id
WHERE p_i.user_id = $1
AND p_i.deleted_at IS NULL
)
SELECT * FROM r
UNION ALL
SELECT p_i.id, m_d.created_at, m_d.attempts,
CASE
WHEN m_d.confirmed_at IS NULL THEN _SENT
END AS status
FROM pay_ins p_i
LEFT JOIN (
SELECT max(t.id) AS id, t.pay_ins_id
FROM table_two t
GROUP BY t.pay_ins_id
) last_md on last_md.pay_ins_id = p_i.id
LEFT JOIN table_two m_d on m_d.pay_ins_id = last_md.pay_ins_id and m_d.id = last_md.id
AND NOT EXISTS (
SELECT * FROM r
);
END;
$$;
best
This part will eliminate all rows from the UNION clause if any rows exist in r:
AND NOT EXISTS (
SELECT * FROM r
);
It should instead be something like:
AND NOT EXISTS (
SELECT FROM r WHERE r.id = p_i.id
)

PostgreSQL array of type table/setof

I need to code a function in postgresql that must 'pick' entire rows from a table depending on a condition and then insert them into an array, and finally return that array.
I'm actually querying the rows I need but there's an special column from the table that I must evaluate first to see if I can retrieve it or not (insert to the array), depending on the type of user is doing the query (customer or worker).
For example:
The special column is named "watchers" and has two variants: 'my_team' and 'only_me' and also has another column named "user_id".
If the column says 'my_team', the row can be inserted into the array with no problems, but if it says 'only_me' I must compare the id of the user calling the function and the user registered in that row, if they match I can insert it into the array, otherwise I can't.
Thing is that as I've read so far it seems that it can't be done with arrays, so I'd like to know if there's another way of doing this, maybe with an extra table or something?
Thanks for your help!
This is the code I have:
CREATE OR REPLACE FUNCTION public.calculates_permission_by_task(task_id integer)
RETURNS SETOF permission
LANGUAGE plpgsql
STABLE
AS $function$
DECLARE
num int := 5;
record permission;
ret_permissions permission [];
BEGIN
FOR record IN (select
p.id, p.slug
from task as t
join service_request_form as srf on t.service_request_form_id = srf.id
join service_group as sg on srf.service_group_id = sg.id
join worker_group as wg on wg.service_group_id = sg.id
join worker_group_member as wgm on wg.id = wgm.worker_group_id
join role as r on r.id = wgm.role_id
join permission_role as pr on r.id = pr.role_id
join permission as p on p.id = pr.permission_id
where t.id = task_id
and wgm.user_id = 'LFudaU6jzid4SKFlU8MgFAwezyP2'
and pr.value <> 'off'
and pr.value <> 'no')
LOOP
/* Here is where I pretend to do the comparison and insert data into the array */
ret_permission := array_append(ret_permissions, record.id);
END LOOP;
RETURN ret_permissions;
END
$function$
You could implement the logic in the query itself.
Assuming that the user_id column you are refering to corresponds to wgm.user_id, that is already in the query, the where clause of the query would become:
where
t.id = task_id
and pr.value not in ('off', 'no')
and (
?.watchers = 'my_team'
or (
?.watchers = 'only_me'
or wgm.user_id = 'LFudaU6jzid4SKFlU8MgFAwezyP2'
)
)
You don't tell which table watches belong to, so I used ?, which you need to replace that with the relevant table alias.
If there are just two possible values for watchers, then we can simplify the predicates:
where
t.id = task_id
and pr.value not in ('off', 'no')
and (?.watchers = 'my_team' or wgm.user_id = 'LFudaU6jzid4SKFlU8MgFAwezyP2')
You don't need an array
you don't need a cursor loop
You don't even need plpgsql
plain SQL wiill suffice:
CREATE TABLE permission (
id integer
, slug text
);
INSERT INTO permission(id,slug) VALUES (1,'WTF' );
CREATE TABLE task (
id integer
, service_request_form_id integer
);
INSERT INTO task(id, service_request_form_id) VALUES (1,1 );
CREATE TABLE service_request_form (
id integer
, service_group_id integer
);
INSERT INTO service_request_form(id, service_group_id) VALUES (1,1 );
CREATE TABLE service_group (
id integer
);
INSERT INTO service_group(id) VALUES (1);
CREATE TABLE worker_group (
id integer
, service_group_id integer
);
INSERT INTO worker_group(id, service_group_id) VALUES (1,1 );
CREATE TABLE worker_group_member (
worker_group_id integer
, role_id integer
, user_id text
);
INSERT INTO worker_group_member(worker_group_id, role_id,user_id) VALUES (1,1,'LFudaU6jzid4SKFlU8MgFAwezyP2' );
CREATE TABLE zrole (
id integer
);
INSERT INTO zrole(id) VALUES (1 );
CREATE TABLE permission_role (
role_id integer
, permission_id integer
, value text
);
INSERT INTO permission_role(role_id,permission_id,value) VALUES (1,1,'Yes' );
CREATE OR REPLACE FUNCTION public.calculates_permission_by_task(task_id integer)
RETURNS SETOF permission
LANGUAGE sql STABLE SECURITY INVOKER ROWS 30 -- COST 1
AS $func$
SELECT p.id, p.slug
FROM permission as p
WHERE EXISTS (
SELECT *
FROM task as t
JOIN service_request_form as srf
on t.service_request_form_id = srf.id
JOIN service_group as sg
on srf.service_group_id = sg.id
JOIN worker_group as wg
on wg.service_group_id = sg.id
JOIN worker_group_member as wgm
on wg.id = wgm.worker_group_id
JOIN zrole as zr
on zr.id = wgm.role_id
JOIN permission_role as pr
on zr.id = pr.role_id
WHERE p.id = pr.permission_id
AND t.id = task_id
and wgm.user_id = 'LFudaU6jzid4SKFlU8MgFAwezyP2'
and pr.value NOT IN( 'off' , 'no' )
-- -------------------------
-- Add the needed logic
-- (From #GMB) here
-- -------------------------
);
$func$
;
-- Usage:
-- --------------
SELECT * -- p.id, p.slug
FROM public.calculates_permission_by_task(42);

Building dymanic query with many laterail joins

I am building a dynamic query based on user-submitted conditions on user-specified fields.
I have a two tables
Visitors(id, name, email...)
and a
Trackings(id, visitor_id, field, string_value, integer_value, boolean_value, date_value)
The conditions comes in the form of an array of SQL fragments built earlier. Default filters for attributes that are hardcoded on the visitors table, and custom filters, for the values that are user-submitted and stored in an EAV schema (trackings)
Example:
Default:
{"name ILIKE 'Jack'", "(last_seen < (current_date - (7 || ' days')::interval))"}
Custom:
{"field = 'number_of_orders' > 10", "is_pro_user = true"}
A visitor can have many trackings, each one recording some custom, user-submitted data field for that visitor. But each visitor also has some default data that lies on the table itself, such as email, name or last_seen etc.
Now, users should be to ask queries such as:
Give me all the visitors for which no custom field named number_of_orders has been recored (is unknown)
Give me all visitors with the default attribute name set to Jack and for who the custom attribute total_purchase_value is greater than 1000
My attempt at solving it was using a stored procedure that dynamically concatenated a series of conditions using AND (for default data on the visitor table), and OR statements (for the custom data in the trackings table) inside a WHERE-caluse
CREATE OR REPLACE FUNCTION find_matching_visitors(app_id text, default_filters text[], custom_filters text[])
RETURNS TABLE (
id varchar
) AS
$body$
DECLARE
default_filterstring text;
custom_filterstring text;
default_filter_length integer;
custom_filter_length integer;
sql VARCHAR;
BEGIN
default_filter_length := COALESCE(array_length(default_filters, 1), 0);
custom_filter_length := COALESCE(array_length(custom_filters, 1), 0);
default_filterstring := array_to_string(default_filters, ' AND ');
custom_filterstring := array_to_string(custom_filters, ' OR ');
IF custom_filterstring = '' or custom_filterstring is null THEN
custom_filterstring := '1=1';
END IF;
IF default_filterstring = '' or default_filterstring is null THEN
default_filterstring := '1=1';
END IF;
sql := format('
SELECT v.id FROM visitors v
LEFT JOIN trackings t on v.id = t.visitor_id
WHERE v.app_id = app_id and (%s) and (%s)
group by v.id
having case when %s > 0 then count(v.id) = %s else true end
', custom_filterstring, default_filterstring, custom_filter_length, custom_filter_length);
RETURN QUERY EXECUTE sql;
END;
$body$
LANGUAGE 'plpgsql';
This works great, but AFAIK there is not way to express the unknown filter for a custom attribute. In that it would require using a left outer join, or a not exists subquery with access to the outer scope.
I am now looking at alternative ways to accomplish the same as above, but also support this kind of query. I am thinking something like the below, using a series of lateral joins for each condition, but this seems like it won't perform very well as soon as there is more than 1-2 conditions/joins.
select v.id, v.name from visitors v
inner join lateral ( <-- custom fields
select * from trackings t
where field = 'admin'
) as t1 on t1.visitor_id = v.id
inner join lateral (
select * from trackings t
where field = 'users_created'
) as t2 on t2.visitor_id = v.id
inner join lateral (
select * from trackings t
where field = 'teams_created' and integer_value > 0
) as t3 on t3.visitor_id = v.id
where v.app_id = 'ASnYW1-RgCl0I' and (v.type = 'lead' or v.type = 'user')
and name ILIKE 'mads' and email is not null // <-- default fields
Any suggestions?

How can I perform the Count function with a where clause?

I have my database setup to allow a user to "Like" or "Dislike" a post. If it is liked, the column isliked = true, false otherwise (null if nothing.)
The problem is, I am trying to create a view that shows all Posts, and also shows a column with how many 'likes' and 'dislikes' each post has. Here is my SQL; I'm not sure where to go from here. It's been a while since I've worked with SQL and everything I've tried so far has not given me what I want.
Perhaps my DB isn't setup properly for this. Here is the SQL:
Select trippin.AccountData.username, trippin.PostData.posttext,
trippin.CategoryData.categoryname, Count(trippin.LikesDislikesData.liked)
as TimesLiked from trippin.PostData
inner join trippin.AccountData on trippin.PostData.accountid = trippin.AccountData.id
inner join trippin.CategoryData on trippin.CategoryData.id = trippin.PostData.categoryid
full outer join trippin.LikesDislikesData on trippin.LikesDislikesData.postid =
trippin.PostData.id
full outer join trippin.LikesDislikesData likes2 on trippin.LikesDislikesData.accountid =
trippin.AccountData.id
Group By (trippin.AccountData.username), (trippin.PostData.posttext), (trippin.categorydata.categoryname);
Here's my table setup (I've only included relevant columns):
LikesDislikesData
isliked(bit) || accountid(string) || postid(string
PostData
id(string) || posttext || accountid(string)
AccountData
id(string) || username(string)
CategoryData
categoryname(string)
Problem 1: FULL OUTER JOIN versus LEFT OUTER JOIN. Full outer joins are seldom what you want, it means you want all data specified on the "left" and all data specified on the "right", that are matched and unmatched. What you want is all the PostData on the "left" and any matching Likes data on the "right". If some right hand side rows don't match something on the left, then you don't care about it. Almost always work from left to right and join results that are relevant.
Problem 2: table alias. Where ever you alias a table name - such as Likes2 - then every instance of that table within the query needs to use that alias. Straight after you declare the alias Likes2, your join condition refers back to trippin.LikesDislikesData, which is the first instance of the table. Given the second one in joining on a different field I suspect that the postid and accountid are being matched on the same row, therefore it should be AND together, not a separate table instance. EDIT reading your schema closer, it seems this wouldn't be needed at all.
Problem 3: to solve you Counts problem separate them using CASE statements. Count will add the number of non NULL values returned for each CASE. If the likes.liked = 1, then return 1 otherwise return NULL. The NULL will be returned if the columns contains a 0 or a NULL.
SELECT trippin.PostData.Id, trippin.AccountData.username, trippin.PostData.posttext,
trippin.CategoryData.categoryname,
SUM(CASE WHEN likes.liked = 1 THEN 1 ELSE 0 END) as TimesLiked,
SUM(CASE WHEN likes.liked = 0 THEN 1 ELSE 0 END) as TimesDisLiked
FROM trippin.PostData
INNER JOIN trippin.AccountData ON trippin.PostData.accountid = trippin.AccountData.id
INNER JOIN trippin.CategoryData ON trippin.CategoryData.id = trippin.PostData.categoryid
LEFT OUTER JOIN trippin.LikesDislikesData likes ON likes.postid = trippin.PostData.id
-- remove AND likes.accountid = trippin.AccountData.id
GROUP BY trippin.PostData.Id, (trippin.AccountData.username), (trippin.PostData.posttext), (trippin.categorydata.categoryname);
Then "hide" the PostId column in the User Interface.
Instead of selecting Count(trippin.LikesDislikesData.liked) you could put in a select statement:
Select AccountData.username, PostData.posttext, CategoryData.categoryname,
(select Count(*)
from LikesDislikesData as likes2
where likes2.postid = postdata.id
and likes2.liked = 'like' ) as TimesLiked
from PostData
inner join AccountData on PostData.accountid = AccountData.id
inner join CategoryData on CategoryData.id = PostData.categoryid
USE AdventureWorksDW2008R2
GO
SET NOCOUNT ON
GO
/*
Default
*/
SET TRANSACTION ISOLATION LEVEL READ COMMITTED
GO
BEGIN TRAN
IF OBJECT_ID('tempdb.dbo.#LikesDislikesData') IS NOT NULL
BEGIN
DROP TABLE #LikesDislikesData
END
CREATE TABLE #LikesDislikesData(
isLiked bit
,accountid VARCHAR(50)
,postid VARCHAR(50)
);
IF OBJECT_ID('tempdb.dbo.#PostData') IS NOT NULL
BEGIN
DROP TABLE #PostData
END
CREATE TABLE #PostData(
postid INT IDENTITY(1,1) NOT NULL
,accountid VARCHAR(50)
,posttext VARCHAR(50)
);
IF OBJECT_ID('tempdb.dbo.#AccountData') IS NOT NULL
BEGIN
DROP TABLE #AccountData
END
CREATE TABLE #AccountData(
accountid INT
,username VARCHAR(50)
);
IF OBJECT_ID('tempdb.dbo.#CategoryData') IS NOT NULL
BEGIN
DROP TABLE #CategoryData
END
CREATE TABLE #CategoryData(
categoryname VARCHAR(50)
);
INSERT INTO #AccountData VALUES ('1', 'user1')
INSERT INTO #PostData VALUES('1','this is a post')
INSERT INTO #LikesDislikesData (isLiked ,accountid, postid)
SELECT '1', P.accountid, P.postid
FROM #PostData P
WHERE P.posttext = 'this is a post'
SELECT *
FROM #PostData
SELECT *
FROM #LikesDislikesData
SELECT *
FROM #AccountData
SELECT COUNT(L.isLiked) 'Likes'
,P.posttext
,A.username
FROM #PostData P
JOIN #LikesDislikesData L
ON P.accountid = L.accountid
AND L.IsLiked = 1
JOIN #AccountData A
ON P.accountid = A.accountid
GROUP BY P.posttext, A.username
SELECT X.likes, Y.dislikes
FROM (
(SELECT COUNT(isliked)as 'likes', accountid
FROM #LikesDislikesData
WHERE isLiked = 1
GROUP BY accountid
) X
JOIN
(SELECT COUNT(isliked)as 'dislikes', accountid
FROM #LikesDislikesData
WHERE isLiked = 0
GROUP BY accountid) Y
ON x.accountid = y.accountid)
IF (XACT_STATE() = 1 AND ERROR_STATE() = 0)
BEGIN
COMMIT TRAN
END
ELSE IF (##TRANCOUNT > 0)
BEGIN
ROLLBACK TRAN
END
How do you think about the solution? We create a new table SummaryReport(PostID,AccountID,NumberOfLikedTime,NumberOfDislikedTimes).
An user clicks on LIKE or DISLIKE button we update the table. After that, you can query as you desire. Another advantage, the table can be served reporting purpose.