robconery.com

Using Custom Types in Postgres

November 03, 2014 |
I'm building out a pretty detailed application using Postgres and Node - mostly Postgres - trying to flex as much of its power as I can. For me, this means kicking ORMs to the curb and relying on Postgres' amazing function features
***

Using Types For Fun and Profit

In my last post about pulling documents from queries I showed an interesting way to return a result set using row_to_json to crunch down 1-many records into a JSON array. This works pretty well and is really fast - but it's not exactly pretty:

create or replace function get_member(member_id bigint)
returns table (
  id bigint,
  email varchar(255),
  first varchar(25),
  last varchar(25),
  last_signin_at  timestamptz,
  notes json,
  logs json,
  roles json
)
as $$

DECLARE

  found_user members;
  parsed_logs json;
  parsed_roles json;
  parsed_notes json;

BEGIN
  select * from members where members.id = member_id into found_user;

  select json_agg(x) into parsed_logs from
  (select * from logs where logs.member_id=found_user.id) x;

  select json_agg(y) into parsed_notes from
  (select * from notes where notes.member_id=found_user.id) y;

  select json_agg(z) into parsed_roles from
  (select * from roles
  inner join members_roles on roles.id = members_roles.role_id
  where members_roles.member_id=found_user.id) z;

  return query
  select found_user.id, found_user.email, found_user.first, found_user.last, found_user.last_signin_at,
  parsed_notes, parsed_logs, parsed_roles

END;

$$ LANGUAGE PLPGSQL;

Beauty is in the eye of the beholder I suppose - this looks nice to me, but one thing stands out: I don't like the anonymous table return style. I think I'll probably want to use that again somewhere so let's set that up.

The first thing to do is resolve it to a type:

create type member_summary as (
  id bigint,
  email varchar(255),
  first varchar(25),
  last varchar(25),
  last_signin_at  timestamptz,
  notes json,
  logs json,
  roles json
);

Lovely. This is a composite type in Postgres - you can define your own base types if you want - but that's a whole other story. This composite type will do nicely.

Now we can rewrite the function to be a bit more concise:

create or replace function get_member(member_id bigint)
returns setof member_type
as $$

DECLARE

  found_user members;
  parsed_logs json;
  parsed_roles json;
  parsed_notes json;

BEGIN
  select * from members where members.id = member_id into found_user;

  select json_agg(x) into parsed_logs from
  (select * from logs where logs.member_id=found_user.id) x;

  select json_agg(y) into parsed_notes from
  (select * from notes where notes.member_id=found_user.id) y;

  select json_agg(z) into parsed_roles from
  (select * from roles
  inner join members_roles on roles.id = members_roles.role_id
  where members_roles.member_id=found_user.id) z;

  return query
  select found_user.id, found_user.email, found_user.first, found_user.last, found_user.last_signin_at,
  parsed_notes, parsed_logs, parsed_roles

END;

$$ LANGUAGE PLPGSQL;

Much better. You'll notice that instead of saying returns TABLE I now need to say it's a setof a type. A "type" in Postgres can be a base type (like int, varchar, etc) or a table - which is a composite type by itself. members is a type. If you want to create your own for reusability - you sure can!

Now we can reuse this type if we like - say by finding a member by email:

create or replace function get_member_by_email(member_email varchar(255))
returns setof member_type
as $$

DECLARE
  found_id bigint;
BEGIN
  select id from members into found_id where members.email = member_email;
  return query
  select * from get_member(found_id);

END;

$$ LANGUAGE PLPGSQL;

Enums

I also have a logging table that keeps track of things in the system. For that, I like to know what type of log is being stored. If I was being strict, I'd have two tables, like this:

create table log_types(
  id serial primary key not null,
  description varchar(25)
);
create table logs(
        id serial primary key not null,
        subject_id int not null references log_types(id),
        member_id bigint not null references members(id) on delete cascade,
        entry text not null,
        data json,
        created_at timestamptz default current_timestamp
);

This works fine and there's a nice Foreign Key constraint in there to be sure I have some type of description. However there's a simpler way that, to me, is a bit more descriptive:

create type log_type as ENUM(
  'registration', 'authentication', 'activity', 'system'
);

create table logs(
    id serial primary key not null,
    subject_id log_type not null,
    member_id bigint not null references members(id) on delete cascade,
    entry text not null,
    data json,
    created_at timestamptz default current_timestamp
);

insert into logs (subject, member_id, entry)
values ('registration',11111,'Member registered');

This works basically the same way, but instead of having a simple integer in my logs table, I have the description itself with a constraint on it that it must contain one of the specified values.

Lovely. There's a lot more we can do here on the write-side of working with data. I'll cover that in the next post.

Join over 15,000 programmers just like you and me

I have a problem when it comes to trying new things and learning about computer science stuff. I'm self-taught, so it's imperative I keep up with what's going on. I love sharing, so sign up and I'll send along what I've learned right to your inbox.