PostgreSQL inherited table and insert triggers - sql

I'm trying to follow the advice here to create a vertically partitioned table for storing time series data.
So far, my schema looks like this:
CREATE TABLE events
(
topic text,
t timestamp,
value integer,
primary key(topic, t)
);
CREATE TABLE events_2014
(
primary key (topic, t),
check (t between '2014-01-01' and '2015-01-01')
) INHERITS (events);
Now I'm trying to create an INSTEAD OF INSERT trigger so that events can be inserted on the events table and the row will end up in the right sub-table. But the documentation says that INSTEAD OF INSERT triggers can only be created on views, not tables (or subtables):
CREATE OR REPLACE FUNCTION insert_events () RETURNS TRIGGER AS $insert_events$ BEGIN
IF new.t between '2014-01-01' and '2015-01-01' THEN
INSERT INTO events_2014 SELECT new.*;
...
END IF
RETURN NULL;
END;
$insert_events$ LANGUAGE PLPGSQL;
CREATE TRIGGER insert_events INSTEAD OF INSERT ON events FOR EACH ROW EXECUTE PROCEDURE insert_events();
ERROR: "events" is a table
DETAIL: Tables cannot have INSTEAD OF triggers.
What's the right way of doing this?

You need to declare BEFORE INSERT triggers.
Documentation on partitioning is a great source of knowledge in this matter and is full of examples.
Example function from docs
CREATE OR REPLACE FUNCTION measurement_insert_trigger()
RETURNS TRIGGER AS $$
BEGIN
IF ( NEW.logdate >= DATE '2006-02-01' AND
NEW.logdate < DATE '2006-03-01' ) THEN
INSERT INTO measurement_y2006m02 VALUES (NEW.*);
ELSIF ( NEW.logdate >= DATE '2006-03-01' AND
NEW.logdate < DATE '2006-04-01' ) THEN
INSERT INTO measurement_y2006m03 VALUES (NEW.*);
...
ELSIF ( NEW.logdate >= DATE '2008-01-01' AND
NEW.logdate < DATE '2008-02-01' ) THEN
INSERT INTO measurement_y2008m01 VALUES (NEW.*);
ELSE
RAISE EXCEPTION 'Date out of range. Fix the measurement_insert_trigger() function!';
END IF;
RETURN NULL;
END;
$$
LANGUAGE plpgsql;
Example trigger from docs
CREATE TRIGGER insert_measurement_trigger
BEFORE INSERT ON measurement
FOR EACH ROW EXECUTE PROCEDURE measurement_insert_trigger();
Returning NULL from BEFORE trigger will keep the parent table empty.

Related

Trigger for conditional insert into table

I have subscription data that is being appended to a table in real-time (via kafka). i have set up a trigger such that once the data is added it is checked for consistency. If checks pass some of the data should be added to other tables (that have master information on the customer profile etc.). The checks function i wrote works fine but i keep getting errors on the function used in the trigger. The function for the trigger is:
CREATE OR REPLACE FUNCTION update_tables()
RETURNS TRIGGER
LANGUAGE plpgsql
AS
$$
BEGIN
CASE (SELECT check_incoming_data()) WHEN 0
THEN INSERT INTO
sub_master(sub_id, sub_date, customer_id, product_id)
VALUES(
(SELECT sub_id::int FROM sub_realtime WHERE CTID = (SELECT MAX(CTID) FROM sub_realtime)),
(SELECT sub_date::date FROM sub_realtime WHERE CTID = (SELECT MAX(CTID) FROM sub_realtime)),
(SELECT customer_id::int FROM sub_realtime WHERE CTID = (SELECT MAX(CTID) FROM sub_realtime)),
(SELECT product_id::int FROM sub_realtime WHERE CTID = (SELECT MAX(CTID) FROM sub_realtime))
);
RETURN sub_master;
END CASE;
RETURN sub_master;
END;
$$
The trigger is then:
CREATE TRIGGER incoming_data
AFTER INSERT
ON claims_realtime_3
FOR EACH ROW
EXECUTE PROCEDURE update_tables();
What I am saying is 'if checks pass then select data from the last added row and add them to the master table'. What is the best way to structure this query?
Thanks a lot!
The trigger functions are executed for each row and you must use a record type variable called "NEW" which is automatically created by the database in the trigger functions. "NEW" gets only inserted records. For example, I want to insert data to users_log table when inserting records to users table.
CREATE OR REPLACE FUNCTION users_insert()
RETURNS trigger
LANGUAGE plpgsql
AS $function$
begin
insert into users_log
(
username,
first_name,
last_name
)
select
new.username,
new.first_name,
new.last_name;
return new;
END;
$function$;
create trigger store_data_to_history_insert
before insert
on users for each row execute function users_insert();

What are the different ways of adding a constraint so that only items that are available on the order date can be inserted?

order.date must be between item.date_from and item.date_to... what are the different ways of doing that?
CREATE TABLE "item" (
"id" SERIAL PRIMARY KEY,
"date_from" DATE NOT NULL,
"date_to" DATE NOT NULL
);
CREATE TABLE "order" (
"id" SERIAL PRIMARY KEY,
"date" DATE NOT NULL
);
CREATE TABLE "order_item" (
"order" INTEGER NOT NULL REFERENCES "order",
"item" INTEGER NOT NULL REFERENCES "item"
);
Check constraints work on simple expressions. For example, a simple sanity check on the order: check( date > '2010-01-01'). There's also exclusion constraints which check no two rows have the same value as defined by the exclusion. But, with the exception of foreign key constraints, constraints don't query other tables.
You can solve this with a trigger on insert and update, and I'll go into that below, but its better to solve this sort of problem with referential integrity. However, I can't think of a way to do that.
You can make a view of available items for the order. Here $1 is the date of the order.
create temporary view items_available_to_order
select *
-- pluralize table names to avoid conflicting with keywords and columns
from items
-- date_from and date_to has become a single daterange when_available
where items.when_available #> $1
Then only insert items from that view.
If you want to go the trigger route (you can do both) write a function which checks whether an order's item is valid. It either raises an exception or returns a trigger. new is the inserted row, or the row after an update.
I changed some of the table and column names and types to avoid common pitfalls.
create function check_item_order_is_valid()
returns trigger
language 'plpgsql'
as $body$
declare
item_is_available boolean;
begin
select
items.when_available #> orders.ordered_on into item_is_available
from item_orders
join items on items.id = new.order_id
join orders on orders.id = new.item_id;
if( not item_is_available) then
raise exception 'Item #% is not available for order #%',
new.item_id, new.order_id;
end if;
return new;
end
$body$
Then define a trigger to call the function when rows are inserted or updated in the item/order table.
create trigger check_item_orders
before insert or update
on item_orders
for each row
execute function check_item_order_is_valid();
Demonstration.
What if the valid range of an item changes? You need an update trigger on item to check that its orders are still valid. Maybe. Depends on your business logic.
A test example:
CREATE OR REPLACE FUNCTION public.item_date()
RETURNS trigger
LANGUAGE plpgsql
AS $function$
DECLARE
order_date date;
from_date date;
to_date date;
BEGIN
select into order_date "date" from "order" where id = new.order;
select into from_date, to_date date_from, date_to from item where id = new.item;
--Use date range to test whether order date is in item date range.
if order_date <# daterange(from_date, to_date, '[]') then
return new;
else
return null;
end if;
END;
$function$
create trigger item_date_check before insert or update on order_item for each row execute function item_date();
insert into item values (1, '09/01/2021', '10/31/2021');
insert into item values (2, '07/01/2021', '08/31/2021');
insert into "order" values (1, '09/05/2021');
insert into order_item values (1, 1);
NOTICE: Order date 2021-09-05, from_date 2021-09-01, to_date 2021-10-31
INSERT 0 1
--Returning NULL causes the INSERT not to happen.
insert into order_item values (1, 2);
NOTICE: Order date 2021-09-05, from_date 2021-07-01, to_date 2021-08-31
INSERT 0 0
Note that I had to quote "order" as that is a reserved word also. You might to take a look at Key(reserved) Words. For range functions/operators see Range Function. For general information on range(s) see Range Types

How can I create a Postgres 11 trigger function which inserts a new row in table 'b' upon insert or update to table 'a'?

I'm running Postgres 11 on RDS.
I'm trying to create a simple trigger function to insert records into table 'test_alias' whenever a row is inserted into table 'test_values'.
I have the following tables:
CREATE TABLE the_schema.test_values (
id INTEGER NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
created_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
value_1 TEXT NOT NULL,
value_2 TEXT NOT NULL,
value_quantity INTEGER NOT NULL
);
CREATE TABLE the_schema.test_alias (
id INTEGER NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
created_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
value_1_copy TEXT NOT NULL,
value_2_copy TEXT NOT NULL,
value_quantity_copy INTEGER NOT NULL
);
My trigger function is like so:
CREATE OR REPLACE FUNCTION the_schema.populate_test_alias()
RETURNS TRIGGER AS
$BODY$
BEGIN
IF NEW.the_schema.test_values THEN
INSERT INTO the_schema.test_alias (value_1_copy, value_2_copy, value_quantity_copy)
VALUES (NEW.value_1, NEW.value_2, NEW.value_quantity);
END IF;
return null;
END;
$BODY$ LANGUAGE plpgsql;
And here is the trigger:
CREATE TRIGGER TRG_TEST_ALIAS
AFTER INSERT OR UPDATE ON the_schema.test_values
FOR EACH ROW
execute procedure the_schema.populate_test_alias();
Upon INSERT like so:
INSERT INTO the_schema.test_values (value_1, value_2, value_quantity)
VALUES ('abc', 'xyz', 1);
I get this error:
ERROR: missing FROM-clause entry for table "the_schema"
LINE 1: SELECT NEW.the_schema.test_values
I've also tried an equivalent setup with the default schema, and it still errors (though with a different error):
ERROR: record "new" has no field "test_values"
CONTEXT: SQL statement "SELECT NEW.test_values"
PL/pgSQL function populate_test_alias() line 3 at IF
It seems to me that there must be an error in the way I'm using the NEW keyword, but as far as I can tell, the way I've used it in the function is the same as several examples I've referred to (online/SO and hard copy), and I can't figure it out.
All guidance is much appreciated!
example of similar question for reference, includes links to official docs (which I've also read but clearly don't understand as I should):
[https://stackoverflow.com/questions/11001118/postgres-trigger-after-insert-accessing-new]
NEW references the inserted or updated row. Therefore NEW. only makes sense with a field identifier.
Also value_1, value_2 and value_quantity have a NOT NULL constraint, which means that you need not test for them.
So you can just drop the whole conditional:
CREATE OR REPLACE FUNCTION the_schema.populate_test_alias()
RETURNS TRIGGER AS
$BODY$
BEGIN
--IF NEW.the_schema.test_values THEN
INSERT INTO the_schema.test_alias (value_1_copy, value_2_copy, value_quantity_copy)
VALUES (NEW.value_1, NEW.value_2, NEW.value_quantity);
--END IF;
return null;
END;
$BODY$ LANGUAGE plpgsql;

How to restrict any DML operation in a stored procedure

I am trying to insert some records into a table for a particular month. How do I restrict any DML operations on that table for rest of the other months in a stored procedure (without any trigger or constraints). Please help me on this, Thanks in advance.
Pass the "particular month" as a parameter and use it in code throughout the procedure, most probably in WHERE clauses or IFs, CASEs etc.
Create 2 users:
The first (we can call it DATA_OWNER) will own the tables containing the data and the stored procedures/packages that perform the DML on those tables; and
The second (we can call it END_USER) will be the database user that your end users connect as.
Then you can create the tables:
CREATE TABLE data_owner.table_name (
id NUMBER
GENERATED ALWAYS AS IDENTITY
PRIMARY KEY,
datetime DATE
NOT NULL,
value NUMBER
);
CREATE TABLE data_owner.table_name_insert_bounds (
start_datetime DATE,
end_datetime DATE,
CONSTRAINT table_name_insert_bounds__pk PRIMARY KEY ( start_datetime, end_datetime ),
CONSTRAINT table_name_insert_bounds__chk CHECK ( start_datetime <= end_datetime )
);
And a package containing your stored procedures:
CREATE PACKAGE data_owner.table_name_management_pkg IS
PROCEDURE add_value(
i_datetime IN TABLE_NAME.DATETIME%TYPE,
i_value IN TABLE_NAME.VALUE%TYPE
);
END;
/
CREATE PACKAGE BODY data_owner.table_name_management_pkg IS
PROCEDURE add_value(
i_datetime IN TABLE_NAME.DATETIME%TYPE,
i_value IN TABLE_NAME.VALUE%TYPE
)
IS
valid_datetime NUMBER;
BEGIN
SELECT CASE
WHEN EXISTS(
SELECT 1
FROM table_name_insert_bounds
WHERE i_datetime BETWEEN start_datetime AND end_datetime
)
THEN 1
ELSE 0
END
INTO valid_datetime
FROM DUAL;
IF valid_datetime = 0 THEN
RAISE_APPLICATION_ERROR(
-20000,
'Date-time is outside of current insertion range.'
);
END IF;
INSERT INTO table_name ( datetime, value )
VALUES ( i_datetime, i_value );
END;
END;
/
And then grant permissions to the end user:
GRANT SELECT ON data_owner.table_name TO end_user;
GRANT EXECUTE ON data_owner.table_name_management_pkg TO end_user;
Then the end user can only perform DML actions via the stored procedure in the package (they have no permissions to perform DML directly on the table bypassing the procedure) but could also SELECT all the data and the data owner can set the date bounds which the end user is able to insert within (and the end user cannot modify these bounds).
For example:
If the data owner sets these boundaries:
INSERT INTO table_name_insert_bounds ( start_datetime, end_datetime )
VALUES ( DATE '2020-01-01', DATE '2020-02-01' );
Then the end user can run:
BEGIN
data_owner.table_name_management_pkg.ADD_VALUE( DATE '2020-01-10', 42 );
END;
/
and one row will be inserted but if they try to:
BEGIN
data_owner.table_name_management_pkg.ADD_VALUE( DATE '2020-02-02', 13 );
END;
/
Then will get the exception:
ORA-20000: Date-time is outside of current insertion range.
db<>fiddle here

Trigger to delete past records in postgresql

I want only to maintain present 1 month records log details. need to delete past record log details.I tried this code however could not work this,
create sequence userseq1;
CREATE TABLE users
( id integer NOT NULL DEFAULT nextval('userseq1'::regclass)
);
INSERT INTO users VALUES(126);
CREATE TABLE History
( userid integer
, createdate timestamp
);
CREATE OR REPLACE FUNCTION recordcreatetime() RETURNS trigger language plpgsql
AS $$
DECLARE
BEGIN
INSERT INTO History values(new.id,NOW() );
RETURN NEW;
END;
$$;
CREATE TRIGGER user_hist
BEFORE INSERT ON users
FOR EACH ROW
EXECUTE procedure recordcreatetime();
However it is working to insert values sequencing one by one adding.I want to delete the previous 1 month record Log details.I tried this below code and it is not working
CREATE OR REPLACE FUNCTION trf_keep_row_number_steady()
RETURNS TRIGGER AS
$body$
DECLARE
BEGIN
IF (SELECT count(createdate) FROM history) > rownum_limit
THEN
DELETE FROM history
WHERE createdate = (SELECT min(createdate) FROM history);
END IF;
END;
$body$
LANGUAGE plpgsql;
CREATE TRIGGER tr_keep_row_number_steady
AFTER INSERT ON history
FOR EACH ROW EXECUTE PROCEDURE trf_keep_row_number_steady();
I can see in your second code block, you have a trigger on history table and you are trying to DELETE FROM history in that same trigger.
Insert / Update / Delete on a table through a trigger on the same table is not allowed. Please think of some other alternative, e.g., running a separate DELETE statement for the required cleanup of rows before or after your main INSERT statement.