Skip to content

Computed columns returning a domain over a composite type are not exposed #3057

Description

@hannesj

Summary

PostgreSQL computed columns are dropped from the GraphQL schema when its return type is a domain over a composite type. No error, the field just doesn't appear. The same function returning the underlying composite
directly works. Domain columns of the same domain type work fine.

Steps to reproduce

Schema:

create type money_raw as (amount numeric(10,2), currency_code text);
create domain money as money_raw check (length((value).currency_code) = 3);

create table thing (
  id serial primary key,
  net_amount money,
  tax_amount money
);

create function thing_total_amount(t thing) returns money as $$
  select case
    when t.net_amount is null or t.tax_amount is null then null::money
    else row((t.net_amount).amount + (t.tax_amount).amount,
             (t.net_amount).currency_code)::money
  end
$$ language sql stable;

create function thing_total_amount_raw(t thing) returns money_raw as $$
  select row((t.net_amount).amount + (t.tax_amount).amount,
             (t.net_amount).currency_code)::money_raw
$$ language sql stable;

Build the schema and inspect the Thing type.

Expected results

Thing.totalAmount: Money — the computed column should be exposed using the same GraphQL output type (Money) that the domain columns already use.

Actual results

  • thing_total_amount (returns the domain money) → no totalAmount field at all.
  • thing_total_amount_raw (returns the composite money_raw) → field appears, typed as a separate MoneyRaw object type.
  • Scalars and domain-over-scalar return types work as expected.

Additional context

  • postgraphile@5.0.3, graphile-build-pg@5.0.2, @dataplan/pg@1.0.3, grafast@1.0.2, graphile-build@5.0.2, graphql@16.14.0
  • Node 22, schema built via makeSchema
  • No custom behavior tags involved

Root cause (traced in graphile-build-pg@5.0.2/dist/plugins/PgProceduresPlugin.js):

@dataplan/pg's domainOfCodec builds the domain codec by spreading the inner composite codec (...innerCodec), so the domain codec inherits .attributes from money_raw (and additionally carries domainOfCodec). In PgProceduresPlugin, the resource-building branch is gated only on returnCodec.attributes || returnCodec.arrayOfCodec?.attributes, so the domain-over-composite return is routed into the composite/table-return branch. That branch resolves the backing pg_class from the return type's typrelid:

const pgClass = await info.helpers.pgIntrospection.getClass(serviceName, pgType.typrelid);
if (!pgClass) return null; // resource silently dropped

A domain's pg_type.typrelid is 0, so getClass returns null and the entire function resource is dropped. (For comparison: a bare composite has a real typrelid, so its resource builds — but it then registers/uses a separate MoneyRaw object type rather than the domain's Money.)

Domain columns are unaffected because PgTablesPlugin registers the Money output type for the domain codec (keyed by codec identity) and PgAttributesPlugin resolves columns through that registration.

Possible Solution

The domain-over-composite return should not be treated as a table/composite return; it should keep its own (domain) codec, which already has a registered output type. Excluding domain codecs from the composite branch so they fall through to the scalar/else branch (which builds the resource with codec: returnCodec) fixes it, the field is then exposed using the domain's already-registered Money type, and no duplicate MoneyRaw type is created:

                     if (!returnCodec.isAnonymous &&
+                        !returnCodec.domainOfCodec &&
                         (returnCodec.attributes || returnCodec.arrayOfCodec?.attributes)) {

Verified against graphile-build-pg@5.0.2: with this one-line change, a computed column returning a domain-over-composite is exposed using the same GraphQL type as the domain columns (Money), and the stray MoneyRaw type disappears.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions