Skip to content

Commit 070e8bd

Browse files
committed
fix(graphql-codegen): handle pg resource collisions and cleanup pools
1 parent e3a90d7 commit 070e8bd

5 files changed

Lines changed: 137 additions & 24 deletions

File tree

graphile/graphile-settings/src/plugins/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ export {
2222
ConflictDetectorPreset,
2323
} from './conflict-detector';
2424

25+
// Resolve procedure/table resource-name collisions
26+
export {
27+
ProcedureResourceNameConflictPlugin,
28+
ProcedureResourceNameConflictPreset,
29+
} from './procedure-resource-name-conflict';
30+
2531
// Inflector logger for debugging
2632
export {
2733
InflectorLoggerPlugin,
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import type { GraphileConfig } from 'graphile-config';
2+
import { gatherConfig } from 'graphile-build';
3+
4+
const TABLE_LIKE_RELKINDS = new Set(['r', 'v', 'm', 'f', 'p']);
5+
6+
/**
7+
* Ensures procedure resources never collide with table/view resource names.
8+
*
9+
* Graphile v5 registers both table resources and function resources in the same
10+
* registry namespace. If a function and a table resolve to the same resource
11+
* name (e.g. both "table_grant"), schema build fails with p2rc.
12+
*
13+
* This plugin renames only the function resource when that collision occurs,
14+
* preserving table resource names.
15+
*/
16+
export const ProcedureResourceNameConflictPlugin: GraphileConfig.Plugin = {
17+
name: 'ProcedureResourceNameConflictPlugin',
18+
version: '1.0.0',
19+
description:
20+
'Renames colliding procedure resources to avoid table/procedure name conflicts',
21+
gather: gatherConfig({
22+
hooks: {
23+
async pgProcedures_PgResourceOptions(info, event) {
24+
const { serviceName, pgProc, resourceOptions } = event;
25+
const pgService = info.resolvedPreset.pgServices?.find(
26+
(svc) => svc.name === serviceName,
27+
);
28+
if (!pgService) return;
29+
30+
const schemas = pgService.schemas ?? ['public'];
31+
const introspectedService =
32+
await info.helpers.pgIntrospection.getService(serviceName);
33+
const tableResourceNames = new Set<string>();
34+
35+
for (const pgClass of introspectedService.introspection.classes) {
36+
const namespace = pgClass.getNamespace()?.nspname;
37+
if (!namespace || !schemas.includes(namespace)) continue;
38+
if (!TABLE_LIKE_RELKINDS.has(pgClass.relkind)) continue;
39+
tableResourceNames.add(
40+
info.inflection.tableResourceName({ serviceName, pgClass }),
41+
);
42+
}
43+
44+
const originalName = resourceOptions.name;
45+
if (!tableResourceNames.has(originalName)) return;
46+
47+
const registryBuilder = await info.helpers.pgRegistry.getRegistryBuilder();
48+
const existingResourceNames = new Set(
49+
Object.keys(registryBuilder.getRegistryConfig().pgResources),
50+
);
51+
52+
const arity =
53+
typeof pgProc.pronargs === 'number'
54+
? pgProc.pronargs
55+
: (resourceOptions.parameters?.length ?? 0);
56+
57+
const baseName = `${originalName}__proc${arity}`;
58+
let candidate = baseName;
59+
let index = 2;
60+
while (
61+
tableResourceNames.has(candidate) ||
62+
existingResourceNames.has(candidate)
63+
) {
64+
candidate = `${baseName}_${index++}`;
65+
}
66+
67+
resourceOptions.name = candidate;
68+
resourceOptions.extensions =
69+
resourceOptions.extensions ?? Object.create(null);
70+
(resourceOptions.extensions as unknown as Record<string, unknown>)
71+
.procedureResourceNameCollision = {
72+
originalName,
73+
renamedTo: candidate,
74+
};
75+
76+
console.warn(
77+
`[ProcedureResourceNameConflictPlugin] Renamed procedure resource '${originalName}' to '${candidate}' to avoid a table/view conflict.`,
78+
);
79+
},
80+
},
81+
}),
82+
};
83+
84+
export const ProcedureResourceNameConflictPreset: GraphileConfig.Preset = {
85+
plugins: [ProcedureResourceNameConflictPlugin],
86+
};

graphile/graphile-settings/src/presets/constructive-preset.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { PostGraphileConnectionFilterPreset } from 'postgraphile-plugin-connecti
33
import { MinimalPreset } from '../plugins/minimal-preset';
44
import { InflektPreset } from '../plugins/custom-inflector';
55
import { ConflictDetectorPreset } from '../plugins/conflict-detector';
6+
import { ProcedureResourceNameConflictPreset } from '../plugins/procedure-resource-name-conflict';
67
import { InflectorLoggerPreset } from '../plugins/inflector-logger';
78
import { NoUniqueLookupPreset } from '../plugins/primary-key-only';
89
import { EnableAllFilterColumnsPreset } from '../plugins/enable-all-filter-columns';
@@ -52,6 +53,7 @@ export const ConstructivePreset: GraphileConfig.Preset = {
5253
extends: [
5354
MinimalPreset,
5455
ConflictDetectorPreset,
56+
ProcedureResourceNameConflictPreset,
5557
InflektPreset,
5658
InflectorLoggerPreset,
5759
NoUniqueLookupPreset,

graphql/codegen/src/core/introspect/source/pgpm-module.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
*/
1010
import { PgpmPackage } from '@pgpmjs/core';
1111
import { buildSchema, introspectionFromSchema } from 'graphql';
12-
import { getPgPool } from 'pg-cache';
12+
import { getPgPool, pgCache } from 'pg-cache';
1313
import { createEphemeralDb, type EphemeralDbResult } from 'pgsql-client';
1414
import { deployPgpm } from 'pgsql-seed';
1515

@@ -250,6 +250,11 @@ export class PgpmModuleSchemaSource implements SchemaSource {
250250

251251
return { introspection };
252252
} finally {
253+
// Ensure pg-cache releases all connections to this ephemeral DB
254+
// before we attempt DROP DATABASE in teardown.
255+
pgCache.delete(dbConfig.database);
256+
await pgCache.waitForDisposals();
257+
253258
// Clean up the ephemeral database
254259
teardown({ keepDb });
255260

graphql/server/src/schema.ts

Lines changed: 37 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { printSchema, getIntrospectionQuery, buildClientSchema } from 'graphql'
22
import { ConstructivePreset, makePgService } from 'graphile-settings'
33
import { makeSchema } from 'graphile-build'
4-
import { getPgPool } from 'pg-cache'
4+
import { buildConnectionString } from 'pg-cache'
5+
import { getPgEnvOptions } from 'pg-env'
56
import type { GraphileConfig } from 'graphile-config'
67
import * as http from 'node:http'
78
import * as https from 'node:https'
9+
import pg from 'pg'
810

911
export type BuildSchemaOptions = {
1012
database?: string;
@@ -17,30 +19,42 @@ export async function buildSchemaSDL(opts: BuildSchemaOptions): Promise<string>
1719
const database = opts.database ?? 'constructive'
1820
const schemas = Array.isArray(opts.schemas) ? opts.schemas : []
1921

20-
// Get pool config for connection string
21-
const pool = getPgPool({ database })
22-
const poolConfig = (pool as any).options || {}
23-
const connectionString = `postgres://${poolConfig.user || 'postgres'}:${poolConfig.password || ''}@${poolConfig.host || 'localhost'}:${poolConfig.port || 5432}/${database}`
22+
// Resolve connection string without touching pg-cache (important for ephemeral DB teardown).
23+
const pgConfig = getPgEnvOptions({ database })
24+
const connectionString = buildConnectionString(
25+
pgConfig.user,
26+
pgConfig.password,
27+
pgConfig.host,
28+
pgConfig.port,
29+
pgConfig.database,
30+
)
2431

25-
// Build v5 preset
26-
const preset: GraphileConfig.Preset = {
27-
extends: [
28-
ConstructivePreset,
29-
...(opts.graphile?.extends ?? []),
30-
],
31-
...(opts.graphile?.disablePlugins && { disablePlugins: opts.graphile.disablePlugins }),
32-
...(opts.graphile?.plugins && { plugins: opts.graphile.plugins }),
33-
...(opts.graphile?.schema && { schema: opts.graphile.schema }),
34-
pgServices: [
35-
makePgService({
36-
connectionString,
37-
schemas,
38-
}),
39-
],
40-
}
32+
// Use an explicitly managed pool so schema build connections are always released.
33+
const schemaPool = new pg.Pool({ connectionString })
4134

42-
const { schema } = await makeSchema(preset)
43-
return printSchema(schema)
35+
try {
36+
// Build v5 preset
37+
const preset: GraphileConfig.Preset = {
38+
extends: [
39+
ConstructivePreset,
40+
...(opts.graphile?.extends ?? []),
41+
],
42+
...(opts.graphile?.disablePlugins && { disablePlugins: opts.graphile.disablePlugins }),
43+
...(opts.graphile?.plugins && { plugins: opts.graphile.plugins }),
44+
...(opts.graphile?.schema && { schema: opts.graphile.schema }),
45+
pgServices: [
46+
makePgService({
47+
pool: schemaPool,
48+
schemas,
49+
}),
50+
],
51+
}
52+
53+
const { schema } = await makeSchema(preset)
54+
return printSchema(schema)
55+
} finally {
56+
await schemaPool.end()
57+
}
4458
}
4559

4660
// Fetch GraphQL Schema SDL from a running GraphQL endpoint via introspection.

0 commit comments

Comments
 (0)