PageSourceSearch

https://postgraphile.org/assets/js/b19c9096.5225d8cc.js

js postgraphile.org collected 2026-10-03 19:03:09 UTC 15,461 bytes, 1 lines download raw bytes

1"use strict";(globalThis.webpackChunk_localrepo_postgraphile_website=globalThis.webpackChunk_localrepo_postgraphile_website||[]).push([[2434],{77111(e,n,t){t.r(n),t.d(n,{assets:()=>s,contentTitle:()=>l,default:()=>d,frontMatter:()=>r,metadata:()=>i,toc:()=>c});const i=JSON.parse('{"id":"polymorphism","title":"Polymorphism","description":"Polymorphism in PostgreSQL schemas can take many forms. For PostGraphile to","source":"@site/versioned_docs/version-5/polymorphism.md","sourceDirName":".","slug":"/polymorphism","permalink":"/postgraphile/5/polymorphism","draft":false,"unlisted":false,"editUrl":"https://github.com/graphile/crystal/tree/main/postgraphile/website/postgraphile/polymorphism.md","tags":[],"version":"5","lastUpdatedAt":1756661370000,"frontMatter":{"title":"Polymorphism"},"sidebar":"docs","previous":{"title":"Views","permalink":"/postgraphile/5/views"},"next":{"title":"Refs","permalink":"/postgraphile/5/refs"}}');var o=t(31085),a=t(71184);const r={title:"Polymorphism"},l="Polymorphism in PostGraphile",s={},c=[{value:"@interface mode",id:"interface-mode",level:2},{value:"@interface mode",id:"interface-mode-1",level:2},{value:"@interface mode",id:"interface-mode-2",level:2},{value:"@unionMember",id:"unionmember",level:2}];function p(e){const n={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",header:"header",li:"li",mdxAdmonitionTitle:"mdxAdmonitionTitle",p:"p",pre:"pre",ul:"ul",...(0,a.R)(),...e.components};return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)(n.header,{children:(0,o.jsx)(n.h1,{id:"polymorphism-in-postgraphile",children:"Polymorphism in PostGraphile"})}),"\n",(0,o.jsx)(n.p,{children:"Polymorphism in PostgreSQL schemas can take many forms. For PostGraphile to\ndetect and generate GraphQL interfaces and unions (rather than regular object\ntypes) from your database tables, you need to provide PostGraphile some hints\nor instructions; one way to do this is with smart tags. Which smart tags to\napply will depend on what shape your data is."}),"\n",(0,o.jsx)(n.admonition,{type:"tip",children:(0,o.jsxs)(n.p,{children:["Under the hood, PostGraphile polymorphism is powered by\n",(0,o.jsx)(n.a,{href:"https://grafast.org/grafast/step-library/dataplan-pg/polymorphism",children:"@dataplan/pg's polymorphism"}),";\nsome restrictions may apply to what PostGraphile generates for polymorphic\ntypes and fields based on what ",(0,o.jsx)(n.code,{children:"@dataplan/pg"})," supports. For example, CRUD\nmutations are not generated for polymorphic types at this time \u2014 see instead\n",(0,o.jsx)(n.a,{href:"./custom-mutations",children:"Custom Mutations"}),"."]})}),"\n",(0,o.jsxs)(n.h2,{id:"interface-mode",children:["@interface mode",":single"]}),"\n",(0,o.jsx)(n.p,{children:"In the simplest case you have a single table that represents all of your\npossible polymorphic types, perhaps using a 'type' column or similar to\nindicate the underlying type. For this style, you should:"}),"\n",(0,o.jsxs)(n.ul,{children:["\n",(0,o.jsxs)(n.li,{children:["add the ",(0,o.jsx)(n.code,{children:"@interface"}),"\nsmart tag to the table, with ",(0,o.jsx)(n.code,{children:"mode:single"})," and the name of the type column indicated via the\n",(0,o.jsx)(n.code,{children:"type:"})," parameter."]}),"\n",(0,o.jsxs)(n.li,{children:["add ",(0,o.jsx)(n.code,{children:"@type"})," smart tags to the table for each possible value of the ",(0,o.jsx)(n.code,{children:"type"})," column"]}),"\n"]}),"\n",(0,o.jsx)(n.p,{children:"For example you might have a table like this one:"}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-sql",children:"create type polymorphic.item_type as enum (\n  'TOPIC',\n  'POST',\n  'DIVIDER',\n  'CHECKLIST',\n  'CHECKLIST_ITEM'\n);\n\ncreate table polymorphic.single_table_items (\n  id serial primary key,\n\n  -- Rails-style polymorphic column\n  type polymorphic.item_type not null default 'POST'::polymorphic.item_type,\n\n  -- Shared attributes:\n  parent_id int references polymorphic.single_table_items on delete cascade,\n  root_topic_id int constraint single_table_items_root_topic_fkey references polymorphic.single_table_items on delete cascade,\n  position bigint not null default 0,\n  created_at timestamptz not null default now(),\n  updated_at timestamptz not null default now(),\n  is_explicitly_archived bool not null default false,\n  archived_at timestamptz,\n\n  -- Attributes that may be used by one or more item subtypes.\n  title text,\n  description text,\n  note text,\n  color text\n);\n\n"})}),"\n",(0,o.jsx)(n.p,{children:"To have PostGraphile generate an interface for this table, you might use a comment such as:"}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-sql",children:"comment on table polymorphic.single_table_items is $$\n  @interface mode:single type:type\n  @type TOPIC name:SingleTableTopic attributes:title\n  @type POST name:SingleTablePost attributes:title,description,note\n  @type DIVIDER name:SingleTableDivider attributes:title,color\n  @type CHECKLIST name:SingleTableChecklist attributes:title\n  @type CHECKLIST_ITEM name:SingleTableChecklistItem attributes:description,note\n  $$;\n"})}),"\n",(0,o.jsxs)(n.p,{children:["The ",(0,o.jsx)(n.code,{children:"@type"})," tags take the value of the ",(0,o.jsx)(n.code,{children:"type"})," column as the first argument and\nthen accept ",(0,o.jsx)(n.code,{children:"name:"})," and ",(0,o.jsx)(n.code,{children:"attributes:"})," arguments; ",(0,o.jsx)(n.code,{children:"name"})," being the name to create\nthe GraphQL object type with and ",(0,o.jsx)(n.code,{children:"attributes:"})," to be a comma-separated list of any\ncolumns that are specific to this type (and shouldn't be on the main\ninterface)."]}),"\n",(0,o.jsx)(n.admonition,{type:"tip",children:(0,o.jsxs)(n.p,{children:["The ",(0,o.jsx)(n.code,{children:"type"})," column doesn't have to be an enum, any string-y type is fine but do\nensure that all possible values it can have are declared in your ",(0,o.jsx)(n.code,{children:"@type"})," smart\ntags!"]})}),"\n",(0,o.jsx)(n.admonition,{type:"tip",children:(0,o.jsxs)(n.p,{children:["For polymorphism to work, your table must have a primary key; we\ncurrently do not automatically detect the lack of primary key, so\nyou may receive unex
1pected results (typically ",(0,o.jsx)(n.code,{children:"null"}),"s) without one.\nIf you have a ",(0,o.jsx)(n.code,{children:"view"})," instead of a ",(0,o.jsx)(n.code,{children:"table"}),", make sure to define a\nprimary key using the ",(0,o.jsx)(n.code,{children:"@primaryKey"})," smart tag."]})}),"\n",(0,o.jsxs)(n.h2,{id:"interface-mode-1",children:["@interface mode",":relational"]}),"\n",(0,o.jsxs)(n.p,{children:["A slightly more complex case is having a table that defines the common fields\nas above, but then you join in additional data from other tables in a\nrelational manner. This is similar to the above, but we use ",(0,o.jsx)(n.code,{children:"mode:relational"}),"\nand the ",(0,o.jsx)(n.code,{children:"@type"})," tags declare the table that each type ",(0,o.jsx)(n.code,{children:"references"})," rather than\nthe columns that are specific to that type."]}),"\n",(0,o.jsx)(n.p,{children:"For example, for the following set of related relationally-polymorphic tables:"}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-sql",children:"create table polymorphic.relational_items (\n  id serial primary key,\n\n  -- This column is used to tell us which table we need to join to\n  type polymorphic.item_type not null default 'POST'::polymorphic.item_type,\n\n  -- Shared attributes (also 'id'):\n  parent_id int references polymorphic.relational_items on delete cascade,\n  root_topic_id int, -- constraint being created below\n  position bigint not null default 0,\n  created_at timestamptz not null default now(),\n  updated_at timestamptz not null default now(),\n  is_explicitly_archived bool not null default false,\n  archived_at timestamptz\n);\n\ncreate table polymorphic.relational_topics (\n  id int primary key references polymorphic.relational_items,\n  title text not null\n);\nalter table polymorphic.relational_items add constraint relational_items_root_topic_fkey foreign key (root_topic_id) references polymorphic.relational_topics on delete cascade;\ncreate table polymorphic.relational_posts (\n  id int primary key references polymorphic.relational_items,\n  title text not null,\n  description text default '-- Enter description here --',\n  note text\n);\ncreate table polymorphic.relational_dividers (\n  id int primary key references polymorphic.relational_items,\n  title text,\n  color text\n);\ncreate table polymorphic.relational_checklists (\n  id int primary key references polymorphic.relational_items,\n  title text not null\n);\ncreate table polymorphic.relational_checklist_items (\n  id int primary key references polymorphic.relational_items,\n  description text not null,\n  note text\n);\n"})}),"\n",(0,o.jsx)(n.p,{children:"You might use a comment such as this one:"}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-sql",children:"comment on table polymorphic.relational_items is $$\n  @interface mode:relational type:type\n  @type TOPIC references:relational_topics\n  @type POST references:relational_posts\n  @type DIVIDER references:relational_dividers\n  @type CHECKLIST references:relational_checklists\n  @type CHECKLIST_ITEM references:relational_checklist_items\n  $$;\n"})}),"\n",(0,o.jsxs)(n.h2,{id:"interface-mode-2",children:["@interface mode",":union"]}),"\n",(0,o.jsxs)(n.p,{children:["Sometimes you might have multiple independent tables that you want to be part\nof an interface, for this you can define a composite type that declares the\nshared attributes, and then add the ",(0,o.jsx)(n.code,{children:"@interface"})," smart tag to this composite\ntype, with ",(0,o.jsx)(n.code,{children:"mode:union"})," (this is an SQL ",(0,o.jsx)(n.code,{children:"union"}),", not a GraphQL ",(0,o.jsx)(n.code,{children:"union"}),"!)."]}),"\n",(0,o.jsx)(n.p,{children:"For example, imagine that we have a table for AWS applications and a table for\nGCP applications (and maybe more for other cloud providers):"}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-sql",children:"create table polymorphic.aws_applications (\n  id int primary key,\n  name text not null,\n  last_deployed timestamptz,\n  person_id int references polymorphic.people,\n  organization_id int references polymorphic.organizations,\n  aws_i
1d text\n);\n\ncreate table polymorphic.gcp_applications (\n  id int primary key,\n  name text not null,\n  last_deployed timestamptz,\n  person_id int references polymorphic.people,\n  organization_id int references polymorphic.organizations,\n  gcp_id text\n);\n"})}),"\n",(0,o.jsxs)(n.p,{children:["To have all of these tables implement the ",(0,o.jsx)(n.code,{children:"Application"})," interface, we might\nadd the following to our database:"]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-sql",children:"-- Declare a composite type to detail the common fields\ncreate type polymorphic.applications as (\n  id int,\n  name text,\n  last_deployed timestamptz\n);\n\n-- Mark this composite type as an interface named Application\ncomment on type polymorphic.applications is $$\n  @interface mode:union\n  @name Application\n  $$;\n\n-- Have our tables implement this interface\ncomment on table polymorphic.aws_applications is $$\n  @implements Application\n  $$;\ncomment on table polymorphic.gcp_applications is $$\n  @implements Application\n  $$;\n"})}),"\n",(0,o.jsxs)(n.admonition,{type:"warning",children:[(0,o.jsxs)(n.mdxAdmonitionTitle,{children:["Cannot return mode",":union"," from functions"]}),(0,o.jsxs)(n.p,{children:["You will not be able to return the ",(0,o.jsx)(n.code,{children:"mode:union"})," type from a PostgreSQL function.\nConsider the above example - if we receive\n",(0,o.jsx)(n.code,{children:'{id: 1, name: "Foo", last_deployed: null}'})," how can we know if this came from\n",(0,o.jsx)(n.code,{children:"aws_applications"})," or ",(0,o.jsx)(n.code,{children:"gcp_applications"}),"? Instead, consider using\n",(0,o.jsx)(n.a,{href:"/postgraphile/5/extend-schema",children:(0,o.jsx)(n.code,{children:"extendSchema"})}),"."]})]}),"\n",(0,o.jsx)(n.h2,{id:"unionmember",children:"@unionMember"}),"\n",(0,o.jsxs)(n.p,{children:["For cases where you want multiple tables that don't necessarily share any\nfields or relations to be part of a GraphQL union, you can give them the\n",(0,o.jsx)(n.code,{children:"@unionMember"})," smart tag."]}),"\n",(0,o.jsx)(n.p,{children:"For example, consider these independent tables:"}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-sql",children:"create table polymorphic.people (\n  person_id serial primary key,\n  username text not null unique\n);\n\ncreate table polymorphic.organizations (\n  organization_id serial primary key,\n  name text not null unique\n);\n"})}),"\n",(0,o.jsx)(n.p,{children:"We could define a union that contains them both via:"}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-sql",children:"comment on table polymorphic.people is $$\n  @unionMember PersonOrOrganization\n  $$;\ncomment on table polymorphic.organizations is $$\n  @unionMember PersonOrOrganization\n  $$;\n"})}),"\n",(0,o.jsxs)(n.p,{children:["And we could reference this type from an ",(0,o.jsx)(n.code,{children:"@ref"})," smart tag; for example:"]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-sql",children:"create table polymorphic.log_entries (\n  id serial primary key,\n  person_id int references polymorphic.people on delete cascade,\n  organization_id int references polymorphic.organizations on delete cascade,\n  text text not null,\n  constraint owned_by_person_or_organization check ((person_id is null) <> (organization_id is null))\n);\n\ncomment on table polymorphic.log_entries is $$\n  @ref author to:PersonOrOrganization singular\n  @refVia author via:(person_id)->people(person_id)\n  @refVia author via:(organization_id)->organizations(organization_id)\n  $$;\n"})}),"\n",(0,o.jsxs)(n.p,{children:["Now ",(0,o.jsx)(n.code,{children:"LogEntry.author"})," will be a ",(0,o.jsx)(n.code,{children:"PersonOrOrganization"})," by following the given relationships."]}),"\n",(0,o.jsxs)(n.admonition,{type:"tip",children:[(0,o.jsxs)(n.p,{children:["Since the ",(0,o.jsx)(n.code,{children:"log_entries"})," table only has one reference to ",(0,o.jsx)(n.code,{children:"people"})," and one to\n",(0,o.jsx)(n.code,{children:"organizations"})," we can use a shorthand for the ",(0,o.jsx)(n.code,{children:"via:"}),"s:"]}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-sql",children:"comment on table polymorphic.log_entries is $$\n  @ref author to:PersonOrOrganization singular\n  @refVia author via:people\n  @refVia author via:organizations\n  $$;\n"})})]})]})}function d(e={}){const{wrapper:n}={...(0,a.R)(),...e.components};return n?(0,o.jsx)(n,{...e,children:(0,o.jsx)(p,{...e})}):p(e)}},71184(e,n,t){t.d(n,{R:()=>r,x:()=>l});var i=t(14041);const o={},a=i.createContext(o);function r(e){const n=i.useContext(a);return i.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function l(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:r(e.components),i.createElement(a.Provider,{value:n},e.children)}}}]);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.