2

I'm creating a user table using postgres. My table is the following...

create table if not exists "user" (
    user_id bigserial primary key,
    username varchar(20) not null,
    "password" varchar(100) not null,
    created_datetime timestamp default (now() at time zone 'utc')
);

Is it possible for create_datetime to only use the default column, even if an insert script attempts to inserts a timestamp into created_datetime? Preferably postgres would even thrown an error if an insert script tries to insert a timestamp into the create_datetime column.

3 Answers 3

2

Create a trigger that will fill that column disregarding any value provided in INSERT:

create or replace function t_user_created_datetime() returns trigger as $$
begin
  new.created_datetime := now() at time zone 'utc';
  return new;
end;
$$ language plpgsql;

create trigger t_user_created_datetime
  before insert on "user"
  for each row execute procedure t_user_created_datetime();

Check:

test=# insert into "user"(user_id, username, "password", created_datetime) values(1, 'test', 'test', '1900-01-01'::timestamp);
INSERT 0 1
test=# select * from "user";
 user_id | username | password |      created_datetime
---------+----------+----------+----------------------------
       1 | test     | test     | 2017-07-27 18:21:24.501701
(1 row)

With such trigger, you can remove default (now() at time zone 'utc') from the table's definition, because it becomes useless.

If you do want to see an error when the column's value is explicitly set in INSERT, then change the trigger function to something like this:

create or replace function t_user_created_datetime() returns trigger as $$
begin
  if new.created_datetime is not null then
    raise exception 'It is forbidden to explicitly set "created_datetime" during INSERT to "user" table.';
  end if;
  new.created_datetime := now() at time zone 'utc';
  return new;
end;
$$ language plpgsql;

In this case, the column created_datetime must not have default because otherwise you'll see that error always.

P.S. I stronly recommend consider using timestamptz – it is also 8 bytes, like timestamp, but case save a lot of efforts if you need (or will need in the future) to deal with multiple time zones.

Sign up to request clarification or add additional context in comments.

1 Comment

Thanks for the timestamptz suggestion!
2

Revoke the insert and update privileges on the table from all the roles:

revoke insert, update
on table "user"
from public, application_role
cascade

Then grant the insert and update privileges to the application role on the other columns only:

grant 
    insert (user_id, username, "password"),
    update (username, "password")
on table "user"
to application_role

Only the table owner will be able to insert and update on the created_datetime column.

3 Comments

"On the other hand, if a role has been granted privileges on a table, then revoking the same privileges from individual columns will have no effect." From there. So it should be revoke insert, update on ...; and then grant insert(...), update(...) on ...;
@Abelisto Ok I think I fixed it.
Except the unclear error message (something like "permission denied on relation user") it is probably the most efficient and flexible solution. PS: why not just grant insert(user_id, user_name, password), update(....?
1

To make the set of answers complete. Use rules.

First we will create the service function to be able to raise exceptions from SQL statements:

create or replace function public.fn_raise(msg text, cond boolean = true)
  returns bool
  immutable
  language plpgsql
as $$ 
begin
  if cond then
    raise exception '%', msg;
  end if;
  return false;
end $$;

Next lets create the test table:

create table t(i int, d timestamptz not null default current_timestamp);

And, finally, rule:

create or replace rule rul_t_insert as on insert to t
where new.d <> current_timestamp
do also
  select fn_raise(format('Can not insert %s into table t', new.d), new.d <> current_timestamp);

Lets test it:

postgres=# insert into t(i) values(1) returning *;
┌───┬───────────────────────────────┐
│ i │               d               │
╞═══╪═══════════════════════════════╡
│ 1 │ 2017-07-28 12:31:37.255392+03 │
└───┴───────────────────────────────┘

postgres=# insert into t(i,d) values(1,null) returning *;
ERROR:  null value in column "d" violates not-null constraint
DETAIL:  Failing row contains (1, null).

postgres=# insert into t(i,d) values(2,'2000-10-10') returning *;
ERROR:  Can not insert 2000-10-10 00:00:00+03 into table t

I the question mentioned only insert but if you also want to block updates of this field, you could to create another rule:

create or replace rule rul_t_update as on update to t
where new.d <> old.d
do also
  select fn_raise(format('Can not change t.d to %s', new.d), new.d <> old.d);

Test:

postgres=# update t set i = 3 where i = 1 returning *;
┌───┬───────────────────────────────┐
│ i │               d               │
╞═══╪═══════════════════════════════╡
│ 3 │ 2017-07-28 12:31:37.255392+03 │
└───┴───────────────────────────────┘

postgres=# update t set i = 4, d = current_timestamp where i = 3 returning *;
ERROR:  Can not change t.d to 2017-07-28 12:39:18.963852+03

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.