Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 69 additions & 7 deletions graphile-build/graphile-build-pg/src/plugins/PgProceduresPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {
EXPORTABLE_OBJECT_CLONE,
gatherConfig,
} from "graphile-build";
import type { PgProc, PgProcArgument } from "pg-introspection";
import type { PgProc, PgProcArgument, PgType } from "pg-introspection";

import { exportNameHint, forbidRequired } from "../utils.ts";
import { version } from "../version.ts";
Expand All @@ -41,6 +41,13 @@ declare global {
pgProc: PgProc;
},
): string;
functionResourceNameShouldPrefixCompositeType(
this: Inflection,
details: {
pgProc: PgProc;
firstArgCompositeType: PgType;
},
): boolean;
Comment on lines +44 to +50
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inflectors always return string, so this isn't an inflector.

Instead, use something like functionResourceNameCompositeTypePrefix(pgProc) which would return the empty string "" for the falsy path, and the prefix to use (e.g. firstArg.type.typname + "_") for the truthy path. Then you would compose it in as normal:

        const computedPrefix = functionResourceNameCompositeTypePrefix(pgProc);
        return `${schemaPrefix}${computedPrefix}${pgProc.proname}`;

Also, firstArgCompositeType can be derived directly from pgProc and so should not be passed.

functionRecordReturnCodecName(
this: Inflection,
details: {
Expand Down Expand Up @@ -130,8 +137,38 @@ export const PgProceduresPlugin: GraphileConfig.Plugin = {
}
const pgNamespace = pgProc.getNamespace()!;
const schemaPrefix = this._schemaPrefix({ serviceName, pgNamespace });
// For computed column functions whose name doesn't follow the
// tablename_funcname convention, prefix with the composite type
// name to ensure unique resource names. This allows overloaded
// functions like code(a.pets) and code(a.buildings) to produce
// distinct names (pets_code, buildings_code).
if (pgProc.provolatile !== "v") {
const firstArg = pgProc.getArguments().find((a) => a.isIn);
if (
firstArg &&
firstArg.type.typtype === "c" &&
this.functionResourceNameShouldPrefixCompositeType({
pgProc,
firstArgCompositeType: firstArg.type,
})
) {
const compositePrefix = firstArg.type.typname + "_";
if (!pgProc.proname.startsWith(compositePrefix)) {
return `${schemaPrefix}${firstArg.type.typname}_${pgProc.proname}`;
}
}
}
return `${schemaPrefix}${pgProc.proname}`;
},
functionResourceNameShouldPrefixCompositeType(
_options,
{ pgProc, firstArgCompositeType },
) {
// By default, only consider a function as a computed column if it
// belongs to the same schema as the composite type. Override this
// inflector to support cross-schema computed columns.
return firstArgCompositeType.typnamespace === pgProc.pronamespace;
},
functionRecordReturnCodecName(options, details) {
return this.upperCamelCase(
this.functionResourceName(details) + "-record",
Expand Down Expand Up @@ -606,7 +643,10 @@ export const PgProceduresPlugin: GraphileConfig.Plugin = {
resourceOptionsByPgProcByService: new Map(),
}),
hooks: {
async pgIntrospection_proc({ helpers, resolvedPreset }, event) {
async pgIntrospection_proc(
{ helpers, resolvedPreset, inflection },
event,
) {
const { entity: pgProc, serviceName } = event;

const pgService = resolvedPreset.pgServices?.find(
Expand Down Expand Up @@ -648,17 +688,39 @@ export const PgProceduresPlugin: GraphileConfig.Plugin = {
return;
}

// We also don’t want procedures that have been defined in our namespace
// twice. This leads to duplicate fields in the API which throws an
// error. In the future we may support this case. For now though, it is
// too complex.
// We don’t want procedures whose inflected resource name clashes
// with another overload — this would produce duplicate fields.
// Overloads targeting distinct composite types get unique names
// from the inflector (e.g. pets_code vs buildings_code).
const overload = introspection.procs.find(
(p) =>
p.pronamespace === pgProc.pronamespace &&
p.proname === pgProc.proname &&
p._id !== pgProc._id,
p._id !== pgProc._id &&
inflection.functionResourceName({
serviceName,
pgProc: p,
}) ===
inflection.functionResourceName({
serviceName,
pgProc,
}),
);
if (overload) {
// Warn if both functions target composite types — the user likely
// intended these as computed columns on different tables, but the
// inflector produced the same name (e.g. cross-schema overloads).
const thisFirstArg = pgProc.getArguments().find((a) => a.isIn);
const otherFirstArg = overload.getArguments().find((a) => a.isIn);
if (
thisFirstArg?.type.typtype === "c" &&
otherFirstArg?.type.typtype === "c" &&
thisFirstArg.type._id !== otherFirstArg.type._id
) {
console.warn(
`Skipping function '${namespace!.nspname}.${pgProc.proname}' because its resource name clashes with an overload. Consider overriding the 'functionResourceNameShouldPrefixCompositeType' inflector to support cross-schema computed columns.`,
);
}
return;
}

Expand Down
27 changes: 27 additions & 0 deletions postgraphile/postgraphile/__tests__/function-overloads-schema.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
-- Test overloaded computed column functions targeting different tables
drop schema if exists function_overloads, function_overloads_other_schema cascade;

create schema function_overloads;
create schema function_overloads_other_schema;
create table function_overloads.pets (id serial primary key, name text);
create table function_overloads.buildings (id serial primary key, address text);

-- Two overloaded functions in the SAME schema as their target tables
create function function_overloads.code(function_overloads.pets) returns text
as $$ select 'P' || $1.id::text; $$ language sql stable;
create function function_overloads.code(function_overloads.buildings) returns text
as $$ select 'B' || $1.id::text; $$ language sql stable;
comment on function function_overloads.code(function_overloads.pets)
is E'@behavior +typeField -queryField';
comment on function function_overloads.code(function_overloads.buildings)
is E'@behavior +typeField -queryField';

-- Cross-schema computed column functions (different schema from target tables)
create function function_overloads_other_schema.age(function_overloads.pets) returns int
as $$ select 42; $$ language sql stable;
create function function_overloads_other_schema.age(function_overloads.buildings) returns int
as $$ select 99; $$ language sql stable;
comment on function function_overloads_other_schema.age(function_overloads.pets)
is E'@behavior +typeField -queryField';
comment on function function_overloads_other_schema.age(function_overloads.buildings)
is E'@behavior +typeField -queryField';
Loading