graphile / graphile/crystal

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

Open
#3,057 0 comments 0 reactions 0 assignees View on GitHub
🐛 bug
Dominant language
TypeScript
Stars
12.9k
Forks
625
Avg merge
5h 23m
Merged PRs (30d)
24

Description

### 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:

```sql
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`:

```js
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:

```diff
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.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.