Background
In a PostgreSQL 9.0 database, there are various tables that have many-to-many relationships. The number of those relationships must be restricted. A couple of example tables include:
CREATE TABLE authentication (
id bigserial NOT NULL, -- Primary key
cookie character varying(64) NOT NULL, -- Authenticates the user with a cookie
ip_address character varying(40) NOT NULL -- Device IP address (IPv6-friendly)
)
CREATE TABLE tag_comment (
id bigserial NOT NULL, -- Primary key
comment_id bigint, -- Foreign key to the comment table
tag_name_id bigint -- Foreign key to the tag name table
)
Different relationships, however, have different limitations. For example, in the authentication
table, a given ip_address
is allowed 1024 cookie
values; whereas, in the tag_comment
table, each comment_id
can have 10 associated tag_name_id
s.
Problem
Currently, a number of functions have these restrictions hard-coded; scattering the limitations throughout the database, and preventing them from being changed dynamically.
Question
How would you impose a maximum many-to-many relationship limit on tables in a generic fashion?
Idea
Create a table to track the limits:
CREATE TABLE imposed_maximums (
id serial NOT NULL,
table_name character varying(128) NOT NULL,
column_group character varying(128) NOT NULL,
column_count character varying(128) NOT NULL,
max_size INTEGER
)
Establish the restrictions:
INSERT INTO imposed_maximums
(table_name, column_group, column_count, max_size) VALUES
('authentication', 'ip_address', 'cookie', 1024);
INSERT INTO imposed_maximums
(table_name, column_group, column_count, max_size) VALUES
('tag_comment', 'comment_id', 'tag_id', 10);
Create a trigger function:
CREATE OR REPLACE FUNCTION impose_maximum()
RETURNS trigger AS
$BODY$
BEGIN
-- Join this up with imposed_maximums somehow?
select
count(1)
from
-- the table name
where
-- the group column = NEW value to INSERT;
RETURN NEW;
END;
Attach the trigger to every table:
CREATE TRIGGER trigger_authentication_impose_maximum
BEFORE INSERT
ON authentication
FOR EACH ROW
EXECUTE PROCEDURE impose_maximum();
Obviously it won't work as written... is there a way to make it work, or otherwise enforce the restrictions such that they are:
- in a single location; and
- not hard-coded?
Thank you!
I've been doing a similar type of generic triggers. The most tricky part is to get the value entry in the
NEW
record based on the column name.I'm doing it the following way:
NEW
data into array;attnum
of the column and use it as an index for the array.This approach works as long as there're no commas in the data :( I don't know of other ways how to convert
NEW
orOLD
variables into the array of values.The following function might help:
This function will check for all conditions defined for a given table.
These function + trigger could be used as a template. If You combine them with @Sorrow 's technique of dynamically generating the functions + triggers, this could solve the OP's problem. Please note that, instead of recalculating the count for every affected row (by calling the COUNT() aggregate function), I maintain an 'incremental' count. This should be cheaper.
RESULTS:
Yes, there is a way to make it work.
In my personal opinion your idea is the way to go. It just needs one level of "meta". So, the table
imposed_restrictions
should have trigger(s), which is (are) firedafter insert
,update
anddelete
. The code should then in turn create, modify or remove triggers and functions.Take a look at
execute
statement of PL/pgSQL, which - essentially - allows you to execute any string. Needless to say, this string may contain definitions of triggers, functions, etc. Obviously, you have the access toOLD
andNEW
in the triggers, so you can fill in the placeholders in the string and you are done.I believe you should be able to accomplish what you want with this answer. Please note that this is my personal view on the topic and it might not be an optimal solution - I would like to see a different, maybe also more efficient, approach.
Edit - Below is a sample from one of my old projects. It is located inside the function that is triggered
before update
(though now I get to think of it, maybe it should have been calledafter
;) And yes, the code is messy, as it does not use the nice$escape$
syntax. I was really, really young then. Nonetheless, the snipped demonstrates that it is possible to achieve what you want.