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

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

Related

Postgresql function (upsert and delete): how to pass a set of rows of table type to function call

I have a table
CREATE TABLE items(
id SERIAL PRIMARY KEY,
group_id INT NOT NULL,
item_id INT NOT NULL,
name TEXT,
.....
.....
);
I am creating a function that
takes set of row values for a single group_id, fail if multiple group_ids present in in input rows
compares it with matching values in the table (only for that group_id
updates changed values (only for the input group_id)
inserts new values
deletes table rows that are absent in the row input (compare rows with group_id and item_id)(only for the input group_id)
this is my function definition
CREATE OR REPLACE FUNCTION update_items(rows_input items[]) RETURNS boolean as $$
DECLARE
rows items[];
group_id_input integer;
BEGIN
-- get single group_id from input rows, fail if multiple group_id's present in input
-- read items of that group_id in table
-- compare input rows and table rows (of the same group_id)
-- create transaction
-- delete absent rows
-- upsert
-- return success of transaction (boolean)
END;
$$ LANGUAGE plpgsql;
I am trying to call the function in a query
select update_items(
(38,1,1283,"Name1"),
(39,1,1471,"Name2"),
(40,1,1333,"Name3")
);
I get the following error
Failed to run sql query: column "Name1" does not exist
I tried removing the id column values: that gives me the same error
What is the correct way to pass row values to a function that accepts table type array as arguments?
updates changed values
inserts new values deletes table rows that are
absent in the row input (compare rows with group_id and item_id)
If you want do upsert, you must upsert with unique constraint.
So there is two unique constraints. primary key(id), (group_id, item_id).
insert on conflict need consider these two unique constraint.
Since You want pass items[] type to the functions. So it also means that any id that is not in the input function arguments will also be deleted.
drop table if exists items cascade;
begin;
CREATE TABLE items(
id bigint GENERATED BY DEFAULT as identity PRIMARY KEY,
group_id INT NOT NULL,
item_id INT NOT NULL,
name TEXT
,unique(group_id,item_id)
);
insert into items values
(38,1,1283,'original_38'),
(39,1,1471,'original_39'),
(40,1,1333,'original_40'),
(42,1,1332,'original_42');
end;
main function:
CREATE OR REPLACE FUNCTION update_items (in_items items[])
RETURNS boolean
AS $FUNC$
DECLARE
iter items;
saved_ids bigint[];
BEGIN
saved_ids := (SELECT ARRAY (SELECT (unnest(in_items)).id));
DELETE FROM items
WHERE NOT (id = ANY (saved_ids));
FOREACH iter IN ARRAY in_items LOOP
INSERT INTO items
SELECT
iter.*
ON CONFLICT (id)
DO NOTHING;
INSERT INTO items
SELECT
iter.*
ON CONFLICT (group_id,
item_id)
DO UPDATE SET
name = EXCLUDED.name;
RAISE NOTICE 'rec.groupid: %, rec.items_id:%', iter.group_id, iter.item_id;
END LOOP;
RETURN TRUE;
END
$FUNC$
LANGUAGE plpgsql;
call it:
SELECT
*
FROM
update_items ('{"(38, 1, 1283, Name1) "," (39, 1, 1471, Name2) "," (40, 1, 1333, Name3)"}'::items[]);
references:
Iterating over integer[] in PL/pgSQL
How to match elements in an array of composite type?
IN vs ANY operator in PostgreSQL
Here's how I achieved UPSERT with DELETE missing rows, if anyone is looking to do the same.
CREATE OR REPLACE FUNCTION update_items(in_rows items[]) RETURNS INT AS $$
DECLARE
in_groups INTEGER[];
in_group_id INTEGER;
in_item_ids INTEGER[];
BEGIN
-- get single group id from input rows, fail if multiple group ids present in input
in_groups = (SELECT ARRAY (SELECT distinct(group_id) FROM UNNEST(in_rows)));
IF ARRAY_LENGTH(in_groups,1)>1 THEN
RAISE EXCEPTION 'Multiple group_ids found in input items: %', in_groups;
END IF;
in_group_id = in_groups[1];
-- delete items of this group that are absent in in_rows
in_item_ids := (SELECT ARRAY (SELECT (UNNEST(in_rows)).item_id));
DELETE FROM items
WHERE
master_code <> ANY (in_item_ids)
AND group_id = in_group_id;
-- upsert in_rows
INSERT INTO items
SELECT * FROM UNNEST(in_rows)
ON CONFLICT (group_id,item_d)
DO UPDATE SET
parent_group_id = EXCLUDED.parent_group_id,
mat_centre_id = EXCLUDED.mat_centre_id,
NAME = EXCLUDED.NAME,
opening_date = EXCLUDED.opening_date;
RETURN in_group_id;
-- return success of transaction (boolean)
END;
$$ LANGUAGE plpgsql;
This function removes rows that are missing from your in_rows

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 create a SQL Tables that updates when the date changes

Currently I'm creating an app that can essentially create post-it notes. I'm working on making my SQL tables for it. What I want to do is make it so the tables data is searchable by date. Multiple posts may be made on a day obviously. So I'm putting the date into a separate table. What I'm wondering is if it's possible to make it so the date column on the date table is not the current date that it will auto increment the Id and create a new column with the current date
CREATE TABLE IF NOT EXISTS ideas (
id SERIAL PRIMARY KEY,
ideas text,
date_id int );
CREATE TABLE IF NOT EXISTS date (
id SERIAL PRIMARY KEY,
table_date CONVERT(VARCHAR(15), GETDATE(),10));
Is the code I have so far any and all suggestions are welcome!
I would recommend using a TRIGGER procedure. You can trigger a function every time an insert is made on the ideas table. This function can check the dates table and make sure the current date exists in there. It can even set the new id of that date in the date_id column in the ideas table.
For example:
DROP TABLE IF EXISTS ideas;
CREATE TABLE ideas (
id SERIAL PRIMARY KEY,
ideas text,
date_id int
);
-- "date" is a reserved word. try to avoid naming a table "date".
DROP TABLE IF EXISTS dates;
CREATE TABLE dates (
id SERIAL PRIMARY KEY,
table_date DATE DEFAULT NOW() -- i would recommend the DATE type here
);
DROP TRIGGER IF EXISTS insert_date_if_absent ON ideas;
DROP FUNCTION IF EXISTS insert_date_if_absent();
CREATE FUNCTION insert_date_if_absent()
RETURNS TRIGGER
AS $$
DECLARE
today date := now();
new_date_id integer;
BEGIN
IF NOT EXISTS (SELECT * FROM dates WHERE table_date = today) THEN
INSERT INTO dates (table_date) VALUES (today) RETURNING id INTO new_date_id;
ELSE
SELECT id FROM dates WHERE table_date = today INTO new_date_id;
END IF;
IF NEW.date_id IS NULL THEN
NEW.date_id := new_date_id;
END IF;
RETURN NEW;
END
$$ LANGUAGE PLPGSQL;
CREATE TRIGGER insert_date_if_absent
BEFORE INSERT ON ideas
FOR EACH ROW
EXECUTE PROCEDURE insert_date_if_absent();
This will allow you to omit date_id when inserting into ideas. If omitted, it will get automatically set by the trigger to the id of today's date.
INSERT INTO ideas (ideas) VALUES ('sup dudeee');
Some other feedback which I incorporated in my answer:
Do not store dates as a VARCHAR, it's less efficient and more hassle. Use a DATE instead.
Do not name tables after reserved words in Postgres. Rather than date, name it dates.

PostgreSQL inherited table and insert triggers

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.

Expected NUMBER got DATE

I am getting an SQL error that makes no sense to me. I created a table called SUBORDER using this command:
CREATE TABLE SUBORDER (
OrderNo int,
SuborderNo int,
ReqShipDate date,
ActualShipDate date,
BranchName varchar(50),
primary key (OrderNo, SuborderNo),
foreign key (OrderNo) references ORDERS(OrderNo) on delete set null,
foreign key (BranchName) references BRANCH(BranchName) on delete set null
);
As you can see, ReqShipDate and ActualShipDate were declared as dates. But then when I run this code:
create or replace trigger shipment_late
before update
on SUBORDER
for each row
declare
reqdate DATE;
actualdate DATE;
begin
select s.ReqShipDate, s.ActualShipDate into reqdate, actualdate
from SUBORDER s
where ( s.OrderNo = :new.OrderNo ) and ( s.SuborderNo = :new.SuborderNo );
if reqdate > actualdate then
insert into LATE_SHIPMENT
select * from SUBORDER s
where ( s.OrderNo = :new.OrderNo ) and ( s.SuborderNo = :new.SuborderNo );
end if;
end;
the SQL console tells me:
Error(12,11): PL/SQL: ORA-00932: inconsistent datatypes: expected
NUMBER got DATE
Why is it expecting a number?
It appears that you just want
create or replace trigger shipment_late
before update
on SUBORDER
for each row
begin
if :new.ReqShipDate > :new.ActualShipDate
then
insert into LATE_SHIPMENT( <<list of columns>> )
values( :new.OrderNo, :new.SuborderNo, ... );
end if;
end;
A couple things to be aware of
It's always a good idea to explicitly list out the columns if you're doing an insert statement. Otherwise, you're relying on an implicit order that isn't going to be obvious to someone reading your code and that creates quite a bit of potential for errors. Listing out the columns, at a minimum, documents what values you are putting where. In this example, it seems unlikely that the late_shipment table has exactly the same set of columns in suborder defined in exactly the same order.
In general, a row-level trigger on a table (SubOrder in this case) cannot query that table. Doing so will generally raise a mutating table exception. That doesn't appear to be necessary here, however. You just need to reference the various attributes of the :new pseudo-record.