@vibeorm/extensions 2.0.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +167 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +966 -0
- package/dist/index.js.map +13 -0
- package/dist/search/index.d.ts +81 -0
- package/dist/search/index.d.ts.map +1 -0
- package/dist/search/index.js +511 -0
- package/dist/search/index.js.map +11 -0
- package/dist/shared.d.ts +35 -0
- package/dist/shared.d.ts.map +1 -0
- package/dist/trgm/index.d.ts +45 -0
- package/dist/trgm/index.d.ts.map +1 -0
- package/dist/trgm/index.js +248 -0
- package/dist/trgm/index.js.map +11 -0
- package/dist/vector/index.d.ts +64 -0
- package/dist/vector/index.d.ts.map +1 -0
- package/dist/vector/index.js +359 -0
- package/dist/vector/index.js.map +11 -0
- package/package.json +63 -0
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/extensions/src/search/index.ts
|
|
3
|
+
import { VibeError as VibeError2, defineExtension, unsupportedCapability } from "@vibeorm/schema";
|
|
4
|
+
import { ParamCollector, getSqlDialect, renderExpr } from "@vibeorm/sql";
|
|
5
|
+
|
|
6
|
+
// packages/extensions/src/shared.ts
|
|
7
|
+
import { VibeError } from "@vibeorm/schema";
|
|
8
|
+
function tableNameOf(params) {
|
|
9
|
+
return params.model.dbName ?? params.model.name;
|
|
10
|
+
}
|
|
11
|
+
function columnNameOf(params) {
|
|
12
|
+
const found = params.model.fields.find((candidate) => candidate.name === params.field);
|
|
13
|
+
if (found === undefined) {
|
|
14
|
+
throw new VibeError({
|
|
15
|
+
code: "VIBE_INTERNAL",
|
|
16
|
+
message: `extension referenced unknown field "${params.field}" on model "${params.model.name}"`,
|
|
17
|
+
meta: { model: params.model.name, field: params.field }
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
return found.dbName ?? found.name;
|
|
21
|
+
}
|
|
22
|
+
function primaryKeyFieldOf(params) {
|
|
23
|
+
const { model } = params;
|
|
24
|
+
if (model.primaryKey !== undefined && model.primaryKey.fields.length === 1) {
|
|
25
|
+
return model.primaryKey.fields[0] ?? null;
|
|
26
|
+
}
|
|
27
|
+
const idField = model.fields.find((field) => field.isId);
|
|
28
|
+
return idField?.name ?? null;
|
|
29
|
+
}
|
|
30
|
+
function declString(decl, key) {
|
|
31
|
+
const value = decl[key];
|
|
32
|
+
return typeof value === "string" ? value : undefined;
|
|
33
|
+
}
|
|
34
|
+
function declStringArray(decl, key) {
|
|
35
|
+
const value = decl[key];
|
|
36
|
+
if (!Array.isArray(value))
|
|
37
|
+
return;
|
|
38
|
+
return value.every((entry) => typeof entry === "string") ? value : undefined;
|
|
39
|
+
}
|
|
40
|
+
function declNumberArray(decl, key) {
|
|
41
|
+
const value = decl[key];
|
|
42
|
+
if (!Array.isArray(value))
|
|
43
|
+
return;
|
|
44
|
+
return value.every((entry) => typeof entry === "number") ? value : undefined;
|
|
45
|
+
}
|
|
46
|
+
var SQL_LITERAL_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
47
|
+
function isSafeSqlName(params) {
|
|
48
|
+
return SQL_LITERAL_NAME.test(params.name);
|
|
49
|
+
}
|
|
50
|
+
async function pgExtensionCheck(params) {
|
|
51
|
+
const { execute, name } = params;
|
|
52
|
+
try {
|
|
53
|
+
const rows = await execute({
|
|
54
|
+
text: "SELECT name, default_version, installed_version FROM pg_available_extensions WHERE name = $1",
|
|
55
|
+
values: [name]
|
|
56
|
+
});
|
|
57
|
+
const row = rows[0];
|
|
58
|
+
if (row === undefined) {
|
|
59
|
+
return { name: `extension ${name}`, ok: false, note: "not available on this server" };
|
|
60
|
+
}
|
|
61
|
+
const installed = row["installed_version"];
|
|
62
|
+
return {
|
|
63
|
+
name: `extension ${name}`,
|
|
64
|
+
ok: true,
|
|
65
|
+
note: installed === null || installed === undefined ? "available, not installed" : `installed ${String(installed)}`
|
|
66
|
+
};
|
|
67
|
+
} catch (error) {
|
|
68
|
+
return {
|
|
69
|
+
name: `extension ${name}`,
|
|
70
|
+
ok: false,
|
|
71
|
+
note: `check failed: ${error instanceof Error ? error.message : String(error)}`
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// packages/extensions/src/search/index.ts
|
|
77
|
+
var VERSION = "2.0.0-alpha.0";
|
|
78
|
+
var SCORE_ALIAS = "_vibe_score";
|
|
79
|
+
var HIGHLIGHT_ALIAS = "_vibe_highlight";
|
|
80
|
+
var WEIGHT_LETTERS = ["A", "B", "C", "D"];
|
|
81
|
+
function resolveSearchEngine(params) {
|
|
82
|
+
const { dialect, provider, requested } = params;
|
|
83
|
+
const native = dialect === "postgres" ? "tsvector" : dialect === "sqlite" ? "fts5" : "fulltext";
|
|
84
|
+
if (requested === "auto")
|
|
85
|
+
return native;
|
|
86
|
+
if (requested === native)
|
|
87
|
+
return native;
|
|
88
|
+
if (requested === "bm25" && dialect === "postgres") {
|
|
89
|
+
if (provider === "pglite") {
|
|
90
|
+
throw unsupportedCapability({
|
|
91
|
+
dialect,
|
|
92
|
+
feature: 'the search engine "bm25" (ParadeDB pg_search)',
|
|
93
|
+
hint: 'pglite does not bundle pg_search \u2014 use engine "auto"/"tsvector"'
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return "bm25";
|
|
97
|
+
}
|
|
98
|
+
throw unsupportedCapability({
|
|
99
|
+
dialect,
|
|
100
|
+
feature: `the search engine "${requested}"`,
|
|
101
|
+
hint: `this dialect supports "${native}"${dialect === "postgres" ? ' and "bm25"' : ""}`
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
function readDeclaration(params) {
|
|
105
|
+
const fields = declStringArray(params.declaration, "fields");
|
|
106
|
+
if (fields === undefined || fields.length === 0)
|
|
107
|
+
return null;
|
|
108
|
+
return {
|
|
109
|
+
fields,
|
|
110
|
+
weights: declNumberArray(params.declaration, "weights"),
|
|
111
|
+
base: declString(params.declaration, "name") ?? `${tableNameOf({ model: params.model })}_search`,
|
|
112
|
+
language: declString(params.declaration, "language")
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function weightLetters(params) {
|
|
116
|
+
const { decl } = params;
|
|
117
|
+
if (decl.weights === undefined || decl.weights.length !== decl.fields.length)
|
|
118
|
+
return null;
|
|
119
|
+
const ranked = [...new Set(decl.weights)].sort((a, b) => b - a);
|
|
120
|
+
return decl.weights.map((weight) => WEIGHT_LETTERS[Math.min(ranked.indexOf(weight), WEIGHT_LETTERS.length - 1)]);
|
|
121
|
+
}
|
|
122
|
+
function tsvectorExpression(params) {
|
|
123
|
+
const { dialect, model, decl, language } = params;
|
|
124
|
+
const quote = (name) => dialect.quoteIdent({ name });
|
|
125
|
+
const letters = weightLetters({ decl });
|
|
126
|
+
const parts = decl.fields.map((field, index) => {
|
|
127
|
+
const vector = `to_tsvector('${language}', coalesce(${quote(columnNameOf({ model, field }))}, ''))`;
|
|
128
|
+
return letters === null ? vector : `setweight(${vector}, '${letters[index]}')`;
|
|
129
|
+
});
|
|
130
|
+
return letters === null && parts.length > 1 ? `to_tsvector('${language}', ${decl.fields.map((field) => `coalesce(${quote(columnNameOf({ model, field }))}, '')`).join(" || ' ' || ")})` : parts.join(" || ");
|
|
131
|
+
}
|
|
132
|
+
function searchArtifacts(ctx) {
|
|
133
|
+
const requested = ctx.options["engine"] ?? "auto";
|
|
134
|
+
const engine = resolveSearchEngine({ dialect: ctx.dialect, provider: ctx.provider, requested });
|
|
135
|
+
const dialect = getSqlDialect({ dialect: ctx.dialect });
|
|
136
|
+
const quote = (name) => dialect.quoteIdent({ name });
|
|
137
|
+
const artifacts = [];
|
|
138
|
+
if (engine === "bm25") {
|
|
139
|
+
artifacts.push({
|
|
140
|
+
id: "search:extension:pg_search",
|
|
141
|
+
owns: { kind: "postgresExtension", name: "pg_search" },
|
|
142
|
+
create: ["CREATE EXTENSION IF NOT EXISTS pg_search;"],
|
|
143
|
+
drop: ["DROP EXTENSION IF EXISTS pg_search;"]
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
for (const [modelName, declaration] of Object.entries(ctx.declarations)) {
|
|
147
|
+
const model = ctx.schema.models.find((candidate) => candidate.name === modelName);
|
|
148
|
+
if (model === undefined)
|
|
149
|
+
continue;
|
|
150
|
+
const decl = readDeclaration({ model, declaration });
|
|
151
|
+
if (decl === null)
|
|
152
|
+
continue;
|
|
153
|
+
const table = tableNameOf({ model });
|
|
154
|
+
const columns = decl.fields.map((field) => columnNameOf({ model, field }));
|
|
155
|
+
if (engine === "tsvector") {
|
|
156
|
+
const language = decl.language ?? ctx.options["language"] ?? "english";
|
|
157
|
+
const tsvColumn = `${decl.base}_tsv`;
|
|
158
|
+
const expression = tsvectorExpression({ dialect, model, decl, language });
|
|
159
|
+
artifacts.push({
|
|
160
|
+
id: `search:${modelName}:column`,
|
|
161
|
+
owns: { kind: "column", table, column: tsvColumn },
|
|
162
|
+
create: [
|
|
163
|
+
`ALTER TABLE ${quote(table)} ADD COLUMN IF NOT EXISTS ${quote(tsvColumn)} tsvector GENERATED ALWAYS AS (${expression}) STORED;`
|
|
164
|
+
],
|
|
165
|
+
drop: [`ALTER TABLE ${quote(table)} DROP COLUMN IF EXISTS ${quote(tsvColumn)};`]
|
|
166
|
+
}, {
|
|
167
|
+
id: `search:${modelName}:index`,
|
|
168
|
+
owns: { kind: "index", table, name: `${decl.base}_idx` },
|
|
169
|
+
create: [
|
|
170
|
+
`CREATE INDEX IF NOT EXISTS ${quote(`${decl.base}_idx`)} ON ${quote(table)} USING gin (${quote(tsvColumn)});`
|
|
171
|
+
],
|
|
172
|
+
drop: [`DROP INDEX IF EXISTS ${quote(`${decl.base}_idx`)};`]
|
|
173
|
+
});
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (engine === "bm25") {
|
|
177
|
+
const pkField = primaryKeyFieldOf({ model });
|
|
178
|
+
if (pkField === null) {
|
|
179
|
+
throw new VibeError2({
|
|
180
|
+
code: "VIBE_SCHEMA_INVALID",
|
|
181
|
+
message: `@@search with engine "bm25" needs a single-column primary key on "${modelName}" (key_field)`,
|
|
182
|
+
meta: { model: modelName }
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
const pkColumn = columnNameOf({ model, field: pkField });
|
|
186
|
+
artifacts.push({
|
|
187
|
+
id: `search:${modelName}:index`,
|
|
188
|
+
owns: { kind: "index", table, name: `${decl.base}_idx` },
|
|
189
|
+
create: [
|
|
190
|
+
`CREATE INDEX IF NOT EXISTS ${quote(`${decl.base}_idx`)} ON ${quote(table)} USING bm25 (${[pkColumn, ...columns].map((column) => quote(column)).join(", ")}) WITH (key_field='${pkColumn}');`
|
|
191
|
+
],
|
|
192
|
+
drop: [`DROP INDEX IF EXISTS ${quote(`${decl.base}_idx`)};`]
|
|
193
|
+
});
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (engine === "fts5") {
|
|
197
|
+
const pkName = primaryKeyFieldOf({ model });
|
|
198
|
+
const pkField = pkName === null ? undefined : model.fields.find((field) => field.name === pkName);
|
|
199
|
+
const pkIsRowidAlias = pkField !== undefined && !pkField.isList && (pkField.type === "Int" || pkField.type === "BigInt");
|
|
200
|
+
if (!pkIsRowidAlias) {
|
|
201
|
+
throw unsupportedCapability({
|
|
202
|
+
dialect: "sqlite",
|
|
203
|
+
feature: `@@search (fts5) on "${modelName}", whose primary key is not a single INTEGER column`,
|
|
204
|
+
hint: "fts5 external-content tables key on the rowid; only an Int/BigInt @id aliases and PINS it \u2014 any other key's implicit rowid can be renumbered by VACUUM, silently mis-linking search hits. Give the searched model an Int/BigInt @id."
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
const fts = `${decl.base}_fts`;
|
|
208
|
+
const columnList = columns.map((column) => quote(column)).join(", ");
|
|
209
|
+
const newList = columns.map((column) => `new.${quote(column)}`).join(", ");
|
|
210
|
+
const oldList = columns.map((column) => `old.${quote(column)}`).join(", ");
|
|
211
|
+
artifacts.push({
|
|
212
|
+
id: `search:${modelName}:fts5-table`,
|
|
213
|
+
owns: {
|
|
214
|
+
kind: "table",
|
|
215
|
+
name: fts,
|
|
216
|
+
auxiliaryPrefixes: [`${fts}_`]
|
|
217
|
+
},
|
|
218
|
+
create: [
|
|
219
|
+
`CREATE VIRTUAL TABLE IF NOT EXISTS ${quote(fts)} USING fts5(${columnList}, content='${table}');`
|
|
220
|
+
],
|
|
221
|
+
drop: [`DROP TABLE IF EXISTS ${quote(fts)};`]
|
|
222
|
+
}, {
|
|
223
|
+
id: `search:${modelName}:fts5-triggers`,
|
|
224
|
+
owns: { kind: "trigger", table, name: `${fts}_ai` },
|
|
225
|
+
create: [
|
|
226
|
+
`CREATE TRIGGER IF NOT EXISTS ${quote(`${fts}_ai`)} AFTER INSERT ON ${quote(table)} BEGIN
|
|
227
|
+
INSERT INTO ${quote(fts)}(rowid, ${columnList}) VALUES (new.rowid, ${newList});
|
|
228
|
+
END;`,
|
|
229
|
+
`CREATE TRIGGER IF NOT EXISTS ${quote(`${fts}_ad`)} AFTER DELETE ON ${quote(table)} BEGIN
|
|
230
|
+
INSERT INTO ${quote(fts)}(${quote(fts)}, rowid, ${columnList}) VALUES ('delete', old.rowid, ${oldList});
|
|
231
|
+
END;`,
|
|
232
|
+
`CREATE TRIGGER IF NOT EXISTS ${quote(`${fts}_au`)} AFTER UPDATE ON ${quote(table)} BEGIN
|
|
233
|
+
INSERT INTO ${quote(fts)}(${quote(fts)}, rowid, ${columnList}) VALUES ('delete', old.rowid, ${oldList});
|
|
234
|
+
INSERT INTO ${quote(fts)}(rowid, ${columnList}) VALUES (new.rowid, ${newList});
|
|
235
|
+
END;`
|
|
236
|
+
],
|
|
237
|
+
drop: [
|
|
238
|
+
`DROP TRIGGER IF EXISTS ${quote(`${fts}_ai`)};`,
|
|
239
|
+
`DROP TRIGGER IF EXISTS ${quote(`${fts}_ad`)};`,
|
|
240
|
+
`DROP TRIGGER IF EXISTS ${quote(`${fts}_au`)};`
|
|
241
|
+
]
|
|
242
|
+
});
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
artifacts.push({
|
|
246
|
+
id: `search:${modelName}:index`,
|
|
247
|
+
owns: { kind: "index", table, name: `${decl.base}_idx` },
|
|
248
|
+
create: [
|
|
249
|
+
`CREATE FULLTEXT INDEX ${quote(`${decl.base}_idx`)} ON ${quote(table)} (${columns.map((column) => quote(column)).join(", ")});`
|
|
250
|
+
],
|
|
251
|
+
drop: [`DROP INDEX ${quote(`${decl.base}_idx`)} ON ${quote(table)};`]
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
return artifacts;
|
|
255
|
+
}
|
|
256
|
+
function highlightColumn(params) {
|
|
257
|
+
const { config, args } = params;
|
|
258
|
+
if (args.highlight === undefined)
|
|
259
|
+
return null;
|
|
260
|
+
const field = config.fields.find((candidate) => candidate.name === args.highlight?.field);
|
|
261
|
+
if (field === undefined) {
|
|
262
|
+
throw new VibeError2({
|
|
263
|
+
code: "VIBE_VALIDATION",
|
|
264
|
+
message: `highlight.field "${args.highlight.field}" is not searched \u2014 declared: ${config.fields.map((candidate) => candidate.name).join(", ")}`,
|
|
265
|
+
meta: { field: args.highlight.field }
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
return field.column;
|
|
269
|
+
}
|
|
270
|
+
function shapeHits(params) {
|
|
271
|
+
const { rows, strip, highlightsRequested, engineHasHighlights } = params;
|
|
272
|
+
return rows.map((raw) => {
|
|
273
|
+
const row = { ...raw };
|
|
274
|
+
const score = Number(row[SCORE_ALIAS]);
|
|
275
|
+
const highlight = row[HIGHLIGHT_ALIAS];
|
|
276
|
+
delete row[SCORE_ALIAS];
|
|
277
|
+
delete row[HIGHLIGHT_ALIAS];
|
|
278
|
+
for (const name of strip)
|
|
279
|
+
delete row[name];
|
|
280
|
+
const highlights = engineHasHighlights && highlightsRequested && highlight !== null && highlight !== undefined ? [String(highlight)] : null;
|
|
281
|
+
return { row, score, highlights };
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
function createSearchDelegate(config) {
|
|
285
|
+
return (ctx) => {
|
|
286
|
+
const dialect = ctx.dialect;
|
|
287
|
+
const quote = (name) => dialect.quoteIdent({ name });
|
|
288
|
+
const t = quote("t");
|
|
289
|
+
return async (args) => {
|
|
290
|
+
if (typeof args.query !== "string" || args.query.trim().length === 0) {
|
|
291
|
+
throw new VibeError2({
|
|
292
|
+
code: "VIBE_VALIDATION",
|
|
293
|
+
message: "search requires a non-empty query string",
|
|
294
|
+
meta: { table: config.table }
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
const take = Math.max(1, Math.trunc(args.take ?? 10));
|
|
298
|
+
const skip = args.skip === undefined ? null : Math.max(0, Math.trunc(args.skip));
|
|
299
|
+
const hlColumn = highlightColumn({ config, args });
|
|
300
|
+
const collector = new ParamCollector(dialect);
|
|
301
|
+
const userWhere = (qualify) => {
|
|
302
|
+
if (args.where === undefined)
|
|
303
|
+
return null;
|
|
304
|
+
const expr = ctx.compileWhere({ where: args.where, qualify });
|
|
305
|
+
return expr === undefined ? null : renderExpr({ dialect, expr, collector });
|
|
306
|
+
};
|
|
307
|
+
let text;
|
|
308
|
+
let strip = [];
|
|
309
|
+
let engineHasHighlights = true;
|
|
310
|
+
if (config.engine === "tsvector") {
|
|
311
|
+
const language = config.language ?? "english";
|
|
312
|
+
const tsv = `${t}.${quote(config.tsvColumn ?? "")}`;
|
|
313
|
+
const tsquery = () => `websearch_to_tsquery('${language}', ${collector.add(args.query)})`;
|
|
314
|
+
text = `SELECT ${t}.*, ts_rank_cd(${tsv}, ${tsquery()}) AS ${quote(SCORE_ALIAS)}`;
|
|
315
|
+
if (hlColumn !== null) {
|
|
316
|
+
text += `, ts_headline('${language}', ${t}.${quote(hlColumn)}, ${tsquery()}) AS ${quote(HIGHLIGHT_ALIAS)}`;
|
|
317
|
+
}
|
|
318
|
+
text += ` FROM ${quote(config.table)} AS ${t} WHERE ${tsv} @@ ${tsquery()}`;
|
|
319
|
+
const where = userWhere("t");
|
|
320
|
+
if (where !== null)
|
|
321
|
+
text += ` AND (${where})`;
|
|
322
|
+
if (args.minScore !== undefined) {
|
|
323
|
+
text += ` AND ts_rank_cd(${tsv}, ${tsquery()}) >= ${collector.add(args.minScore)}`;
|
|
324
|
+
}
|
|
325
|
+
text += ` ORDER BY ${quote(SCORE_ALIAS)} DESC`;
|
|
326
|
+
strip = [config.tsvColumn ?? ""];
|
|
327
|
+
} else if (config.engine === "fts5") {
|
|
328
|
+
const fts = quote(config.ftsTable ?? "");
|
|
329
|
+
text = `SELECT ${t}.*, -bm25(${fts}) AS ${quote(SCORE_ALIAS)}`;
|
|
330
|
+
if (hlColumn !== null) {
|
|
331
|
+
const index = config.fields.findIndex((field) => field.column === hlColumn);
|
|
332
|
+
text += `, snippet(${fts}, ${index}, '<b>', '</b>', '\u2026', 16) AS ${quote(HIGHLIGHT_ALIAS)}`;
|
|
333
|
+
}
|
|
334
|
+
text += ` FROM ${fts} JOIN ${quote(config.table)} AS ${t} ON ${t}.rowid = ${fts}.rowid`;
|
|
335
|
+
text += ` WHERE ${fts} MATCH ${collector.add(args.query)}`;
|
|
336
|
+
const where = userWhere("t");
|
|
337
|
+
if (where !== null)
|
|
338
|
+
text += ` AND (${where})`;
|
|
339
|
+
if (args.minScore !== undefined) {
|
|
340
|
+
text += ` AND -bm25(${fts}) >= ${collector.add(args.minScore)}`;
|
|
341
|
+
}
|
|
342
|
+
text += ` ORDER BY ${quote(SCORE_ALIAS)} DESC`;
|
|
343
|
+
} else if (config.engine === "fulltext") {
|
|
344
|
+
engineHasHighlights = false;
|
|
345
|
+
const match = () => `MATCH (${config.fields.map((field) => `${t}.${quote(field.column)}`).join(", ")}) AGAINST (${collector.add(args.query)} IN NATURAL LANGUAGE MODE)`;
|
|
346
|
+
text = `SELECT ${t}.*, ${match()} AS ${quote(SCORE_ALIAS)} FROM ${quote(config.table)} AS ${t}`;
|
|
347
|
+
text += ` WHERE ${match()}`;
|
|
348
|
+
const where = userWhere("t");
|
|
349
|
+
if (where !== null)
|
|
350
|
+
text += ` AND (${where})`;
|
|
351
|
+
if (args.minScore !== undefined) {
|
|
352
|
+
text += ` AND ${match()} >= ${collector.add(args.minScore)}`;
|
|
353
|
+
}
|
|
354
|
+
text += ` ORDER BY ${quote(SCORE_ALIAS)} DESC`;
|
|
355
|
+
} else {
|
|
356
|
+
const pk = `${t}.${quote(config.pkColumn ?? "")}`;
|
|
357
|
+
text = `SELECT ${t}.*, paradedb.score(${pk}) AS ${quote(SCORE_ALIAS)}`;
|
|
358
|
+
if (hlColumn !== null) {
|
|
359
|
+
text += `, paradedb.snippet(${t}.${quote(hlColumn)}) AS ${quote(HIGHLIGHT_ALIAS)}`;
|
|
360
|
+
}
|
|
361
|
+
text += ` FROM ${quote(config.table)} AS ${t} WHERE (${config.fields.map((field) => `${t}.${quote(field.column)} @@@ ${collector.add(args.query)}`).join(" OR ")})`;
|
|
362
|
+
const where = userWhere("t");
|
|
363
|
+
if (where !== null)
|
|
364
|
+
text += ` AND (${where})`;
|
|
365
|
+
text += ` ORDER BY ${quote(SCORE_ALIAS)} DESC`;
|
|
366
|
+
}
|
|
367
|
+
text += ` LIMIT ${take}`;
|
|
368
|
+
if (skip !== null)
|
|
369
|
+
text += ` OFFSET ${skip}`;
|
|
370
|
+
const rows = await ctx.executeRows({ text, values: collector.values, method: "search" });
|
|
371
|
+
let hits = shapeHits({
|
|
372
|
+
rows,
|
|
373
|
+
strip,
|
|
374
|
+
highlightsRequested: args.highlight !== undefined,
|
|
375
|
+
engineHasHighlights
|
|
376
|
+
});
|
|
377
|
+
if (config.engine === "bm25" && args.minScore !== undefined) {
|
|
378
|
+
const min = args.minScore;
|
|
379
|
+
hits = hits.filter((hit) => hit.score >= min);
|
|
380
|
+
}
|
|
381
|
+
return hits;
|
|
382
|
+
};
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
function search(options = {}) {
|
|
386
|
+
return defineExtension({
|
|
387
|
+
name: "search",
|
|
388
|
+
version: VERSION,
|
|
389
|
+
options: { ...options },
|
|
390
|
+
requires: { postgresql: null, pglite: null, sqlite: null, mysql: null },
|
|
391
|
+
schema: {
|
|
392
|
+
modelAttributes: [
|
|
393
|
+
{
|
|
394
|
+
name: "search",
|
|
395
|
+
parse: ({ args, fieldNames }) => {
|
|
396
|
+
const fields = args.fields ?? [];
|
|
397
|
+
if (fields.length === 0) {
|
|
398
|
+
return { error: "@@search requires a field list, e.g. @@search([title, body])" };
|
|
399
|
+
}
|
|
400
|
+
const missing = fields.find((name2) => !fieldNames.includes(name2));
|
|
401
|
+
if (missing !== undefined) {
|
|
402
|
+
return { error: `@@search references unknown field "${missing}"` };
|
|
403
|
+
}
|
|
404
|
+
const name = args.named["name"];
|
|
405
|
+
if (name !== undefined && (typeof name !== "string" || !isSafeSqlName({ name }))) {
|
|
406
|
+
return { error: `name must be a plain identifier` };
|
|
407
|
+
}
|
|
408
|
+
const weights = args.named["weights"];
|
|
409
|
+
if (weights !== undefined) {
|
|
410
|
+
if (!Array.isArray(weights) || weights.length !== fields.length || weights.some((weight) => typeof weight !== "number")) {
|
|
411
|
+
return { error: `weights must be a number list matching the field list` };
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
const language = args.named["language"];
|
|
415
|
+
if (language !== undefined && (typeof language !== "string" || !isSafeSqlName({ name: language }))) {
|
|
416
|
+
return { error: `language must be a plain text-search config name` };
|
|
417
|
+
}
|
|
418
|
+
return { declaration: { fields, ...args.named } };
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
],
|
|
422
|
+
indexKinds: ["bm25", "fts5", "fulltext", "gin"]
|
|
423
|
+
},
|
|
424
|
+
migrate: { artifacts: searchArtifacts },
|
|
425
|
+
client: {
|
|
426
|
+
model: ({ model, dialect, provider, options: extOptions, declaration, typeNames }) => {
|
|
427
|
+
const decl = readDeclaration({ model, declaration });
|
|
428
|
+
if (decl === null)
|
|
429
|
+
return null;
|
|
430
|
+
const requested = extOptions["engine"] ?? "auto";
|
|
431
|
+
const engine = resolveSearchEngine({ dialect, provider, requested });
|
|
432
|
+
const language = decl.language ?? extOptions["language"] ?? "english";
|
|
433
|
+
if (!isSafeSqlName({ name: language })) {
|
|
434
|
+
throw new VibeError2({
|
|
435
|
+
code: "VIBE_SCHEMA_INVALID",
|
|
436
|
+
message: `search language "${language}" is not a plain text-search config name`,
|
|
437
|
+
meta: { model: model.name, language }
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
const pkField = primaryKeyFieldOf({ model });
|
|
441
|
+
const config = {
|
|
442
|
+
engine,
|
|
443
|
+
table: tableNameOf({ model }),
|
|
444
|
+
fields: decl.fields.map((field) => ({
|
|
445
|
+
name: field,
|
|
446
|
+
column: columnNameOf({ model, field })
|
|
447
|
+
})),
|
|
448
|
+
...engine === "tsvector" ? { tsvColumn: `${decl.base}_tsv`, language } : {},
|
|
449
|
+
...engine === "fts5" ? { ftsTable: `${decl.base}_fts` } : {},
|
|
450
|
+
...engine === "bm25" && pkField !== null ? { pkColumn: columnNameOf({ model, field: pkField }) } : {}
|
|
451
|
+
};
|
|
452
|
+
const fieldUnion = decl.fields.map((field) => JSON.stringify(field)).join(" | ");
|
|
453
|
+
return {
|
|
454
|
+
kind: "method",
|
|
455
|
+
runtimeGlue: `createSearchDelegate(${JSON.stringify(config)})`,
|
|
456
|
+
imports: [{ from: "@vibeorm/extensions/search", names: ["createSearchDelegate"] }],
|
|
457
|
+
dtsImports: [{ from: "@vibeorm/extensions/search", names: ["SearchHit"] }],
|
|
458
|
+
dts: [
|
|
459
|
+
`readonly search: (args: {`,
|
|
460
|
+
` readonly query: string;`,
|
|
461
|
+
` readonly where?: ${typeNames.whereInput};`,
|
|
462
|
+
` readonly take?: number;`,
|
|
463
|
+
` readonly skip?: number;`,
|
|
464
|
+
` readonly minScore?: number;`,
|
|
465
|
+
` readonly highlight?: { readonly field: ${fieldUnion} };`,
|
|
466
|
+
`}) => Promise<SearchHit<${typeNames.payload}>[]>;`
|
|
467
|
+
]
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
},
|
|
471
|
+
doctor: async ({ execute, dialect, options: extOptions }) => {
|
|
472
|
+
if (dialect === "postgres") {
|
|
473
|
+
const requested = extOptions["engine"] ?? "auto";
|
|
474
|
+
if (requested === "bm25") {
|
|
475
|
+
return { checks: [await pgExtensionCheck({ execute, name: "pg_search" })] };
|
|
476
|
+
}
|
|
477
|
+
return { checks: [{ name: "tsvector", ok: true, note: "built in" }] };
|
|
478
|
+
}
|
|
479
|
+
if (dialect === "sqlite") {
|
|
480
|
+
try {
|
|
481
|
+
const rows = await execute({
|
|
482
|
+
text: "SELECT 1 AS ok FROM pragma_compile_options WHERE compile_options = 'ENABLE_FTS5'"
|
|
483
|
+
});
|
|
484
|
+
return {
|
|
485
|
+
checks: [
|
|
486
|
+
rows.length > 0 ? { name: "fts5", ok: true, note: "compiled in" } : { name: "fts5", ok: false, note: "sqlite built without FTS5" }
|
|
487
|
+
]
|
|
488
|
+
};
|
|
489
|
+
} catch (error) {
|
|
490
|
+
return {
|
|
491
|
+
checks: [
|
|
492
|
+
{
|
|
493
|
+
name: "fts5",
|
|
494
|
+
ok: false,
|
|
495
|
+
note: `check failed: ${error instanceof Error ? error.message : String(error)}`
|
|
496
|
+
}
|
|
497
|
+
]
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
return { checks: [{ name: "fulltext", ok: true, note: "built in (InnoDB FULLTEXT)" }] };
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
export {
|
|
506
|
+
search,
|
|
507
|
+
resolveSearchEngine,
|
|
508
|
+
createSearchDelegate
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
//# debugId=CE3DBDCF2300506B64756E2164756E21
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/search/index.ts", "../src/shared.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * `search` — unified full-text search (extensions.md §4).\n *\n * One API, four engines resolved per dialect at GENERATE time:\n * - `tsvector` (postgres/pglite): generated STORED tsvector column + GIN,\n * websearch_to_tsquery / ts_rank_cd / ts_headline.\n * - `bm25` (postgres + ParadeDB pg_search, opt-in via `engine: \"bm25\"`):\n * bm25 index + `@@@` + paradedb.score/snippet.\n * - `fts5` (sqlite): external-content virtual table + sync triggers,\n * MATCH / bm25() / snippet().\n * - `fulltext` (mysql): FULLTEXT index + MATCH … AGAINST; highlights are\n * typed `null` (mysql has none).\n *\n * `engine: \"auto\"` picks the dialect's native engine (never bm25 — ParadeDB\n * availability is unknowable at generate time; opt in explicitly).\n */\n\nimport { VibeError, defineExtension, unsupportedCapability } from \"@vibeorm/schema\";\nimport type {\n Dialect,\n ExtensionArtifact,\n ExtensionSchemaContext,\n ModelIR,\n Provider,\n VibeExtension,\n} from \"@vibeorm/schema\";\nimport { ParamCollector, getSqlDialect, renderExpr } from \"@vibeorm/sql\";\nimport type { SqlDialect } from \"@vibeorm/sql\";\nimport type { ExtensionMountContext } from \"@vibeorm/runtime\";\nimport {\n columnNameOf,\n declNumberArray,\n declString,\n declStringArray,\n isSafeSqlName,\n pgExtensionCheck,\n primaryKeyFieldOf,\n tableNameOf,\n} from \"../shared.ts\";\n\n// ─── Options & types ──────────────────────────────────────────────\n\nexport type SearchEngine = \"bm25\" | \"tsvector\" | \"fts5\" | \"fulltext\";\n\nexport type SearchOptions = {\n /** Engine choice; `\"auto\"` (default) resolves per dialect. */\n readonly engine?: SearchEngine | \"auto\";\n /** Default text-search language for the tsvector engine (default \"english\"). */\n readonly language?: string;\n};\n\n/** One search hit. `highlights` is `null` on engines without highlighting. */\nexport type SearchHit<T> = {\n readonly row: T;\n readonly score: number;\n readonly highlights: readonly string[] | null;\n};\n\nconst VERSION = \"2.0.0-alpha.0\";\nconst SCORE_ALIAS = \"_vibe_score\";\nconst HIGHLIGHT_ALIAS = \"_vibe_highlight\";\nconst WEIGHT_LETTERS = [\"A\", \"B\", \"C\", \"D\"] as const;\n\n// ─── Engine resolution (generate/migrate time) ────────────────────\n\n/** Resolve the engine for a dialect; throws on impossible explicit choices. */\nexport function resolveSearchEngine(params: {\n dialect: Dialect;\n provider: Provider;\n requested: SearchEngine | \"auto\";\n}): SearchEngine {\n const { dialect, provider, requested } = params;\n const native: SearchEngine = dialect === \"postgres\" ? \"tsvector\" : dialect === \"sqlite\" ? \"fts5\" : \"fulltext\";\n if (requested === \"auto\") return native;\n if (requested === native) return native;\n if (requested === \"bm25\" && dialect === \"postgres\") {\n if (provider === \"pglite\") {\n throw unsupportedCapability({\n dialect,\n feature: 'the search engine \"bm25\" (ParadeDB pg_search)',\n hint: 'pglite does not bundle pg_search — use engine \"auto\"/\"tsvector\"',\n });\n }\n return \"bm25\";\n }\n throw unsupportedCapability({\n dialect,\n feature: `the search engine \"${requested}\"`,\n hint: `this dialect supports \"${native}\"${dialect === \"postgres\" ? ' and \"bm25\"' : \"\"}`,\n });\n}\n\n// ─── Declaration ──────────────────────────────────────────────────\n\ntype SearchDecl = {\n readonly fields: readonly string[];\n readonly weights: readonly number[] | undefined;\n readonly base: string;\n readonly language: string | undefined;\n};\n\nfunction readDeclaration(params: {\n model: ModelIR;\n declaration: Readonly<Record<string, unknown>>;\n}): SearchDecl | null {\n const fields = declStringArray(params.declaration, \"fields\");\n if (fields === undefined || fields.length === 0) return null;\n return {\n fields,\n weights: declNumberArray(params.declaration, \"weights\"),\n base: declString(params.declaration, \"name\") ?? `${tableNameOf({ model: params.model })}_search`,\n language: declString(params.declaration, \"language\"),\n };\n}\n\n/** Weight letter per field: highest weight → 'A', capped at 'D'. */\nfunction weightLetters(params: { decl: SearchDecl }): readonly string[] | null {\n const { decl } = params;\n if (decl.weights === undefined || decl.weights.length !== decl.fields.length) return null;\n const ranked = [...new Set(decl.weights)].sort((a, b) => b - a);\n return decl.weights.map(\n (weight) => WEIGHT_LETTERS[Math.min(ranked.indexOf(weight), WEIGHT_LETTERS.length - 1)] as string,\n );\n}\n\nfunction tsvectorExpression(params: {\n dialect: SqlDialect;\n model: ModelIR;\n decl: SearchDecl;\n language: string;\n}): string {\n const { dialect, model, decl, language } = params;\n const quote = (name: string): string => dialect.quoteIdent({ name });\n const letters = weightLetters({ decl });\n const parts = decl.fields.map((field, index) => {\n const vector = `to_tsvector('${language}', coalesce(${quote(columnNameOf({ model, field }))}, ''))`;\n return letters === null ? vector : `setweight(${vector}, '${letters[index]}')`;\n });\n return letters === null && parts.length > 1\n ? `to_tsvector('${language}', ${decl.fields\n .map((field) => `coalesce(${quote(columnNameOf({ model, field }))}, '')`)\n .join(\" || ' ' || \")})`\n : parts.join(\" || \");\n}\n\n// ─── Artifacts ────────────────────────────────────────────────────\n\nfunction searchArtifacts(ctx: ExtensionSchemaContext): readonly ExtensionArtifact[] {\n const requested = (ctx.options[\"engine\"] as SearchEngine | \"auto\" | undefined) ?? \"auto\";\n const engine = resolveSearchEngine({ dialect: ctx.dialect, provider: ctx.provider, requested });\n const dialect = getSqlDialect({ dialect: ctx.dialect });\n const quote = (name: string): string => dialect.quoteIdent({ name });\n const artifacts: ExtensionArtifact[] = [];\n\n if (engine === \"bm25\") {\n artifacts.push({\n id: \"search:extension:pg_search\",\n owns: { kind: \"postgresExtension\", name: \"pg_search\" },\n create: [\"CREATE EXTENSION IF NOT EXISTS pg_search;\"],\n drop: [\"DROP EXTENSION IF EXISTS pg_search;\"],\n });\n }\n\n for (const [modelName, declaration] of Object.entries(ctx.declarations)) {\n const model = ctx.schema.models.find((candidate) => candidate.name === modelName);\n if (model === undefined) continue;\n const decl = readDeclaration({ model, declaration });\n if (decl === null) continue;\n const table = tableNameOf({ model });\n const columns = decl.fields.map((field) => columnNameOf({ model, field }));\n\n if (engine === \"tsvector\") {\n const language = decl.language ?? (ctx.options[\"language\"] as string | undefined) ?? \"english\";\n const tsvColumn = `${decl.base}_tsv`;\n const expression = tsvectorExpression({ dialect, model, decl, language });\n artifacts.push(\n {\n id: `search:${modelName}:column`,\n owns: { kind: \"column\", table, column: tsvColumn },\n create: [\n `ALTER TABLE ${quote(table)} ADD COLUMN IF NOT EXISTS ${quote(tsvColumn)} tsvector GENERATED ALWAYS AS (${expression}) STORED;`,\n ],\n drop: [`ALTER TABLE ${quote(table)} DROP COLUMN IF EXISTS ${quote(tsvColumn)};`],\n },\n {\n id: `search:${modelName}:index`,\n owns: { kind: \"index\", table, name: `${decl.base}_idx` },\n create: [\n `CREATE INDEX IF NOT EXISTS ${quote(`${decl.base}_idx`)} ON ${quote(table)} USING gin (${quote(tsvColumn)});`,\n ],\n drop: [`DROP INDEX IF EXISTS ${quote(`${decl.base}_idx`)};`],\n },\n );\n continue;\n }\n\n if (engine === \"bm25\") {\n const pkField = primaryKeyFieldOf({ model });\n if (pkField === null) {\n throw new VibeError({\n code: \"VIBE_SCHEMA_INVALID\",\n message: `@@search with engine \"bm25\" needs a single-column primary key on \"${modelName}\" (key_field)`,\n meta: { model: modelName },\n });\n }\n const pkColumn = columnNameOf({ model, field: pkField });\n artifacts.push({\n id: `search:${modelName}:index`,\n owns: { kind: \"index\", table, name: `${decl.base}_idx` },\n create: [\n `CREATE INDEX IF NOT EXISTS ${quote(`${decl.base}_idx`)} ON ${quote(table)} USING bm25 (${[pkColumn, ...columns]\n .map((column) => quote(column))\n .join(\", \")}) WITH (key_field='${pkColumn}');`,\n ],\n drop: [`DROP INDEX IF EXISTS ${quote(`${decl.base}_idx`)};`],\n });\n continue;\n }\n\n if (engine === \"fts5\") {\n // N7 (SQL review): the external-content table and its sync triggers key\n // on the host's ROWID. Only a single Int/BigInt @id aliases (and pins)\n // the rowid; any other primary key leaves an IMPLICIT rowid that VACUUM\n // may renumber — a documented sqlite non-guarantee — after which the\n // shadow table silently points at the WRONG rows. Refuse at generate\n // time (earliest-moment rule); recovery for a database that already\n // drifted is `INSERT INTO <fts>(<fts>) VALUES('rebuild')`.\n const pkName = primaryKeyFieldOf({ model });\n const pkField = pkName === null ? undefined : model.fields.find((field) => field.name === pkName);\n const pkIsRowidAlias =\n pkField !== undefined && !pkField.isList && (pkField.type === \"Int\" || pkField.type === \"BigInt\");\n if (!pkIsRowidAlias) {\n throw unsupportedCapability({\n dialect: \"sqlite\",\n feature: `@@search (fts5) on \"${modelName}\", whose primary key is not a single INTEGER column`,\n hint: \"fts5 external-content tables key on the rowid; only an Int/BigInt @id aliases and PINS it — any other key's implicit rowid can be renumbered by VACUUM, silently mis-linking search hits. Give the searched model an Int/BigInt @id.\",\n });\n }\n const fts = `${decl.base}_fts`;\n const columnList = columns.map((column) => quote(column)).join(\", \");\n const newList = columns.map((column) => `new.${quote(column)}`).join(\", \");\n const oldList = columns.map((column) => `old.${quote(column)}`).join(\", \");\n artifacts.push(\n {\n id: `search:${modelName}:fts5-table`,\n owns: {\n kind: \"table\",\n name: fts,\n auxiliaryPrefixes: [`${fts}_`],\n },\n create: [\n `CREATE VIRTUAL TABLE IF NOT EXISTS ${quote(fts)} USING fts5(${columnList}, content='${table}');`,\n ],\n drop: [`DROP TABLE IF EXISTS ${quote(fts)};`],\n },\n {\n id: `search:${modelName}:fts5-triggers`,\n owns: { kind: \"trigger\", table, name: `${fts}_ai` },\n create: [\n `CREATE TRIGGER IF NOT EXISTS ${quote(`${fts}_ai`)} AFTER INSERT ON ${quote(table)} BEGIN\\n INSERT INTO ${quote(fts)}(rowid, ${columnList}) VALUES (new.rowid, ${newList});\\nEND;`,\n `CREATE TRIGGER IF NOT EXISTS ${quote(`${fts}_ad`)} AFTER DELETE ON ${quote(table)} BEGIN\\n INSERT INTO ${quote(fts)}(${quote(fts)}, rowid, ${columnList}) VALUES ('delete', old.rowid, ${oldList});\\nEND;`,\n `CREATE TRIGGER IF NOT EXISTS ${quote(`${fts}_au`)} AFTER UPDATE ON ${quote(table)} BEGIN\\n INSERT INTO ${quote(fts)}(${quote(fts)}, rowid, ${columnList}) VALUES ('delete', old.rowid, ${oldList});\\n INSERT INTO ${quote(fts)}(rowid, ${columnList}) VALUES (new.rowid, ${newList});\\nEND;`,\n ],\n drop: [\n `DROP TRIGGER IF EXISTS ${quote(`${fts}_ai`)};`,\n `DROP TRIGGER IF EXISTS ${quote(`${fts}_ad`)};`,\n `DROP TRIGGER IF EXISTS ${quote(`${fts}_au`)};`,\n ],\n },\n );\n continue;\n }\n\n // fulltext (mysql): no IF NOT EXISTS form exists — migrate's presence\n // check (introspected index name) decides whether the create runs.\n artifacts.push({\n id: `search:${modelName}:index`,\n owns: { kind: \"index\", table, name: `${decl.base}_idx` },\n create: [\n `CREATE FULLTEXT INDEX ${quote(`${decl.base}_idx`)} ON ${quote(table)} (${columns\n .map((column) => quote(column))\n .join(\", \")});`,\n ],\n drop: [`DROP INDEX ${quote(`${decl.base}_idx`)} ON ${quote(table)};`],\n });\n }\n return artifacts;\n}\n\n// ─── Runtime delegate (generated-glue target) ─────────────────────\n\n/** Baked per-model config the generator writes into the client. */\nexport type SearchDelegateConfig = {\n readonly engine: SearchEngine;\n readonly table: string;\n /** Searched fields in declaration order → column names. */\n readonly fields: readonly { readonly name: string; readonly column: string }[];\n /** tsvector engine: the generated column. */\n readonly tsvColumn?: string;\n /** fts5 engine: the shadow table. */\n readonly ftsTable?: string;\n /** tsvector engine: regconfig name (validated identifier). */\n readonly language?: string;\n /** bm25 engine: key_field column. */\n readonly pkColumn?: string;\n};\n\nexport type SearchArgs = {\n readonly query: string;\n readonly where?: Record<string, unknown>;\n readonly take?: number;\n readonly skip?: number;\n readonly minScore?: number;\n readonly highlight?: { readonly field: string };\n};\n\nexport type SearchMethod = (args: SearchArgs) => Promise<SearchHit<Record<string, unknown>>[]>;\n\nfunction highlightColumn(params: { config: SearchDelegateConfig; args: SearchArgs }): string | null {\n const { config, args } = params;\n if (args.highlight === undefined) return null;\n const field = config.fields.find((candidate) => candidate.name === args.highlight?.field);\n if (field === undefined) {\n throw new VibeError({\n code: \"VIBE_VALIDATION\",\n message: `highlight.field \"${args.highlight.field}\" is not searched — declared: ${config.fields\n .map((candidate) => candidate.name)\n .join(\", \")}`,\n meta: { field: args.highlight.field },\n });\n }\n return field.column;\n}\n\nfunction shapeHits(params: {\n rows: readonly Record<string, unknown>[];\n strip: readonly string[];\n highlightsRequested: boolean;\n engineHasHighlights: boolean;\n}): SearchHit<Record<string, unknown>>[] {\n const { rows, strip, highlightsRequested, engineHasHighlights } = params;\n return rows.map((raw) => {\n const row: Record<string, unknown> = { ...raw };\n const score = Number(row[SCORE_ALIAS]);\n const highlight = row[HIGHLIGHT_ALIAS];\n delete row[SCORE_ALIAS];\n delete row[HIGHLIGHT_ALIAS];\n for (const name of strip) delete row[name];\n const highlights =\n engineHasHighlights && highlightsRequested && highlight !== null && highlight !== undefined\n ? [String(highlight)]\n : null;\n return { row, score, highlights };\n });\n}\n\n/**\n * The generated client's glue target: bakes engine + names at generate time,\n * mounts as the `search` method at runtime (D9).\n */\nexport function createSearchDelegate(\n config: SearchDelegateConfig,\n): (ctx: ExtensionMountContext) => SearchMethod {\n return (ctx: ExtensionMountContext): SearchMethod => {\n const dialect = ctx.dialect;\n const quote = (name: string): string => dialect.quoteIdent({ name });\n const t = quote(\"t\");\n\n return async (args: SearchArgs): Promise<SearchHit<Record<string, unknown>>[]> => {\n if (typeof args.query !== \"string\" || args.query.trim().length === 0) {\n throw new VibeError({\n code: \"VIBE_VALIDATION\",\n message: \"search requires a non-empty query string\",\n meta: { table: config.table },\n });\n }\n const take = Math.max(1, Math.trunc(args.take ?? 10));\n const skip = args.skip === undefined ? null : Math.max(0, Math.trunc(args.skip));\n const hlColumn = highlightColumn({ config, args });\n const collector = new ParamCollector(dialect);\n\n const userWhere = (qualify: string): string | null => {\n if (args.where === undefined) return null;\n const expr = ctx.compileWhere({ where: args.where, qualify });\n return expr === undefined ? null : renderExpr({ dialect, expr, collector });\n };\n\n let text: string;\n let strip: readonly string[] = [];\n let engineHasHighlights = true;\n\n if (config.engine === \"tsvector\") {\n const language = config.language ?? \"english\";\n const tsv = `${t}.${quote(config.tsvColumn ?? \"\")}`;\n const tsquery = (): string => `websearch_to_tsquery('${language}', ${collector.add(args.query)})`;\n text = `SELECT ${t}.*, ts_rank_cd(${tsv}, ${tsquery()}) AS ${quote(SCORE_ALIAS)}`;\n if (hlColumn !== null) {\n text += `, ts_headline('${language}', ${t}.${quote(hlColumn)}, ${tsquery()}) AS ${quote(HIGHLIGHT_ALIAS)}`;\n }\n text += ` FROM ${quote(config.table)} AS ${t} WHERE ${tsv} @@ ${tsquery()}`;\n const where = userWhere(\"t\");\n if (where !== null) text += ` AND (${where})`;\n if (args.minScore !== undefined) {\n text += ` AND ts_rank_cd(${tsv}, ${tsquery()}) >= ${collector.add(args.minScore)}`;\n }\n text += ` ORDER BY ${quote(SCORE_ALIAS)} DESC`;\n strip = [config.tsvColumn ?? \"\"];\n } else if (config.engine === \"fts5\") {\n const fts = quote(config.ftsTable ?? \"\");\n text = `SELECT ${t}.*, -bm25(${fts}) AS ${quote(SCORE_ALIAS)}`;\n if (hlColumn !== null) {\n const index = config.fields.findIndex((field) => field.column === hlColumn);\n text += `, snippet(${fts}, ${index}, '<b>', '</b>', '…', 16) AS ${quote(HIGHLIGHT_ALIAS)}`;\n }\n text += ` FROM ${fts} JOIN ${quote(config.table)} AS ${t} ON ${t}.rowid = ${fts}.rowid`;\n text += ` WHERE ${fts} MATCH ${collector.add(args.query)}`;\n const where = userWhere(\"t\");\n if (where !== null) text += ` AND (${where})`;\n if (args.minScore !== undefined) {\n text += ` AND -bm25(${fts}) >= ${collector.add(args.minScore)}`;\n }\n text += ` ORDER BY ${quote(SCORE_ALIAS)} DESC`;\n } else if (config.engine === \"fulltext\") {\n engineHasHighlights = false;\n const match = (): string =>\n `MATCH (${config.fields.map((field) => `${t}.${quote(field.column)}`).join(\", \")}) AGAINST (${collector.add(args.query)} IN NATURAL LANGUAGE MODE)`;\n text = `SELECT ${t}.*, ${match()} AS ${quote(SCORE_ALIAS)} FROM ${quote(config.table)} AS ${t}`;\n text += ` WHERE ${match()}`;\n const where = userWhere(\"t\");\n if (where !== null) text += ` AND (${where})`;\n if (args.minScore !== undefined) {\n text += ` AND ${match()} >= ${collector.add(args.minScore)}`;\n }\n text += ` ORDER BY ${quote(SCORE_ALIAS)} DESC`;\n } else {\n // bm25 (ParadeDB). minScore filters client-side (paradedb.score is not\n // reliably WHERE-safe); highlights via paradedb.snippet.\n const pk = `${t}.${quote(config.pkColumn ?? \"\")}`;\n text = `SELECT ${t}.*, paradedb.score(${pk}) AS ${quote(SCORE_ALIAS)}`;\n if (hlColumn !== null) {\n text += `, paradedb.snippet(${t}.${quote(hlColumn)}) AS ${quote(HIGHLIGHT_ALIAS)}`;\n }\n text += ` FROM ${quote(config.table)} AS ${t} WHERE (${config.fields\n .map((field) => `${t}.${quote(field.column)} @@@ ${collector.add(args.query)}`)\n .join(\" OR \")})`;\n const where = userWhere(\"t\");\n if (where !== null) text += ` AND (${where})`;\n text += ` ORDER BY ${quote(SCORE_ALIAS)} DESC`;\n }\n\n text += ` LIMIT ${take}`;\n if (skip !== null) text += ` OFFSET ${skip}`;\n\n const rows = await ctx.executeRows({ text, values: collector.values, method: \"search\" });\n let hits = shapeHits({\n rows,\n strip,\n highlightsRequested: args.highlight !== undefined,\n engineHasHighlights,\n });\n if (config.engine === \"bm25\" && args.minScore !== undefined) {\n const min = args.minScore;\n hits = hits.filter((hit) => hit.score >= min);\n }\n return hits;\n };\n };\n}\n\n// ─── Factory ──────────────────────────────────────────────────────\n\n/**\n * Unified full-text search.\n *\n * @example schema\n * // @@search([title, body], name: \"post_search\", weights: [2, 1])\n * const hits = await db.post.search({ query: \"sqlite full text\", take: 20 });\n * // → [{ row, score, highlights }]\n */\nexport function search(options: SearchOptions = {}): VibeExtension {\n return defineExtension({\n name: \"search\",\n version: VERSION,\n options: { ...options },\n requires: { postgresql: null, pglite: null, sqlite: null, mysql: null },\n\n schema: {\n modelAttributes: [\n {\n name: \"search\",\n parse: ({ args, fieldNames }) => {\n const fields = args.fields ?? [];\n if (fields.length === 0) {\n return { error: \"@@search requires a field list, e.g. @@search([title, body])\" };\n }\n const missing = fields.find((name) => !fieldNames.includes(name));\n if (missing !== undefined) {\n return { error: `@@search references unknown field \"${missing}\"` };\n }\n const name = args.named[\"name\"];\n if (name !== undefined && (typeof name !== \"string\" || !isSafeSqlName({ name }))) {\n return { error: `name must be a plain identifier` };\n }\n const weights = args.named[\"weights\"];\n if (weights !== undefined) {\n if (\n !Array.isArray(weights) ||\n weights.length !== fields.length ||\n weights.some((weight) => typeof weight !== \"number\")\n ) {\n return { error: `weights must be a number list matching the field list` };\n }\n }\n const language = args.named[\"language\"];\n if (language !== undefined && (typeof language !== \"string\" || !isSafeSqlName({ name: language }))) {\n return { error: `language must be a plain text-search config name` };\n }\n return { declaration: { fields, ...args.named } };\n },\n },\n ],\n indexKinds: [\"bm25\", \"fts5\", \"fulltext\", \"gin\"],\n },\n\n migrate: { artifacts: searchArtifacts },\n\n client: {\n model: ({ model, dialect, provider, options: extOptions, declaration, typeNames }) => {\n const decl = readDeclaration({ model, declaration });\n if (decl === null) return null;\n const requested = (extOptions[\"engine\"] as SearchEngine | \"auto\" | undefined) ?? \"auto\";\n const engine = resolveSearchEngine({ dialect, provider, requested });\n const language =\n decl.language ?? (extOptions[\"language\"] as string | undefined) ?? \"english\";\n if (!isSafeSqlName({ name: language })) {\n throw new VibeError({\n code: \"VIBE_SCHEMA_INVALID\",\n message: `search language \"${language}\" is not a plain text-search config name`,\n meta: { model: model.name, language },\n });\n }\n\n const pkField = primaryKeyFieldOf({ model });\n const config: SearchDelegateConfig = {\n engine,\n table: tableNameOf({ model }),\n fields: decl.fields.map((field) => ({\n name: field,\n column: columnNameOf({ model, field }),\n })),\n ...(engine === \"tsvector\" ? { tsvColumn: `${decl.base}_tsv`, language } : {}),\n ...(engine === \"fts5\" ? { ftsTable: `${decl.base}_fts` } : {}),\n ...(engine === \"bm25\" && pkField !== null\n ? { pkColumn: columnNameOf({ model, field: pkField }) }\n : {}),\n };\n\n const fieldUnion = decl.fields.map((field) => JSON.stringify(field)).join(\" | \");\n return {\n kind: \"method\",\n runtimeGlue: `createSearchDelegate(${JSON.stringify(config)})`,\n imports: [{ from: \"@vibeorm/extensions/search\", names: [\"createSearchDelegate\"] }],\n dtsImports: [{ from: \"@vibeorm/extensions/search\", names: [\"SearchHit\"] }],\n dts: [\n `readonly search: (args: {`,\n ` readonly query: string;`,\n ` readonly where?: ${typeNames.whereInput};`,\n ` readonly take?: number;`,\n ` readonly skip?: number;`,\n ` readonly minScore?: number;`,\n ` readonly highlight?: { readonly field: ${fieldUnion} };`,\n `}) => Promise<SearchHit<${typeNames.payload}>[]>;`,\n ],\n };\n },\n },\n\n doctor: async ({ execute, dialect, options: extOptions }) => {\n if (dialect === \"postgres\") {\n const requested = (extOptions[\"engine\"] as SearchEngine | \"auto\" | undefined) ?? \"auto\";\n if (requested === \"bm25\") {\n return { checks: [await pgExtensionCheck({ execute, name: \"pg_search\" })] };\n }\n return { checks: [{ name: \"tsvector\", ok: true, note: \"built in\" }] };\n }\n if (dialect === \"sqlite\") {\n try {\n const rows = await execute({\n text: \"SELECT 1 AS ok FROM pragma_compile_options WHERE compile_options = 'ENABLE_FTS5'\",\n });\n return {\n checks: [\n rows.length > 0\n ? { name: \"fts5\", ok: true, note: \"compiled in\" }\n : { name: \"fts5\", ok: false, note: \"sqlite built without FTS5\" },\n ],\n };\n } catch (error) {\n return {\n checks: [\n {\n name: \"fts5\",\n ok: false,\n note: `check failed: ${error instanceof Error ? error.message : String(error)}`,\n },\n ],\n };\n }\n }\n return { checks: [{ name: \"fulltext\", ok: true, note: \"built in (InnoDB FULLTEXT)\" }] };\n },\n });\n}\n",
|
|
6
|
+
"/**\n * Helpers shared by the tier-1 extensions. Nothing here is privileged —\n * userland extensions can do everything these do with the same public hooks.\n */\n\nimport { VibeError } from \"@vibeorm/schema\";\nimport type {\n ExtensionDoctorCheck,\n ExtensionSqlExecutor,\n ModelIR,\n} from \"@vibeorm/schema\";\n\n// ─── IR lookups ───────────────────────────────────────────────────\n\n/** Table name of a model (`dbName ?? name`). */\nexport function tableNameOf(params: { model: ModelIR }): string {\n return params.model.dbName ?? params.model.name;\n}\n\n/** Column name of a field (`dbName ?? name`); the field must exist. */\nexport function columnNameOf(params: { model: ModelIR; field: string }): string {\n const found = params.model.fields.find((candidate) => candidate.name === params.field);\n if (found === undefined) {\n throw new VibeError({\n code: \"VIBE_INTERNAL\",\n message: `extension referenced unknown field \"${params.field}\" on model \"${params.model.name}\"`,\n meta: { model: params.model.name, field: params.field },\n });\n }\n return found.dbName ?? found.name;\n}\n\n/** The model's single primary-key field name, for key_field-style options. */\nexport function primaryKeyFieldOf(params: { model: ModelIR }): string | null {\n const { model } = params;\n if (model.primaryKey !== undefined && model.primaryKey.fields.length === 1) {\n return model.primaryKey.fields[0] ?? null;\n }\n const idField = model.fields.find((field) => field.isId);\n return idField?.name ?? null;\n}\n\n// ─── Declaration readers (ExtensionDeclIR.options are untyped JSON) ─\n\nexport function declString(decl: Readonly<Record<string, unknown>>, key: string): string | undefined {\n const value = decl[key];\n return typeof value === \"string\" ? value : undefined;\n}\n\nexport function declStringArray(\n decl: Readonly<Record<string, unknown>>,\n key: string,\n): readonly string[] | undefined {\n const value = decl[key];\n if (!Array.isArray(value)) return undefined;\n return value.every((entry): entry is string => typeof entry === \"string\") ? value : undefined;\n}\n\nexport function declNumberArray(\n decl: Readonly<Record<string, unknown>>,\n key: string,\n): readonly number[] | undefined {\n const value = decl[key];\n if (!Array.isArray(value)) return undefined;\n return value.every((entry): entry is number => typeof entry === \"number\") ? value : undefined;\n}\n\n// ─── Identifier safety ────────────────────────────────────────────\n\nconst SQL_LITERAL_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;\n\n/**\n * Names an extension splices as SQL LITERALS (regconfig names, key_field\n * options) must be plain identifiers — everything else is rejected at parse\n * time so no schema-borne text ever reaches SQL unquoted.\n */\nexport function isSafeSqlName(params: { name: string }): boolean {\n return SQL_LITERAL_NAME.test(params.name);\n}\n\n// ─── Doctor helpers ───────────────────────────────────────────────\n\n/** Availability of a postgres extension, per pg_available_extensions. */\nexport async function pgExtensionCheck(params: {\n execute: ExtensionSqlExecutor;\n name: string;\n}): Promise<ExtensionDoctorCheck> {\n const { execute, name } = params;\n try {\n const rows = await execute({\n text: \"SELECT name, default_version, installed_version FROM pg_available_extensions WHERE name = $1\",\n values: [name],\n });\n const row = rows[0];\n if (row === undefined) {\n return { name: `extension ${name}`, ok: false, note: \"not available on this server\" };\n }\n const installed = row[\"installed_version\"];\n return {\n name: `extension ${name}`,\n ok: true,\n note: installed === null || installed === undefined ? \"available, not installed\" : `installed ${String(installed)}`,\n };\n } catch (error) {\n return {\n name: `extension ${name}`,\n ok: false,\n note: `check failed: ${error instanceof Error ? error.message : String(error)}`,\n };\n }\n}\n"
|
|
7
|
+
],
|
|
8
|
+
"mappings": "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",
|
|
9
|
+
"debugId": "CE3DBDCF2300506B64756E2164756E21",
|
|
10
|
+
"names": []
|
|
11
|
+
}
|
package/dist/shared.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers shared by the tier-1 extensions. Nothing here is privileged —
|
|
3
|
+
* userland extensions can do everything these do with the same public hooks.
|
|
4
|
+
*/
|
|
5
|
+
import type { ExtensionDoctorCheck, ExtensionSqlExecutor, ModelIR } from "@vibeorm/schema";
|
|
6
|
+
/** Table name of a model (`dbName ?? name`). */
|
|
7
|
+
export declare function tableNameOf(params: {
|
|
8
|
+
model: ModelIR;
|
|
9
|
+
}): string;
|
|
10
|
+
/** Column name of a field (`dbName ?? name`); the field must exist. */
|
|
11
|
+
export declare function columnNameOf(params: {
|
|
12
|
+
model: ModelIR;
|
|
13
|
+
field: string;
|
|
14
|
+
}): string;
|
|
15
|
+
/** The model's single primary-key field name, for key_field-style options. */
|
|
16
|
+
export declare function primaryKeyFieldOf(params: {
|
|
17
|
+
model: ModelIR;
|
|
18
|
+
}): string | null;
|
|
19
|
+
export declare function declString(decl: Readonly<Record<string, unknown>>, key: string): string | undefined;
|
|
20
|
+
export declare function declStringArray(decl: Readonly<Record<string, unknown>>, key: string): readonly string[] | undefined;
|
|
21
|
+
export declare function declNumberArray(decl: Readonly<Record<string, unknown>>, key: string): readonly number[] | undefined;
|
|
22
|
+
/**
|
|
23
|
+
* Names an extension splices as SQL LITERALS (regconfig names, key_field
|
|
24
|
+
* options) must be plain identifiers — everything else is rejected at parse
|
|
25
|
+
* time so no schema-borne text ever reaches SQL unquoted.
|
|
26
|
+
*/
|
|
27
|
+
export declare function isSafeSqlName(params: {
|
|
28
|
+
name: string;
|
|
29
|
+
}): boolean;
|
|
30
|
+
/** Availability of a postgres extension, per pg_available_extensions. */
|
|
31
|
+
export declare function pgExtensionCheck(params: {
|
|
32
|
+
execute: ExtensionSqlExecutor;
|
|
33
|
+
name: string;
|
|
34
|
+
}): Promise<ExtensionDoctorCheck>;
|
|
35
|
+
//# sourceMappingURL=shared.d.ts.map
|