sqlpostgresqlcreate-functioncomposite-types

Extend existing PostgreSQL type


Is it possible to extend an existing datatype in PostgreSQL? Essentially, I want the equivalent of this TypeScript but in SQL:

interface Meeting {
  id: number;
  start: Date;
  end: Date;
  description: string;
}

interface ServiceHour extends Meeting {
  total: number;
  hours: number;
}

Because I have a function that returns all the columns from a meetings table and then the additional two total and hours columns that are computed at query time. Here's what that function looks like:

create or replace function user_hours(org_id text, user_id text)
returns table (like meeting_instances)
as $$
select (sum(hours) over (order by _.instance_time)) total, * from (
  select
    extract(epoch from ((meeting_instances.time).to - (meeting_instances.time).from)) / 60 / 60 as hours, 
    meeting_instances.*
  from meeting_instances inner join relation_people on relation_people.meeting = meeting_instances.id
  where relation_people.user = user_id
  and meeting_instances.org = org_id
  and meeting_instances.instance_time <= current_date
) as _;
$$
language sql stable;

And right now, I'm getting a type mismatch error because table (like meeting_instances) isn't the same as a table with the meeting_instances columns and the two new hours and total columns. What I want to be able to do is something like this (obviously the syntax below doesn't actually exist... but I'm not sure if there's another way to do this with a similar shorthand syntax):

returns table (total float, hours float, meeting_instances.*)
returns table (total float, hours float) intersect (like meeting_instances)
returns table (total float, hours float) extends (like meeting_instances)

My current workaround is to create a view and then have that function simply query that view and return the view's type.


Solution

  • To your core question:

    Is it possible to extend an existing datatype?

    No, not possible. Not in the RETURNS clause of CREATE FUNCTION, not anywhere else in PostgreSQL up to version 14.

    You can return a field of composite type, plus additional fields of any type. But that's subtly different:

    CREATE FUNCTION user_hours_plus( ...)
      RETURNS TABLE (my_meeting meeting_instances, hours numeric, total numeric) ...
    

    Calling that function with:

    SELECT * FROM user_hours_plus('a', 'b');
    

    Returns a nested composite type as one of the return columns, like:

    (1,"2017-01-03","2017-01-04", foo) | 123 | 345
    

    To decompose the composite type, you could call with:

    SELECT (my_meeting).*, hours, total FROM user_hours_plus('a', 'b');
    

    But I wouldn't go there.

    My current workaround is to create a view and then have that function simply query that view and return the view's type.

    Then just use the view. Don't create an additional function on top of it. Case closed.

    If you actually meant to say:

    ... then use the row type of the view in the RETURNS clause of the function

    Then we are back to your question. CREATE VIEW to (implicitly) register the extended row type is a valid option - especially since SELECT * is a convenient syntax shorthand for the case at had. But for starters,

    RETURNS TABLE (LIKE meeting_instances)
    

    ... isn't documented syntax for CREATE FUNCTION. Nobody should use it. Might be removed without notice in one of the next versions.
    The canonical, equivalent, documented syntax is:

    RETURNS SETOF meeting_instances
    

    (LIKE some_table) is documented syntax for CREATE TABLE. The RETURNS clause of CREATE FUNCTION currently supports the same, but it's not documented, so don't use it.

    Back to your workaround with creating a VIEW. If you have no use for the VIEW, other than registering that extended row type, consider CREATE TYPE instead. Unfortunately, CREATE TYPE does not allow LIKE other_type syntax, either. You have to spell out all columns (attributes) of the composite type. Like:

    CREATE TYPE meeting_plus AS (
       id numeric
     , start date
     , "end" date
     , description text
     , total numeric
     , hours numeric
    );
    

    Then you can use:

    RETURNS SETOF meeting_plus
    

    Just like you wanted.

    But for just the one function, I would use RETURNS TABLE() instead and spell out the return type:

    RETURNS TABLE (
       id numeric
     , start date
     , "end" date
     , description text
     , total numeric
     , hours numeric)
    

    Oh, and I wouldn't use "start" and "end" as identifiers in Postgres. Both are reserved words in standard SQL. "end" is absolutely reserved in Postgres and must always be double-quoted.