Skip to content

Commit 090c885

Browse files
committed
Add functionResourceNameShouldPrefixCompositeType inflector with cross-schema warning
1 parent 4a8e6b7 commit 090c885

35 files changed

+281
-213
lines changed

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

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

2828
import { exportNameHint, forbidRequired } from "../utils.ts";
2929
import { version } from "../version.ts";
@@ -41,6 +41,13 @@ declare global {
4141
pgProc: PgProc;
4242
},
4343
): string;
44+
functionResourceNameShouldPrefixCompositeType(
45+
this: Inflection,
46+
details: {
47+
pgProc: PgProc;
48+
firstArgCompositeType: PgType;
49+
},
50+
): boolean;
4451
functionRecordReturnCodecName(
4552
this: Inflection,
4653
details: {
@@ -137,7 +144,14 @@ export const PgProceduresPlugin: GraphileConfig.Plugin = {
137144
// distinct names (pets_code, buildings_code).
138145
if (pgProc.provolatile !== "v") {
139146
const firstArg = pgProc.getArguments().find((a) => a.isIn);
140-
if (firstArg && firstArg.type.typtype === "c") {
147+
if (
148+
firstArg &&
149+
firstArg.type.typtype === "c" &&
150+
this.functionResourceNameShouldPrefixCompositeType({
151+
pgProc,
152+
firstArgCompositeType: firstArg.type,
153+
})
154+
) {
141155
const compositePrefix = firstArg.type.typname + "_";
142156
if (!pgProc.proname.startsWith(compositePrefix)) {
143157
return `${schemaPrefix}${firstArg.type.typname}_${pgProc.proname}`;
@@ -146,6 +160,15 @@ export const PgProceduresPlugin: GraphileConfig.Plugin = {
146160
}
147161
return `${schemaPrefix}${pgProc.proname}`;
148162
},
163+
functionResourceNameShouldPrefixCompositeType(
164+
_options,
165+
{ pgProc, firstArgCompositeType },
166+
) {
167+
// By default, only consider a function as a computed column if it
168+
// belongs to the same schema as the composite type. Override this
169+
// inflector to support cross-schema computed columns.
170+
return firstArgCompositeType.typnamespace === pgProc.pronamespace;
171+
},
149172
functionRecordReturnCodecName(options, details) {
150173
return this.upperCamelCase(
151174
this.functionResourceName(details) + "-record",
@@ -684,6 +707,20 @@ export const PgProceduresPlugin: GraphileConfig.Plugin = {
684707
}),
685708
);
686709
if (overload) {
710+
// Warn if both functions target composite types — the user likely
711+
// intended these as computed columns on different tables, but the
712+
// inflector produced the same name (e.g. cross-schema overloads).
713+
const thisFirstArg = pgProc.getArguments().find((a) => a.isIn);
714+
const otherFirstArg = overload.getArguments().find((a) => a.isIn);
715+
if (
716+
thisFirstArg?.type.typtype === "c" &&
717+
otherFirstArg?.type.typtype === "c" &&
718+
thisFirstArg.type._id !== otherFirstArg.type._id
719+
) {
720+
console.warn(
721+
`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.`,
722+
);
723+
}
687724
return;
688725
}
689726

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
-- Test overloaded computed column functions targeting different tables
2+
drop schema if exists function_overloads, function_overloads_other_schema cascade;
3+
4+
create schema function_overloads;
5+
create schema function_overloads_other_schema;
6+
create table function_overloads.pets (id serial primary key, name text);
7+
create table function_overloads.buildings (id serial primary key, address text);
8+
9+
-- Two overloaded functions in the SAME schema as their target tables
10+
create function function_overloads.code(function_overloads.pets) returns text
11+
as $$ select 'P' || $1.id::text; $$ language sql stable;
12+
create function function_overloads.code(function_overloads.buildings) returns text
13+
as $$ select 'B' || $1.id::text; $$ language sql stable;
14+
comment on function function_overloads.code(function_overloads.pets)
15+
is E'@behavior +typeField -queryField';
16+
comment on function function_overloads.code(function_overloads.buildings)
17+
is E'@behavior +typeField -queryField';
18+
19+
-- Cross-schema computed column functions (different schema from target tables)
20+
create function function_overloads_other_schema.age(function_overloads.pets) returns int
21+
as $$ select 42; $$ language sql stable;
22+
create function function_overloads_other_schema.age(function_overloads.buildings) returns int
23+
as $$ select 99; $$ language sql stable;
24+
comment on function function_overloads_other_schema.age(function_overloads.pets)
25+
is E'@behavior +typeField -queryField';
26+
comment on function function_overloads_other_schema.age(function_overloads.buildings)
27+
is E'@behavior +typeField -queryField';

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

Lines changed: 10 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,7 @@ drop schema if exists
3030
issue_2334,
3131
relay,
3232
cjk,
33-
scifi,
34-
function_overloads,
35-
function_overloads_other_schema
33+
scifi
3634
cascade;
3735
drop extension if exists tablefunc;
3836
drop extension if exists intarray;
@@ -2476,7 +2474,7 @@ create function function_returning_enum.applicants_next_stage(
24762474
a function_returning_enum.applicants
24772475
) returns function_returning_enum.stage_options_enum_domain
24782476
as $$
2479-
select (case when a.stage = 'round 2' then 'hired'
2477+
select (case when a.stage = 'round 2' then 'hired'
24802478
else 'rejected' end)::function_returning_enum.stage_options_enum_domain;
24812479
$$ language sql stable;
24822480
comment on function function_returning_enum.applicants_next_stage is E'@filterable';
@@ -2496,7 +2494,7 @@ create function function_returning_enum.text_length(
24962494
min_length int
24972495
) returns function_returning_enum.length
24982496
as $$
2499-
select (case when length(text) < min_length then 'too_short'
2497+
select (case when length(text) < min_length then 'too_short'
25002498
else 'ok' end)::function_returning_enum.length;
25012499
$$ language sql stable;
25022500

@@ -2511,23 +2509,23 @@ comment on function function_returning_enum.applicants_name_length is E'@filtera
25112509
create function function_returning_enum.applicants_by_stage(
25122510
wanted_stage function_returning_enum.stage_options_enum_domain
25132511
) returns setof function_returning_enum.applicants
2514-
as $$
2512+
as $$
25152513
select * from function_returning_enum.applicants a where a.stage = wanted_stage;
25162514
$$ language sql stable;
25172515

25182516
create function function_returning_enum.applicants_by_favorite_pet(
25192517
pet function_returning_enum.animal_type
25202518
) returns setof function_returning_enum.applicants
2521-
as $$
2519+
as $$
25222520
select * from function_returning_enum.applicants a where a.favorite_pet = pet;
25232521
$$ language sql stable;
25242522

25252523
create function function_returning_enum.applicants_pet_food(
25262524
a function_returning_enum.applicants
25272525
) returns function_returning_enum.animal_type
25282526
as $$
2529-
select (case
2530-
when a.favorite_pet = 'FISH' then null
2527+
select (case
2528+
when a.favorite_pet = 'FISH' then null
25312529
when a.favorite_pet = 'CAT' then 'FISH'
25322530
when a.favorite_pet = 'DOG' then 'CAT'
25332531
else null
@@ -2541,16 +2539,16 @@ comment on domain function_returning_enum.transportation is E'@enum enum_table_t
25412539
create function function_returning_enum.applicants_by_transportation(
25422540
transportation function_returning_enum.transportation
25432541
) returns setof function_returning_enum.applicants
2544-
as $$
2542+
as $$
25452543
select * from function_returning_enum.applicants a where a.transportation = applicants_by_transportation.transportation;
25462544
$$ language sql stable;
25472545

25482546
create function function_returning_enum.applicants_favorite_pet_transportation(
25492547
a function_returning_enum.applicants
25502548
) returns function_returning_enum.transportation
25512549
as $$
2552-
select (case
2553-
when a.favorite_pet = 'FISH' then 'SUBWAY'
2550+
select (case
2551+
when a.favorite_pet = 'FISH' then 'SUBWAY'
25542552
when a.favorite_pet = 'CAT' then 'CAR'
25552553
when a.favorite_pet = 'DOG' then 'BIKE'
25562554
else null
@@ -2583,31 +2581,3 @@ create table scifi."Accessory" (
25832581
"Name" varchar(200) not null,
25842582
"Id" integer not null primary key
25852583
);
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';

postgraphile/postgraphile/__tests__/queries/v4/procedure-query.json5

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -558,7 +558,7 @@
558558
],
559559
totalCount: 11,
560560
},
561-
compoundTypeQueryCompoundTypeArray: [
561+
queryCompoundTypeArray: [
562562
{
563563
a: 419,
564564
b: "easy cheesy baked potatoes",

postgraphile/postgraphile/__tests__/queries/v4/procedure-query.mermaid

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ graph TD
3636
Bucket25("Bucket 25 (listItem)<br /><br />ROOT __Item{25}ᐸ268ᐳ[284]"):::bucket
3737
Bucket26("Bucket 26 (listItem)<br /><br />ROOT __Item{26}ᐸ274ᐳ[285]"):::bucket
3838
Bucket27("Bucket 27 (nullableBoundary)<br />Deps: 281<br /><br />ROOT PgSelectSingle{23}ᐸcompound_type_array_queryᐳ[281]"):::bucket
39-
Bucket28("Bucket 28 (nullableBoundary)<br />Deps: 283<br /><br />ROOT PgSelectSingle{24}ᐸcompound_type_query_compound_type_arrayᐳ[283]"):::bucket
39+
Bucket28("Bucket 28 (nullableBoundary)<br />Deps: 283<br /><br />ROOT PgSelectSingle{24}ᐸquery_compound_type_arrayᐳ[283]"):::bucket
4040
Bucket29("Bucket 29 (nullableBoundary)<br />Deps: 285<br /><br />ROOT __Item{26}ᐸ274ᐳ[285]"):::bucket
4141
Bucket51("Bucket 51 (nullableBoundary)<br />Deps: 392<br /><br />ROOT PgClassExpression{1}ᐸ__compound...uery__.”g”ᐳ[392]"):::bucket
4242
Bucket52("Bucket 52 (listItem)<br />Deps: 459<br /><br />ROOT __Item{52}ᐸ287ᐳ[394]"):::bucket
@@ -81,7 +81,7 @@ graph TD
8181
Bucket91("Bucket 91 (nullableBoundary)<br />Deps: 434<br /><br />ROOT PgClassExpression{71}ᐸ__query_in...al_set__.vᐳ[434]"):::bucket
8282
Bucket92("Bucket 92 (nullableBoundary)<br />Deps: 435, 434, 644<br /><br />ROOT Edge{71}[435]"):::bucket
8383
Bucket93("Bucket 93 (nullableBoundary)<br />Deps: 454<br /><br />ROOT PgClassExpression{27}ᐸ__compound...uery__.”g”ᐳ[454]"):::bucket
84-
Bucket94("Bucket 94 (nullableBoundary)<br />Deps: 455<br /><br />ROOT PgClassExpression{28}ᐸ__compound...rray__.”g”ᐳ[455]"):::bucket
84+
Bucket94("Bucket 94 (nullableBoundary)<br />Deps: 455<br /><br />ROOT PgClassExpression{28}ᐸ__query_co...rray__.”g”ᐳ[455]"):::bucket
8585
Bucket95("Bucket 95 (nullableBoundary)<br />Deps: 434<br /><br />ROOT PgClassExpression{71}ᐸ__query_in...al_set__.vᐳ[434]"):::bucket
8686
Bucket96("Bucket 96 (nullableBoundary)<br />Deps: 623<br /><br />ROOT PgSelectSingle{72}ᐸcompound_type_set_queryᐳ[623]"):::bucket
8787
Bucket97("Bucket 97 (nullableBoundary)<br />Deps: 624<br /><br />ROOT PgSelectSingle{73}ᐸtable_set_queryᐳ[624]"):::bucket
@@ -314,7 +314,7 @@ graph TD
314314
PgSelect236 --> Connection238
315315
PgSelect244[["PgSelect[244∈0] ➊<br />ᐸint_set_queryᐳ"]]:::plan
316316
Object10 & PgFromExpression246 --> PgSelect244
317-
PgSelect260[["PgSelect[260∈0] ➊<br />ᐸcompound_type_query_compound_type_arrayᐳ"]]:::plan
317+
PgSelect260[["PgSelect[260∈0] ➊<br />ᐸquery_compound_type_arrayᐳ"]]:::plan
318318
PgFromExpression262{{"PgFromExpression[262∈0] ➊"}}:::plan
319319
Object10 & PgFromExpression262 --> PgSelect260
320320
PgSelect369[["PgSelect[369∈0] ➊<br />ᐸint_set_query(aggregate)ᐳ"]]:::plan
@@ -690,7 +690,7 @@ graph TD
690690
__Item280 --> PgSelectSingle281
691691
__Item282[/"__Item[282∈24]<br />ᐸ279ᐳ"\]:::itemplan
692692
PgSelectRows279 ==> __Item282
693-
PgSelectSingle283{{"PgSelectSingle[283∈24]<br />ᐸcompound_type_query_compound_type_arrayᐳ"}}:::plan
693+
PgSelectSingle283{{"PgSelectSingle[283∈24]<br />ᐸquery_compound_type_arrayᐳ"}}:::plan
694694
__Item282 --> PgSelectSingle283
695695
__Item284[/"__Item[284∈25]<br />ᐸ268ᐳ"\]:::itemplan
696696
PgClassExpression268 ==> __Item284
@@ -712,21 +712,21 @@ graph TD
712712
PgSelectSingle281 --> PgClassExpression454
713713
PgClassExpression456{{"PgClassExpression[456∈27]<br />ᐸ__compound....”foo_bar”ᐳ"}}:::plan
714714
PgClassExpression451 o--o PgClassExpression456
715-
PgClassExpression437{{"PgClassExpression[437∈28]<br />ᐸ__compound...rray__.”a”ᐳ"}}:::plan
715+
PgClassExpression437{{"PgClassExpression[437∈28]<br />ᐸ__query_co...rray__.”a”ᐳ"}}:::plan
716716
PgSelectSingle283 --> PgClassExpression437
717-
PgClassExpression440{{"PgClassExpression[440∈28]<br />ᐸ__compound...rray__.”b”ᐳ"}}:::plan
717+
PgClassExpression440{{"PgClassExpression[440∈28]<br />ᐸ__query_co...rray__.”b”ᐳ"}}:::plan
718718
PgClassExpression437 o--o PgClassExpression440
719-
PgClassExpression443{{"PgClassExpression[443∈28]<br />ᐸ__compound...rray__.”c”ᐳ"}}:::plan
719+
PgClassExpression443{{"PgClassExpression[443∈28]<br />ᐸ__query_co...rray__.”c”ᐳ"}}:::plan
720720
PgClassExpression440 o--o PgClassExpression443
721-
PgClassExpression446{{"PgClassExpression[446∈28]<br />ᐸ__compound...rray__.”d”ᐳ"}}:::plan
721+
PgClassExpression446{{"PgClassExpression[446∈28]<br />ᐸ__query_co...rray__.”d”ᐳ"}}:::plan
722722
PgClassExpression443 o--o PgClassExpression446
723-
PgClassExpression449{{"PgClassExpression[449∈28]<br />ᐸ__compound...rray__.”e”ᐳ"}}:::plan
723+
PgClassExpression449{{"PgClassExpression[449∈28]<br />ᐸ__query_co...rray__.”e”ᐳ"}}:::plan
724724
PgClassExpression446 o--o PgClassExpression449
725-
PgClassExpression452{{"PgClassExpression[452∈28]<br />ᐸ__compound...rray__.”f”ᐳ"}}:::plan
725+
PgClassExpression452{{"PgClassExpression[452∈28]<br />ᐸ__query_co...rray__.”f”ᐳ"}}:::plan
726726
PgClassExpression449 o--o PgClassExpression452
727-
PgClassExpression455{{"PgClassExpression[455∈28]<br />ᐸ__compound...rray__.”g”ᐳ"}}:::plan
727+
PgClassExpression455{{"PgClassExpression[455∈28]<br />ᐸ__query_co...rray__.”g”ᐳ"}}:::plan
728728
PgSelectSingle283 --> PgClassExpression455
729-
PgClassExpression457{{"PgClassExpression[457∈28]<br />ᐸ__compound....”foo_bar”ᐳ"}}:::plan
729+
PgClassExpression457{{"PgClassExpression[457∈28]<br />ᐸ__query_co....”foo_bar”ᐳ"}}:::plan
730730
PgClassExpression452 o--o PgClassExpression457
731731
Access438{{"Access[438∈29]<br />ᐸ285.secondsᐳ"}}:::plan
732732
__Item285 --> Access438

postgraphile/postgraphile/__tests__/queries/v4/procedure-query.sql

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -255,16 +255,16 @@ select
255255
from "a"."static_big_integer"() as __static_big_integer__(v);
256256

257257
select
258-
__compound_type_query_compound_type_array__."a"::text as "0",
259-
__compound_type_query_compound_type_array__."b" as "1",
260-
__compound_type_query_compound_type_array__."c"::text as "2",
261-
__compound_type_query_compound_type_array__."d" as "3",
262-
__compound_type_query_compound_type_array__."e"::text as "4",
263-
__compound_type_query_compound_type_array__."f"::text as "5",
264-
to_char(__compound_type_query_compound_type_array__."g", 'YYYY_MM_DD_HH24_MI_SS.US'::text) as "6",
265-
__compound_type_query_compound_type_array__."foo_bar"::text as "7",
266-
(not (__compound_type_query_compound_type_array__ is null))::text as "8"
267-
from unnest("a"."query_compound_type_array"($1::"c"."compound_type")) as __compound_type_query_compound_type_array__;
258+
__query_compound_type_array__."a"::text as "0",
259+
__query_compound_type_array__."b" as "1",
260+
__query_compound_type_array__."c"::text as "2",
261+
__query_compound_type_array__."d" as "3",
262+
__query_compound_type_array__."e"::text as "4",
263+
__query_compound_type_array__."f"::text as "5",
264+
to_char(__query_compound_type_array__."g", 'YYYY_MM_DD_HH24_MI_SS.US'::text) as "6",
265+
__query_compound_type_array__."foo_bar"::text as "7",
266+
(not (__query_compound_type_array__ is null))::text as "8"
267+
from unnest("a"."query_compound_type_array"($1::"c"."compound_type")) as __query_compound_type_array__;
268268

269269
select
270270
__query_text_array__.v::text as "0"

postgraphile/postgraphile/__tests__/queries/v4/procedure-query.test.graphql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ query {
4747
noArgsQuery
4848
staticBigInteger { edges { node } totalCount }
4949

50-
compoundTypeQueryCompoundTypeArray(object: { a: 419, b: "easy cheesy baked potatoes", c: RED, e: BAR_FOO, f: _EMPTY_, g: {hours: 5 }, fooBar: 8 }) { a b c d e f g { seconds minutes hours } fooBar }
50+
queryCompoundTypeArray(object: { a: 419, b: "easy cheesy baked potatoes", c: RED, e: BAR_FOO, f: _EMPTY_, g: {hours: 5 }, fooBar: 8 }) { a b c d e f g { seconds minutes hours } fooBar }
5151
queryTextArray
5252
queryIntervalArray { seconds minutes hours days months years }
5353
queryIntervalSet { nodes { seconds minutes hours days months years } edges { node { seconds minutes hours } cursor } totalCount }

0 commit comments

Comments
 (0)