-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy pathbm25-codec.ts
More file actions
218 lines (193 loc) · 6.87 KB
/
Copy pathbm25-codec.ts
File metadata and controls
218 lines (193 loc) · 6.87 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
/**
* Bm25CodecPlugin
*
* Teaches PostGraphile v5 how to handle the pg_textsearch `bm25query` type
* and discovers all BM25 indexes in the database.
*
* This plugin:
* 1. Creates a codec for bm25query via gather.hooks.pgCodecs_findPgCodec
* 2. Discovers all BM25 indexes via gather.hooks.pgIntrospection_introspection
* by querying pg_index + pg_am + pg_class + pg_attribute
* 3. Stores discovered BM25 index info in a module-level Map for use by
* the BM25 adapter during the schema build phase
*/
import 'graphile-build-pg';
import type { GraphileConfig } from 'graphile-config';
import sql from 'pg-sql2';
/**
* Represents a discovered BM25 index in the database.
*/
export interface Bm25IndexInfo {
/** Schema name (e.g. 'public') */
schemaName: string;
/** Table name (e.g. 'documents') */
tableName: string;
/** Column name (e.g. 'content') */
columnName: string;
/** Index name (e.g. 'docs_idx') — needed for to_bm25query() */
indexName: string;
}
/**
* Module-level store for discovered BM25 indexes.
* Populated during the gather phase, read during the schema build phase.
*
* Key: "schemaName.tableName.columnName"
* Value: Bm25IndexInfo
*
* NOTE (concurrency + blueprint pooling): this store is process-global and is
* cleared + repopulated per introspection run. Concurrent gathers in one
* process are serialized by the server's build semaphore
* (GRAPHILE_BUILD_CONCURRENCY, default 1), so a single global store is
* tolerated for now; if that guarantee ever moves, key this store by build
* object like meta-schema's per-build cache. The adapter embeds the
* schema-qualified index name from these entries as a bind VALUE at plan
* time; the pooling rewrite seam maps schema identifiers in SQL TEXT only and
* never rewrites values, so shapes carrying bm25 indexes are structurally
* excluded from pooling (see graphql/server pooling-decision.ts) and always
* get per-tenant instances.
*/
export const bm25IndexStore = new Map<string, Bm25IndexInfo>();
/**
* Whether pg_textsearch extension was detected in the database.
*/
export let bm25ExtensionDetected = false;
/**
* The SQL query that discovers BM25 indexes in the database.
* Joins pg_index -> pg_class -> pg_am to find all indexes using the 'bm25'
* access method, then resolves the schema, table, column, and index names.
*/
const BM25_DISCOVERY_SQL = `
SELECT
n.nspname AS schema_name,
c.relname AS table_name,
a.attname AS column_name,
i.relname AS index_name
FROM pg_index ix
JOIN pg_class i ON i.oid = ix.indexrelid
JOIN pg_am am ON am.oid = i.relam
JOIN pg_class c ON c.oid = ix.indrelid
JOIN pg_namespace n ON n.oid = c.relnamespace
JOIN pg_attribute a ON a.attrelid = c.oid AND a.attnum = ANY(ix.indkey)
WHERE am.amname = 'bm25'
`;
export const Bm25CodecPlugin: GraphileConfig.Plugin = {
name: 'Bm25CodecPlugin',
version: '1.0.0',
description: 'Registers a codec for the pg_textsearch bm25query type and discovers BM25 indexes',
gather: {
hooks: {
/**
* Register the bm25query codec when detected during type introspection.
*/
async pgCodecs_findPgCodec(info, event) {
if (event.pgCodec) return;
const { pgType: type, serviceName } = event;
if (type.typname !== 'bm25query') return;
const typeNamespace = await info.helpers.pgIntrospection.getNamespace(
serviceName,
type.typnamespace
);
if (!typeNamespace) return;
const schemaName = typeNamespace.nspname;
event.pgCodec = {
name: 'bm25query',
sqlType: sql.identifier(schemaName, 'bm25query'),
// PG sends bm25query as text
fromPg(value: string): string {
return value;
},
// string -> bm25query text
toPg(value: string): string {
return value;
},
attributes: undefined,
executor: null,
extensions: {
oid: type._id,
pg: { serviceName, schemaName, name: 'bm25query' },
},
};
},
/**
* After introspection completes, query for all BM25 indexes.
* Uses the pgService's adaptorSettings to create a direct pg.Pool
* connection and runs the BM25 discovery query.
*/
async pgIntrospection_introspection(info, event) {
const { serviceName } = event;
// Get the pgService from the resolved preset
const pgService = info.resolvedPreset?.pgServices?.find(
(s: { name?: string }) => (s.name ?? 'main') === serviceName
);
if (!pgService) return;
// Clear previous entries for this introspection run
bm25IndexStore.clear();
try {
const adaptorSettings = (pgService as any).adaptorSettings;
if (!adaptorSettings?.connectionString && !adaptorSettings?.pool) {
return;
}
// Import pg dynamically for the discovery query
const { Pool } = await import('pg');
const existingPool = adaptorSettings.pool;
const pool = existingPool ?? new Pool({
connectionString: adaptorSettings.connectionString,
max: 1,
});
const isOwnPool = !existingPool;
try {
const result = await pool.query(BM25_DISCOVERY_SQL);
if (result.rows && result.rows.length > 0) {
bm25ExtensionDetected = true;
for (const row of result.rows) {
const key = `${row.schema_name}.${row.table_name}.${row.column_name}`;
bm25IndexStore.set(key, {
schemaName: row.schema_name,
tableName: row.table_name,
columnName: row.column_name,
indexName: row.index_name,
});
}
}
} finally {
if (isOwnPool) {
await pool.end();
}
}
} catch {
// pg_textsearch not installed or query failed — gracefully skip
bm25ExtensionDetected = false;
}
},
},
},
schema: {
hooks: {
init: {
before: ['PgCodecs'],
callback(_, build) {
const { setGraphQLTypeForPgCodec } = build;
// Map bm25query codec to String for both input and output
for (const codec of Object.values(build.input.pgRegistry.pgCodecs)) {
if ((codec as any).name === 'bm25query') {
setGraphQLTypeForPgCodec(
codec as any,
'input',
build.graphql.GraphQLString.name
);
setGraphQLTypeForPgCodec(
codec as any,
'output',
build.graphql.GraphQLString.name
);
}
}
return _;
},
},
},
},
};
export const Bm25CodecPreset: GraphileConfig.Preset = {
plugins: [Bm25CodecPlugin],
};