PostgreSQL has a builtin method for adding generated columns to tables. Generated columns are reflected identically to non-generated columns. This is the recommended approach to adding computed fields when your computation meets the restrictions. Namely:
For arbitrary computations that do not meet the requirements for generated columns, a table's reflected GraphQL type can be extended by creating a function that:
accepts a single argument of the table's tuple type
begin;-- A computed field function written in SQL and marked stable might return stale results.-- Directly from the postgres docs(https://www.postgresql.org/docs/current/xfunc-volatility.html):--For functions written in SQL or in any of the standard procedural languages,--there is a second important property determined by the volatility category,--namely the visibility of any data changes that have been made by the SQL--command that is calling the function. A VOLATILE function will see such--changes, a STABLE or IMMUTABLE function will not. This behavior is--implemented using the snapshotting behavior of MVCC (see Chapter 13): STABLE--and IMMUTABLE functions use a snapshot established as of the start of the--calling query, whereas VOLATILE functions obtain a fresh snapshot at the--start of each query they execute.--The solution is to mark these functions as volatilecreatetableparent(iduuidprimarykeydefaultgen_random_uuid(),countint2);createtablechild(iduuidprimarykeydefaultgen_random_uuid(),parent_iduuidreferencesparentnotnull,countint2);-- note that the function is marked stable and in written in sqlcreateorreplacefunction_count(recparent)returnssmallintstablelanguagesqlas$$selectsum(count)fromchildwhereparent_id=rec.id$$;insertintoparent(id,count)values('8bcf0ee4-95ed-445f-808f-17b8194727ca',1);insertintochild(id,parent_id,count)values('57738181-3d0f-45ad-96dd-3ba799b2d21d','8bcf0ee4-95ed-445f-808f-17b8194727ca',2),('cb5993ff-e693-49cd-9114-a6510707e628','8bcf0ee4-95ed-445f-808f-17b8194727ca',3);selectjsonb_pretty(graphql.resolve($$queryParentQuery{parentCollection{edges{node{idcountchildCollection{edges{node{count}}}}}}}$$));jsonb_pretty-----------------------------------------------------------------------{+"data":{+"parentCollection":{+"edges":[+{+"node":{+"id":"8bcf0ee4-95ed-445f-808f-17b8194727ca",+"count":5,+"childCollection":{+"edges":[+{+"node":{+"count":2+}+},+{+"node":{+"count":3+}+}+]+}+}+}+]+}+}+}(1row)-- since _count is stable, the value returned in parent.count field will be stale-- i.e. parent.count is still 5 instead of (3 + 5) = 8selectjsonb_pretty(graphql.resolve($$mutationChildMutation{updateChildCollection(filter:{id:{eq:"57738181-3d0f-45ad-96dd-3ba799b2d21d"}}set:{count:5}){records{idcountparent{idcount}}}}$$));jsonb_pretty-----------------------------------------------------------------------{+"data":{+"updateChildCollection":{+"records":[+{+"id":"57738181-3d0f-45ad-96dd-3ba799b2d21d",+"count":5,+"parent":{+"id":"8bcf0ee4-95ed-445f-808f-17b8194727ca",+"count":5+}+}+]+}+}+}(1row)-- note that the function is marked volatilecreateorreplacefunction_count(recparent)returnssmallintvolatilelanguagesqlas$$selectsum(count)fromchildwhereparent_id=rec.id$$;-- since _count is volatile, the value returned in parent.count field will be fresh-- i.e. parent.count is correctly at (3 + 7) 10selectjsonb_pretty(graphql.resolve($$mutationChildMutation{updateChildCollection(filter:{id:{eq:"57738181-3d0f-45ad-96dd-3ba799b2d21d"}}set:{count:7}){records{idcountparent{idcount}}}}$$));jsonb_pretty-----------------------------------------------------------------------{+"data":{+"updateChildCollection":{+"records":[+{+"id":"57738181-3d0f-45ad-96dd-3ba799b2d21d",+"count":7,+"parent":{+"id":"8bcf0ee4-95ed-445f-808f-17b8194727ca",+"count":10+}+}+]+}+}+}(1row)rollback;
Computed Relationships
Computed relations can be helpful to express relationships:
between entities that don't support foreign keys
too complex to be expressed via a foreign key
If the relationship is simple, but involves an entity that does not support foreign keys e.g. Foreign Data Wrappers / Views, defining a comment directive is the easiest solution. See the view doc for a complete example. Note that for entities that do not support a primary key, like views, you must define one using a comment directive to use them in a computed relationship.
Alternatively, if the relationship is complex, or you need compatibility with PostgREST, you can define a relationship using set returning functions.
To-One
To One relationships can be defined using a function that returns setof <entity> rows 1
createtable"Person"(idintprimarykey,nametext);createtable"Address"(idintprimarykey,"isPrimary"boolnotnulldefaultfalse,"personId"intreferences"Person"(id),addresstext);-- Example computed relationcreatefunction"primaryAddress"("Person")returnssetof"Address"rows1languagesqlas$$selectaddrfrom"Address"addrwhere$1.id=addr."personId"andaddr."isPrimary"limit1$$;insertinto"Person"(id,name)values(1,'Foo Barington');insertinto"Address"(id,"isPrimary","personId",address)values(4,true,1,'1 Main St.');
results in the GraphQL type
123456
typePersonimplementsNode{"""Globally Unique Record Identifier"""
nodeId: ID!...
primaryAddress: Address}
and can be queried like a natively enforced relationship
createtable"Person"(idintprimarykey,nametext);createtable"Address"(idintprimarykey,addresstext);createtable"PersonAtAddress"(idintprimarykey,"personId"intnotnull,"addressId"intnotnull);-- Computed relation to bypass "PersonAtAddress" table for cleaner APIcreatefunction"addresses"("Person")returnssetof"Address"languagesqlas$$selectaddrfrom"PersonAtAddress"pajoin"Address"addronpa."addressId"="addr".idwherepa."personId"=$1.id$$;insertinto"Person"(id,name)values(1,'Foo Barington');insertinto"Address"(id,address)values(4,'1 Main St.');insertinto"PersonAtAddress"(id,"personId","addressId")values(2,1,4);
results in the GraphQL type
1 2 3 4 5 6 7 8 910111213
typePersonimplementsNode{"""Globally Unique Record Identifier"""
nodeId: ID!...addresses(
first: Int
last: Int
before: Cursor
after: Cursor
filter: AddressFilter
orderBy: [AddressOrderBy!]):AddressConnection}
and can be queried like a natively enforced relationship