@vibeorm/extensions 2.0.0-alpha.3 → 2.0.0-alpha.4
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/dist/index.js +113 -30
- package/dist/index.js.map +3 -3
- package/dist/search/index.d.ts +16 -1
- package/dist/search/index.d.ts.map +1 -1
- package/dist/search/index.js +113 -30
- package/dist/search/index.js.map +3 -3
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -101,6 +101,10 @@ function resolveSearchEngine(params) {
|
|
|
101
101
|
hint: `this dialect supports "${native}"${dialect === "postgres" ? ' and "bm25"' : ""}`
|
|
102
102
|
});
|
|
103
103
|
}
|
|
104
|
+
function declRank(declaration) {
|
|
105
|
+
const value = declaration["rank"];
|
|
106
|
+
return value === "ts_rank" || value === "ts_rank_cd" ? value : undefined;
|
|
107
|
+
}
|
|
104
108
|
function readDeclaration(params) {
|
|
105
109
|
const fields = declStringArray(params.declaration, "fields");
|
|
106
110
|
if (fields === undefined || fields.length === 0)
|
|
@@ -109,7 +113,10 @@ function readDeclaration(params) {
|
|
|
109
113
|
fields,
|
|
110
114
|
weights: declNumberArray(params.declaration, "weights"),
|
|
111
115
|
base: declString(params.declaration, "name") ?? `${tableNameOf({ model: params.model })}_search`,
|
|
112
|
-
language: declString(params.declaration, "language")
|
|
116
|
+
language: declString(params.declaration, "language"),
|
|
117
|
+
columnName: declString(params.declaration, "columnName"),
|
|
118
|
+
indexName: declString(params.declaration, "indexName"),
|
|
119
|
+
rank: declRank(params.declaration)
|
|
113
120
|
};
|
|
114
121
|
}
|
|
115
122
|
function weightLetters(params) {
|
|
@@ -119,6 +126,41 @@ function weightLetters(params) {
|
|
|
119
126
|
const ranked = [...new Set(decl.weights)].sort((a, b) => b - a);
|
|
120
127
|
return decl.weights.map((weight) => WEIGHT_LETTERS[Math.min(ranked.indexOf(weight), WEIGHT_LETTERS.length - 1)]);
|
|
121
128
|
}
|
|
129
|
+
function resolveArtifactNames(params) {
|
|
130
|
+
const { decl, engine, dialect } = params;
|
|
131
|
+
if (engine === "fts5" && (decl.columnName !== undefined || decl.indexName !== undefined)) {
|
|
132
|
+
throw unsupportedCapability({
|
|
133
|
+
dialect,
|
|
134
|
+
feature: "@@search columnName/indexName overrides on the fts5 engine",
|
|
135
|
+
hint: "fts5 emits a shadow table + triggers \u2014 there is no column or index to rename; use name: to change the table base"
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
if (decl.columnName !== undefined && engine !== "tsvector") {
|
|
139
|
+
throw unsupportedCapability({
|
|
140
|
+
dialect,
|
|
141
|
+
feature: `@@search columnName on the ${engine} engine`,
|
|
142
|
+
hint: "only the tsvector engine materializes a generated column \u2014 drop columnName or target tsvector"
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
tsvColumn: decl.columnName ?? `${decl.base}_tsv`,
|
|
147
|
+
indexName: decl.indexName ?? `${decl.base}_idx`
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function resolveRank(params) {
|
|
151
|
+
const { decl, options, engine, dialect } = params;
|
|
152
|
+
if (decl.rank !== undefined && engine !== "tsvector") {
|
|
153
|
+
throw unsupportedCapability({
|
|
154
|
+
dialect,
|
|
155
|
+
feature: `@@search rank on the ${engine} engine`,
|
|
156
|
+
hint: "rank chooses between the postgres ranking functions ts_rank and ts_rank_cd \u2014 a tsvector-only concept"
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
if (engine !== "tsvector")
|
|
160
|
+
return;
|
|
161
|
+
const fallback = options["rank"];
|
|
162
|
+
return decl.rank ?? (fallback === "ts_rank" || fallback === "ts_rank_cd" ? fallback : undefined);
|
|
163
|
+
}
|
|
122
164
|
function tsvectorExpression(params) {
|
|
123
165
|
const { dialect, model, decl, language } = params;
|
|
124
166
|
const quote = (name) => dialect.quoteIdent({ name });
|
|
@@ -152,9 +194,10 @@ function searchArtifacts(ctx) {
|
|
|
152
194
|
continue;
|
|
153
195
|
const table = tableNameOf({ model });
|
|
154
196
|
const columns = decl.fields.map((field) => columnNameOf({ model, field }));
|
|
197
|
+
resolveRank({ decl, options: ctx.options, engine, dialect: ctx.dialect });
|
|
155
198
|
if (engine === "tsvector") {
|
|
156
199
|
const language = decl.language ?? ctx.options["language"] ?? "english";
|
|
157
|
-
const tsvColumn =
|
|
200
|
+
const { tsvColumn, indexName } = resolveArtifactNames({ decl, engine, dialect: ctx.dialect });
|
|
158
201
|
const expression = tsvectorExpression({ dialect, model, decl, language });
|
|
159
202
|
artifacts.push({
|
|
160
203
|
id: `search:${modelName}:column`,
|
|
@@ -165,11 +208,11 @@ function searchArtifacts(ctx) {
|
|
|
165
208
|
drop: [`ALTER TABLE ${quote(table)} DROP COLUMN IF EXISTS ${quote(tsvColumn)};`]
|
|
166
209
|
}, {
|
|
167
210
|
id: `search:${modelName}:index`,
|
|
168
|
-
owns: { kind: "index", table, name:
|
|
211
|
+
owns: { kind: "index", table, name: indexName },
|
|
169
212
|
create: [
|
|
170
|
-
`CREATE INDEX IF NOT EXISTS ${quote(
|
|
213
|
+
`CREATE INDEX IF NOT EXISTS ${quote(indexName)} ON ${quote(table)} USING gin (${quote(tsvColumn)});`
|
|
171
214
|
],
|
|
172
|
-
drop: [`DROP INDEX IF EXISTS ${quote(
|
|
215
|
+
drop: [`DROP INDEX IF EXISTS ${quote(indexName)};`]
|
|
173
216
|
});
|
|
174
217
|
continue;
|
|
175
218
|
}
|
|
@@ -183,17 +226,19 @@ function searchArtifacts(ctx) {
|
|
|
183
226
|
});
|
|
184
227
|
}
|
|
185
228
|
const pkColumn = columnNameOf({ model, field: pkField });
|
|
229
|
+
const bm25Names = resolveArtifactNames({ decl, engine, dialect: ctx.dialect });
|
|
186
230
|
artifacts.push({
|
|
187
231
|
id: `search:${modelName}:index`,
|
|
188
|
-
owns: { kind: "index", table, name:
|
|
232
|
+
owns: { kind: "index", table, name: bm25Names.indexName },
|
|
189
233
|
create: [
|
|
190
|
-
`CREATE INDEX IF NOT EXISTS ${quote(
|
|
234
|
+
`CREATE INDEX IF NOT EXISTS ${quote(bm25Names.indexName)} ON ${quote(table)} USING bm25 (${[pkColumn, ...columns].map((column) => quote(column)).join(", ")}) WITH (key_field='${pkColumn}');`
|
|
191
235
|
],
|
|
192
|
-
drop: [`DROP INDEX IF EXISTS ${quote(
|
|
236
|
+
drop: [`DROP INDEX IF EXISTS ${quote(bm25Names.indexName)};`]
|
|
193
237
|
});
|
|
194
238
|
continue;
|
|
195
239
|
}
|
|
196
240
|
if (engine === "fts5") {
|
|
241
|
+
resolveArtifactNames({ decl, engine, dialect: ctx.dialect });
|
|
197
242
|
const pkName = primaryKeyFieldOf({ model });
|
|
198
243
|
const pkField = pkName === null ? undefined : model.fields.find((field) => field.name === pkName);
|
|
199
244
|
const pkIsRowidAlias = pkField !== undefined && !pkField.isList && (pkField.type === "Int" || pkField.type === "BigInt");
|
|
@@ -242,13 +287,14 @@ END;`
|
|
|
242
287
|
});
|
|
243
288
|
continue;
|
|
244
289
|
}
|
|
290
|
+
const fulltextNames = resolveArtifactNames({ decl, engine, dialect: ctx.dialect });
|
|
245
291
|
artifacts.push({
|
|
246
292
|
id: `search:${modelName}:index`,
|
|
247
|
-
owns: { kind: "index", table, name:
|
|
293
|
+
owns: { kind: "index", table, name: fulltextNames.indexName },
|
|
248
294
|
create: [
|
|
249
|
-
`CREATE FULLTEXT INDEX ${quote(
|
|
295
|
+
`CREATE FULLTEXT INDEX ${quote(fulltextNames.indexName)} ON ${quote(table)} (${columns.map((column) => quote(column)).join(", ")});`
|
|
250
296
|
],
|
|
251
|
-
drop: [`DROP INDEX ${quote(
|
|
297
|
+
drop: [`DROP INDEX ${quote(fulltextNames.indexName)} ON ${quote(table)};`]
|
|
252
298
|
});
|
|
253
299
|
}
|
|
254
300
|
return artifacts;
|
|
@@ -297,6 +343,15 @@ function createSearchDelegate(config) {
|
|
|
297
343
|
const take = Math.max(1, Math.trunc(args.take ?? 10));
|
|
298
344
|
const skip = args.skip === undefined ? null : Math.max(0, Math.trunc(args.skip));
|
|
299
345
|
const hlColumn = highlightColumn({ config, args });
|
|
346
|
+
const idOnly = args.select === "id";
|
|
347
|
+
if (idOnly && config.pkColumn === undefined) {
|
|
348
|
+
throw new VibeError2({
|
|
349
|
+
code: "VIBE_VALIDATION",
|
|
350
|
+
message: `select: "id" needs a single-column primary key baked into the client \u2014 ${config.table} has none, or the client predates the projection (run vibeorm generate)`,
|
|
351
|
+
meta: { table: config.table }
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
const projection = idOnly ? `${t}.${quote(config.pkColumn ?? "")}` : `${t}.*`;
|
|
300
355
|
const collector = new ParamCollector(dialect);
|
|
301
356
|
const userWhere = (qualify) => {
|
|
302
357
|
if (args.where === undefined)
|
|
@@ -309,9 +364,10 @@ function createSearchDelegate(config) {
|
|
|
309
364
|
let engineHasHighlights = true;
|
|
310
365
|
if (config.engine === "tsvector") {
|
|
311
366
|
const language = config.language ?? "english";
|
|
367
|
+
const ranker = config.rank ?? "ts_rank_cd";
|
|
312
368
|
const tsv = `${t}.${quote(config.tsvColumn ?? "")}`;
|
|
313
369
|
const tsquery = () => `websearch_to_tsquery('${language}', ${collector.add(args.query)})`;
|
|
314
|
-
text = `SELECT ${
|
|
370
|
+
text = `SELECT ${projection}, ${ranker}(${tsv}, ${tsquery()}) AS ${quote(SCORE_ALIAS)}`;
|
|
315
371
|
if (hlColumn !== null) {
|
|
316
372
|
text += `, ts_headline('${language}', ${t}.${quote(hlColumn)}, ${tsquery()}) AS ${quote(HIGHLIGHT_ALIAS)}`;
|
|
317
373
|
}
|
|
@@ -320,13 +376,13 @@ function createSearchDelegate(config) {
|
|
|
320
376
|
if (where !== null)
|
|
321
377
|
text += ` AND (${where})`;
|
|
322
378
|
if (args.minScore !== undefined) {
|
|
323
|
-
text += ` AND
|
|
379
|
+
text += ` AND ${ranker}(${tsv}, ${tsquery()}) >= ${collector.add(args.minScore)}`;
|
|
324
380
|
}
|
|
325
381
|
text += ` ORDER BY ${quote(SCORE_ALIAS)} DESC`;
|
|
326
|
-
strip = [config.tsvColumn ?? ""];
|
|
382
|
+
strip = idOnly ? [] : [config.tsvColumn ?? ""];
|
|
327
383
|
} else if (config.engine === "fts5") {
|
|
328
384
|
const fts = quote(config.ftsTable ?? "");
|
|
329
|
-
text = `SELECT ${
|
|
385
|
+
text = `SELECT ${projection}, -bm25(${fts}) AS ${quote(SCORE_ALIAS)}`;
|
|
330
386
|
if (hlColumn !== null) {
|
|
331
387
|
const index = config.fields.findIndex((field) => field.column === hlColumn);
|
|
332
388
|
text += `, snippet(${fts}, ${index}, '<b>', '</b>', '\u2026', 16) AS ${quote(HIGHLIGHT_ALIAS)}`;
|
|
@@ -343,7 +399,7 @@ function createSearchDelegate(config) {
|
|
|
343
399
|
} else if (config.engine === "fulltext") {
|
|
344
400
|
engineHasHighlights = false;
|
|
345
401
|
const match = () => `MATCH (${config.fields.map((field) => `${t}.${quote(field.column)}`).join(", ")}) AGAINST (${collector.add(args.query)} IN NATURAL LANGUAGE MODE)`;
|
|
346
|
-
text = `SELECT ${
|
|
402
|
+
text = `SELECT ${projection}, ${match()} AS ${quote(SCORE_ALIAS)} FROM ${quote(config.table)} AS ${t}`;
|
|
347
403
|
text += ` WHERE ${match()}`;
|
|
348
404
|
const where = userWhere("t");
|
|
349
405
|
if (where !== null)
|
|
@@ -354,7 +410,7 @@ function createSearchDelegate(config) {
|
|
|
354
410
|
text += ` ORDER BY ${quote(SCORE_ALIAS)} DESC`;
|
|
355
411
|
} else {
|
|
356
412
|
const pk = `${t}.${quote(config.pkColumn ?? "")}`;
|
|
357
|
-
text = `SELECT ${
|
|
413
|
+
text = `SELECT ${projection}, paradedb.score(${pk}) AS ${quote(SCORE_ALIAS)}`;
|
|
358
414
|
if (hlColumn !== null) {
|
|
359
415
|
text += `, paradedb.snippet(${t}.${quote(hlColumn)}) AS ${quote(HIGHLIGHT_ALIAS)}`;
|
|
360
416
|
}
|
|
@@ -415,6 +471,16 @@ function search(options = {}) {
|
|
|
415
471
|
if (language !== undefined && (typeof language !== "string" || !isSafeSqlName({ name: language }))) {
|
|
416
472
|
return { error: `language must be a plain text-search config name` };
|
|
417
473
|
}
|
|
474
|
+
for (const key of ["columnName", "indexName"]) {
|
|
475
|
+
const value = args.named[key];
|
|
476
|
+
if (value !== undefined && (typeof value !== "string" || !isSafeSqlName({ name: value }))) {
|
|
477
|
+
return { error: `${key} must be a plain identifier` };
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
const rank = args.named["rank"];
|
|
481
|
+
if (rank !== undefined && rank !== "ts_rank" && rank !== "ts_rank_cd") {
|
|
482
|
+
return { error: `rank must be "ts_rank" or "ts_rank_cd"` };
|
|
483
|
+
}
|
|
418
484
|
return { declaration: { fields, ...args.named } };
|
|
419
485
|
}
|
|
420
486
|
}
|
|
@@ -438,6 +504,8 @@ function search(options = {}) {
|
|
|
438
504
|
});
|
|
439
505
|
}
|
|
440
506
|
const pkField = primaryKeyFieldOf({ model });
|
|
507
|
+
const rank = resolveRank({ decl, options: extOptions, engine, dialect });
|
|
508
|
+
const names = resolveArtifactNames({ decl, engine, dialect });
|
|
441
509
|
const config = {
|
|
442
510
|
engine,
|
|
443
511
|
table: tableNameOf({ model }),
|
|
@@ -445,26 +513,41 @@ function search(options = {}) {
|
|
|
445
513
|
name: field,
|
|
446
514
|
column: columnNameOf({ model, field })
|
|
447
515
|
})),
|
|
448
|
-
...engine === "tsvector" ? { tsvColumn:
|
|
516
|
+
...engine === "tsvector" ? { tsvColumn: names.tsvColumn, language, ...rank === undefined ? {} : { rank } } : {},
|
|
449
517
|
...engine === "fts5" ? { ftsTable: `${decl.base}_fts` } : {},
|
|
450
|
-
...
|
|
518
|
+
...pkField !== null ? { pkColumn: columnNameOf({ model, field: pkField }) } : {}
|
|
451
519
|
};
|
|
452
520
|
const fieldUnion = decl.fields.map((field) => JSON.stringify(field)).join(" | ");
|
|
521
|
+
const argLines = [
|
|
522
|
+
` readonly query: string;`,
|
|
523
|
+
` readonly where?: ${typeNames.whereInput};`,
|
|
524
|
+
` readonly take?: number;`,
|
|
525
|
+
` readonly skip?: number;`,
|
|
526
|
+
` readonly minScore?: number;`,
|
|
527
|
+
` readonly highlight?: { readonly field: ${fieldUnion} };`
|
|
528
|
+
];
|
|
529
|
+
const dts = pkField === null ? [
|
|
530
|
+
`readonly search: (args: {`,
|
|
531
|
+
...argLines,
|
|
532
|
+
`}) => Promise<SearchHit<${typeNames.payload}>[]>;`
|
|
533
|
+
] : [
|
|
534
|
+
`readonly search: {`,
|
|
535
|
+
` (args: {`,
|
|
536
|
+
...argLines.map((line) => ` ${line}`),
|
|
537
|
+
` readonly select: "id";`,
|
|
538
|
+
` }): Promise<SearchHit<Pick<${typeNames.payload}, ${JSON.stringify(pkField)}>>[]>;`,
|
|
539
|
+
` (args: {`,
|
|
540
|
+
...argLines.map((line) => ` ${line}`),
|
|
541
|
+
` readonly select?: "row";`,
|
|
542
|
+
` }): Promise<SearchHit<${typeNames.payload}>[]>;`,
|
|
543
|
+
`};`
|
|
544
|
+
];
|
|
453
545
|
return {
|
|
454
546
|
kind: "method",
|
|
455
547
|
runtimeGlue: `createSearchDelegate(${JSON.stringify(config)})`,
|
|
456
548
|
imports: [{ from: "@vibeorm/extensions/search", names: ["createSearchDelegate"] }],
|
|
457
549
|
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
|
-
]
|
|
550
|
+
dts
|
|
468
551
|
};
|
|
469
552
|
}
|
|
470
553
|
},
|
|
@@ -963,4 +1046,4 @@ export {
|
|
|
963
1046
|
createSearchDelegate
|
|
964
1047
|
};
|
|
965
1048
|
|
|
966
|
-
//# debugId=
|
|
1049
|
+
//# debugId=B16F8B32993DB05B64756E2164756E21
|