@vibeorm/extensions 2.1.1 → 2.2.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 +9 -1
- package/dist/index.js +72 -23
- package/dist/index.js.map +6 -6
- package/dist/search/index.d.ts.map +1 -1
- package/dist/search/index.js +33 -9
- package/dist/search/index.js.map +4 -4
- package/dist/shared.d.ts +7 -0
- package/dist/shared.d.ts.map +1 -1
- package/dist/trgm/index.d.ts.map +1 -1
- package/dist/trgm/index.js +23 -3
- package/dist/trgm/index.js.map +4 -4
- package/dist/vector/index.d.ts.map +1 -1
- package/dist/vector/index.js +39 -10
- package/dist/vector/index.js.map +4 -4
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -37,7 +37,9 @@ Each extension declares its schema attributes, the DDL artifacts `db push` / `mi
|
|
|
37
37
|
|
|
38
38
|
## Search
|
|
39
39
|
|
|
40
|
-
Declare the searched fields with `@@search`. Optional named arguments: `name` (artifact base name, a plain identifier), `weights` (one number per field), `language` (a text-search config name, tsvector engine only).
|
|
40
|
+
Declare the searched fields with `@@search`. Optional named arguments: `name` (artifact base name, a plain identifier), `weights` (one finite number per field, **tsvector only**), `language` (a text-search config name, tsvector engine only).
|
|
41
|
+
|
|
42
|
+
Weights express ordinal priority, not numeric multipliers: distinct values descending map to PostgreSQL's A, B, C, D classes, with further classes capped at D. Equal weights share a class. FTS5, MySQL FULLTEXT and ParadeDB BM25 refuse declared weights with `VIBE_UNSUPPORTED_CAPABILITY`; omit `weights` on those engines.
|
|
41
43
|
|
|
42
44
|
```prisma
|
|
43
45
|
model Post {
|
|
@@ -67,6 +69,8 @@ const hits = await db.post.search({
|
|
|
67
69
|
// hits: SearchHit<Post>[] — { row, score, highlights }
|
|
68
70
|
```
|
|
69
71
|
|
|
72
|
+
`take` must be a positive safe integer and `skip` a non-negative safe integer. Negative, fractional and non-finite pagination refuses with `VIBE_VALIDATION` rather than being clamped. `minScore` and vector `maxDistance` must be finite numbers; a negative inner-product distance is valid. Trigram thresholds must be finite numbers in [0, 1]. Vector index tuning requires integers: `m` 2..100, `efConstruction` 4..1000, `lists` 1..32768. The database may impose additional constraints between options.
|
|
73
|
+
|
|
70
74
|
`highlights` is `readonly string[] | null`; it is `null` when no highlight was requested and on engines without highlighting.
|
|
71
75
|
|
|
72
76
|
### Engines per dialect
|
|
@@ -85,6 +89,10 @@ Two gates worth knowing before you pick a dialect:
|
|
|
85
89
|
- `engine: "bm25"` is refused on `pglite` (it does not bundle `pg_search`) and on non-postgres dialects. `"auto"` never selects it, because ParadeDB availability is not knowable at generate time.
|
|
86
90
|
- The `fts5` engine requires the searched model to have a single `Int` or `BigInt` `@id`. That is the only primary key that aliases and pins the SQLite rowid the external-content table keys on; anything else is refused at generate time.
|
|
87
91
|
|
|
92
|
+
### FTS5 installation on populated tables
|
|
93
|
+
|
|
94
|
+
The installation statements create the external-content table, install its synchronization triggers, then run FTS5's `rebuild` command to index historical rows. Apply the full installation in one transaction. This scans the host table and takes SQLite's write lock; allow for that cost in a migration window. It is installation work, not an application-startup hook. Existing installations are not automatically repaired by upgrading the package; review and run an explicit rebuild migration if historical rows were omitted.
|
|
95
|
+
|
|
88
96
|
### Type-ahead (prefix) matching
|
|
89
97
|
|
|
90
98
|
All engines match complete words by default — a user still typing gets nothing for `"piz"`. For search-as-you-type boxes pass `prefix: true`:
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// packages/extensions/src/search/index.ts
|
|
3
3
|
import { VibeError as VibeError2, defineExtension, unsupportedCapability } from "@vibeorm/schema";
|
|
4
|
-
import { ParamCollector, getSqlDialect, renderExpr } from "@vibeorm/sql";
|
|
5
4
|
|
|
6
5
|
// packages/extensions/src/shared.ts
|
|
7
6
|
import { VibeError } from "@vibeorm/schema";
|
|
7
|
+
function finiteNumber(params) {
|
|
8
|
+
const { value, name, integer, min } = params;
|
|
9
|
+
if (typeof value !== "number" || !Number.isFinite(value) || integer === true && !Number.isSafeInteger(value) || min !== undefined && value < min) {
|
|
10
|
+
throw new VibeError({
|
|
11
|
+
code: "VIBE_VALIDATION",
|
|
12
|
+
message: `${name} must be a finite ${integer === true ? "safe integer" : "number"}${min === undefined ? "" : ` >= ${min}`}`,
|
|
13
|
+
meta: { argument: name }
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
8
18
|
function tableNameOf(params) {
|
|
9
19
|
return params.model.dbName ?? params.model.name;
|
|
10
20
|
}
|
|
@@ -74,6 +84,7 @@ async function pgExtensionCheck(params) {
|
|
|
74
84
|
}
|
|
75
85
|
|
|
76
86
|
// packages/extensions/src/search/index.ts
|
|
87
|
+
import { ParamCollector, getSqlDialect, renderExpr } from "@vibeorm/sql";
|
|
77
88
|
var VERSION = "2.0.0-alpha.1";
|
|
78
89
|
var SCORE_ALIAS = "_vibe_score";
|
|
79
90
|
var HIGHLIGHT_ALIAS = "_vibe_highlight";
|
|
@@ -109,6 +120,9 @@ function readDeclaration(params) {
|
|
|
109
120
|
const fields = declStringArray(params.declaration, "fields");
|
|
110
121
|
if (fields === undefined || fields.length === 0)
|
|
111
122
|
return null;
|
|
123
|
+
if (params.declaration["weights"] !== undefined && (!Array.isArray(params.declaration["weights"]) || params.declaration["weights"].length !== fields.length || params.declaration["weights"].some((weight) => typeof weight !== "number" || !Number.isFinite(weight)))) {
|
|
124
|
+
throw new VibeError2({ code: "VIBE_SCHEMA_INVALID", message: "weights must be a finite number list matching the field list", meta: { model: params.model.name } });
|
|
125
|
+
}
|
|
112
126
|
return {
|
|
113
127
|
fields,
|
|
114
128
|
weights: declNumberArray(params.declaration, "weights"),
|
|
@@ -149,6 +163,13 @@ function resolveArtifactNames(params) {
|
|
|
149
163
|
}
|
|
150
164
|
function resolveRank(params) {
|
|
151
165
|
const { decl, options, engine, dialect } = params;
|
|
166
|
+
if (decl.weights !== undefined && engine !== "tsvector") {
|
|
167
|
+
throw unsupportedCapability({
|
|
168
|
+
dialect,
|
|
169
|
+
feature: `@@search weights on the ${engine} engine`,
|
|
170
|
+
hint: "weights are supported only by tsvector; omit weights for this engine"
|
|
171
|
+
});
|
|
172
|
+
}
|
|
152
173
|
if (decl.rank !== undefined && engine !== "tsvector") {
|
|
153
174
|
throw unsupportedCapability({
|
|
154
175
|
dialect,
|
|
@@ -277,7 +298,8 @@ END;`,
|
|
|
277
298
|
`CREATE TRIGGER IF NOT EXISTS ${quote(`${fts}_au`)} AFTER UPDATE ON ${quote(table)} BEGIN
|
|
278
299
|
INSERT INTO ${quote(fts)}(${quote(fts)}, rowid, ${columnList}) VALUES ('delete', old.rowid, ${oldList});
|
|
279
300
|
INSERT INTO ${quote(fts)}(rowid, ${columnList}) VALUES (new.rowid, ${newList});
|
|
280
|
-
END
|
|
301
|
+
END;`,
|
|
302
|
+
`INSERT INTO ${quote(fts)}(${quote(fts)}) VALUES ('rebuild');`
|
|
281
303
|
],
|
|
282
304
|
drop: [
|
|
283
305
|
`DROP TRIGGER IF EXISTS ${quote(`${fts}_ai`)};`,
|
|
@@ -340,6 +362,10 @@ function createSearchDelegate(config) {
|
|
|
340
362
|
meta: { table: config.table }
|
|
341
363
|
});
|
|
342
364
|
}
|
|
365
|
+
const take = finiteNumber({ value: args.take === undefined ? 10 : args.take, name: "take", integer: true, min: 1 });
|
|
366
|
+
const skip = args.skip === undefined ? null : finiteNumber({ value: args.skip, name: "skip", integer: true, min: 0 });
|
|
367
|
+
if (args.minScore !== undefined)
|
|
368
|
+
finiteNumber({ value: args.minScore, name: "minScore" });
|
|
343
369
|
const prefix = args.prefix === true;
|
|
344
370
|
if (prefix && config.engine === "bm25") {
|
|
345
371
|
throw unsupportedCapability({
|
|
@@ -355,8 +381,6 @@ function createSearchDelegate(config) {
|
|
|
355
381
|
return [];
|
|
356
382
|
matchText = config.engine === "tsvector" ? tokens.map((token) => `${token}:*`).join(" & ") : config.engine === "fts5" ? tokens.map((token) => `"${token}"*`).join(" ") : tokens.map((token) => `+${token}*`).join(" ");
|
|
357
383
|
}
|
|
358
|
-
const take = Math.max(1, Math.trunc(args.take ?? 10));
|
|
359
|
-
const skip = args.skip === undefined ? null : Math.max(0, Math.trunc(args.skip));
|
|
360
384
|
const hlColumn = highlightColumn({ config, args });
|
|
361
385
|
const idOnly = args.select === "id";
|
|
362
386
|
if (idOnly && config.pkColumn === undefined) {
|
|
@@ -469,7 +493,7 @@ function search(options = {}) {
|
|
|
469
493
|
if (fields.length === 0) {
|
|
470
494
|
return { error: "@@search requires a field list, e.g. @@search([title, body])" };
|
|
471
495
|
}
|
|
472
|
-
const missing = fields.find((
|
|
496
|
+
const missing = fields.find((name) => !fieldNames.includes(name));
|
|
473
497
|
if (missing !== undefined) {
|
|
474
498
|
return { error: `@@search references unknown field "${missing}"` };
|
|
475
499
|
}
|
|
@@ -479,7 +503,7 @@ function search(options = {}) {
|
|
|
479
503
|
}
|
|
480
504
|
const weights = args.named["weights"];
|
|
481
505
|
if (weights !== undefined) {
|
|
482
|
-
if (!Array.isArray(weights) || weights.length !== fields.length || weights.some((weight) => typeof weight !== "number")) {
|
|
506
|
+
if (!Array.isArray(weights) || weights.length !== fields.length || weights.some((weight) => typeof weight !== "number" || !Number.isFinite(weight))) {
|
|
483
507
|
return { error: `weights must be a number list matching the field list` };
|
|
484
508
|
}
|
|
485
509
|
}
|
|
@@ -603,7 +627,6 @@ function search(options = {}) {
|
|
|
603
627
|
}
|
|
604
628
|
});
|
|
605
629
|
}
|
|
606
|
-
|
|
607
630
|
// packages/extensions/src/vector/index.ts
|
|
608
631
|
import { VibeError as VibeError3, defineExtension as defineExtension2, unsupportedCapability as unsupportedCapability2 } from "@vibeorm/schema";
|
|
609
632
|
import { ParamCollector as ParamCollector2, getSqlDialect as getSqlDialect2, renderExpr as renderExpr2 } from "@vibeorm/sql";
|
|
@@ -621,6 +644,20 @@ var METRIC_OPCLASS = {
|
|
|
621
644
|
var METRICS = ["cosine", "l2", "ip"];
|
|
622
645
|
var INDEX_KINDS = ["hnsw", "ivfflat"];
|
|
623
646
|
var DISTANCE_ALIAS = "_vibe_distance";
|
|
647
|
+
var INDEX_BOUNDS = {
|
|
648
|
+
m: [2, 100],
|
|
649
|
+
efConstruction: [4, 1000],
|
|
650
|
+
lists: [1, 32768]
|
|
651
|
+
};
|
|
652
|
+
function indexOptionError(params) {
|
|
653
|
+
for (const [key, [min, max]] of Object.entries(INDEX_BOUNDS)) {
|
|
654
|
+
const value = params.declaration[key];
|
|
655
|
+
if (value !== undefined && (typeof value !== "number" || !Number.isInteger(value) || value < min || value > max)) {
|
|
656
|
+
return `${key} must be an integer between ${min} and ${max}`;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
624
661
|
function vectorColumnsOf(params) {
|
|
625
662
|
const columns = params.declaration["columns"];
|
|
626
663
|
if (columns === null || typeof columns !== "object" || Array.isArray(columns))
|
|
@@ -657,6 +694,10 @@ function vectorArtifacts(ctx) {
|
|
|
657
694
|
const model = ctx.schema.models.find((candidate) => candidate.name === modelName);
|
|
658
695
|
if (model === undefined)
|
|
659
696
|
continue;
|
|
697
|
+
const invalidOption = indexOptionError({ declaration });
|
|
698
|
+
if (invalidOption !== undefined) {
|
|
699
|
+
throw new VibeError3({ code: "VIBE_SCHEMA_INVALID", message: invalidOption, meta: { model: modelName } });
|
|
700
|
+
}
|
|
660
701
|
const table = tableNameOf({ model });
|
|
661
702
|
const columns = vectorColumnsOf({ declaration });
|
|
662
703
|
const indexFields = declStringArray(declaration, "fields") ?? [];
|
|
@@ -732,6 +773,9 @@ function createVectorNamespace(config) {
|
|
|
732
773
|
const t = quote("t");
|
|
733
774
|
return {
|
|
734
775
|
nearest: async (args) => {
|
|
776
|
+
const take = finiteNumber({ value: args.take === undefined ? 10 : args.take, name: "take", integer: true, min: 1 });
|
|
777
|
+
if (args.maxDistance !== undefined)
|
|
778
|
+
finiteNumber({ value: args.maxDistance, name: "maxDistance" });
|
|
735
779
|
const { column, literal } = resolveColumn({ config, args });
|
|
736
780
|
const metric = args.metric ?? config.metric;
|
|
737
781
|
const op = METRIC_OPERATOR[metric];
|
|
@@ -748,7 +792,7 @@ function createVectorNamespace(config) {
|
|
|
748
792
|
if (args.maxDistance !== undefined) {
|
|
749
793
|
text += ` AND (${col} ${op} ${collector.add(literal)}) < ${collector.add(args.maxDistance)}`;
|
|
750
794
|
}
|
|
751
|
-
text += ` ORDER BY ${col} ${op} ${collector.add(literal)} LIMIT ${
|
|
795
|
+
text += ` ORDER BY ${col} ${op} ${collector.add(literal)} LIMIT ${take}`;
|
|
752
796
|
const rows = await ctx.executeRows({
|
|
753
797
|
text,
|
|
754
798
|
values: collector.values,
|
|
@@ -815,12 +859,9 @@ function vector(options = {}) {
|
|
|
815
859
|
if (kind !== undefined && !INDEX_KINDS.includes(String(kind))) {
|
|
816
860
|
return { error: `index must be one of ${INDEX_KINDS.join(", ")}` };
|
|
817
861
|
}
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
return { error: `${key} must be a number` };
|
|
822
|
-
}
|
|
823
|
-
}
|
|
862
|
+
const invalidOption = indexOptionError({ declaration: args.named });
|
|
863
|
+
if (invalidOption !== undefined)
|
|
864
|
+
return { error: invalidOption };
|
|
824
865
|
const indexName = args.named["indexName"];
|
|
825
866
|
if (indexName !== undefined) {
|
|
826
867
|
if (typeof indexName !== "string" || !isSafeSqlName({ name: indexName })) {
|
|
@@ -897,12 +938,14 @@ function vector(options = {}) {
|
|
|
897
938
|
}
|
|
898
939
|
});
|
|
899
940
|
}
|
|
900
|
-
|
|
901
941
|
// packages/extensions/src/trgm/index.ts
|
|
902
|
-
import { defineExtension as defineExtension3, unsupportedCapability as unsupportedCapability3 } from "@vibeorm/schema";
|
|
942
|
+
import { VibeError as VibeError4, defineExtension as defineExtension3, unsupportedCapability as unsupportedCapability3 } from "@vibeorm/schema";
|
|
903
943
|
import { getSqlDialect as getSqlDialect3 } from "@vibeorm/sql";
|
|
904
944
|
var DEFAULT_THRESHOLD = 0.3;
|
|
905
945
|
var VERSION3 = "2.0.0-alpha.0";
|
|
946
|
+
function validThreshold(value) {
|
|
947
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1;
|
|
948
|
+
}
|
|
906
949
|
function trgmArtifacts(ctx) {
|
|
907
950
|
if (ctx.dialect !== "postgres") {
|
|
908
951
|
throw unsupportedCapability3({
|
|
@@ -942,7 +985,10 @@ function trgmArtifacts(ctx) {
|
|
|
942
985
|
return artifacts;
|
|
943
986
|
}
|
|
944
987
|
function trgm(options = {}) {
|
|
945
|
-
const threshold = options.threshold
|
|
988
|
+
const threshold = options.threshold === undefined ? DEFAULT_THRESHOLD : options.threshold;
|
|
989
|
+
if (!validThreshold(threshold)) {
|
|
990
|
+
throw new VibeError4({ code: "VIBE_VALIDATION", message: "trgm threshold must be a finite number between 0 and 1", meta: { argument: "threshold" } });
|
|
991
|
+
}
|
|
946
992
|
return defineExtension3({
|
|
947
993
|
name: "trgm",
|
|
948
994
|
version: VERSION3,
|
|
@@ -986,6 +1032,9 @@ function trgm(options = {}) {
|
|
|
986
1032
|
}
|
|
987
1033
|
const perUse = typeof operand === "object" && operand !== null && "threshold" in operand ? operand.threshold : undefined;
|
|
988
1034
|
const configured = extOptions["threshold"];
|
|
1035
|
+
if (configured !== undefined && !validThreshold(configured) || perUse !== undefined && !validThreshold(perUse)) {
|
|
1036
|
+
return { error: "threshold must be a finite number between 0 and 1" };
|
|
1037
|
+
}
|
|
989
1038
|
const sessionThreshold = typeof configured === "number" ? configured : DEFAULT_THRESHOLD;
|
|
990
1039
|
const effective = typeof perUse === "number" ? perUse : sessionThreshold;
|
|
991
1040
|
if (effective >= Math.max(sessionThreshold, DEFAULT_THRESHOLD)) {
|
|
@@ -1069,12 +1118,12 @@ function trgm(options = {}) {
|
|
|
1069
1118
|
});
|
|
1070
1119
|
}
|
|
1071
1120
|
export {
|
|
1072
|
-
|
|
1073
|
-
trgm,
|
|
1074
|
-
search,
|
|
1075
|
-
resolveSearchEngine,
|
|
1121
|
+
createSearchDelegate,
|
|
1076
1122
|
createVectorNamespace,
|
|
1077
|
-
|
|
1123
|
+
resolveSearchEngine,
|
|
1124
|
+
search,
|
|
1125
|
+
trgm,
|
|
1126
|
+
vector
|
|
1078
1127
|
};
|
|
1079
1128
|
|
|
1080
|
-
//# debugId=
|
|
1129
|
+
//# debugId=CB654FBC580AA6A164756E2164756E21
|