How to impose this exclusion constraint? - sql

I have a key-value table.
CREATE TABLE keyvalues (
key TEXT NOT NULL,
value TEXT
)
I want to impose a constraint that if a key has an entry with NULL value, it cannot have any other entries.
How do I do that?
To clarify:
I want to allow ("key1", "value1"), ("key1", "value2"). But if I have ("key2", NULL), I want to not allow ("key2", "value3").

You can use a trigger, like this:
CREATE OR REPLACE FUNCTION trigger_function()
RETURNS trigger
LANGUAGE plpgsql
AS $function$
begin
if exists (select 1 from keyvalues key = new.key and value is null) then
RAISE EXCEPTION 'Key-value not allowed';
end if;
RETURN new;
end;
$function$
;
Then you create the trigger on the table
CREATE TRIGGER trigger_on_table
BEFORE INSERT OR UPDATE
ON keyvalues
FOR EACH ROW
EXECUTE PROCEDURE trigger_function();
And test it:
insert INTO keyvalues
SELECT 'a','a'
OK
insert INTO keyvalues
SELECT 'a','b'
OK
insert INTO keyvalues
SELECT 'b',null
OK
insert INTO keyvalues
SELECT 'b','b'
ERROR: Key-value not allowed

Related

Postgres UNIQUE CONSTRAINT/INDEX for string array

I'm trying to prevent the user to insert more then 1 unique array of strings into the table.
I have created a Unique Constraint on the array: CONSTRAINT users_uniq UNIQUE(usersArray),
but the user can still insert the same values to the array but in a different order.
My table:
id
usersArray
1
{011,123}
2
{123,011} // should not be possible
Input : {011,123} --> error unique // the right error
Input : {123,011} --> Worked // Should have return an error instead
How can I make the value {123,011} and {011,123} considered the same?
The trigger solution is not transparent as it is actually modifying the data. Here is an alternative. Create array_sort helper function (it might be useful for other cases too) and an unique index using it.
create or replace function array_sort (arr anyarray) returns anyarray immutable as
$$
select array_agg(x order by x) from unnest(arr) x;
$$ language sql;
create table t (arr integer[]);
create unique index tuix on t (array_sort(arr));
Demo
insert into t values ('{1,2,3}'); -- OK
insert into t values ('{2,1,3}'); -- unique violation
select * from t;
arr
{1,2,3}
A trigger which enforces the order of the items in the array could be one approach. Here's an example:
The fiddle
CREATE TABLE test ( arr int ARRAY, unique (arr) );
CREATE FUNCTION test_insert_trig_func()
RETURNS trigger AS $$
BEGIN
NEW.arr := ARRAY(SELECT unnest(NEW.arr) ORDER BY 1);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER test_insert_trig
BEFORE INSERT ON test
FOR EACH ROW
EXECUTE PROCEDURE test_insert_trig_func()
;
INSERT INTO test VALUES ('{1, 2}');
INSERT INTO test VALUES ('{2, 1}'); -- Generates a unique constraint violation
SELECT * FROM test;
The result:
arr
{1,2}

Replace insert with rule in PostgreSQL

I want to replace INSERT into a table with a PostgreSQL rule.
Here is my two tables:
Resource:
uid (PK): UUID
type (NOT_NULL): ENUM
SpecificResource:
uid (PK, FK on resource.uid): UUID
content (NOT_NULL): JSONB
I want any user to the database to be able to make insert/update/delete on SpecificResource directly without the need to insert/update/delete on Resource.
Here is my unsuccessful try that triggers an infinite recursion loop (indeed because I try to re-insert in specific_resource table with a RULE (...) DO INSTEAD :
CREATE OR REPLACE RULE insert_specific_resource
AS ON INSERT TO specific_resource
DO INSTEAD (
INSERT INTO resource (uid, type)
VALUES (NEW.uid, 'SPECIFIC_RESOURCE');
INSERT INTO specific_resource (uid)
VALUES (new.uid)
);
create or replace function specific_resource_tf()
returns trigger language plpgsql as
$$
begin
insert into resource(uid, type) VALUES (new.uid, 'SPECIFIC_RESOURCE');
return new;
end;
$$;
create trigger specific_resource_t
before insert on specific_resource
for each row
execute procedure specific_resource_tf();
Explanation
After creating trigger specific_resource_t it will be executed before each insert in table specific_resource. The trigger invokes function specific_resource_tf which does what your rule was intended to - inserts a record into resource before proceeding with the insert in table specific_resource.
Illustration (with temporary tables and function)
-- drop table if exists specific_resource; drop table if exists resource;
create temp table resource (uid integer primary key, type text);
create temp table specific_resource (uid integer references resource(uid), content JSONB);
create or replace function pg_temp.specific_resource_tf()
returns trigger language plpgsql as $$
begin
insert into resource(uid, type) VALUES (new.uid, 'SPECIFIC_RESOURCE');
return new;
end;
$$;
create trigger specific_resource_t
before insert on specific_resource
for each row execute procedure pg_temp.specific_resource_tf();
insert into specific_resource values (22, '"test"');
-- does insert in both tables
CREATE OR REPLACE FUNCTION add_specific_plus_resource()
RETURNS TRIGGER LANGUAGE PLPGSQL AS
$$
BEGIN
INSERT INTO resource(uid, type)
VALUES (uid, 'SPECIFIC');
RETURN NEW;
END;
$$;
CREATE OR REPLACE TRIGGER add_specific_resource
BEFORE INSERT ON specific_resource
FOR EACH ROW EXECUTE PROCEDURE add_specific_plus_resource();
The following INSERT works well to add a row into resource but not into specific_resource.
INSERT INTO specific_resource(uid, content)
VALUES ('123e4567-e89b-12d3-a456-426614174000', '"test"');

trigger insert only if tupel satisfies condition

I want to create a trigger which checks before insert if the tupel which is supposed to be inserted holds a specific condition (which also depends on another table).
For example:
create trigger or replace check_tupel
before insert on A
for each row
execute
if exists (select x,y from B where B.x = A.x and B.y = A.y)
Oh I am using postgreSQL 13.
EDIT: Yes I know that I can do this without a trigger, but I am asking for a solution with a trigger for a reason.
I hope there is a way to do this... My other idea was to create a UDF which gets called before insert but I do not know how to check the condition in this UDF and only insert if the function returns true.
If you simply wanna automatically validate a record before inserting it on table A based on table B using a User Defined Function, you do not need a trigger at all. Consider adding a simple CHECK CONSTRAINT:
CREATE TABLE a (
x int,
y int,
CONSTRAINT exists_in_b CHECK (NOT myfunc(x,y))
);
Demo: db<>fiddle
CREATE TABLE b (x int,
y int);
INSERT INTO b VALUES (42,42);
CREATE OR REPLACE FUNCTION myfunc(x int, y int)
RETURNS BOOLEAN AS $BODY$
SELECT EXISTS (SELECT 1 FROM b WHERE b.y =$1 AND b.x=$2 )
$BODY$
LANGUAGE sql;
CREATE TABLE a (
x int,
y int,
CONSTRAINT exists_in_b CHECK (NOT myfunc(x,y)) -- here the magic happens
);
Now, if we try to insert a value that our function does not validate, it raises an exception:
INSERT INTO a VALUES (42,42);
ERROR: new row for relation "a" violates check constraint "exists_in_b"
DETAIL: Failing row contains (42, 42).
SQL state: 23514
EDIT (See comments): Solution using a trigger
CREATE OR REPLACE FUNCTION myfunc()
RETURNS TRIGGER AS $BODY$
BEGIN
IF EXISTS (SELECT 1 FROM b WHERE b.y =NEW.y AND b.x=NEW.x ) THEN
RAISE EXCEPTION 'tuple already exists in "b": % %', NEW.x,NEW.y;
END IF;
RETURN NEW;
END;
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER check_tupel
BEFORE INSERT OR UPDATE ON a
FOR EACH ROW EXECUTE PROCEDURE myfunc();
Demo: db<>fiddle
So you need a trigger solution, sounds like a homework problem. The question then becomes do you want to:
abort the entire operation
slightly squash the row but continue the remainder of the operation.
The following does the second: (See demo)
create or replace
function check_b_has_a()
returns trigger
language plpgsql
as $$
begin
if exists
( select null
from b
where (b.x,b.y) =
(new.x, new.y)
)
then
return null;
else
return new;
end if;
end;
$$;
create trigger a_bir
before insert
on a
for each row
execute function check_b_has_a();

How to obtain primary key value in trigger function if primary key column name is unknown?

I'm using postgres.
Let's say I'm going to create following trigger:
CREATE OR REPLACE FUNCTION auditlogfunc() RETURNS TRIGGER AS $example_table$
BEGIN
INSERT INTO AUDIT(EMP_ID, ENTRY_DATE) VALUES (new.ID, current_timestamp);
RETURN NEW;
END;
$example_table$ LANGUAGE plpgsql;
I want to use this trigger with many tables and not every table has primary key with name 'id' (table can have no 'id' column at all).
So I need to find out in some way how to use primary key in my trigger function no matter which column name it has.
How can I achieve this?
What's your PostgreSQL version? You must query PostgreSQL catalog to achieve what you want. See script below:
I'm assuming that your PostgreSQL has JSONB support (9.4+).
CREATE TABLE public.test_table
(
id BIGINT NOT NULL,
a_column TEXT NOT NULL,
CONSTRAINT test_tablepkey PRIMARY KEY (id)
);
CREATE OR REPLACE FUNCTION auditlogfunc() RETURNS TRIGGER AS $example_table$
DECLARE
reg_id JSONB;
affected_row JSON;
BEGIN
IF TG_OP IN('INSERT', 'UPDATE') THEN
affected_row := row_to_json(NEW);
ELSE
affected_row := row_to_json(OLD);
END IF;
--Get PK columns
--You may want to extract this to a SQL function
WITH pk_columns (attname) AS (
SELECT
CAST(a.attname AS TEXT)
FROM
pg_index i
JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)
WHERE
i.indrelid = TG_RELID
AND i.indisprimary
)
SELECT
json_object_agg(key, value) INTO reg_id
FROM
json_each_text(affected_row)
WHERE
key IN(SELECT attname FROM pk_columns);
--Raise collected PK
RAISE INFO 'PK: %', reg_id;
--TODO: your insert into audit table goes here
RETURN NEW;
END;
$example_table$ LANGUAGE plpgsql;
CREATE TRIGGER tg_audit_cadprodu_row AFTER INSERT OR UPDATE OR DELETE
ON public.test_table FOR EACH ROW EXECUTE PROCEDURE public.auditlogfunc();
--A simple test
INSERT INTO test_table VALUES (CAST((random() * 10000) AS INTEGER), 'Test');
I took Michel Milezzi's answer and cleaned it up so it only returns what we need, with better performance in a more concise statement. Tested on Postgres v12.1. I imagine it would work on 9.4+.
CREATE OR REPLACE FUNCTION auditlogfunc() RETURNS TRIGGER AS $example_table$
DECLARE
col TEXT = (
SELECT attname
FROM pg_index
JOIN pg_attribute ON
attrelid = indrelid
AND attnum = ANY(indkey)
WHERE indrelid = TG_RELID AND indisprimary
);
BEGIN
INSERT INTO AUDIT(EMP_ID, ENTRY_DATE) VALUES ((row_to_json(NEW) ->> col), current_timestamp);
RETURN NEW;
END;
$example_table$ LANGUAGE plpgsql;
First I'm querying for the name of the table's primary key column, and storing it in the col variable. I filter the query by the special variable TG_RELID, which is the object ID of the table which caused the trigger. (See the Postgres Docs.)
Then we can insert into the audit table. All I've changed from the question is new.ID. Now that we know the name of the primary key column, we can convert the new row--the row that will be (or has been) inserted, updated, or deleted--to json, and select the value with the col variable. This should work for both BEFORE and AFTER triggers. If triggering on DELETE, change row_to_json(NEW) to row_to_json(OLD).

PostgreSQL transactional DDL and to_regclass

Following the suggestion at this question, I'm using the to_regclass function to check if a table exists, creating it if it doesn't. However, it appears that if the table was created in the current transaction, to_regclass still returns null.
Is this behaviour expected? Or is this a bug?
Detail
Here's a short example of where this goes wrong:
begin;
create schema test;
create table test.test ( id serial, category integer );
create or replace function test.test_insert () returns trigger as $$
declare
child_table_name text;
table_id text;
begin
child_table_name = concat('test.test_', text(new.category));
table_id = to_regclass(child_table_name::cstring);
if table_id is null then
execute format('create table %I ( primary key (id), check ( category = %L ) ) inherits (test.test)', child_table_name, new.category);
end if;
execute format ('insert into %I values ($1.*)', child_table_name) using new;
return null;
end;
$$ language plpgsql;
create trigger test_insert before insert on test.test for each row execute procedure test.test_insert();
insert into test.test (category) values (1);
insert into test.test (category) values (1);
insert into test.test (category) values (1);
commit;
You're using the %I format specifier incorrectly.
If your category is 1, then you end up calling to_regclass('test.test_1'), i.e. checking for the table test_1 in schema test.
However, format('create table %I', 'test.test_1') will treat the format argument as a single identifier and quote it accordingly, evaluating to 'create table "test.test_1"'. This will create a table called "test.test_1" in your default schema (probably public).
Instead, you need to treat your schema and table names as separate identifiers. Define your table name as:
child_table_name = format('test.%I', 'test_' || new.category);
... and when building your SQL strings, just substitute this value directly (i.e. with %s rather than %I).