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
11 changes: 11 additions & 0 deletions .changeset/slow-hounds-impress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"graphile-build-pg": patch
"postgraphile": patch
---

Add opt-in `PgFunctionOverloadsPreset` which factors the input argument types
into function resource names (e.g. `code(pets)` becomes `code__pets`), enabling
support for overloaded functions such as computed column functions targeting
different tables. Without the preset, overloaded functions are skipped as
before; a warning is now logged when the skipped overloads look like computed
columns.
4 changes: 4 additions & 0 deletions graphile-build/graphile-build-pg/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ export { PgEnumDomainsPlugin } from "./plugins/PgEnumDomainsPlugin.ts";
export { PgEnumTablesPlugin } from "./plugins/PgEnumTablesPlugin.ts";
export { PgFakeConstraintsPlugin } from "./plugins/PgFakeConstraintsPlugin.ts";
export { PgFirstLastBeforeAfterArgsPlugin } from "./plugins/PgFirstLastBeforeAfterArgsPlugin.ts";
export {
PgFunctionOverloadsPlugin,
PgFunctionOverloadsPreset,
} from "./plugins/PgFunctionOverloadsPlugin.ts";
export { PgIndexBehaviorsPlugin } from "./plugins/PgIndexBehaviorsPlugin.ts";
export { PgInterfaceModeUnionAllRowsPlugin } from "./plugins/PgInterfaceModeUnionAllRowsPlugin.ts";
export { PgIntrospectionPlugin } from "./plugins/PgIntrospectionPlugin.ts";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import "graphile-config";

import type { PgProc } from "pg-introspection";

import { version } from "../version.ts";

declare global {
namespace GraphileBuild {
interface Inflection {
/**
* The signature to append to a function's resource name to
* disambiguate it from its overloads, based on the names of its input
* argument types.
*/
functionResourceNameSignature(
this: Inflection,
details: {
serviceName: string;
pgProc: PgProc;
},
): string;
}
}

namespace GraphileConfig {
interface Plugins {
PgFunctionOverloadsPlugin: true;
}
}
}

export const PgFunctionOverloadsPlugin: GraphileConfig.Plugin = {
name: "PgFunctionOverloadsPlugin",
description:
"Factors the input argument types into function resource names so that overloaded functions receive distinct names rather than being skipped",
version: version,

inflection: {
replace: {
functionResourceName(previous, options, details) {
if (!previous) {
throw new Error(`No functionResourceName inflector found!`);
}
return `${previous(details)}${this.functionResourceNameSignature(
details,
)}`;
},
},
add: {
functionResourceNameSignature(_options, { pgProc }) {
return pgProc
.getArguments()
.filter((a) => a.isIn)
.map((a) => "__" + a.type.typname)
.join("");
},
},
},
};

export const PgFunctionOverloadsPreset: GraphileConfig.Preset = {
plugins: [PgFunctionOverloadsPlugin],
};
37 changes: 29 additions & 8 deletions graphile-build/graphile-build-pg/src/plugins/PgProceduresPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -606,7 +606,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 +651,35 @@ 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.
const overload = introspection.procs.find(
// We don't want procedures whose inflected resource name clashes
// with another overload; this would produce duplicate fields.
const name = inflection.functionResourceName({ serviceName, pgProc });
const forbiddenOverload = 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,
}) === name,
);
if (overload) {
if (forbiddenOverload) {
// Warn if both functions target composite types; the user likely
// intended these as computed columns on different tables.
const thisFirstArg = pgProc.getArguments().find((a) => a.isIn);
const otherFirstArg = forbiddenOverload
.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 it has overloads and they generate the same name. Consider using 'PgFunctionOverloadsPreset' to factor argument types into resource names.`,
);
}
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