14

I have tables that use UUIDs. I want to be able to insert a new row with or without a UUID as sometimes the client will generate the UUID other times it won't.

Each table has this at it's core:

CREATE TABLE IF NOT EXISTS person (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid()
);

I'm trying to use a function to insert rows. I'd like to be able to hand a NULL id and get a default value (a generated UUID). I have something like this:

CREATE OR REPLACE FUNCTION create_person(
    id UUID
) RETURNS BOOLEAN LANGUAGE plpgsql SECURITY DEFINER AS $$
BEGIN
    INSERT INTO person( id )
    VALUES (
        COALESCE(id,default)
    );
    RETURN FOUND;
END;
$$;

I've tried this:

INSERT INTO person ( id ) VALUES (
    COALESCE(id, default),
);

and this:

INSERT INTO person ( id ) VALUES (
  CASE WHEN id IS NULL THEN default ELSE id END
);

This works, but it repeats the gen_random_uuid() code:

INSERT INTO person ( id ) VALUES (
  COALESCE(id, gen_random_uuid()),
);

similarly this works too but has the same problems:

INSERT INTO person ( id ) VALUES (
    CASE WHEN id IS NULL THEN gen_random_uuid() ELSE id END
);

Is there a way to do this where I don't have to repeat the gen_random_uuid() code?

Would this be better done with plpgsql?

Sambeau
  • 245
  • 1
  • 3
  • 12
  • 2
    As you are using PL/pgSQL, I would use an `if id is not null then insert .. values (id) else insert ... values (default)` –  Aug 26 '16 at 11:52
  • What is this `default`? I cannot see where it's defined. Is that visible? Can you print it? Another angle is: I would try to name this `default` something else that's not an SQL keyword, like `default_value`. – Tamas Rev Aug 26 '16 at 12:00
  • @tamas-rev It's the `default` keyword that I want. I would like Postgres to insert a default value there (as defined in the CREATE TABLE). – Sambeau Aug 26 '16 at 12:03
  • @horse_with_no_name Thanks for the suggestion. The issue there is, in reality, there are other fields that have defaults that I would also like to be auto-filled. For instance in the real version of this `person` table there is a `timezone` field that needs to autofill to something sensible. – Sambeau Aug 26 '16 at 12:07
  • One thought was to always do a create with default values and then do an update immediately as part of the function. It feels a bit wrong to me. – Sambeau Aug 26 '16 at 12:12
  • I think, the `case when is is null ...` solution is fine. You can replace the `gen_random_uuid()` with another custom function. So you can tweak the UUID generating algorithm at one place. Still, you have to wire the function name into every `insert into` clause. – Tamas Rev Aug 26 '16 at 12:14
  • @tamas-rev the problem there is that if anyone creates a person without using the function it won't get a UUID (unless I repeat the `gen_random_uuid()`—which I can do, but it feels wrong!) – Sambeau Aug 26 '16 at 12:17
  • 2
    I think triggers can help you. E.g they can run something `before insert` for you. I can't test it because I don't have postgres on my machine. However, [this page](http://www.postgresqltutorial.com/creating-first-trigger-postgresql/) is a good place to start. – Tamas Rev Aug 26 '16 at 12:21
  • 1
    Third time lucky I think tamas has the right answer. `BEFORE INSERT` Triggers let you change the values of any insert and swap out nulls for a new value. – Philip Couling Aug 26 '16 at 12:35
  • I don't know why I didn't think of that, I already use a trigger for keeping a revision counter. I'll give that a go, thanks @philip-couling – Sambeau Aug 26 '16 at 12:43
  • Thanks @tamas-rev – Sambeau Aug 26 '16 at 12:43

3 Answers3

9

The core problem is the special nature of the key word DEFAULT in a VALUES expression attached to an INSERT. Per documentation:

In a VALUES list appearing at the top level of an INSERT, an expression can be replaced by DEFAULT to indicate that the destination column's default value should be inserted. DEFAULT cannot be used when VALUES appears in other contexts.

Bold emphasis mine. Specifically, DEFAULT cannot be argument to a function:

COALESCE(function_parameter, DEFAULT)  -- not possible

Possible solution

There are various ways, depending on exact requirements.

This function doesn't need to know the actual default of person.id - which seems to be what you are after:

CREATE OR REPLACE FUNCTION create_person(_id UUID)
  RETURNS boolean LANGUAGE plpgsql SECURITY DEFINER AS
$func$
BEGIN
   IF _id IS NULL THEN                 -- no UUID provided
      INSERT INTO myschema.person(id)  -- see below about schema name
      VALUES (DEFAULT);                -- use the key word DEFAULT
   ELSE                                -- UUID provided
      INSERT INTO myschema.person(id)
      VALUES (_id);
   END IF;

   RETURN FOUND;                       -- (return value pointless so far)
END
$func$;

Avoid using the same name for parameters and involved table columns. Since function parameters are visible inside every SQL command in the function body, it can lead to very confusing naming conflicts (even if target columns of INSERT are exempt from this in modern Postgres). I use _id as parameter name instead.

Default values for other columns not mentioned in the INSERT are filled in automatically. I use the key word DEFAULT because we are required to list at least one target column for the INSERT.

The boolean return value is pointless in this demo because it is always true (unless you have triggers that might skip the row).

Related answer with possible alternatives and a lot of explanation:

Aside: You should schema-qualify all function and table names in a SECURITY DEFINER function - or (probably better if you aren't sure) explicitly set the search_path to defend against possible attacks. More:

Community
  • 1
  • 1
Erwin Brandstetter
  • 605,456
  • 145
  • 1,078
  • 1,228
  • Great answer, I think I have another work around. I'm pulling metadata and have entries like `nextval('schema.table_id_seq'::regclass)` which (at least in this case) works in the COALESCE. – jcalfee314 Nov 08 '22 at 15:34
7

There's no way to re-use the defined default on the column. The default is only there to define what happens if an INSERT doesn't specify a value. By this definition a null value is still "specified" and therefore default can't be used.

Your comment that someone might not use the function indicates that a trigger is better for your requirements than a simple function.

https://www.postgresql.org/docs/current/static/plpgsql-trigger.html

CREATE OR REPLACE FUNCTION default_id() RETURNS TRIGGER AS $default_id$
    BEGIN       
        IF (NEW.id IS NULL) THEN
            NEW.id := gen_random_uuid();
        END IF;
        RETURN NEW;
    END;
$default_id$ LANGUAGE plpgsql;

CREATE TRIGGER default_id_trigger
BEFORE INSERT OR UPDATE ON person
    FOR EACH ROW EXECUTE PROCEDURE default_id();

If you do want to do this with a function then the simplest way is just to assign the value before inserting:

CREATE OR REPLACE FUNCTION create_person(
    id UUID
) RETURNS BOOLEAN LANGUAGE plpgsql SECURITY DEFINER AS $$
BEGIN
    IF id IS NULL THEN
        id := gen_random_uuid();
    END IF;
    -- OR
    -- id := coalesce(id, gen_random_uuid());
    INSERT INTO person( id )
    VALUES (id);
    RETURN FOUND;
END;
$$;
Philip Couling
  • 13,581
  • 5
  • 53
  • 85
  • It looks like that won't work, either. `id` is a `PRIMARY KEY` and therefore cannot be `NULL`. `PostgreSQL said: null value in column "id" violates not-null constraint` – Sambeau Aug 26 '16 at 13:19
  • Are you sure you created a trigger `BEFORE INSERT` and not `AFTER INSERT`. I've just tested this and it works fine for `INTEGER` so I see no reason why it doesn't for `UUID`. – Philip Couling Aug 26 '16 at 14:19
  • Indeed I had. This seems to work fine for me now. Thanks! – Sambeau Aug 26 '16 at 14:53
  • 1
    @Sambeau Notice that a trigger is an obscure element in the db design and it can be confusing for a non experienced SQL developer in more than one way. – Clodoaldo Neto Aug 26 '16 at 15:05
  • @clodoaldo-neto I realise. Thanks for your help. This is mostly a learning exercise for me and an experiment with client-generated UUIDs and table revisions. It's not for production yet. – Sambeau Aug 26 '16 at 15:22
-1

Overload the gen_random_uuid function:

create or replace function gen_random_uuid()
returns uuid as $$
    select 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11'::uuid;
$$ language sql;


create or replace function gen_random_uuid(_id uuid)
returns uuid as $$
    select coalesce(_id, gen_random_uuid());
$$ language sql;

Create the table as usual:

create table if not exists person (
    id uuid default gen_random_uuid()
);

Call the gen_random_uuid function inside the create_person function:

create or replace function create_person(_id uuid)
returns boolean as $$
    insert into person (id) values (gen_random_uuid(_id))
    returning true;
$$ language sql;

Then all the insert variations will work:

insert into person (id) values (default);
select create_person(null);
select create_person('b1eebc99-9c0b-4ef8-bb6d-6bb9bd380a22'::uuid);
Clodoaldo Neto
  • 118,695
  • 26
  • 233
  • 260
  • 2
    Sometimes I *have* to pass an id to the create function. That's the whole point when using UUIDs. Sometimes the server will create an object, sometimes a client will. – Sambeau Aug 26 '16 at 12:40
  • @Sambeau It sounds like bad design – Clodoaldo Neto Aug 26 '16 at 12:42
  • This doesn't answer the question as it only works because you dropped `PRIMARY KEY` from id. If you put it back you get `ERROR: duplicate key value violates unique constraint "person_pkey"` `DETAIL: Key (id)=(a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11) already exists.` – Sambeau Aug 26 '16 at 13:33
  • 1
    @Sambeau I droped the primary key because my dummy `gen_random_uuid` function always returns the same uuid. If it generated a random one it would work with a primary key. – Clodoaldo Neto Aug 26 '16 at 14:09
  • My apologies. I see that now. That does indeed work. – Sambeau Aug 26 '16 at 14:39
  • However it still has `insert into person (id) values (gen_random_uuid(_id))` so it's basically equivalent to `COALESCE(id, gen_random_uuid())` – Sambeau Aug 26 '16 at 15:24