PostgreSQL: how to return composite type - postgresql

I'm trying to get a composite value with a stored function in PostreSQL as follows.
I created a type called PersonId, and I used the type in a table called Person.
And I inserted values into the table.
CREATE TYPE PersonId AS
(
id VARCHAR(32),
issuer VARCHAR(32)
);
CREATE TABLE Person
(
key INTEGER,
pid PersonId
);
INSERT INTO Person VALUES (1, ('111','ABC'));
INSERT INTO Person VALUES (2, ('222','DEF'));
CREATE OR REPLACE FUNCTION Person_lookup_id
(
p_key IN Person.key%TYPE
)
RETURNS Person.pid%TYPE
LANGUAGE plpgsql
AS $BODY$
DECLARE
v_pid Person.pid%TYPE;
BEGIN
SELECT pid INTO v_pid
FROM Person
WHERE key = p_key;
RETURN v_pid;
EXCEPTION
WHEN no_data_found THEN
RETURN NULL;
END;
$BODY$;
However, the result was different from what I had expected.
Actually I expected the value 111 would be in id column, and ABC in issuer column. But 111 and ABC were combined in id column.
# select person_lookup_id(1);
person_lookup_id
------------------
("(111,ABC)",)
(1 row)
# select * from person_lookup_id(1);
id | issuer
-----------+--------
(111,ABC) |
(1 row)
Where was I wrong?

Since pid is a composite you must extract its columns otherwise you are inserting the whole composite into the first column of the v_pid variable
select (pid).* into v_pid

Related

Update hstore values with other hstore values

I have a summary table that is updated with new data on a regulary basis. One of the columns is of type hstore. When I update with new data I want to add the value of a key to the existing value of the key if the key exists, otherwise I want to add the pair to the hstore.
Existing data:
id sum keyvalue
--------------------------------------
1 2 "key1"=>"1","key2"=>"1"
New data:
id sum keyvalue
--------------------------------------------------
1 3 "key1"=>"1","key2"=>"1","key3"=>"1"
Wanted result:
id sum keyvalue
--------------------------------------------------
1 5 "key1"=>"2","key2"=>"2","key3"=>"1"
I want to do this in a on conflict part of an insert.
The sum part was easy. But I have not found how to concatenate the hstore in this way.
There is nothing built int. You have to write a function that accepts to hstore values and merges them in the way you want.
create function merge_and_increment(p_one hstore, p_two hstore)
returns hstore
as
$$
select hstore_agg(hstore(k,v))
from (
select k, sum(v::int)::text as v
from (
select *
from each(p_one) as t1(k,v)
union all
select *
from each(p_two) as t2(k,v)
) x
group by k
) s
$$
language sql;
The hstore_agg() function isn't built-in as well, but it's easy to define it:
create aggregate hstore_agg(hstore)
(
sfunc = hs_concat(hstore, hstore),
stype = hstore
);
So the result of this:
select merge_and_increment(hstore('"key1"=>"1","key2"=>"1"'), hstore('"key1"=>"1","key2"=>"1","key3"=>"1"'))
is:
merge_and_increment
-------------------------------------
"key1"=>"2", "key2"=>"2", "key3"=>"1"
Note that the function will fail miserably if there are values that can't be converted to an integer.
With an insert statement you can use it like this:
insert into the_table (id, sum, data)
values (....)
on conflict (id) do update
set sum = the_table.sum + excluded.sum,
data = merge_and_increment(the_table.data, excluded.data);
demo:db<>fiddle
CREATE OR REPLACE FUNCTION sum_hstore(_old hstore, _new hstore) RETURNS hstore
AS $$
DECLARE
_out hstore;
BEGIN
SELECT
hstore(array_agg(key), array_agg(value::text))
FROM (
SELECT
key,
SUM(value::int) AS value
FROM (
SELECT * FROM each('"key1"=>"1","key2"=>"1"'::hstore)
UNION ALL
SELECT * FROM each('"key1"=>"1","key2"=>"1","key3"=>"1"')
) s
GROUP BY key
) s
INTO _out;
RETURN _out;
END;
$$
LANGUAGE plpgsql;
each() expands the key/value pairs into one row per pair with columns key and value
convert type text into type int and group/sum the values
Aggregate into a new hstore value using the hstore(array, array) function. The array elements are the values of the key column and the values of the value column.
You can do such an update:
UPDATE mytable
SET keyvalue = sum_hstore(keyvalue, '"key1"=>"1","key2"=>"1","key3"=>"1"')
WHERE id = 1;

Insert into a PostgreSql table from a custom-declared-type array

I create a custom type like so:
CREATE TYPE employee AS (employee_id INTEGER, name VARCHAR(200), age INTEGER);
I then make a function which receives an array of this type as a parameter:
CREATE FUNCTION insert_office(v_office_id INTEGER, v_office_name VARCHAR(400), v_employees employee[])
RETURNS void
AS $BODY$
INSERT INTO office (office_id, office_name)
VALUES(v_office_id, v_office_name)
--here I need to insert the array of employees (v_employees) into the employees table
$BODY$
LANGUAGE SQL;
Given that the employees table is set up to match the properties of the employee type:
CREATE TABLE employee (employee_id INTEGER, name VARCHAR(200), age INTEGER)
How can I simply transfer this array of type employee to the employee table? Despite several attempts, I can't get the syntax right. (PostgreSql 9.6)
You need to unnest the elements of the array:
insert into employees (employee_id, name, age)
select employee_id, name, age
from unnest(v_employees);

Default value for column postgres function with argument

I have a postgres function that takes one argument. I want to make this function the default value for a column, but I'm not sure how to pass the argument into the table definition.
This is what I mean, I have two columns in the table that look like this:
trade_id INTEGER NOT NULL
group_id INTEGER DEFAULT trade_id_f(argument_goes_here);
I want to make the DEFAULT value of group_id to be the return value of trade_id_f(trade_id) where trade_id is the trade_id of the record to be inserted.
I'm new to all things postgres functions, is this possible?
Unfortunately, you cannot do that, because of (for the documentation):
The value is any variable-free expression (subqueries and
cross-references to other columns in the current table are not
allowed).
You can use a trigger, e.g.:
create table the_table (
trade_id int not null,
group_id int);
create or replace function trade_id_trigger ()
returns trigger language plpgsql as $$
begin
new.group_id:= new.trade_id+ 1;
return new;
end $$;
create trigger trade_id_trigger
before insert or update on the_table
for each row execute procedure trade_id_trigger();
insert into the_table values (1,1);
select * from the_table;
trade_id | group_id
----------+----------
1 | 2
(1 row)

Need foreign key as array

CREATE TABLE test ( id int PRIMARY KEY , name );
CREATE TABLE test1 ( id integer[] REFERENCES test , rollid int );
ERROR: foreign key constraint "test3_id_fkey" cannot be implemented
DETAIL: Key columns "id" and "id" are of incompatible types: integer[] and integer.
after that I try to another way also
CREATE TABLE test1 ( id integer[] , rollid int);
ALTER TABLE test1 ADD CONSTRAINT foreignkeyarray FOREIGN KEY (id) REFERENCES test;
ERROR: foreign key constraint "fkarray" cannot be implemented
DETAIL: Key columns "id" and "id" are of incompatible types: integer[] and integer.
so I try create a foreign key array means it say error. please tell me anyone?
postgresql version is 9.1.
What you're trying to do simply can't be done. At all. No ifs, no buts.
Create a new table, test1_test, containing two fields, test1_id, test_id. Put the foreign keys as needed on that one, and make test1's id an integer.
Using arrays with foreign element keys is usually a sign of incorrect design. You need to do separate table with one to many relationship.
But technically it is possible. Example of checking array values without triggers. One reusable function with paramethers and dynamic sql. Tested on PostgreSQL 10.5
create schema if not exists test;
CREATE OR REPLACE FUNCTION test.check_foreign_key_array(data anyarray, ref_schema text, ref_table text, ref_column text)
RETURNS BOOL
RETURNS NULL ON NULL INPUT
LANGUAGE plpgsql
AS
$body$
DECLARE
fake_id text;
sql text default format($$
select id::text
from unnest($1) as x(id)
where id is not null
and id not in (select %3$I
from %1$I.%2$I
where %3$I = any($1))
limit 1;
$$, ref_schema, ref_table, ref_column);
BEGIN
EXECUTE sql USING data INTO fake_id;
IF (fake_id IS NOT NULL) THEN
RAISE NOTICE 'Array element value % does not exist in column %.%.%', fake_id, ref_schema, ref_table, ref_column;
RETURN false;
END IF;
RETURN true;
END
$body$;
drop table if exists test.t1, test.t2;
create table test.t1 (
id integer generated by default as identity primary key
);
create table test.t2 (
id integer generated by default as identity primary key,
t1_ids integer[] not null check (test.check_foreign_key_array(t1_ids, 'test', 't1', 'id'))
);
insert into test.t1 (id) values (default), (default), (default); --ok
insert into test.t2 (id, t1_ids) values (default, array[1,2,3]); --ok
insert into test.t2 (id, t1_ids) values (default, array[1,2,3,555]); --error
If you are able to put there just values from test.id, then you can try this:
CREATE OR REPLACE FUNCTION test_trigger() RETURNS trigger
LANGUAGE plpgsql AS $BODY$
DECLARE
val integer;
BEGIN
SELECT id INTO val
FROM (
SELECT UNNEST(id) AS id
FROM test1
) AS q
WHERE id = OLD.id;
IF val IS NULL THEN RETURN OLD;
ELSE
RAISE 'Integrity Constraint Violation: ID "%" in Test1', val USING ERRCODE = '23000';
RETURN NULL;
END IF;
END; $BODY$;
-- DROP TRIGGER test_delete_trigger ON test;
CREATE TRIGGER test_delete_trigger BEFORE DELETE OR UPDATE OF id ON test
FOR EACH ROW EXECUTE PROCEDURE test_trigger();

Update column of a inserted row with with its generated id in a single query

Say I have a table, created as follows:
CREATE TABLE test_table (id serial, unique_id varchar(50) primary key, name varchar(50));
test_table
----------
id | unique_id | name
In that table, I would like to update the unique_id field with the newly inserted id concatenated with the inserted name in a single go.
Usually this is accomplished by two queries. (PHP way)
$q = "INSERT INTO table (unique_id,name) values ('uid','abc') returning id||name as unique_id;";
$r = pg_query($dbconn,$q);
$row = pg_fetch_array($r);
$q1 = "UPDATE test_table set unique_id =".$row['unique_id']." where unique_id='uid'";
$r1 = pg_query($dbconn,$q1);
Is there any way to do the above in a single query?
You can have several options here, you could create a AFTER trigger which uses the generated ID for an direct update of the same row:
CREATE TRIGGER test_table_insert ON AFTER INSERT ON test_table FOR EACH ROW EXECUTE PROCEDURE test_table_insert();
And in your function you update the value:
CREATE FUNCTION test_table_insert() RETURNS TRIGGER AS $$
BEGIN
UPDATE test_table SET uniqid = NEW.id::text || NEW.name WHERE id = NEW.id;
END;
$$ LANGUAGE plpgsql;
You need to add the function before the trigger.
An other option would be to do it directly in the insert:
INSERT INTO table (id, unique_id, name) values (nextval('test_table_id_seq'), 'abc', currval('test_table_id_seq')::text || 'abc') returning id;
But as a_horse_with_no_name pointed out, I think you may have a problem in your database design.