9

Is it possible to create an Algebraic Data Type in Postgres and then use it as a column type?

For example:

CREATE TYPE hoofed AS ENUM('horse', 'goat');

CREATE TYPE monkey AS ENUM('chimp','macaque');

CREATE TYPE ANIMAL AS ENUM(hoofed, monkey);

This fails with:

syntax error at or near "hoofed"
LINE 1: CREATE TYPE ANIMAL AS ENUM(hoofed, monkey);

Is it possible to do something like this?

Ultimately what I would then like to be able to do is something like so:

CREATE TABLE zoo (
    a ANIMAL,
    name text
);

INSERT INTO zoo(a, name) VALUES('horse', 'bob');
INSERT INTO zoo(a, name) VALUES('macaque', 'jimmy');

And for both of the records to be independently valid.

EDIT: @Abihabi87's response below does allow me to create, in effect, a product type, but it still does not allow me to create a union type as desired.

Abraham P
  • 15,029
  • 13
  • 58
  • 126

4 Answers4

5

You cant create type enum from others enum type:

you can create ANIMAL that like:

CREATE TYPE ANIMAL AS (h hoofed,m monkey);

Example in use:

CREATE TABLE your_table
(
    a ANIMAL
);

INSERT INTO your_table(a) select (select ('horse','macaque')::ANIMAL);
Piotr Rogowski
  • 3,642
  • 19
  • 24
  • 1
    How would I then use that as a column type? CREATE TABLE example( col1 ANIMAL ); INSERT INTO example(col1) VALUES('horse'); throws an exception: [ERROR] error: malformed record literal: "horse" – Abraham P Jun 08 '17 at 09:37
  • So this actually creates records like: SELECT * FROM your_table; a ----------------- (horse,macaque) (1 row) What I'm actually after is for both of the following to be independently valid.. a ----------------- 'horse' a ----------------- 'macaque' I have updated the question for clarity – Abraham P Jun 08 '17 at 10:14
  • I think this is the right answer for bounty award. He deserve it. –  Jun 12 '17 at 14:07
  • @AsifAli It doesn't answer the original question. The suggested answer doesn't validate data correctly. We'd need postgres to ensure that for any given x::ANIMAL, exactly one field is non null. If that's impossible to ensure in Postgres, then an answer would have to say so in order to address the original question. – Fried Brice Sep 10 '18 at 16:52
4

With ENUM types, you cannot achieve dynamic type composition/union. However, with DOMAIN types, you could achieve something similar:

create function valid_any_domain(anyelement, variadic regtype[])
  returns boolean
  language plpgsql
  immutable
as $func$
declare
  t regtype;
begin
  foreach t in array $2 loop
    begin
      execute format('select $1::%s', t) using $1;
    exception
      when not_null_violation or check_violation then
        continue;
    end;
    return true;
  end loop;

  return false;
end;
$func$;

create domain hoofed as text
  check (value in ('horse', 'goat'));

create domain monkey as text
  check (value in ('chimp','macaque'));

create domain animal as text
  check (valid_any_domain(value, 'hoofed', 'monkey'));

Changing the base types will dynamically change the composite/union type too, but still requires a manual constraint validation (especially, when some value(s) are removed from the valid spectrum):

alter domain hoofed drop constraint hoofed_check;
alter domain hoofed add check (value in ('horse', 'goat', 'zebra'));
alter domain animal validate constraint animal_check;

http://rextester.com/MBVC62095

Note: however, with DOMAIN types, you will lose an ENUM property: the custom ordering. DOMAINs will always use the underlying type's ordering.

pozs
  • 34,608
  • 5
  • 57
  • 63
3

Use the function:

create or replace function create_enum(name, variadic regtype[])
returns void language plpgsql as $$
begin
    execute format(
        'create type %I as enum(%s)', 
        $1, 
        string_agg(quote_literal(enumlabel), ',' order by enumtypid, enumsortorder))
    from pg_enum
    where enumtypid = any($2);
end $$;

Pass the name of a new type and a list of enum types as arguments:

select create_enum('animal', 'hoofed', 'monkey');

select enum_range(null::animal) as animal;

           animal           
----------------------------
 {horse,goat,chimp,macaque}
(1 row)
klin
  • 112,967
  • 15
  • 204
  • 232
3

Effectively you are trying to merge two enum types.
There are some open questions:

  • Can there be duplicate strings?
  • Is the design supposed to be static (changes to enum type hoofed do not change type animal later) or dynamic (the opposite).
  • Merge exactly two enum types or more?
  • Since the order of elements is significant, what is the order of elements in animal supposed to be?
  • Is this a one-time operation or intended for repeated use?

Assuming no duplicates, static design, two enum types, existing order of elements as appended and one-time operation.

You can use the built-in enum support function enum_range(anyenum) to get an array of all elements for a given enum type.

DO
$$
BEGIN
EXECUTE (
   SELECT 'CREATE TYPE animal AS ENUM (' 
        || array_to_string(enum_range(null::hoofed)::text[]
                        || enum_range(null::monkey)::text[], ''',''')
        || ''')'
   );
END
$$;
Erwin Brandstetter
  • 605,456
  • 145
  • 1,078
  • 1,228
  • That's actually really cool! Is it possible for future updates to the underlying ENUM to update the new ENUM dynamically? – Abraham P Jun 13 '17 at 22:20
  • @AbrahamP: No, that's not possible this way. You can drop and recreate the `animal` type or you can write server-side code to add / remove elements dynamically, but you need to consider dependencies and implications for existing data first. – Erwin Brandstetter Jun 17 '17 at 16:20
  • @AbrahamP A [DDL event trigger](https://www.postgresql.org/docs/current/event-triggers.html) should be able to do that. – Bergi Nov 17 '19 at 02:16