Skip to content

Commit 4a8e6b7

Browse files
committed
Move overload disambiguation into functionResourceName inflector and simplify overload detection
1 parent 8e3ef40 commit 4a8e6b7

58 files changed

Lines changed: 18486 additions & 38523 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

graphile-build/graphile-build-pg/src/plugins/PgProceduresPlugin.ts

Lines changed: 40 additions & 141 deletions
Original file line numberDiff line numberDiff line change
@@ -23,32 +23,11 @@ import {
2323
EXPORTABLE_OBJECT_CLONE,
2424
gatherConfig,
2525
} from "graphile-build";
26-
import type { PgProc, PgProcArgument, PgType } from "pg-introspection";
26+
import type { PgProc, PgProcArgument } from "pg-introspection";
2727

2828
import { exportNameHint, forbidRequired } from "../utils.ts";
2929
import { version } from "../version.ts";
3030

31-
/**
32-
* Given a type OID, walk through domain chains and return the type if
33-
* it resolves to a composite type (typtype 'c' with a typrelid), or
34-
* null otherwise.
35-
*/
36-
function resolveToCompositeType(
37-
typeOid: string,
38-
types: ReadonlyArray<PgType>,
39-
): PgType | null {
40-
let type = types.find((t) => t._id === typeOid);
41-
// Follow domain chains (typtype 'd') to reach the underlying type
42-
while (type && type.typtype === "d" && type.typbasetype) {
43-
type = types.find((t) => t._id === type!.typbasetype!);
44-
}
45-
// Return only if it resolved to a composite (table) type
46-
if (type && type.typtype === "c" && type.typrelid) {
47-
return type;
48-
}
49-
return null;
50-
}
51-
5231
declare global {
5332
namespace GraphileBuild {
5433
interface BehaviorStrings {
@@ -151,6 +130,20 @@ export const PgProceduresPlugin: GraphileConfig.Plugin = {
151130
}
152131
const pgNamespace = pgProc.getNamespace()!;
153132
const schemaPrefix = this._schemaPrefix({ serviceName, pgNamespace });
133+
// For computed column functions whose name doesn't follow the
134+
// tablename_funcname convention, prefix with the composite type
135+
// name to ensure unique resource names. This allows overloaded
136+
// functions like code(a.pets) and code(a.buildings) to produce
137+
// distinct names (pets_code, buildings_code).
138+
if (pgProc.provolatile !== "v") {
139+
const firstArg = pgProc.getArguments().find((a) => a.isIn);
140+
if (firstArg && firstArg.type.typtype === "c") {
141+
const compositePrefix = firstArg.type.typname + "_";
142+
if (!pgProc.proname.startsWith(compositePrefix)) {
143+
return `${schemaPrefix}${firstArg.type.typname}_${pgProc.proname}`;
144+
}
145+
}
146+
}
154147
return `${schemaPrefix}${pgProc.proname}`;
155148
},
156149
functionRecordReturnCodecName(options, details) {
@@ -235,68 +228,10 @@ export const PgProceduresPlugin: GraphileConfig.Plugin = {
235228
const executor =
236229
info.helpers.pgIntrospection.getExecutorForService(serviceName);
237230

238-
let name = info.inflection.functionResourceName({
231+
const name = info.inflection.functionResourceName({
239232
serviceName,
240233
pgProc,
241234
});
242-
243-
// When overloaded functions target distinct composite types, each
244-
// is allowed through the overload check in pgIntrospection_proc.
245-
// However, the resource registry keys by name (see datasource.ts),
246-
// so we must ensure each resource has a unique name. We suffix with
247-
// the target table's schema and name, for example `code(a.pets)`
248-
// becomes `code_a_pets`.
249-
// GraphQL field names are unaffected since computedAttributeField
250-
// inflection uses resource.extensions.pg.name (the raw SQL name).
251-
{
252-
const introspection = (
253-
await info.helpers.pgIntrospection.getIntrospection()
254-
).find((n) => n.pgService.name === serviceName)!.introspection;
255-
// Check if another proc in the same namespace shares this name
256-
let hasOverload = false;
257-
for (const p of introspection.procs) {
258-
if (
259-
p.pronamespace === pgProc.pronamespace &&
260-
p.proname === pgProc.proname &&
261-
p._id !== pgProc._id
262-
) {
263-
hasOverload = true;
264-
break;
265-
}
266-
}
267-
if (hasOverload) {
268-
// Resolve the first argument to its target table type
269-
const firstArgType = pgProc.proargtypes?.[0];
270-
if (firstArgType) {
271-
const composite = resolveToCompositeType(
272-
firstArgType,
273-
introspection.types,
274-
);
275-
if (composite) {
276-
// Look up the pg_class and pg_namespace for the target
277-
// table so we can build the suffix
278-
let className: string | undefined;
279-
let schemaName: string | undefined;
280-
for (const c of introspection.classes) {
281-
if (c._id === composite.typrelid) {
282-
className = c.relname;
283-
for (const n of introspection.namespaces) {
284-
if (n._id === c.relnamespace) {
285-
schemaName = n.nspname;
286-
break;
287-
}
288-
}
289-
break;
290-
}
291-
}
292-
if (className && schemaName) {
293-
name = `${name}_${schemaName}_${className}`;
294-
}
295-
}
296-
}
297-
}
298-
}
299-
300235
const identifier = `${serviceName}.${namespace.nspname}.${
301236
pgProc.proname
302237
}(${pgProc.getArguments().map(argTypeName).join(",")})`;
@@ -685,7 +620,10 @@ export const PgProceduresPlugin: GraphileConfig.Plugin = {
685620
resourceOptionsByPgProcByService: new Map(),
686621
}),
687622
hooks: {
688-
async pgIntrospection_proc({ helpers, resolvedPreset }, event) {
623+
async pgIntrospection_proc(
624+
{ helpers, resolvedPreset, inflection },
625+
event,
626+
) {
689627
const { entity: pgProc, serviceName } = event;
690628

691629
const pgService = resolvedPreset.pgServices?.find(
@@ -727,65 +665,26 @@ export const PgProceduresPlugin: GraphileConfig.Plugin = {
727665
return;
728666
}
729667

730-
// We don’t want procedures that have been defined in our namespace
731-
// twice, as this leads to duplicate fields in the API. However, we
732-
// allow overloads where each targets a distinct composite type as its
733-
// first argument (i.e. computed columns on different tables), since
734-
// each will be exposed on a different GraphQL type.
735-
{
736-
let hasOverload = false;
737-
for (const p of introspection.procs) {
738-
if (
739-
p.pronamespace === pgProc.pronamespace &&
740-
p.proname === pgProc.proname &&
741-
p._id !== pgProc._id
742-
) {
743-
hasOverload = true;
744-
break;
745-
}
746-
}
747-
if (hasOverload) {
748-
// This proc has overloads — check if it qualifies as a computed
749-
// column on a unique composite type.
750-
const thisFirstArgType = pgProc.proargtypes?.[0];
751-
if (!thisFirstArgType) {
752-
// No first arg — not a computed column candidate
753-
return;
754-
}
755-
const thisComposite = resolveToCompositeType(
756-
thisFirstArgType,
757-
introspection.types,
758-
);
759-
if (!thisComposite) {
760-
// First arg doesn’t resolve to a composite type
761-
return;
762-
}
763-
// Check that no other overload targets the same composite type;
764-
// if two overloads both target, they'd produce duplicate fields on
765-
// the same GraphQL type so we must skip.
766-
for (const p of introspection.procs) {
767-
if (
768-
p.pronamespace === pgProc.pronamespace &&
769-
p.proname === pgProc.proname &&
770-
p._id !== pgProc._id
771-
) {
772-
const otherFirstArgType = p.proargtypes?.[0];
773-
if (!otherFirstArgType) continue;
774-
const otherComposite = resolveToCompositeType(
775-
otherFirstArgType,
776-
introspection.types,
777-
);
778-
if (
779-
otherComposite &&
780-
otherComposite._id === thisComposite._id
781-
) {
782-
// Another overload targets the same composite type — skip
783-
return;
784-
}
785-
}
786-
}
787-
// This proc targets a unique composite type — allow it through
788-
}
668+
// We don’t want procedures whose inflected resource name clashes
669+
// with another overload — this would produce duplicate fields.
670+
// Overloads targeting distinct composite types get unique names
671+
// from the inflector (e.g. pets_code vs buildings_code).
672+
const overload = introspection.procs.find(
673+
(p) =>
674+
p.pronamespace === pgProc.pronamespace &&
675+
p.proname === pgProc.proname &&
676+
p._id !== pgProc._id &&
677+
inflection.functionResourceName({
678+
serviceName,
679+
pgProc: p,
680+
}) ===
681+
inflection.functionResourceName({
682+
serviceName,
683+
pgProc,
684+
}),
685+
);
686+
if (overload) {
687+
return;
789688
}
790689

791690
await helpers.pgProcedures.getResourceOptions(serviceName, pgProc);

postgraphile/postgraphile/__tests__/kitchen-sink-schema.sql

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ drop schema if exists
3030
issue_2334,
3131
relay,
3232
cjk,
33-
scifi
33+
scifi,
34+
function_overloads,
35+
function_overloads_other_schema
3436
cascade;
3537
drop extension if exists tablefunc;
3638
drop extension if exists intarray;
@@ -2574,20 +2576,38 @@ create table cjk."期间" (
25742576

25752577
--------------------------------------------------------------------------------
25762578

2577-
-- Test overloaded computed column functions targeting different tables.
2578-
create table a.pets (id serial primary key, name text);
2579-
create table a.buildings (id serial primary key, address text);
2580-
create function a.code(a.pets) returns text as $$ select 'P' || $1.id::text; $$ language sql stable;
2581-
create function a.code(a.buildings) returns text as $$ select 'B' || $1.id::text; $$ language sql stable;
2582-
comment on function a.code(a.pets) is E'@behavior +typeField';
2583-
comment on function a.code(a.buildings) is E'@behavior +typeField';
2584-
2585-
--------------------------------------------------------------------------------
2586-
25872579
-- SCIFI: Snake Case Is For Initiates.
25882580
-- All the cool kids use UpperCamelCase
25892581
create schema scifi;
25902582
create table scifi."Accessory" (
25912583
"Name" varchar(200) not null,
25922584
"Id" integer not null primary key
25932585
);
2586+
2587+
--------------------------------------------------------------------------------
2588+
2589+
-- Test overloaded computed column functions targeting different tables
2590+
create schema function_overloads;
2591+
create schema function_overloads_other_schema;
2592+
create table function_overloads.pets (id serial primary key, name text);
2593+
create table function_overloads.buildings (id serial primary key, address text);
2594+
2595+
-- Two overloaded functions in the SAME schema as their target tables
2596+
create function function_overloads.code(function_overloads.pets) returns text
2597+
as $$ select 'P' || $1.id::text; $$ language sql stable;
2598+
create function function_overloads.code(function_overloads.buildings) returns text
2599+
as $$ select 'B' || $1.id::text; $$ language sql stable;
2600+
comment on function function_overloads.code(function_overloads.pets)
2601+
is E'@behavior +typeField -queryField';
2602+
comment on function function_overloads.code(function_overloads.buildings)
2603+
is E'@behavior +typeField -queryField';
2604+
2605+
-- Cross-schema computed column functions (different schema from target tables)
2606+
create function function_overloads_other_schema.age(function_overloads.pets) returns int
2607+
as $$ select 42; $$ language sql stable;
2608+
create function function_overloads_other_schema.age(function_overloads.buildings) returns int
2609+
as $$ select 99; $$ language sql stable;
2610+
comment on function function_overloads_other_schema.age(function_overloads.pets)
2611+
is E'@behavior +typeField -queryField';
2612+
comment on function function_overloads_other_schema.age(function_overloads.buildings)
2613+
is E'@behavior +typeField -queryField';

0 commit comments

Comments
 (0)