Algebraic Data Types in Postgres - sql

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.

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);

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.

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)

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
$$;

Related

Define ordering scheme for user defined domain type

Given a domain like
CREATE DOMAIN my_type AS TEXT NOT NULL;
And a table
CREATE TABLE my_types (
id SERIAL PRIMARY KEY,
my_value my_type
);
INSERT INTO my_types (my_value) VALUES ('not an asterisk'), ('*'), ('also not an asterisk');
I can define a sorting function like this:
CREATE OR REPLACE FUNCTION
sort_my_type(my_type)
RETURNS INT LANGUAGE plpgsql IMMUTABLE AS
$$
BEGIN
IF $1 = '*' THEN
RETURN -1;
ELSE
RETURN 1;
END IF;
END;
$$;
And use it to order my query:
SELECT * FROM my_types ORDER BY sort_my_type(my_value);
Which makes sure that any value that equals '*' will be first.
How can I drop the need to specify the function? I'm not really interested in implementing comparison functions (unless I need to), I just want to define a "natural order" for my_type. It seems to essentially inherit its default ordering scheme from TEXT as of now.
So the desired outcome is that the query looks like
SELECT * FROM my_types ORDER BY my_value;
(This happens to work currently, because my desired outcome in this example happens to coincide with how TEXT is ordered. The real example is a little more thorough and cannot rely on the default ordering of text.)
Quoting Gordon Linoff's:
You can't do this with DOMAIN because that is essentially a subset of an existing type -- unless there is a collation that matches what you want. You could define a customer collation, but that might be more trouble than it is worth

How return a list in PL/SQL

I'm trying to return a list of char in a function PL/SQL with Oracle 11, but without succes.
I've got a few difficulties for understand their running...
For example, i have this code created for test:
create type test is table of varchar(500);
/
CREATE OR REPLACE FUNCTION test2 (id INT)
RETURN test
is
tt_t test;
BEGIN
SELECT descriptifSpecifique INTO tt_t(1)
FROM DECOMPOSE
where idRecette=id
AND idEtape=2;
SELECT descriptifSpecifique INTO tt_t(2)
FROM DECOMPOSE
where idRecette=id
AND idEtape=3;
RETURN tt_t;
END;
/
show errors function test;
The fonction is created without compilation's problem, but at the execution, I have this message: ORA-06531: Reference to uninitialized collection.
Also, how can I return a type (with a varchar and a int generated by a select for example) IN PL/SQL. Because when I try to make a declaration of type with RECORD, and RETURN this type, I have compilation's problem because type is not declarated...
Thank you
You are basically doing it correctly. But you need to EXTEND your collection before you put new elements into it.
Personally, I prefer to BULK COLLECT into the collection to avoid having to EXTEND and manage the entries at each index. Like this (code untested):
CREATE OR REPLACE FUNCTION test2 (id INT)
RETURN test
is
tt_t test;
BEGIN
SELECT descriptifSpecifique
BULK COLLECT INTO tt_t
FROM DECOMPOSE
where idRecette=id
AND idEtape IN (2,3)
ORDER BY idEtape;
RETURN tt_t;
END;
/
To return a TYPE having multiple columns, you need to create two types: an OBJECT type and a TABLE OF that object type.
Like so,
CREATE TYPE test_rec IS OBJECT ( a_varchar VARCHAR2(500), a_number NUMBER);
CREATE TYPE test_tbl IS TABLE OF test_rec;
Then, you can modify your function accordingly:
CREATE OR REPLACE FUNCTION test2 (id INT)
RETURN test_tbl
is
tt_t test_tbl;
BEGIN
SELECT test_rec(idEtape, descriptifSpecifique)
BULK COLLECT INTO tt_t
FROM DECOMPOSE
where idRecette=id
AND idEtape IN (2,3)
ORDER BY idEtape;
RETURN tt_t;
END;
/

Create array of custom domain postgres

Because of the inherit limitations of enum (you can't add values to the enum from within a function), I'm switching to custom domains with a check constraint verifying the values. I need to be able to create arrays of my custom enums, but when I try something like this:
CREATE DOMAIN foo AS text CHECK (VALUE IN ('foo', 'bar'));
CREATE TABLE foo_table(foo_column foo[]);
I get the error
type "foo[]" does not exist
Doing some googling, I found this from 2004 which made it look like support for this was coming. Is there a way to do this?
Thanks!
UPDATE
I've come up with a hacky solution, which I'll put as the answer if no one comes up with a better solution in a few days. This solution means you can't reuse a type to be an array, you have to create a separate type that acts as the array:
CREATE DOMAIN foo_group AS text[] CHECK (VALUE <# ARRAY['foo', 'bar']);
CREATE TABLE foo_table(foo_column foo_group);
The following work:
INSERT INTO foo_table VALUES(ARRAY['foo']);
INSERT INTO foo_table VALUES(ARRAY['foo', 'bar']);
INSERT INTO foo_table VALUES(ARRAY['bar']);
The following don't:
INSERT INTO foo_table VALUES(ARRAY['foo', 'baz']);
INSERT INTO foo_table VALUES(ARRAY['baz']);
Another possible workaround is:
CREATE TYPE foo_tup AS (item foo);
Domain types can wrapped in tuples like this and that gives you an array constructor. The downside is now you probably want to create casts:
select array[row('foo')::foo_tup, row('bar')];
For example you could create a function and a cast:
create function foo_tup(foo) returns foo_tup language sql as $$
select row($1)::foo_tup;
$$ immutable;
create function foo(foo_tup) returns foo language sql as $$
select $1.item;
$$;
create cast (foo as foo_tup) with function foo_tup(foo);
create cast (foo_tup as foo) with function foo(foo_tup);
Then aggregation becomes easy:
select array_agg(myfoo::foo_tup) from my_table;
though you get extra parentheses.

SQL Function (plpgsql) Type-Defined Outputs?

I've been defining the output of methods as...
CREATE TABLE person AS (
id serial PRIMARY KEY,
name text NOT NULL,
fingerprint text
);
CREATE TYPE person_output AS (
name text
);
CREATE OR REPLACE FUNCTION my_schema.create_person(_name text)
RETURNS person_output AS $$
DECLARE
output person_output;
BEGIN
INSERT INTO person (name) VALUES (_name) RETURNING name INTO person_output;
RETURN person_output;
END;
$$ LANGUAGE plpgsql;
I'm wondering if this rule of creating a type for what the function should output is standard within industry? Or, would it be better to create a view and just return 1 or raised errors depending if it succeeded or failed, and then calling the view in server-side python code to return?
I could see type-based outputs being good for creating "upgrades" like, CREATE TYPE person_output_v002 for the next-version, and creating a template for adapting, but then again, that wouldn't be needed if there were views.
The only reason I have decided not to use views is because I have things like groups of people and I'd like to create views for the groups, but I'd need to add a WHERE group_id = _GROUP_ID type of thing when selecting the view which I think isn't possible.
Might there be a way to select VIEWS with additional query parameters?
---- UPDATE ------
When there are certain outputs like current_user repeated as RETURN _current_user_output across multiple FUNCTIONs, is it a good idea to create one TYPE current_user_output so that we can be sure all FUNCTIONs which output the current_user are returning the same thing?
You do not need a row type for that at all. Not even a PL/pgSQL function. Simplify:
CREATE OR REPLACE FUNCTION my_schema.create_person(_name text)
RETURNS text AS
$func$
INSERT INTO person (name)
VALUES (_name)
RETURNING name;
$func$ LANGUAGE sql;
There is no rule of creating a type for what the function should output. The requirement is to declare the return type, and that's required by SQL which demands to know the type at execution time. To return multiple columns, you don't have to create a type. You can use RETURNS TABLE:
RETURNS TABLE (col1 int, col2 text, ...)
I suggest to read these two chapters of the manual:
Returning From a Function
CREATE FUNCTION
And you can use the readily defined row type of existing tables or polymorphic types:
Refactor a PL/pgSQL function to return the output of various SELECT queries
Added question:
... so that we can be sure all FUNCTIONs which output the current_user are returning the same thing?
If your functions just return text, you can be just as sure. It might make sense for row-types. Then it can be useful to define a composite type to share. Complicates maintenance. If you want to change the return type, you have to change alls functions at once. But that may be as intended ...

Is it possible to have PostgreSQL function return “arbitrary” type?

Is it possible to have an arbitrary return type defined for some PostgreSQL function?
The idea is that depending on the call, the output may be returned differently.
For example, let’s say we have:
TypeA : (name, email)
TypeB : (name, email, address, phone)
We may have a function:
func1(name varchar);
But return type could be either: TypeA or TypeB
So, is it possible to define func1, so that the arbitrary return type works?
EDIT:
IF the solution is refcursor ... Could someone please write an answer based on the example in my question? That would help a lot!
You have a few options. The first is to use a polymorphic type which would be cast on call, the second would be to use a cast, and a third would be to return a refcursor.
Polymorphic type
In this case, you'd do something like:
CREATE FUNCTION foo (bar varchar, baz ANYELEMENT) returns ANYELEMENT AS
$$
SELECT 'test'::TEXT;
$$ language sql;
Then to call it you would cast the NULL argument on call:
SELECT * FROM foo('test', null::varchar);
The real problem you have with this is that you are going to have to specify a type on every call.
Single Return type with Cast
In your example, one type has a subset of fields of another type. So you could:
CREATE TYPE all_info AS (
name text,
email text,
address text,
phone text
);
CREATE TYPE email_only AS (
name text,
email text
);
CREATE FUNCTION email_only(all_info) returns email_only LANGUAGE SQL IMMUTABLE AS $$
SELECT $1.name, $1.email;
$$;
CREATE CAST (all_info as email_only) WITH FUNCTION email_only(all_info);
Then you create your function to return all_info and you can cast on output. Something like:
SELECT (f::email_only).* FROM my_function('foo') f;
Note these two allow you to use SQL language functions which refcursors do not.
Refcursor
In this case you have to use plpgsql
CREATE OR REPLACE FUNCTION foo(bar varchar) RETURNS refcursor LANGUAGE plpgsql AS
$$
DECLARE a REFCURSOR;
BEGIN
OPEN a FOR SELECT ....;
RETURN a;
END;
$$;
In general I think it is easiest to start with the superset and cast approach than it is the others. Refcursors are possibly a second approach. Last would be insisting on a cast for a type.