Let's say we have a table like:
Table "someschema.todo"
Column | Type | Nullable | Default
--------------+--------------+----------+----------------------------------
id | integer | not null | nextval('todo_id_seq'::regclass)
contract_id | integer | not null |
title | text | |
description | text | |
status | todo_status | | 'INCOMPLETE'::todo_status
Foreign-key constraints:
"todo_contract_id_fkey" FOREIGN KEY (contract_id) REFERENCES contract(id)
Row Level Security is enabled, and we'll assume that the rules have been set up appropriately with one exception: a user type who would not normally be allowed to UPDATE rows in this table is required to be able to bump the status column from one enum value to another. Say, from INCOMPLETE to PENDING. That user type also needs to be able to UPDATE the table at other times (depending on conditions related to the contract_id fkey) so we can't just use a blanket column grant.
Arguably, this could make the column a candidate for inclusion in a new todo_status table, but let's just rule that out for the moment. Now we could write a trigger to check every column by name to see if it had been modified and only allow those queries which modify status and nothing else... but that seems fragile (what if we later add another column?) and painful.
Is there a way within a trigger to allow a modification of "no column except status"? In other words, "deny access unless the only column modified is status".
Supplemental: is there a way to accomplish this using a check_expression or using_expression within CREATE POLICY that I haven't considered? I've been assuming that because we don't have the NEW values in using_expression or the OLD values in check_expression, I can't use RLS to achieve what we need.
A trigger would be relatively robust
CREATE OR REPLACE FUNCTION validate_update()
RETURNS trigger AS
$BODY$
DECLARE
v_key TEXT;
v_value TEXT;
valid_update boolean := true;
BEGIN
FOR v_key, v_value IN select key, value from each(hstore(NEW)) each LOOP
if (coalesce(v_value,'') != coalesce((hstore(OLD) -> v_key),'')) then
if (v_key != 'status') then
valid_update := false;
end if;
end if;
END LOOP;
if (valid_update) then
raise info 'good update';
return NEW;
else
raise info 'bad update';
return null;
end if;
END;
$BODY$
LANGUAGE plpgsql;
create trigger validate_update before update on someschema.todo
for each row execute procedure validate_update();
Related
I have a PostgreSQL database with the following tables
person
city
country
Each person is linked to a city through a city_id foreign key, and each city is linked to a country through country_id in a similar way.
To easily view the names of each person's city and country, I create a view:
CREATE VIEW person_view AS
SELECT
person.id,
person.name,
city.name as city,
country.name as country
FROM person
LEFT JOIN city ON person.city_id = city.id
LEFT JOIN country ON city.country_id = country.id
Which gives something easy to read.
| id | name | city | country |
------------------------------------------
| 1 | Steve | New York | United States |
| 2 | Rachel | Paris | France |
Now, using a program like dbeaver, I was hoping to manage these entries using this view. Instead of looking up IDs whenever a person's city/country needs to change, it'd be much easier just to type in changes in the view and have those changes carry over to the original tables.
I thought that this was what an updatable view was meant for, but dbeaver will not allow this view to be updated directly, and suggests implementing INSTEAD OF UPDATE triggers or ON UPDATE DO INSTEAD rules.
Am I approaching this correctly? Is the operation I've described here what updatable views are meant to do?
demo
Only show update trigger. You can do similar thing for insert and delete.
CREATE OR REPLACE FUNCTION person_view_upd_trig_fn ()
RETURNS TRIGGER
AS $$
BEGIN
IF tg_op = 'UPDATE' THEN
IF NEW.name <> OLD.name THEN
RAISE NOTICE 'update person name';
UPDATE
person
SET
name = NEW.name
WHERE
id = OLD.id;
RETURN new;
END IF;
IF NEW.city <> OLD.city AND NEW.country = OLD.country THEN
RAISE NOTICE 'update city name';
IF (
SELECT
count(DISTINCT country_id)
FROM
city
WHERE
name = OLD.city OR name = NEW.city) = 2 THEN
RAISE EXCEPTION 'not good';
END IF;
IF (
SELECT
city_id
FROM
city
WHERE
name = NEW.city) IS NULL THEN
RAISE EXCEPTION 'city not in the list';
END IF;
UPDATE
person
SET
city_id = (
SELECT
city_id
FROM
city
WHERE
name = NEW.city)
WHERE
id = OLD.id;
RETURN new;
END IF;
IF NEW.country <> OLD.country AND NEW.city <> OLD.city THEN
RAISE NOTICE 'updating person country & city';
IF NOT EXISTS (
SELECT
FROM
country
WHERE
name = NEW.country) THEN
RAISE EXCEPTION 'not good';
END IF;
UPDATE
person
SET
city_id = (
SELECT
city_id
FROM
city
WHERE
name = NEW.city)
WHERE
id = OLD.id;
RETURN new;
END IF;
RAISE NOTICE 'new.person_view:%', new;
RAISE NOTICE 'old.person_view:%', old;
RETURN NULL;
END IF;
END
$$
LANGUAGE plpgsql;
create trigger:
CREATE TRIGGER person_view_upd_trig
INSTEAD OF UPDATE ON person_view
FOR EACH ROW EXECUTE PROCEDURE person_view_upd_trig_fn();
person_view id column cannot update, update will have no effect. all other 3 column can update.
IF tg_op = 'UPDATE' THEN is not that redundant. You can add more control block, like IF tg_op = 'DELETE THEN in this function. Then one function, you can control 3 actions/trigger(delete, update, insert), instead of 3 function and 3 triggers.
https://www.postgresql.org/docs/current/plpgsql-trigger.html
A trigger function must return either NULL or a record/row value
having exactly the structure of the table the trigger was fired for.
and
INSTEAD OF triggers (which are always row-level triggers, and may only
be used on views) can return null to signal that they did not perform
any updates, and that the rest of the operation for this row should be
skipped (i.e., subsequent triggers are not fired, and the row is not
counted in the rows-affected status for the surrounding
INSERT/UPDATE/DELETE). Otherwise a nonnull value should be returned,
to signal that the trigger performed the requested operation. For
INSERT and UPDATE operations, the return value should be NEW, which
the trigger function may modify to support INSERT RETURNING and UPDATE
RETURNING (this will also affect the row value passed to any
subsequent triggers, or passed to a special EXCLUDED alias reference
within an INSERT statement with an ON CONFLICT DO UPDATE clause). For
DELETE operations, the return value should be OLD.
I have a shared trigger function used by multiple tables.
Each tables's primary key uses the format pk_ + tablename i.e. pk_account or pk_order
How can I dynamically reference the NEW object column in a trigger using a variable?
CREATE OR REPLACE FUNCTION shared_test_trigger_fn()
RETURNS trigger AS
$$
DECLARE
primary_key TEXT DEFAULT CONCAT('pk_', TG_TABLE_NAME);
primary_value TEXT DEFAULT CONCAT('NEW.pk_', TG_TABLE_NAME);
BEGIN
RAISE NOTICE 'The PK field is called %', primary_key;
RAISE NOTICE 'The PK value is %', primary_value;
RETURN NEW;
END;
$$
LANGUAGE 'plpgsql';
DROP TRIGGER IF EXISTS account_test_trigger ON account;
CREATE TRIGGER account_test_trigger BEFORE UPDATE
ON account
FOR EACH ROW
EXECUTE PROCEDURE shared_test_trigger_fn();
UPDATE account SET created = created WHERE pk_account = 1;
EDIT1:
This trigger is simply an example to demonstrate the issue. The scenario is, we have a number of tables sharing a similar format with the PK field including the table name. Rather than code & maintain multiple similar trigger functions we are looking to have a single trigger function that can work with multiple tables.
It can be done fairly easy in plpython3u:
CREATE OR REPLACE FUNCTION public.pk_tablename()
RETURNS trigger
LANGUAGE plpython3u
AS $function$
pk = "pk_" + TD["table_name"]
pk_val = TD["new"][pk]
plpy.notice("PK value is " + str(pk_val))
$function$
CREATE TRIGGER trg_pk_tablename BEFORE UPDATE
ON animals
FOR EACH ROW
EXECUTE PROCEDURE pk_tablename();
animals
Table "public.animals"
Column | Type | Collation | Nullable | Default
------------+------------------------+-----------+----------+---------
pk_animals | integer | | not null |
cond | character varying(200) | | not null |
animal | character varying(200) | | not null |
Indexes:
"animals_pkey" PRIMARY KEY, btree (pk_animals)
Triggers:
trg_pk_tablename BEFORE UPDATE ON animals FOR EACH ROW EXECUTE FUNCTION pk_tablename()
update animals set cond = 'good' where pk_animals = 200;
NOTICE: PK value is 200
UPDATE 1
I have made attempts using plpgsql but have not found the magic sauce yet.
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.
I am currently using postgres 8.3. I have created a table that acts as a dirty flag table for members that exist in another table. I have applied triggers after insert or update on the members table that will insert/update a record on the modifications table with a value of true. The trigger seems to work, however I am noticing that something is flipping the boolean is_modified value. I have no idea how to go about trying to isolate what could be flipping it.
Trigger function:
BEGIN;
CREATE OR REPLACE FUNCTION set_member_as_modified() RETURNS TRIGGER AS $set_member_as_modified$
BEGIN
LOOP
-- first try to update the key
UPDATE member_modification SET is_modified = TRUE, updated = current_timestamp WHERE "memberID" = NEW."memberID";
IF FOUND THEN
RETURN NEW;
END IF;
--member doesn't exist in modification table, so insert them
-- if someone else inserts the same key conncurrently, raise a unique-key failure
BEGIN
INSERT INTO member_modification("memberID",is_modified,updated) VALUES(NEW."memberID", TRUE,current_timestamp);
RETURN NEW;
EXCEPTION WHEN unique_violation THEN
-- do nothing, and loop to try the update again
END;
END LOOP;
END;
$set_member_as_modified$ LANGUAGE plpgsql;
COMMIT;
CREATE TRIGGER set_member_as_modified AFTER INSERT OR UPDATE ON members FOR EACH ROW EXECUTE PROCEDURE set_member_as_modified();
Here is the sql I run and the results:
$CREATE TRIGGER set_member_as_modified AFTER INSERT OR UPDATE ON members FOR EACH ROW EXECUTE PROCEDURE set_member_as_modified();
Results:
UPDATE 1
bluesky=# select * from member_modification;
-[ RECORD 1 ]---+---------------------------
modification_id | 14
is_modified | t
updated | 2011-05-26 09:49:47.992241
memberID | 182346
bluesky=# select * from member_modification;
-[ RECORD 1 ]---+---------------------------
modification_id | 14
is_modified | f
updated | 2011-05-26 09:49:47.992241
memberID | 182346
As you can see something flipped the is_modified value. Is there anything in postgres I can use to determine what queries/processes are acting on this table?
Are you sure you've posted everything needed? The two queries on member_modification suggest that a separate query is being run in between, which sets is_modified back to false.
You could add an text[] field to member_modification, e.g. query_trace text[] not null default '{}', then and a before insert/update trigger on each row on that table which goes something like:
NEW.query_trace := NEW.query_trace || current_query();
If current_query() is not available in 8.3, see this:
http://www.postgresql.org/docs/8.3/static/monitoring-stats.html
SELECT pg_stat_get_backend_pid(s.backendid) AS procpid,
pg_stat_get_backend_activity(s.backendid) AS current_query
FROM (SELECT pg_stat_get_backend_idset() AS backendid) AS s;
You could then get the list of subsequent queries that affected it:
select query_trace[i] from generate_series(1, array_length(query_trace, 1)) as i
(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 |