You could do something like this:
Example table:
CREATE TABLE t1 (user_id text, photo_id text, photo_limit_count integer NOT NULL, date_created timestamptz);
Add a check constraint:
ALTER TABLE t1 ADD CONSTRAINT photo_limit_count_chk CHECK (photo_limit_count > 0 AND photo_limit_count <= 30);
Create a unique index, to make sure that there can't accidentally be duplicates for the user_id and number of photos:
CREATE UNIQUE INDEX t1_user_id_photo_limit_count ON t1(user_id,photo_limit_count);
Create a trigger to auto increment the photo_limit_count for you:
CREATE OR REPLACE FUNCTION trig_inc_photo_limit() RETURNS TRIGGER AS $BODY$
DECLARE
row_count integer;
BEGIN
SELECT count(*) INTO row_count FROM t1 WHERE user_id = NEW.user_id;
IF row_count = 0 THEN
NEW.photo_limit_count = 1;
RETURN NEW;
ELSE
NEW.photo_limit_count = row_count + 1;
RETURN NEW;
END IF;
END;
$BODY$ LANGUAGE plpgsql;
CREATE TRIGGER t1_inc_photo_limit_trigger BEFORE INSERT ON t1 FOR EACH ROW EXECUTE PROCEDURE trig_inc_photo_limit();
Do an insert:
INSERT INTO t1 (user_id, photo_id, date_created) VALUES ('a','1',now());
postgres@[local]:5432:postgres:=# INSERT INTO t1 (user_id, photo_id, date_created) VALUES ('a','1',now());
INSERT 0 1
Time: 34.559 ms
postgres@[local]:5432:postgres:=#
Insert 30 more times:
postgres@[local]:5432:postgres:=# INSERT INTO t1 (user_id, photo_id, date_created) VALUES ('a','1',now());
ERROR: new row for relation "t1" violates check constraint "photo_limit_count_chk"
DETAIL: Failing row contains (a, 1, 31, 2015-08-06 22:52:10.594288-07).
Time: 0.178 ms
postgres@[local]:5432:postgres:=# SELECT count(*) FROM t1 WHERE user_id = 'a';
count
-------
30
(1 row)
Time: 0.225 ms
postgres@[local]:5432:postgres:=#
Doing the same thing again for b, 31 times:
postgres@[local]:5432:postgres:=# INSERT INTO t1 (user_id, photo_id, date_created) VALUES ('b','1',now());
ERROR: new row for relation "t1" violates check constraint "photo_limit_count_chk"
DETAIL: Failing row contains (b, 1, 31, 2015-08-06 22:52:10.594288-07).
Time: 0.322 ms
postgres@[local]:5432:postgres:=# SELECT user_id, count(*) FROM t1 GROUP BY user_id HAVING count(*) = 30;
user_id | count
---------+-------
b | 30
a | 30
(2 rows)
Time: 0.419 ms
postgres@[local]:5432:postgres:=#
Hope that helps with your problem. =)
pg_advisory_xact_lock()
can also be a good solution. You can take the lock with the user ID as the lock key, then query the count, then do the insert if the count is < 30. It's easy and flexible, but again, requires application code to be written well and play nice (they have to all agree to take the lock before doing any inserts).