I am a bit confused on how to deal with looping on multiple select statements using PL/SQL for PostgreSQL. I have written a function that works as expected for multiple select statements that each return a single row (see code below).
I have a middle table (called bridge_table) that breaks up many-to-many relationship between tables 'stock' and 'client', where one client_id can match up with multiple stock_id.
How/Where do I add in the necessary LOOP syntax to grab all the necessary stock_id's for a client_id? Is this a scenario where a cursor might be useful?
Thanks ahead of time!
CREATE OR REPLACE FUNCTION my_func(
a_id INTEGER,
b_id INTEGER,
c_id INTEGER)
RETURNS BOOLEAN AS $$
DECLARE
current_time TIMESTAMP;
new_inv INTEGER;
inv_id INTEGER;
inv_q INTEGER;
current_quant INTEGER;
still_true boolean := true;
BEGIN
current_time := clock_timestamp();
SELECT i_id INTO inv_id
FROM bridge_table
WHERE bridge_table.client_id=c_id;
SELECT inv_quant INTO inv_q
FROM bridge_table
WHERE bridge_table.client_id=c_id;
SELECT quantity INTO current_quant
FROM stock
WHERE stock.i_id=inv_id;
IF current_quant < inv_q THEN
RAISE NOTICE 'STOP';
still_true := false;
ELSE
new_inv := current_quant - inv_q;
UPDATE stock
SET quantity = new_inv
WHERE i_id=inv_id;
END IF;
IF still_true = true THEN
INSERT INTO log_table (log_id, u_id, client_id, time)
VALUES (a_id, b_id, c_id, current_time);
ELSE
RAISE NOTICE 'TRANSACTION WILL NOT OCCUR';
END IF;
RETURN still_true;
END;
$$ LANGUAGE plpgsql;
Edit:
Table DDLs
CREATE TABLE stock
(
i_id INTEGER PRIMARY KEY,
name VARCHAR(120) NOT NULL,
quantity INTEGER NOT NULL
);
CREATE TABLE clients
(
client_id INTEGER PRIMARY KEY,
name VARCHAR(120) NOT NULL
);
CREATE TABLE bridge_table
(
client_id INTEGER REFERENCES clients (client_id),
i_id INTEGER REFERENCES stock (i_id),
inv_quant INTEGER NOT NULL
);
CREATE TABLE log
(
log_id INTEGER PRIMARY KEY,
u_id INTEGER REFERENCES users (user_id),
client_id INTEGER REFERENCES clients (client_id),
time TIMESTAMP NOT NULL
);
Related
I have a the following DDL and would like to generate an ID on insert to the table, but ONLY if the ID has not been provided already.
drop table if exists table1;
create table table1 (
pk_id INT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
other_id INT GENERATED ALWAYS AS IDENTITY -- DO THIS ONLY IF NULL
)
I was thinking of using a stored procedure to do this and have attempted a start with the below code, but have been unable to get it working:
create or replace function conditionally_generated_identity(other_id integer)
returns table (id integer) as $$
begin
if other_id is null then
return query other_id generated always as identity;
elsif other_id is not null then
return query other_id;
end if;
end;
$$ language plpgsql;
I have the following small MVWE for a basic micro-auditing system which works fine but lacks a functionality:
DROP TABLE IF EXISTS audit CASCADE;
CREATE TABLE audit(
Id BIGSERIAL NOT NULL
,TimeValue TIMESTAMP NOT NULL
,RoleName NAME NOT NULL
,Operation NAME NOT NULL
,SchemaName NAME NOT NULL
,TableName NAME NOT NULL
,Identifiers BIGINT[]
---
,PRIMARY KEY(Id)
);
-- Audit Trigger:
DROP FUNCTION IF EXISTS audit_trigger() CASCADE;
CREATE OR REPLACE FUNCTION audit_trigger()
RETURNS TRIGGER AS
$BODY$
BEGIN
INSERT INTO audit(TimeValue, RoleName, Operation, SchemaName, TableName) VALUES
(now()::TIMESTAMP, current_user, TG_OP, TG_TABLE_SCHEMA, TG_RELNAME);
RETURN NULL;
END;
$BODY$
LANGUAGE plpgsql SECURITY DEFINER;
-- Channels:
DROP TABLE IF EXISTS channels CASCADE;
CREATE TABLE channels(
Id INTEGER NOT NULL
,UserKey TEXT NOT NULL
,Active BOOLEAN NOT NULL DEFAULT(TRUE)
---
,PRIMARY KEY(Id)
,UNIQUE(UserKey)
);
CREATE TRIGGER channel_audit_trigger BEFORE INSERT OR UPDATE OR DELETE ON channels
FOR EACH STATEMENT EXECUTE PROCEDURE audit_trigger();
-- Perform some operations:
INSERT INTO channels(
SELECT C.Id, 'Channel-' || C.Id
FROM generate_series(1, 300, 10) AS C(Id)
);
DELETE FROM channels WHERE id < 10;
UPDATE channels
SET UserKey = 'wild channel'
WHERE id = 21;
I would like to add into the last column of audit table, identifiers of rows that have been inserted/updated/deleted in channels.
I have used STATEMENT level because I just need to collects identifiers in a array. But I do not find how to access DML statistics. Conversely at the ROW level I must handle OLD and NEW cases and I cannot succeed in aggregate all touched identifier.
How can I proceed in order to fill the last column of audit table with touched identifiers?
Update
Finally I reached my goal, but this solution might not be scalable and may have some unwanted drawbacks (I am open to any constructive feedback and advice).
Basically, how I have solved my problem:
Log at ROW level BEFORE DML is performed into a table audit_rowlevel;
Aggregate the freshly added content of audit_rowlevel into audit_statementlevel at STATEMENT level AFTER DML is performed;
Minimal Working Example is now:
DROP TABLE IF EXISTS audit_rowlevel CASCADE;
CREATE TABLE audit_rowlevel(
Id BIGSERIAL NOT NULL
,Aggregated BOOLEAN NOT NULL DEFAULT(FALSE)
,TimeValue TIMESTAMP NOT NULL
-- https://www.postgresql.org/docs/current/static/functions-info.html
,RoleName NAME NOT NULL
,ClientIP INET NOT NULL
,ClientPid INTEGER NOT NULL
-- https://www.postgresql.org/docs/current/static/plpgsql-trigger.html
,Operation TEXT NOT NULL
,SchemaName NAME NOT NULL
,TableName NAME NOT NULL
,RowId BIGINT NOT NULL
-- https://www.postgresql.org/docs/current/static/functions-json.html
,OldValue JSONB
,NewValue JSONB
---
,PRIMARY KEY(Id)
);
-- Row Level Trigger:
DROP FUNCTION IF EXISTS audit_rowlevel_trigger() CASCADE;
CREATE OR REPLACE FUNCTION audit_rowlevel_trigger()
RETURNS TRIGGER AS
$BODY$
DECLARE
history BOOLEAN := (TG_NARGS > 0) AND (TG_ARGV[0]::BOOLEAN);
rowid BIGINT;
oldvalue JSONB;
newvalue JSONB;
BEGIN
-- Handle NEW:
IF TG_OP = ANY('{INSERT,UPDATE}') THEN
IF history THEN
newvalue := to_jsonb(NEW);
END IF;
rowid := NEW.Id::BIGINT;
END IF;
-- Handle OLD:
IF TG_OP = ANY('{UPDATE,DELETE}') THEN
IF history THEN
oldvalue := to_jsonb(OLD);
END IF;
rowid := OLD.Id::BIGINT;
END IF;
-- INSERT:
INSERT INTO audit_rowlevel(TimeValue, RoleName, ClientIP, ClientPID, Operation, SchemaName, TableName, RowId, NewValue, OldValue) VALUES
(now()::TIMESTAMP, current_user, inet_client_addr(), pg_backend_pid(), TG_OP, TG_TABLE_SCHEMA, TG_RELNAME, RowId, NewValue, OldValue);
-- RETURN:
IF TG_OP = ANY('{INSERT,UPDATE}') THEN
RETURN NEW;
ELSIF TG_OP = 'DELETE' THEN
RETURN OLD;
ELSE
RETURN NULL;
END IF;
END;
$BODY$
LANGUAGE plpgsql SECURITY DEFINER;
-- Statement Level Trigger:
DROP TABLE IF EXISTS audit_statementlevel CASCADE;
CREATE TABLE audit_statementlevel(
Id BIGSERIAL NOT NULL
,TimeValue TIMESTAMP NOT NULL
,RoleName NAME NOT NULL
,ClientIP INET NOT NULL
,ClientPid INTEGER NOT NULL
,Operation TEXT NOT NULL
,SchemaName NAME NOT NULL
,TableName NAME NOT NULL
,RowCount BIGINT NOT NULL
,RowIds BIGINT[] NOT NULL
,AuditIds BIGINT[] NOT NULL
---
,PRIMARY KEY(Id)
);
-- Row Level Trigger:
DROP FUNCTION IF EXISTS audit_statementlevel_trigger() CASCADE;
CREATE OR REPLACE FUNCTION audit_statementlevel_trigger()
RETURNS TRIGGER AS
$BODY$
DECLARE
rowcount BIGINT;
BEGIN
WITH
A AS (
SELECT
TimeValue, RoleName, ClientIP, ClientPid, Operation, SchemaName, TableName
,COUNT(*)
,array_agg(RowId)
,array_agg(Id)
FROM
audit_rowlevel
WHERE
NOT Aggregated
GROUP BY
TimeValue, RoleName, ClientIP, ClientPid, Operation, SchemaName, TableName
ORDER BY
TimeValue
),
B AS (
INSERT INTO audit_statementlevel(TimeValue, RoleName, ClientIP, ClientPid, Operation, SchemaName, TableName, RowCount, RowIds, AuditIds)
(SELECT * FROM A)
RETURNING AuditIds
),
C AS (
SELECT array_agg(DISTINCT T.id) AS Ids FROM B, unnest(B.AuditIds) AS T(id)
)
UPDATE
audit_rowlevel
SET
Aggregated = TRUE
FROM
C
WHERE
Id = ANY(C.Ids);
RETURN NULL;
END;
$BODY$
LANGUAGE plpgsql SECURITY DEFINER;
-- Channels:
DROP TABLE IF EXISTS channels CASCADE;
CREATE TABLE channels(
Id INTEGER NOT NULL
,UserKey TEXT NOT NULL
,Active BOOLEAN NOT NULL DEFAULT(TRUE)
---
,PRIMARY KEY(Id)
,UNIQUE(UserKey)
);
CREATE TRIGGER channel_audit_rowlevel_trigger BEFORE INSERT OR UPDATE OR DELETE ON channels
FOR EACH ROW EXECUTE PROCEDURE audit_rowlevel_trigger(TRUE);
CREATE TRIGGER channel_audit_statementlevel_trigger AFTER INSERT OR UPDATE OR DELETE ON channels
FOR EACH STATEMENT EXECUTE PROCEDURE audit_statementlevel_trigger();
-- Perform some operations:
INSERT INTO channels(
SELECT C.Id, 'Channel-' || C.Id
FROM generate_series(1, 300, 10) AS C(Id)
);
DELETE FROM channels WHERE id < 10;
UPDATE channels
SET UserKey = 'wild channel'
WHERE id = 21;
I am interested knowing if this solution looks right to professional developers.
Am I going into the good direction or is this solution evil?
I have the following problem with two procedures updating a product and its attributes on Postgresql 9.4. First off the tables:
CREATE TABLE product
(
id uuid NOT NULL,
version bigint NOT NULL,
is_part_of_import_id uuid,
name text NOT NULL,
owner_id uuid NOT NULL,
replaced_by_id uuid,
state character varying(255) NOT NULL,
replaces_id uuid,
last_update timestamp without time zone,
...
)
CREATE TABLE attribute_value
(
id uuid NOT NULL,
version bigint NOT NULL,
last_update timestamp without time zone,
product_id uuid NOT NULL,
replaced_by_id uuid,
replaces_id uuid,
value text,
...
)
There is a one-to-many relationship between product and attribute_value. I have a procedure to update the attribute_value
CREATE OR REPLACE FUNCTION update_attribute_value(attr_val attribute_value)
RETURNS UUID AS
$BODY$
DECLARE new_id UUID;
BEGIN
attr_val.replaces_id := attr_val.id;
attr_val.id := uuid_generate_v4();
attr_val.last_update := NOW();
INSERT INTO attribute_value SELECT attr_val.* RETURNING attribute_value.id INTO new_id;
UPDATE attribute_value SET replaced_by_id = new_id WHERE id = attr_val.replaces_id;
RETURN attr_val.id;
END;
$BODY$
LANGUAGE plpgsql VOLATILE
and a procedure to update the product calling the update_attribute_value_procedure to update all attribute_values of the product as well
CREATE OR REPLACE FUNCTION update_product(prod product)
RETURNS UUID AS
$BODY$
DECLARE new_id UUID;
DECLARE attr_val attribute_value%ROWTYPE;
BEGIN
prod.replaces_id := prod.id;
prod.id := uuid_generate_v4();
prod.last_update := NOW();
INSERT INTO product SELECT prod.* RETURNING id INTO new_id;
UPDATE product SET replaced_by_id = new_id WHERE id = prod.replaces_id;
FOR attr_val IN
SELECT * FROM attribute_value WHERE product_id = prod.replaces_id
LOOP
attr_val.product_id = new_id;
PERFORM update_attribute_value(attr_val);
END LOOP;
RETURN new_id;
END;
$BODY$
LANGUAGE plpgsql VOLATILE
When I run update_product like this
select update_product(row('140613c5-3e83-4ae2-986e-5c6b824d5766', 0, '0ba5a6c8-2513-4163-a5bd-c460e10d2059', null, null,
'ce594f2c-87f9-497a-a0e5-7d3fbc4abd8a', null, 'aeabe6fe-b9e1-47e5-96a7-7bf16c56ddf4', 'Rotak 43 1', 'eaf6bea0-99c4-4759-8c38-d35b9ae11403', null,
'PENDING', null, null)::product);
I get the following error output:
ERROR: cannot assign non-composite value to a row variable
SQL state: 42804
Context: PL/pgSQL function change_original_attribute_value() line 15 at assignment
SQL statement "INSERT INTO attribute_value SELECT attr_val.* RETURNING attribute_value.id"
PL/pgSQL function update_attribute_value(attribute_value) line 9 at SQL statement
SQL statement "SELECT update_attribute_value(attr_val)"
PL/pgSQL function update_product(product) line 17 at PERFORM
This error makes absolutely no sense to me in this context. Does anyone has an idea whats wrong in here, or could this be a bug?
I found the error. The problem was inside the function change_original_attribute_value() that is triggered on insert of a product. In there a function was called that returns a UUID, but the type it was assigned to was a rowtype.
We are looking to convert our database over to Postgres (9.3.5), which I have no experience with, and I am trying to get our audit tables up and running. I understand that each table will need its own trigger, but all triggers can call a single function.
The trigger on the table is passing a list of the columns that need to be audited since some of our columns are not tracked.
Here are some of the posts I followed:
- https://stackoverflow.com/a/7915100/229897
- http://www.postgresql.org/docs/9.3/static/plpgsql-statements.html
- http://www.postgresql.org/docs/9.4/static/plpgsql-trigger.html
When I run this I get the error: ERROR: syntax error at or near "$1"
DROP TABLE IF EXISTS people;
DROP TABLE IF EXISTS a_people;
CREATE TABLE IF NOT EXISTS people (
record_id SERIAL PRIMARY KEY NOT NULL,
first_name VARCHAR NOT NULL,
last_name VARCHAR NOT NULL,
last_updated_on TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS a_people (
record_id SERIAL PRIMARY KEY NOT NULL,
a_record_id INT,
first_name VARCHAR NULL,
last_name VARCHAR NULL,
last_updated_on TIMESTAMP
);
/******************************************************/
--the function
CREATE OR REPLACE FUNCTION audit_func()
RETURNS TRIGGER AS
$BODY$
DECLARE
audit TEXT := TG_TABLE_SCHEMA || '.a_' || TG_TABLE_NAME;
cols TEXT := TG_ARGV[0];
BEGIN
EXECUTE format('INSERT INTO %1$s(a_%2$s) SELECT %2$s FROM ($1)', audit, cols) USING OLD;
NEW.last_updated_on = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$BODY$
LANGUAGE plpgsql;
/******************************************************/
--the trigger calling the function to update inbound records
CREATE TRIGGER build_user_full_name_trg
BEFORE UPDATE
ON people
FOR EACH ROW WHEN (OLD.* IS DISTINCT FROM NEW.*)
EXECUTE PROCEDURE audit_func('record_id,first_name,last_name');
/******************************************************/
INSERT INTO people (first_name, last_name) VALUES ('George','Lincoln');
UPDATE people SET last_name = 'Washington' WHERE first_name = 'George';
SELECT * FROM people;
I welcome your assistance (and patience)!
This subselect should work:
EXECUTE format('INSERT INTO %1$s(a_%2$s) SELECT %2$s FROM (select ($1).*) XX', audit, cols) USING OLD;
I would like to add a constraint that will check values from related table.
I have 3 tables:
CREATE TABLE somethink_usr_rel (
user_id BIGINT NOT NULL,
stomethink_id BIGINT NOT NULL
);
CREATE TABLE usr (
id BIGINT NOT NULL,
role_id BIGINT NOT NULL
);
CREATE TABLE role (
id BIGINT NOT NULL,
type BIGINT NOT NULL
);
(If you want me to put constraint with FK let me know.)
I want to add a constraint to somethink_usr_rel that checks type in role ("two tables away"), e.g.:
ALTER TABLE somethink_usr_rel
ADD CONSTRAINT CH_sm_usr_type_check
CHECK (usr.role.type = 'SOME_ENUM');
I tried to do this with JOINs but didn't succeed. Any idea how to achieve it?
CHECK constraints cannot currently reference other tables. The manual:
Currently, CHECK expressions cannot contain subqueries nor refer to
variables other than columns of the current row.
One way is to use a trigger like demonstrated by #Wolph.
A clean solution without triggers: add redundant columns and include them in FOREIGN KEY constraints, which are the first choice to enforce referential integrity. Related answer on dba.SE with detailed instructions:
Enforcing constraints “two tables away”
Another option would be to "fake" an IMMUTABLE function doing the check and use that in a CHECK constraint. Postgres will allow this, but be aware of possible caveats. Best make that a NOT VALID constraint. See:
Disable all constraints and table checks while restoring a dump
A CHECK constraint is not an option if you need joins. You can create a trigger which raises an error instead.
Have a look at this example: http://www.postgresql.org/docs/9.1/static/plpgsql-trigger.html#PLPGSQL-TRIGGER-EXAMPLE
CREATE TABLE emp (
empname text,
salary integer,
last_date timestamp,
last_user text
);
CREATE FUNCTION emp_stamp() RETURNS trigger AS $emp_stamp$
BEGIN
-- Check that empname and salary are given
IF NEW.empname IS NULL THEN
RAISE EXCEPTION 'empname cannot be null';
END IF;
IF NEW.salary IS NULL THEN
RAISE EXCEPTION '% cannot have null salary', NEW.empname;
END IF;
-- Who works for us when she must pay for it?
IF NEW.salary < 0 THEN
RAISE EXCEPTION '% cannot have a negative salary', NEW.empname;
END IF;
-- Remember who changed the payroll when
NEW.last_date := current_timestamp;
NEW.last_user := current_user;
RETURN NEW;
END;
$emp_stamp$ LANGUAGE plpgsql;
CREATE TRIGGER emp_stamp BEFORE INSERT OR UPDATE ON emp
FOR EACH ROW EXECUTE PROCEDURE emp_stamp();
...i did it so (nazwa=user name, firma = company name) :
CREATE TABLE users
(
id bigserial CONSTRAINT firstkey PRIMARY KEY,
nazwa character varying(20),
firma character varying(50)
);
CREATE TABLE test
(
id bigserial CONSTRAINT firstkey PRIMARY KEY,
firma character varying(50),
towar character varying(20),
nazwisko character varying(20)
);
ALTER TABLE public.test ENABLE ROW LEVEL SECURITY;
CREATE OR REPLACE FUNCTION whoIAM3() RETURNS varchar(50) as $$
declare
result varchar(50);
BEGIN
select into result users.firma from users where users.nazwa = current_user;
return result;
END;
$$ LANGUAGE plpgsql;
CREATE POLICY user_policy ON public.test
USING (firma = whoIAM3());
CREATE FUNCTION test_trigger_function()
RETURNS trigger AS $$
BEGIN
NEW.firma:=whoIam3();
return NEW;
END
$$ LANGUAGE 'plpgsql'
CREATE TRIGGER test_trigger_insert BEFORE INSERT ON test FOR EACH ROW EXECUTE PROCEDURE test_trigger_function();