postgresql message_id auto increment for each user - postgresql

I am building chatroom using Go,PostgreSQL.
I have this table -> messages
id | user | message_id | message
I want for each user, message_id Start from 1 and And increase automatically.
for example :
id | user | message_id | message
1 | one | 1 | Hello
2 | two | 1 | Hi!
3 | one | 2 | How are you?
4 | one | 3 | :)
Thanks in advance.

Without duplicate values and to safely increase message_id field we use SELECT FOR UPDATE command.
FOR UPDATE causes the rows retrieved by the SELECT statement to be locked as though for update. This prevents them from being locked, modified or deleted by other transactions until the current transaction ends. That is, other transactions that attempt UPDATE, DELETE, SELECT FOR UPDATE, SELECT FOR NO KEY UPDATE, SELECT FOR SHARE or SELECT FOR KEY SHARE of these rows will be blocked until the current transaction ends; conversely, SELECT FOR UPDATE will wait for a concurrent transaction that has run any of those commands on the same row, and will then lock and return the updated row.
Now let's write a simple function for inserting messages:
CREATE OR REPLACE FUNCTION insert_message(p_user character varying, p_message text)
RETURNS void
LANGUAGE plpgsql
AS $function$
DECLARE
v_new_id integer;
v_id integer;
begin
-- selecting user last inserting message and get last message_id, and blocking this record for update command
select mm.id, mm.message_id into v_id, v_new_id
from messages mm
where
mm."user" = p_user
order by
mm.message_id desc
limit 1
for update;
if (v_new_id is null) then
v_new_id = 0;
end if;
insert into messages
(
"user",
message_id,
message_text
) values
(
p_user,
v_new_id + 1,
p_message
);
-- after inserting message we must unblock record
if (v_new_id > 0) then
update messages
set
updated_date = now()
where id = v_id;
end if;
END;
$function$
;
As you can see from the code for increasing message_id we must filter records by user and then the ordered message_id field descending. Given that in future this table will have million records then performance will be bad. For good performancing let's write new function:
CREATE OR REPLACE FUNCTION insert_message(p_user character varying, p_message text)
RETURNS void
LANGUAGE plpgsql
AS $function$
DECLARE
v_new_id integer;
v_id integer;
begin
-- in users table add new last_id field, for storing last message_id value by user
-- and then block this record
select last_id into v_new_id from users
where "user" = p_user
for update;
insert into messages
(
"user",
message_id,
message_text
) values
(
p_user,
v_new_id + 1,
p_message
);
-- unblocking user record
update users
set
last_id = v_new_id + 1
where "user" = p_user;
END;
$function$
;
This code was simple and high-performance.

Related

user creation dumping using trigger and fucntions

As PostgreSQL doesn't dump object creation date so I want to manually dump user creation date by using trigger and functions. I have created trigger and functions but it's not working.
CREATE TABLE user_audits (
usesysid INT GENERATED ALWAYS AS IDENTITY,
usename varchar NOT NULL,
created_on TIMESTAMP(6) NOT NULL
);
============================
CREATE OR REPLACE FUNCTION user_creation()
RETURNS TRIGGER
LANGUAGE PLPGSQL
AS
$$
BEGIN
IF NEW.usename <> OLD.usename THEN
INSERT INTO user_audits(usesysid,usename,created_on)
VALUES(usesysid,usename,now());
END IF;
RETURN NEW;
END;
$$
=================================
CREATE TRIGGER user_creation
BEFORE UPDATE
ON user
FOR EACH ROW
EXECUTE PROCEDURE user_creation();
This is important for audit purpose, for now I am using log file to check creation date but it will rotate after sometime.
Please suggest the better way to dump user creation date in table so that I can retrieve the information anytime.
Thanks
I created a similar excercise with the following tables:
The user_tbl table having only a identity column usersysid and the username
CREATE TABLE user_tbl (
usersysid INT GENERATED ALWAYS AS IDENTITY,
username varchar NOT NULL
);
The user_audits table, slightly modified version of yours: where i added an id identity field. I removed the identity from the usersysid field (since it'll be populated with the one coming from user_tbl)
CREATE TABLE user_audits (
id INT GENERATED ALWAYS AS IDENTITY,
usersysid INT,
username varchar NOT NULL,
created_on TIMESTAMP(6) NOT NULL
);
Now the function, I check if the OLD.username is null, this means that is an insert, if NEW.username <> OLD.username then is an update.
CREATE OR REPLACE FUNCTION user_creation()
RETURNS TRIGGER
LANGUAGE PLPGSQL
AS
$$
BEGIN
IF OLD.username is null OR NEW.username <> OLD.username THEN
INSERT INTO user_audits(usersysid,username,created_on)
VALUES(NEW.usersysid,NEW.username,now());
END IF;
RETURN NEW;
END;
$$
;
And finally the trigger, which is fired both on INSERT or UPDATE
CREATE TRIGGER user_creation
BEFORE INSERT OR UPDATE
ON user_tbl
FOR EACH ROW
EXECUTE PROCEDURE user_creation();
Now if I create two new rows and update one with the following
insert into user_tbl (username) values('Carlo');
insert into user_tbl (username) values('Gianni');
update user_tbl set username='Giorgio' where usersysid=1;
I end up with the user_tbl containing the 2 expected rows
defaultdb=> select * from user_tbl;
usersysid | username
-----------+----------
2 | Gianni
1 | Giorgio
(2 rows)
and the user_audits tables containing 3 rows (2 for the insert + 1 for the update)
defaultdb=> select * from user_audits;
id | usersysid | username | created_on
----+-----------+----------+----------------------------
1 | 1 | Carlo | 2021-06-04 13:57:44.810889
2 | 2 | Gianni | 2021-06-04 13:58:14.680878
3 | 1 | Giorgio | 2021-06-04 13:58:44.702364
(3 rows)

PostgreSQL: `NEW.<value>` returning `NULL` when `AFTER TRIGGER` is fired

Description of the problem:
Hi everyone, i have this table:
CREATE TABLE COMPORDINE (
CodProdotto CHAR(5) NOT NULL CHECK(CodProdotto ~* '^[0-9]+$'),
CodOrdine CHAR(5) CHECK(CodOrdine ~* '^[0-9]+$'),
Prezzo REAL NOT NULL, CHECK(Prezzo >= 0.0)
CONSTRAINT OrdineFK FOREIGN KEY(CodOrdine) REFERENCES ORDINE(CodOrdine)
CONSTRAINT ProdottoFK FOREIGN KEY(CodProdotto) REFERENCES PRODOTTO(CodProdotto)
);
on which I declared a TRIGGER:
CREATE TRIGGER updatePrezzoOrdine
AFTER INSERT ON COMPORDINE
FOR EACH ROW
EXECUTE PROCEDURE updatePrezzoOrdine();
and a TRIGGER FUNCTION:
CREATE OR REPLACE FUNCTION updatePrezzoOrdine()
RETURNS TRIGGER
LANGUAGE plpgsql
AS
$$
DECLARE
Totale REAL;
BEGIN
SELECT SUM(Prezzo) INTO Totale
FROM COMPORDINE AS CO
WHERE CO.CodOrdine = NEW.CodOrdine
GROUP BY CodOrdine;
UPDATE ORDINE SET PrezzoTotale = PrezzoTotale + Totale
WHERE CodOrdine = NEW.CodOrdine;
RETURN NEW;
END
$$
Everything works and the trigger just fires fine, but no UPDATE is done on table ORDINE (on line 12).
What I experience (and tried):
I've tried to put some print instructions to see if the query was working:
RAISE NOTICE 'NEW.CodOrdine = %', NEW.CodOrdine;
RAISE NOTICE 'NEW.CodProdotto = %', NEW.CodProdotto;
RAISE NOTICE 'NEW.Prezzo = %', NEW.Prezzo;
but the output returns that new row inserted:
INSERT INTO COMPORDINE VALUES ('12345', '11111', 1.80)
has null values:
NOTICE: NEW.CodOrdine = <NULL>
NOTICE: NEW.CodProdotto = <NULL>
NOTICE: NEW.Prezzo = <NULL>
So the UPDATE "fails" because no row in ORDINE matches NULL values:
CodOrdine
PrezzoTotale
DataAcquisto
CodCliente
CodDipendente
12345
default (0.0)
2020-12-12
00000
99999
Apparently the problem could be related to the inserted row, may it?
What I expect:
After INSERT I would the ORDINE.PrezzoTotale column to be updated with the correct total price of every item in COMPORDINE that matches COMPORDINE.CodOrdine = ORDINE.CodOrdine in this way:
INSERT INTO COMPORDINE VALUES ('12345', '11111', 1.80)
CodOrdine
PrezzoTotale
DataAcquisto
CodCliente
CodDipendente
12345
1.80
2020-12-12
00000
99999
Big thanks to anyone in advance
[EDIT #1]:
As asked, here's the output of \d compordine:
**"public.compordine" table:**
| Colonna | Tipo | Ordinamento | Pu‗ essere null | Default|
|------------|--------------|-------------|-----------------|--------|
|codprodotto | character(5) | | not null | |
|codordine | character(5) | | not null | |
|prezzo | real | | not null | |
**Indexes**:
"compordine_codprodotto_key" UNIQUE CONSTRAINT, btree (codprodotto)
**Check constraints**:
"compordine_codordine_check" CHECK (codordine ~* '^[0-9]+$'::text)
"compordine_codprodotto_check" CHECK (codprodotto ~* '^[0-9]+$'::text)
"compordine_prezzo_check" CHECK (prezzo > 0::double precision)
**Referential integrity constraints**:
"ordinefk" FOREIGN KEY (codordine) REFERENCES ordine(codordine) ON UPDATE CASCADE
"prodottofk" FOREIGN KEY (codprodotto) REFERENCES prodotto(codprodotto) ON UPDATE CASCADE
**Triggers**:
updateprezzoordine AFTER INSERT ON compordine FOR EACH STATEMENT EXECUTE FUNCTION updateprezzoordine()
**Disabled triggers**:
updatescorte BEFORE INSERT ON compordine FOR EACH ROW EXECUTE FUNCTION updatescorte()
[EDIT #2]:
So, I did several other attempts to find a solution (SPOILER ALERT: no solution found); what I did, was to create another function and trigger that simply prints the NEW.<column> value after an INSERT statement on a different table called CLIENTE (my suspect was that all this issue was releated only to COMPORDINE table but I was wrong), here's the code:
CREATE TABLE CLIENTE (
CodCliente CHAR(5) PRIMARY KEY, CHECK(CodCliente ~* '^[0-9]+$'),
Nome VARCHAR(255) NOT NULL, CHECK(Nome ~* '^[A-Za-z ]+$'),
Cognome VARCHAR(255) NOT NULL, CHECK(Cognome ~* '^[A-Za-z ]+$'),
CodiceFiscale CHAR(16) NOT NULL, CHECK(CodiceFiscale ~* '^[A-Z]{6}\d{2}[A-Z]\d{2}[A-Z]\d{3}[A-Z]$'),
Indirizzo VARCHAR(300) NOT NULL,
Email VARCHAR(255) NOT NULL, CHECK(Email ~* '^[A-Za-z0-9._%-]+#[A-Za-z0-9.-]+[.][A-za-z]+$'),
UNIQUE(CodiceFiscale, Email)
);
CREATE TRIGGER print
BEFORE INSERT ON Cliente
EXECUTE PROCEDURE print();
CREATE FUNCTION print()
RETURNS TRIGGER
LANGUAGE plpgsql
AS
$$
BEGIN
RAISE NOTICE 'CodCliente = %', NEW.CodCliente;
RETURN NEW;
END
$$
INSERT INTO CLIENTE VALUES ('34567', 'Gianfranco', 'Rana', 'GNFRNA87F24E123A', 'Via Delle Rane 23', 'franco.rane#ravioli.it')
Output returns:
NEW.CodCliente = <NULL>
While it should be:
NEW.CodCliente = <34567>
I simply can't understand why, shouldn't NEW in an AFTER INSERT TRIGGER returns the value this way? :
Event
OLD
NEW
INSERT
NULL
New Record
DELETE
Old Record
NULL
UPDATE
Original Record
Updated Record
Had the same issue. For me the issue was the way I created the trigger:
create trigger pending_balance_add after insert on pending_balance
execute function update_pending_balance();
I forgot to tell that "for each row" and perhaps "procedure" instead of function:
create trigger pending_balance_add before insert on pending_balance
for each row
execute procedure update_pending_balance();
Now inside of update_pending_balance I have correct new.amount, etc.
Regarding your issue, it shows that
Triggers:
updateprezzoordine AFTER INSERT ON compordine FOR EACH STATEMENT EXECUTE FUNCTION updateprezzoordine()
Disabled triggers:
updatescorte BEFORE INSERT ON compordine FOR EACH ROW EXECUTE FUNCTION updatescorte()
When trigger is executed for statement it has NEW.FIELD values as NULL
PostgreSQL Can't change values in AFTER trigger. Change the trigger to BEFORE.
Does not matter if you change the variable record NEW, the data was already changed in the table record. AFTER triggers ignore NEW changes. You can read and write but only read matters.

Postgres Count records inserted/ updated

I'm trying to keey track of a clients database with which we sync. I need to record records_added (INSERTs) and records_updated (UPDATEs) to our table.
I'm using an UPSERT to handle the sync, and a trigger to update a table keeping track of insert/updates.
The issue is counting records that have are updated. I have 40+ columns to check, do I have to put all these in my check logic? Is there a more elegant way?
Section of code in question:
select
case
when old.uuid = new.uuid
and (
old.another_field != new.another_field,
old.and_another_field != new.and_another_field,
-- many more columns here << This is particularly painful
) then 1
else 0
end into update_count;
Reproducible example:
-- create tables
CREATE TABLE IF NOT EXISTS example (uuid serial primary key, another_field int, and_another_field int);
CREATE TABLE IF NOT EXISTS tracker_table (
records_added integer DEFAULT 0,
records_updated integer DEFAULT 0,
created_at date unique
);
-- create function
CREATE OR REPLACE FUNCTION update_records_inserted () RETURNS TRIGGER AS $body$
DECLARE update_count INT;
DECLARE insert_count INT;
BEGIN
-- ---------------- START OF BLOCK IN QUESTION -----------------
select
case
when old.uuid = new.uuid
and (
old.another_field != new.another_field
-- many more columns here
) then 1
else 0
end into update_count;
-- ------------------ END OF BLOCK IN QUESTION ------------------
-- count INSERTs
select
case
when old.uuid is null
and new.uuid is not null then 1
else 0
end into insert_count;
-- --log the counts
-- raise notice 'update %', update_count;
-- raise notice 'insert %', insert_count;
-- insert or update count to tracker table
insert into
tracker_table(
created_at,
records_added,
records_updated
)
VALUES
(CURRENT_DATE, insert_count, update_count) ON CONFLICT (created_at) DO
UPDATE
SET
records_added = tracker_table.records_added + insert_count,
records_updated = tracker_table.records_updated + update_count;
RETURN NEW;
END;
$body$ LANGUAGE plpgsql;
-- Trigger
DROP TRIGGER IF EXISTS example_trigger ON example;
CREATE TRIGGER example_trigger
AFTER
INSERT
OR
UPDATE
ON example FOR EACH ROW EXECUTE PROCEDURE update_records_inserted ();
-- A query to insert, then update when number of uses > 1
insert into example(whatever) values (2, 3) ON CONFLICT(uuid) DO UPDATE SET another_field=excluded.another_field+1;

Raise error without rollback in plpgsql/postgresql

I have two stored functions. delete_item which deletes one item, logging it's success or failure in an actionlog table, it returns a 1 on errors and 0 when running successfully.
Secondly I have another function remove_expired that finds what to delete and loops through it calling delete_item.
All this is intended to be called using a simple bash script (hard requirement from operations, so calling like this is not up for discussion), and it have to give an error code when things doesn't work for their reporting tools.
We want all the deletions possible to succeed (we don't expect errors, but humans are still humans, and errors does happen), so if we want to delete 10 items and 1 fails, we still want the other 9 to be deleted.
Secondly we would really like the logs to be in the table actionlog both in the success and error case. I.e., we want that log to be complete.
Since plpgsql functions doesn't allow manual transaction management that seems to not be an option (unless I missed a way to circumvent this?).
The only way I've found so far to achieve this is to wrap scripts around it outside plpgsql, but we would very much like this to be possible in pure plpgsql so we can just give operations a pssql -C ... command and then they shouldn't be concerned with anything else.
SQL to reproduce the problem:
DROP FUNCTION IF EXISTS remove_expired(timestamp with time zone);
DROP FUNCTION IF EXISTS delete_item(integer);
DROP TABLE IF EXISTS actionlog;
DROP TABLE IF EXISTS evil;
DROP TABLE IF EXISTS test;
CREATE TABLE test (
id serial primary key not null,
t timestamp with time zone not null
);
CREATE TABLE evil (
test_id integer not null references test(id)
);
CREATE TABLE actionlog (
eventTime timestamp with time zone not null default now(),
message text not null
);
INSERT INTO test (actualTime, t)
VALUES ('2020-04-01T10:00:00+0200'),
('2020-04-01T10:15:00+0200'), -- Will not be deleable due to foreign key
('2020-04-01T10:30:00+0200')
;
INSERT INTO evil (test_id) SELECT id FROM test WHERE id = 2;
CREATE OR REPLACE FUNCTION remove_expired(timestamp with time zone)
RETURNS void
AS
$$
DECLARE
test_id int;
failure_count int = 0;
BEGIN
FOR test_id IN
SELECT id FROM test WHERE t < $1
LOOP
failure_count := delete_item(test_id) + failure_count;
END LOOP;
IF failure_count > 0 THEN
-- I want this to cause 'psql ... -c "SELECT * FROM remove_expred...' to exit with exit code != 0
RAISE 'There was one or more errors deleting. See the log for details';
END IF;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION delete_item(integer)
RETURNS integer
AS
$$
BEGIN
DELETE FROM test WHERE id = $1;
INSERT INTO actionlog (message)
VALUES ('Deleted with ID: ' || $1);
RETURN 0;
EXCEPTION WHEN OTHERS THEN
INSERT INTO actionlog (message)
VALUES ('Error deleting ID: ' || $1 || '. The error was: ' || SQLERRM);
RETURN 1;
END
$$ LANGUAGE plpgsql;
Thanks in advance for any useful input
You can have something close to what your expect in PostgreSQL 11 or PostgreSQL 12 but only with procedures because as already said functions will always roll everything back in case of errors.
With:
DROP PROCEDURE IF EXISTS remove_expired(timestamp with time zone);
DROP PROCEDURE IF EXISTS delete_item(integer);
DROP FUNCTION IF EXISTS f_removed_expired;
DROP SEQUENCE IF EXISTS failure_count_seq;
DROP TABLE IF EXISTS actionlog;
DROP TABLE IF EXISTS evil;
DROP TABLE IF EXISTS test;
CREATE TABLE test (
id serial primary key not null,
t timestamp with time zone not null
);
CREATE TABLE evil (
test_id integer not null references test(id)
);
CREATE TABLE actionlog (
eventTime timestamp with time zone not null default now(),
message text not null
);
INSERT INTO test (t)
VALUES ('2020-04-01T10:00:00+0200'),
('2020-04-01T10:15:00+0200'), -- Will not be removed due to foreign key
('2020-04-01T10:30:00+0200')
;
select * from test where t < current_timestamp;
INSERT INTO evil (test_id) SELECT id FROM test WHERE id = 2;
CREATE SEQUENCE failure_count_seq MINVALUE 0;
SELECT SETVAL('failure_count_seq', 0, true);
CREATE OR REPLACE PROCEDURE remove_expired(timestamp with time zone)
AS
$$
DECLARE
test_id int;
failure_count int = 0;
return_code int;
BEGIN
FOR test_id IN
SELECT id FROM test WHERE t < $1
LOOP
call delete_item(test_id);
COMMIT;
END LOOP;
SELECT currval('failure_count_seq') INTO failure_count;
IF failure_count > 0 THEN
-- I want this to cause 'psql ... -c "SELECT * FROM remove_expred...' to exit with exit code != 0
RAISE 'There was one or more errors deleting. See the log for details';
END IF;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE PROCEDURE delete_item(in integer)
AS
$$
DECLARE
forget_value int;
BEGIN
DELETE FROM test WHERE id = $1;
INSERT INTO actionlog (message)
VALUES ('Deleted with ID: ' || $1);
EXCEPTION WHEN OTHERS THEN
INSERT INTO actionlog (message)
VALUES ('Error deleting ID: ' || $1 || '. The error was: ' || SQLERRM);
COMMIT;
SELECT NEXTVAL('failure_count_seq') INTO forget_value;
END
$$ LANGUAGE plpgsql;
--
I get:
select * from test;
id | t
----+------------------------
1 | 2020-04-01 10:00:00+02
2 | 2020-04-01 10:15:00+02
3 | 2020-04-01 10:30:00+02
(3 rows)
select current_timestamp;
current_timestamp
-------------------------------
2020-04-01 16:52:26.171975+02
(1 row)
call remove_expired(current_timestamp);
psql:test.sql:80: ERROR: There was one or more errors deleting. See the log for details
CONTEXT: PL/pgSQL function remove_expired(timestamp with time zone) line 17 at RAISE
select currval('failure_count_seq');
currval
---------
1
(1 row)
select * from test;
id | t
----+------------------------
2 | 2020-04-01 10:15:00+02
(1 row)
select * from actionlog;
eventtime | message
-------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------
2020-04-01 16:52:26.172173+02 | Deleted with ID: 1
2020-04-01 16:52:26.179794+02 | Error deleting ID: 2. The error was: update or delete on table "test" violates foreign key constraint "evil_test_id_fkey" on table "evil"
2020-04-01 16:52:26.196503+02 | Deleted with ID: 3
(3 rows)
I use a sequence to record the number of failures: you can use this sequence to test for failures and return the right return code.

PostgreSQL: UPDATE implies move across partitions

(Note: updated with adopted answer below.)
For a PostgreSQL 8.1 (or later) partitioned table, how does one define an UPDATE trigger and procedure to "move" a record from one partition to the other, if the UPDATE implies a change to the constrained field that defines the partition segregation?
For example, I've a table records partitioned into active and inactive records like so:
create table RECORDS (RECORD varchar(64) not null, ACTIVE boolean default true);
create table ACTIVE_RECORDS ( check (ACTIVE) ) inherits RECORDS;
create table INACTIVE_RECORDS ( check (not ACTIVE) ) inherits RECORDS;
The INSERT trigger and function work well: new active records get put in one table, and new inactive records in another. I would like UPDATEs to the ACTIVE field to "move" a record from one one descendant table to the other, but am encountering an error which suggests that this may not be possible.
Trigger specification and error message:
pg=> CREATE OR REPLACE FUNCTION record_update()
RETURNS TRIGGER AS $$
BEGIN
IF (NEW.active = OLD.active) THEN
RETURN NEW;
ELSIF (NEW.active) THEN
INSERT INTO active_records VALUES (NEW.*);
DELETE FROM inactive_records WHERE record = NEW.record;
ELSE
INSERT INTO inactive_records VALUES (NEW.*);
DELETE FROM active_records WHERE record = NEW.record;
END IF;
RETURN NULL;
END;
$$
LANGUAGE plpgsql;
pg=> CREATE TRIGGER record_update_trigger
BEFORE UPDATE ON records
FOR EACH ROW EXECUTE PROCEDURE record_update();
pg=> select * from RECORDS;
record | active
--------+--------
foo | t -- 'foo' record actually in table ACTIVE_RECORDS
bar | f -- 'bar' record actually in table INACTIVE_RECORDS
(2 rows)
pg=> update RECORDS set ACTIVE = false where RECORD = 'foo';
ERROR: new row for relation "active_records" violates check constraint "active_records_active_check"
Playing with the trigger procedure (returning NULL and so forth) suggests to me that the constraint is checked, and the error raised, before my trigger is invoked, meaning that my current approach won't work. Can this be gotten to work?
ADDITIONAL ANSWER
pg's [list partitioning][2] appears to be the easiest way to accomplish this:
-- untested!
create table RECORDS (..., ACTIVE boolean...)
partition by list(ACTIVE) (
partition ACTIVE_RECORDS values (true),
partition INACTIVE_RECORDS values (false)
)
UPDATE/ANSWER
Below is the UPDATE trigger procedure I ended up using, the same procedure assigned to each of the partitions. Credit is entirely to Bell, whose answer gave me the key insight to trigger on the partitions:
CREATE OR REPLACE FUNCTION record_update()
RETURNS TRIGGER AS $$
BEGIN
IF ( (TG_TABLE_NAME = 'active_records' AND NOT NEW.active)
OR
(TG_TABLE_NAME = 'inactive_records' AND NEW.active) ) THEN
DELETE FROM records WHERE record = NEW.record;
INSERT INTO records VALUES (NEW.*);
RETURN NULL;
END IF;
RETURN NEW;
END;
$$
LANGUAGE plpgsql;
It can be made to work, the trigger that does the move just needs to be defined for each partition, not the whole table. So start as you did for table definitions and the INSERT trigger
CREATE TABLE records (
record varchar(64) NOT NULL,
active boolean default TRUE
);
CREATE TABLE active_records (CHECK (active)) INHERITS (records);
CREATE TABLE inactive_records (CHECK (NOT active)) INHERITS (records);
CREATE OR REPLACE FUNCTION record_insert()
RETURNS TRIGGER AS $$
BEGIN
IF (TRUE = NEW.active) THEN
INSERT INTO active_records VALUES (NEW.*);
ELSE
INSERT INTO inactive_records VALUES (NEW.*);
END IF;
RETURN NULL;
END;
$$
LANGUAGE plpgsql;
CREATE TRIGGER record_insert_trigger
BEFORE INSERT ON records
FOR EACH ROW EXECUTE PROCEDURE record_insert();
... let's have some test data ...
INSERT INTO records VALUES ('FirstLittlePiggy', TRUE);
INSERT INTO records VALUES ('SecondLittlePiggy', FALSE);
INSERT INTO records VALUES ('ThirdLittlePiggy', TRUE);
INSERT INTO records VALUES ('FourthLittlePiggy', FALSE);
INSERT INTO records VALUES ('FifthLittlePiggy', TRUE);
Now the triggers on the partitions. The if NEW.active = OLD.active check is implicit in checking the value of active since we know what's allowed to be in the table in the first place.
CREATE OR REPLACE FUNCTION active_partition_constraint()
RETURNS TRIGGER AS $$
BEGIN
IF NOT (NEW.active) THEN
INSERT INTO inactive_records VALUES (NEW.*);
DELETE FROM active_records WHERE record = NEW.record;
RETURN NULL;
ELSE
RETURN NEW;
END IF;
END;
$$
LANGUAGE plpgsql;
CREATE TRIGGER active_constraint_trigger
BEFORE UPDATE ON active_records
FOR EACH ROW EXECUTE PROCEDURE active_partition_constraint();
CREATE OR REPLACE FUNCTION inactive_partition_constraint()
RETURNS TRIGGER AS $$
BEGIN
IF (NEW.active) THEN
INSERT INTO active_records VALUES (NEW.*);
DELETE FROM inactive_records WHERE record = NEW.record;
RETURN NULL;
ELSE
RETURN NEW;
END IF;
END;
$$
LANGUAGE plpgsql;
CREATE TRIGGER inactive_constraint_trigger
BEFORE UPDATE ON inactive_records
FOR EACH ROW EXECUTE PROCEDURE inactive_partition_constraint();
... and test the results ...
scratch=> SELECT * FROM active_records;
record | active
------------------+--------
FirstLittlePiggy | t
ThirdLittlePiggy | t
FifthLittlePiggy | t
(3 rows)
scratch=> UPDATE records SET active = FALSE WHERE record = 'ThirdLittlePiggy';
UPDATE 0
scratch=> SELECT * FROM active_records;
record | active
------------------+--------
FirstLittlePiggy | t
FifthLittlePiggy | t
(2 rows)
scratch=> SELECT * FROM inactive_records;
record | active
-------------------+--------
SecondLittlePiggy | f
FourthLittlePiggy | f
ThirdLittlePiggy | f
(3 rows)
Beware that you can partition by list and let the database do all the hard work to move rows among partitions.
(untested for 8.4 but most probably working, as for pilcrow comment).
In the following example, a table is created and partitioned by list, using one of the columns in the primary key.
create table t (
-- natural primary key
doc_type varchar not null default 'PRODUCT',
doc_id int not null generated always as identity,
-- content columns
title varchar not null,
-- primary key
primary key (doc_type, doc_id)
)
partition by list(doc_type);
-- partitions of t
create table t_product partition of t for values in ('PRODUCT');
create table t_default partition of t default;
Then we insert some data that should end in t_product or t_default, depending on the value of doc_type.
insert into t (doc_type, title) values
('PRODUCT', 'My first product'), -- 1
('ARTICLE', 'My first article'), -- 2
('TOPIC', 'My first topic'), -- 3
('PRODUCT', 'My second product'), -- 4
('PRODUCT', 'My third product'), -- 5
('ARTICLE', 'My second article'), -- 6
('TOPIC', 'My second topic'), -- 7
('PRODUCT', 'My fourth product'); -- 8
We check rows are automatically moved to the right table
select * from t_product;
doc_type|doc_id|title |
--------+------+-----------------+
PRODUCT | 1|My first product |
PRODUCT | 4|My second product|
PRODUCT | 5|My third product |
PRODUCT | 8|My fourth product|
Now, let us convert a PRODUCT into an ARTICLE to see what happens.
update t
set doc_type = 'ARTICLE'
where doc_type = 'PRODUCT'
and doc_id = 1;
It can be seen the row is not in the t_product partition anymore
select * from t_product;
doc_type|doc_id|title |
--------+------+-----------------+
PRODUCT | 4|My second product|
PRODUCT | 5|My third product |
PRODUCT | 8|My fourth product|
but in the t_default partition.
doc_type|doc_id|title |
--------+------+-----------------+
ARTICLE | 2|My first article |
TOPIC | 3|My first topic |
ARTICLE | 6|My second article|
TOPIC | 7|My second topic |
ARTICLE | 1|My first product |