Postgres: inserting dynamic number of columns and values in table - postgresql

I am very new to Postgres SQL. My requirement is to pass a dynamic number of columnId, columnValue pair and insert this combination in a table(Example: employeeId, employeeName combination). The length of list can be anything. I am thinking of building dynamic query at the code-side and pass it as string to function and execute the statement. Is there any better approach for this problem. Any example or idea will be much appreciated.

If you are allowed to pass that information as a structured JSON value, this gets quite easy. Postgres has a feature to map a JSON value to a table type using the function json_populate_record
Sample table:
create table some_table
(
id integer primary key,
some_name text,
some_date date,
some_number integer
);
The insert function:
create function do_insert(p_data text)
returns void
as
$$
insert into some_table (id, some_name, some_date, some_number)
select (json_populate_record(null::some_table, p_data::json)).*;
$$
language sql;
Then you can use:
select do_insert('{"id": 42, "some_name": "Arthur"}');
select do_insert('{"id": 1, "some_value": 42}');
Note that columns that are not part of the passed JSON string are explicitly set to NULL using this approach.
If the passed string contains column names that do not exist, they are simply ignored, so
select do_insert('{"id": 12, "some_name": "Arthur", "not_there": 123}');
will ignore the not_there "column".
Online example: https://rextester.com/JNIBL25827
Edit
A similar approach can be used for updating:
create function do_update(p_data text)
returns void
as
$$
update some_table
set id = t.id,
some_name = t.some_name,
some_date = t.some_date,
some_number = t.some_number
from json_populate_record(null::some_table, p_data::json) as t;
$$
language sql;
or using insert on conflict to cover both use cases with one function:
create function do_upsert(p_data text)
returns void
as
$$
insert into some_table (id, some_name, some_date, some_number)
select (json_populate_record(null::some_table, p_data::json)).*
on conflict (id) do update
set id = excluded.id,
some_name = excluded.some_name,
some_date = excluded.some_date,
some_number = excluded.some_number
$$
language sql;

Related

Function using '= ALL ($1)' returning no values when multiple params passed

CREATE FUNCTION test(VARIADIC arr text[])
RETURNS TABLE (data_time TIMESTAMPTZ, id text, data jsonb)
AS 'SELECT data_timestamp, key, value
FROM hit_count
CROSS JOIN jsonb_each(data)
WHERE key = ALL ($1)'
LANGUAGE SQL;
If I call the function above with select test('123') it works fine. If I call it with select test('123','234') it returns nothing ie
test
------
(0 rows)
However if I define it as
CREATE FUNCTION test(VARIADIC arr text[])
RETURNS TABLE (data_time TIMESTAMPTZ, id text, data jsonb)
AS 'SELECT data_timestamp, key, value
FROM hit_count
CROSS JOIN jsonb_each(data)
WHERE key != ALL ($1)'
LANGUAGE SQL;
then the function returns all the data but those that fit the condition
Any ideas??
ALL was used as opposed to ANY the corrected function looks like this
CREATE FUNCTION test(VARIADIC arr text[])
RETURNS TABLE (data_time TIMESTAMPTZ, id text, data jsonb)
AS 'SELECT data_timestamp, key, value
FROM hit_count
CROSS JOIN jsonb_each(data)
WHERE key = ANY ($1)'
LANGUAGE SQL;

Postgresql: Partitioning a table for values in SELECT

I want to use PostgreSQL's declarative table partitioning on a column with UUID values. I want it partitioned by values that are returned by a SELECT.
CREATE TABLE foo (
id uuid NOT NULL,
type_id uuid NOT NULL,
-- other columns
PRIMARY KEY (id, type_id)
) PARTITION BY LIST (type_id);
CREATE TABLE foo_1 PARTITION OF foo
FOR VALUES IN (SELECT id FROM type_ids WHERE type_name = 'type1');
CREATE TABLE foo_2 PARTITION OF foo
FOR VALUES IN (SELECT id FROM type_ids WHERE type_name = 'type2');
I don't want to use specific UUIDs in FOR VALUES IN ('uuid'), since the UUIDs may differ by environment (dev, qa, prod). However, the SELECT syntax doesn't seem to be accepted by Postgres. Any suggestions?
I just wanted the SELECT to be evaluated at the table creation time
You should have made that clear in the question, not in a comment.
In that case - if this is a one time thing, you can use dynamic SQL, e.g. wrapped into a procedure
create procedure create_partition(p_part_name text, p_type_name text)
as
$$
declare
l_sql text;
l_id uuid;
begin
select id
into l_id
from type_ids
where type_name = p_type_name;
l_sql := format('CREATE TABLE %I PARTITION OF foo FOR VALUES IN (%L)', p_part_name, l_id);
execute l_sql;
end;
$$
language plpgsql;
Then you can do:
call create_partition('foo_1', 'type1');
call create_partition('foo_2', 'type2');

Filter bigint values on insert Postgresql

I have 2 tables in Postgresql with the same schema, the only difference is that in one of the table id field is of type bigint. Schema of the table I need to fill with data looks like this:
create table test_int_table(
id int,
description text,
hash_code int
);
I need to copy the data from test_table with bigint id to public.test_int_table. And some of the values which are bigger than id range should be filtered out. How can I track those values without hardcoding the range?
I can do something like this, but I would like to build more generic solution:
insert into test_int_table
select * from test_table as test
where test.id not between 2147483647 and 9223372036854775808
By generic I mean without constraints on the columns names and their number. So that in case, I have multiple columns of bigint type in other tables how can I filter all of their columns values generically (without specifying a column name)?
There is no generic solution, as far as I can tell.
But I would write it as
INSERT INTO test_int_table
SELECT *
FROM test_table AS t
WHERE t.id BETWEEN -2147483647 AND 2147483647;
You can do something like this if you want to track :
Create a function like this :
CREATE OR REPLACE FUNCTION convert_to_integer(v_input bigint)
RETURNS INTEGER AS $$
DECLARE v_int_value INTEGER DEFAULT NULL;
BEGIN
BEGIN
v_int_value := v_input::INTEGER;
EXCEPTION WHEN OTHERS THEN
RAISE NOTICE 'Invalid integer value: "%". Returning NULL.', v_input;
RETURN NULL;
END;
RETURN v_int_value;
END;
$$ LANGUAGE plpgsql;
and write a query like this :
INSERT INTO test_int_table SELECT * FROM test_table AS t WHERE convert_to_integer(t.id) is not null;
Or you can modify a function to return 0.

Postgres stored procedure/function

New to Stored Procedures , have a requirement where I need to execute multiple queries inside stored procedure and return results. I would like to know whether this is possible or not ..
Ex :
Query 1 returns a list of userid ..
Select userid from user where username = ?
For each userid from the above query , I need to execute three different queries like
Query 2 select session_details from session where userid = ?
Query 3 select location from location where userid = ?
The return value should be a collection of , session_details and location.
Is this possible,can you provide some hints?
You can loop through query results like so:
FOR id IN Select userid from user where username = ?
LOOP
...
END LOOP;
As #Fahad Anjum says in his comment, its better if you can do it in a query. But if that's not posible, you have tree posibilities to achive what you want.
SETOF
TABLE
refcursor
1. SETOF
You can return a set of values. The set can be an existing table, a temporal table, or a TYPE you define.
TYPE example:
-- In your case the type could be (userid integer, session integer, location text)
CREATE TYPE tester AS (id integer);
-- The pl returns a SETOF the created type.
CREATE OR REPLACE FUNCTION test() RETURNS SETOF tester
AS $$
BEGIN
RETURN QUERY SELECT generate_series(1, 3) as id;
END;
$$ LANGUAGE plpgsql
-- Then, you get the set by selecting the PL as if it were a table.
SELECT * FROM test();
Table and Temp Table examples:
-- Create a temporal table o a regular table:
CREATE TEMP TABLE test_table(id integer);
-- or CREATE TABLE test_table(id integer);
-- or use an existing table in your schema(s);
-- The pl returns a SETOF the table you need
CREATE OR REPLACE FUNCTION test() RETURNS SETOF test_table
AS $$
BEGIN
RETURN QUERY SELECT generate_series(1, 3) as id;
END;
$$ LANGUAGE plpgsql
-- Then, you get the set by selecting the PL as if it were a table.
SELECT * FROM test();
-- NOTE: Since you are only returning a SETOF the table,
-- you don't insert any data into the table.
-- So, if you select the 'temp' table you won't see any changes.
SELECT * FROM test_table
2. TABLE
A PL can return a table, it would be similar to create a temporal table and then return a SETOF, but, in this case you declare de 'temp' table on the 'returns' sentence of the PL.
-- Next to TABLE you define the columns of the table the PL will return
CREATE OR REPLACE FUNCTION test() RETURNS TABLE (id integer)
AS $$
BEGIN
RETURN QUERY SELECT generate_series(1, 3) as id;
END;
$$ LANGUAGE plpgsql
-- As the other examples, you select the PL to get the data.
SELECT * FROM test();
3. refcursor
This one is the more complex solution. You return a cursor, not the actual data. If you need 'dynamic' values for your returning set, this is the solution.
But since you need static data, you won't need this option.
The use of any of these ways depends on any specific case, if you use regularly the userid,session,location in different ways and PLs, it would be better to Use the SETOF with a type.
If you have a table that has the userid,session,location columns, it's better to return a SETOF table.
If you just use the userid,session,location for one case, then it would be better to use a 'RETURNS TABLE' approach.
If you need to return a dynamic set you would have to use cursors... but that solution is really more advanced.
Based solely on your example, here's probably the easiest way to do it:
CREATE FUNCTION my_func(user_id INTEGER)
RETURNS TABLE (userid INTEGER, session INTEGER, location TEXT) AS
$$
SELECT u.userid, s.session, l.location
FROM -- etc... your query here
$$
LANGUAGE SQL STABLE;
Addressing comment:
That's a bit of a different question. One question is how to return multiple records containing multiple fields in a stored procedure. One way is as above.
The other question is how to write a query that gets data from multiple tables. Again, there are many ways to do it. One way is (again, based on my interpretation of your requirements in the examples):
SELECT userid
, ARRAY_AGG(SELECT session_details FROM session s WHERE s.userid = u.userid)
, ARRAY_AGG(SELECT l.location FROM location l WHERE l.userid = u.userid)
FROM user u
WHERE username = user_name
This will return one record containing the user_id, an array of session_details for that user, and an array of locations for that user.
Then the function can be changed to:
CREATE FUNCTION my_func(user_name TEXT, OUT userid INTEGER, OUT session_details TEXT[], OUT locations TEXT[])
AS $$
SELECT userid
, ARRAY(SELECT session_details FROM session s WHERE s.userid = u.userid)
, ARRAY(SELECT l.location FROM location l WHERE l.userid = u.userid)
FROM user u
WHERE username = user_name;
$$ LANGUAGE SQL STABLE;

Is it possible to write a postgres function that will handle a many to many join?

I have a job table. I have an industries table. Jobs and industries have a many to many relationship via a join table called industriesjobs. Both tables have uuid is their primary key. My question is two fold. Firstly is it feasible to write two functions to insert data like this? If this is feasible then my second question is how do I express an array of the uuid column type. I'm unsure of the syntax.
CREATE OR REPLACE FUNCTION linkJobToIndustries(jobId uuid, industiresId uuid[]) RETURNS void AS $$
DECLARE
industryId uuid[];
BEGIN
FOREACH industryId SLICE 1 IN ARRAY industriesId LOOP
INSERT INTO industriesjobs (industry_id, job_id) VALUES (industryId, jobId);
END LOOP;
RETURN;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION insertJobWithIndistries(orginsation varchar, title varchar, addressId uuid, industryIds uuid[]) RETURNS uuid AS $$
DECLARE
jobId uuid;
BEGIN
INSERT INTO jobs ("organisation", "title", "address_id") VALUES (orginsation, title, addressId) RETURNING id INTO jobId;
SELECT JobbaLinkJobToIndustries(jobId, industryIds);
END;
$$ LANGUAGE plpgsql;
SELECT jobId FROM insertJobWithIndistries(
'Acme Inc'::varchar,
'Bomb Tester'::varchar,
'0030cfb3-1a03-4c5a-9afa-6b69376abe2e',
{ 19c2e0ee-acd5-48b2-9fac-077ad4d49b19, 21f8ffb7-e155-4c8f-acf0-9e991325784, 28c18acd-99ba-46ac-a2dc-59ce952eecf2 }
);
Thanks in advance.
Key to a solution are the function unnest() to (per documentation):
expand an array to a set of rows
And a data-modifying CTE.
A simple query does the job:
WITH ins_job AS (
INSERT INTO jobs (organisation, title, address_id)
SELECT 'Acme Inc', 'Bomb Tester', '0030cfb3-1a03-4c5a-9afa-6b69376abe2e' -- job-data here
RETURNING id
)
INSERT INTO industriesjobs (industry_id, job_id)
SELECT indid, id
FROM ins_job i -- that's a single row, so a CROSS JOIN is OK
, unnest('{19c2e0ee-acd5-48b2-9fac-077ad4d49b19
, 21f8ffb7-e155-4c8f-acf0-9e9913257845
, 28c18acd-99ba-46ac-a2dc-59ce952eecf2}'::uuid[]) indid; -- industry IDs here
Also demonstrating proper syntax for an array of uuid. (White space between elements and separators is irrelevant while not inside double-quotes.)
One of your UUIDs was one character short:
21f8ffb7-e155-4c8f-acf0-9e991325784
Must be something like:
21f8ffb7-e155-4c8f-acf0-9e9913257845 -- one more character
If you need functions, you do that, too:
CREATE OR REPLACE FUNCTION link_job_to_industries(_jobid uuid, _indids uuid[])
RETURNS void AS
$func$
INSERT INTO industriesjobs (industry_id, job_id)
SELECT _indid, _jobid
FROM unnest(_indids) _indid;
$func$ LANGUAGE sql;
Etc.
Related:
Insert data in 3 tables at a time using Postgres
How to insert multiple rows using a function in PostgreSQL