@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 @@
|
|
|
1
|
+
{"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../src/shared.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EACV,oBAAoB,EACpB,oBAAoB,EACpB,OAAO,EACR,MAAM,iBAAiB,CAAC;AAIzB,gDAAgD;AAChD,wBAAgB,WAAW,CAAC,MAAM,EAAE;IAAE,KAAK,EAAE,OAAO,CAAA;CAAE,GAAG,MAAM,CAE9D;AAED,uEAAuE;AACvE,wBAAgB,YAAY,CAAC,MAAM,EAAE;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAU9E;AAED,8EAA8E;AAC9E,wBAAgB,iBAAiB,CAAC,MAAM,EAAE;IAAE,KAAK,EAAE,OAAO,CAAA;CAAE,GAAG,MAAM,GAAG,IAAI,CAO3E;AAID,wBAAgB,UAAU,CAAC,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGnG;AAED,wBAAgB,eAAe,CAC7B,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EACvC,GAAG,EAAE,MAAM,GACV,SAAS,MAAM,EAAE,GAAG,SAAS,CAI/B;AAED,wBAAgB,eAAe,CAC7B,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EACvC,GAAG,EAAE,MAAM,GACV,SAAS,MAAM,EAAE,GAAG,SAAS,CAI/B;AAMD;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAE/D;AAID,yEAAyE;AACzE,wBAAsB,gBAAgB,CAAC,MAAM,EAAE;IAC7C,OAAO,EAAE,oBAAoB,CAAC;IAC9B,IAAI,EAAE,MAAM,CAAC;CACd,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAwBhC"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `trgm` — pg_trgm fuzzy matching (extensions.md §6). Operators-only: it
|
|
3
|
+
* claims NO client surface. `where: { name: { similarTo: "jon" } }` compiles
|
|
4
|
+
* to a bound predicate plus `gin_trgm_ops` indexes over the fields declared
|
|
5
|
+
* in `@@trgm([...])`. Postgres/pglite only; anywhere else →
|
|
6
|
+
* VIBE_UNSUPPORTED_CAPABILITY.
|
|
7
|
+
*
|
|
8
|
+
* INDEXABILITY (SQL review M8 + PG-1): a bare `similarity(col, $q) >= $t` is
|
|
9
|
+
* a guaranteed full scan — GIN/GiST trigram indexes serve the `%` operator,
|
|
10
|
+
* not the function. The predicate compiles as
|
|
11
|
+
* `(col % $q AND similarity(col, $q) >= $t)` only when the effective
|
|
12
|
+
* threshold is ≥ max(configured threshold, 0.3): `%` prunes at the
|
|
13
|
+
* CONNECTION's `pg_trgm.similarity_threshold`, and pooled connections that
|
|
14
|
+
* `onConnect` never touched keep the server default 0.3 — so the gate floor
|
|
15
|
+
* must assume 0.3 even when a lower threshold was configured. Below the
|
|
16
|
+
* floor, the predicate falls back to the full-scan form — correct on every
|
|
17
|
+
* connection. A postgresql.conf value raised ABOVE the floor can still
|
|
18
|
+
* prune; the doctor's similarity_threshold check surfaces it.
|
|
19
|
+
*/
|
|
20
|
+
import type { VibeExtension } from "@vibeorm/schema";
|
|
21
|
+
export type TrgmOptions = {
|
|
22
|
+
/**
|
|
23
|
+
* Similarity threshold for `similarTo` (default 0.3). Also applied to the
|
|
24
|
+
* session's `pg_trgm.similarity_threshold` on `$connect()` so raw `%`
|
|
25
|
+
* queries agree with the operator.
|
|
26
|
+
*/
|
|
27
|
+
readonly threshold?: number;
|
|
28
|
+
};
|
|
29
|
+
/** Operand of `similarTo`: the text, or text plus a per-use threshold. */
|
|
30
|
+
export type SimilarToOperand = string | {
|
|
31
|
+
readonly text: string;
|
|
32
|
+
readonly threshold?: number;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* pg_trgm fuzzy matching.
|
|
36
|
+
*
|
|
37
|
+
* @example vibeorm.config.ts / VibeClient
|
|
38
|
+
* extensions: [trgm({ threshold: 0.3 })]
|
|
39
|
+
*
|
|
40
|
+
* @example schema
|
|
41
|
+
* // @@trgm([name]) → gin_trgm_ops index on "name"
|
|
42
|
+
* await db.user.findMany({ where: { name: { similarTo: "jon" } } });
|
|
43
|
+
*/
|
|
44
|
+
export declare function trgm(options?: TrgmOptions): VibeExtension;
|
|
45
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/trgm/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAGH,OAAO,KAAK,EAGV,aAAa,EACd,MAAM,iBAAiB,CAAC;AAMzB,MAAM,MAAM,WAAW,GAAG;IACxB;;;;OAIG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B,CAAC;AAEF,0EAA0E;AAC1E,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAiD/F;;;;;;;;;GASG;AACH,wBAAgB,IAAI,CAAC,OAAO,GAAE,WAAgB,GAAG,aAAa,CA0K7D"}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/extensions/src/trgm/index.ts
|
|
3
|
+
import { defineExtension, unsupportedCapability } from "@vibeorm/schema";
|
|
4
|
+
import { getSqlDialect } 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/trgm/index.ts
|
|
77
|
+
var DEFAULT_THRESHOLD = 0.3;
|
|
78
|
+
var VERSION = "2.0.0-alpha.0";
|
|
79
|
+
function trgmArtifacts(ctx) {
|
|
80
|
+
if (ctx.dialect !== "postgres") {
|
|
81
|
+
throw unsupportedCapability({
|
|
82
|
+
dialect: ctx.dialect,
|
|
83
|
+
feature: "the trgm extension (pg_trgm)",
|
|
84
|
+
hint: "pg_trgm exists on postgres/pglite only"
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
const dialect = getSqlDialect({ dialect: ctx.dialect });
|
|
88
|
+
const quote = (name) => dialect.quoteIdent({ name });
|
|
89
|
+
const artifacts = [
|
|
90
|
+
{
|
|
91
|
+
id: "trgm:extension",
|
|
92
|
+
owns: { kind: "postgresExtension", name: "pg_trgm" },
|
|
93
|
+
create: ["CREATE EXTENSION IF NOT EXISTS pg_trgm;"],
|
|
94
|
+
drop: ["DROP EXTENSION IF EXISTS pg_trgm;"]
|
|
95
|
+
}
|
|
96
|
+
];
|
|
97
|
+
for (const [modelName, declaration] of Object.entries(ctx.declarations)) {
|
|
98
|
+
const model = ctx.schema.models.find((candidate) => candidate.name === modelName);
|
|
99
|
+
if (model === undefined)
|
|
100
|
+
continue;
|
|
101
|
+
const table = tableNameOf({ model });
|
|
102
|
+
for (const field of declStringArray(declaration, "fields") ?? []) {
|
|
103
|
+
const column = columnNameOf({ model, field });
|
|
104
|
+
const indexName = `${table}_${column}_trgm_idx`;
|
|
105
|
+
artifacts.push({
|
|
106
|
+
id: `trgm:${modelName}:${field}`,
|
|
107
|
+
owns: { kind: "index", table, name: indexName },
|
|
108
|
+
create: [
|
|
109
|
+
`CREATE INDEX IF NOT EXISTS ${quote(indexName)} ON ${quote(table)} USING gin (${quote(column)} gin_trgm_ops);`
|
|
110
|
+
],
|
|
111
|
+
drop: [`DROP INDEX IF EXISTS ${quote(indexName)};`]
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return artifacts;
|
|
116
|
+
}
|
|
117
|
+
function trgm(options = {}) {
|
|
118
|
+
const threshold = options.threshold ?? DEFAULT_THRESHOLD;
|
|
119
|
+
return defineExtension({
|
|
120
|
+
name: "trgm",
|
|
121
|
+
version: VERSION,
|
|
122
|
+
options: { ...options },
|
|
123
|
+
requires: { postgresql: { extension: "pg_trgm" }, pglite: { extension: "pg_trgm" } },
|
|
124
|
+
schema: {
|
|
125
|
+
modelAttributes: [
|
|
126
|
+
{
|
|
127
|
+
name: "trgm",
|
|
128
|
+
parse: ({ args, fieldNames }) => {
|
|
129
|
+
const fields = args.fields ?? [];
|
|
130
|
+
if (fields.length === 0) {
|
|
131
|
+
return { error: "@@trgm requires a field list, e.g. @@trgm([name])" };
|
|
132
|
+
}
|
|
133
|
+
const missing = fields.find((name) => !fieldNames.includes(name));
|
|
134
|
+
if (missing !== undefined) {
|
|
135
|
+
return { error: `@@trgm references unknown field "${missing}"` };
|
|
136
|
+
}
|
|
137
|
+
return { declaration: { fields } };
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
],
|
|
141
|
+
indexKinds: ["gin_trgm"]
|
|
142
|
+
},
|
|
143
|
+
migrate: { artifacts: trgmArtifacts },
|
|
144
|
+
sql: {
|
|
145
|
+
whereOperators: [
|
|
146
|
+
{
|
|
147
|
+
name: "similarTo",
|
|
148
|
+
compile: ({ column, operand, dialect, options: extOptions }) => {
|
|
149
|
+
if (dialect !== "postgres") {
|
|
150
|
+
throw unsupportedCapability({
|
|
151
|
+
dialect,
|
|
152
|
+
feature: 'the "similarTo" operator (pg_trgm)',
|
|
153
|
+
hint: "pg_trgm exists on postgres/pglite only"
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
const text = typeof operand === "string" ? operand : typeof operand === "object" && operand !== null && ("text" in operand) ? operand.text : undefined;
|
|
157
|
+
if (typeof text !== "string") {
|
|
158
|
+
return { error: "expects a string or { text, threshold? }" };
|
|
159
|
+
}
|
|
160
|
+
const perUse = typeof operand === "object" && operand !== null && "threshold" in operand ? operand.threshold : undefined;
|
|
161
|
+
const configured = extOptions["threshold"];
|
|
162
|
+
const sessionThreshold = typeof configured === "number" ? configured : DEFAULT_THRESHOLD;
|
|
163
|
+
const effective = typeof perUse === "number" ? perUse : sessionThreshold;
|
|
164
|
+
if (effective >= Math.max(sessionThreshold, DEFAULT_THRESHOLD)) {
|
|
165
|
+
return {
|
|
166
|
+
fragment: {
|
|
167
|
+
parts: [
|
|
168
|
+
"(",
|
|
169
|
+
{ ident: column },
|
|
170
|
+
" % ",
|
|
171
|
+
{ param: text },
|
|
172
|
+
" AND similarity(",
|
|
173
|
+
{ ident: column },
|
|
174
|
+
", ",
|
|
175
|
+
{ param: text },
|
|
176
|
+
") >= ",
|
|
177
|
+
{ param: effective },
|
|
178
|
+
")"
|
|
179
|
+
]
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
fragment: {
|
|
185
|
+
parts: [
|
|
186
|
+
"similarity(",
|
|
187
|
+
{ ident: column },
|
|
188
|
+
", ",
|
|
189
|
+
{ param: text },
|
|
190
|
+
") >= ",
|
|
191
|
+
{ param: effective }
|
|
192
|
+
]
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
]
|
|
198
|
+
},
|
|
199
|
+
runtime: {
|
|
200
|
+
onConnect: async ({ execute, dialect, options: extOptions }) => {
|
|
201
|
+
if (dialect !== "postgres" || typeof extOptions["threshold"] !== "number")
|
|
202
|
+
return;
|
|
203
|
+
await execute({
|
|
204
|
+
text: `SET pg_trgm.similarity_threshold = ${Number(threshold)}`
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
doctor: async ({ execute, dialect }) => {
|
|
209
|
+
if (dialect !== "postgres") {
|
|
210
|
+
return { checks: [{ name: "trgm dialect", ok: false, note: "postgres/pglite only" }] };
|
|
211
|
+
}
|
|
212
|
+
const checks = [await pgExtensionCheck({ execute, name: "pg_trgm" })];
|
|
213
|
+
const floor = Math.max(threshold, DEFAULT_THRESHOLD);
|
|
214
|
+
try {
|
|
215
|
+
const rows = await execute({
|
|
216
|
+
text: `SELECT current_setting('pg_trgm.similarity_threshold', true) AS "threshold"`
|
|
217
|
+
});
|
|
218
|
+
const raw = rows[0]?.["threshold"];
|
|
219
|
+
if (raw === undefined || raw === null || raw === "") {
|
|
220
|
+
checks.push({
|
|
221
|
+
name: "trgm similarity_threshold",
|
|
222
|
+
ok: true,
|
|
223
|
+
note: `not set in this session \u2014 the 0.3 default applies (\u2264 gate floor ${floor})`
|
|
224
|
+
});
|
|
225
|
+
return { checks };
|
|
226
|
+
}
|
|
227
|
+
const server = Number(raw);
|
|
228
|
+
checks.push(Number.isNaN(server) ? { name: "trgm similarity_threshold", ok: false, note: "could not read pg_trgm.similarity_threshold" } : server <= floor ? { name: "trgm similarity_threshold", ok: true, note: `server ${server} \u2264 gate floor ${floor}` } : {
|
|
229
|
+
name: "trgm similarity_threshold",
|
|
230
|
+
ok: false,
|
|
231
|
+
note: `server pg_trgm.similarity_threshold=${server} exceeds the %-gate floor ${floor} \u2014 indexed similarTo queries can drop rows; lower the GUC or raise the extension threshold`
|
|
232
|
+
});
|
|
233
|
+
} catch {
|
|
234
|
+
checks.push({
|
|
235
|
+
name: "trgm similarity_threshold",
|
|
236
|
+
ok: false,
|
|
237
|
+
note: "reading pg_trgm.similarity_threshold failed"
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
return { checks };
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
export {
|
|
245
|
+
trgm
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
//# debugId=0582C70BE4C3EDA564756E2164756E21
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/trgm/index.ts", "../src/shared.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * `trgm` — pg_trgm fuzzy matching (extensions.md §6). Operators-only: it\n * claims NO client surface. `where: { name: { similarTo: \"jon\" } }` compiles\n * to a bound predicate plus `gin_trgm_ops` indexes over the fields declared\n * in `@@trgm([...])`. Postgres/pglite only; anywhere else →\n * VIBE_UNSUPPORTED_CAPABILITY.\n *\n * INDEXABILITY (SQL review M8 + PG-1): a bare `similarity(col, $q) >= $t` is\n * a guaranteed full scan — GIN/GiST trigram indexes serve the `%` operator,\n * not the function. The predicate compiles as\n * `(col % $q AND similarity(col, $q) >= $t)` only when the effective\n * threshold is ≥ max(configured threshold, 0.3): `%` prunes at the\n * CONNECTION's `pg_trgm.similarity_threshold`, and pooled connections that\n * `onConnect` never touched keep the server default 0.3 — so the gate floor\n * must assume 0.3 even when a lower threshold was configured. Below the\n * floor, the predicate falls back to the full-scan form — correct on every\n * connection. A postgresql.conf value raised ABOVE the floor can still\n * prune; the doctor's similarity_threshold check surfaces it.\n */\n\nimport { defineExtension, unsupportedCapability } from \"@vibeorm/schema\";\nimport type {\n ExtensionArtifact,\n ExtensionSchemaContext,\n VibeExtension,\n} from \"@vibeorm/schema\";\nimport { getSqlDialect } from \"@vibeorm/sql\";\nimport { columnNameOf, declStringArray, pgExtensionCheck, tableNameOf } from \"../shared.ts\";\n\n// ─── Options ──────────────────────────────────────────────────────\n\nexport type TrgmOptions = {\n /**\n * Similarity threshold for `similarTo` (default 0.3). Also applied to the\n * session's `pg_trgm.similarity_threshold` on `$connect()` so raw `%`\n * queries agree with the operator.\n */\n readonly threshold?: number;\n};\n\n/** Operand of `similarTo`: the text, or text plus a per-use threshold. */\nexport type SimilarToOperand = string | { readonly text: string; readonly threshold?: number };\n\nconst DEFAULT_THRESHOLD = 0.3;\nconst VERSION = \"2.0.0-alpha.0\";\n\n// ─── Artifacts ────────────────────────────────────────────────────\n\nfunction trgmArtifacts(ctx: ExtensionSchemaContext): readonly ExtensionArtifact[] {\n if (ctx.dialect !== \"postgres\") {\n throw unsupportedCapability({\n dialect: ctx.dialect,\n feature: \"the trgm extension (pg_trgm)\",\n hint: \"pg_trgm exists on postgres/pglite only\",\n });\n }\n const dialect = getSqlDialect({ dialect: ctx.dialect });\n const quote = (name: string): string => dialect.quoteIdent({ name });\n\n const artifacts: ExtensionArtifact[] = [\n {\n id: \"trgm:extension\",\n owns: { kind: \"postgresExtension\", name: \"pg_trgm\" },\n create: [\"CREATE EXTENSION IF NOT EXISTS pg_trgm;\"],\n drop: [\"DROP EXTENSION IF EXISTS pg_trgm;\"],\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 table = tableNameOf({ model });\n for (const field of declStringArray(declaration, \"fields\") ?? []) {\n const column = columnNameOf({ model, field });\n const indexName = `${table}_${column}_trgm_idx`;\n artifacts.push({\n id: `trgm:${modelName}:${field}`,\n owns: { kind: \"index\", table, name: indexName },\n create: [\n `CREATE INDEX IF NOT EXISTS ${quote(indexName)} ON ${quote(table)} USING gin (${quote(column)} gin_trgm_ops);`,\n ],\n drop: [`DROP INDEX IF EXISTS ${quote(indexName)};`],\n });\n }\n }\n return artifacts;\n}\n\n// ─── Factory ──────────────────────────────────────────────────────\n\n/**\n * pg_trgm fuzzy matching.\n *\n * @example vibeorm.config.ts / VibeClient\n * extensions: [trgm({ threshold: 0.3 })]\n *\n * @example schema\n * // @@trgm([name]) → gin_trgm_ops index on \"name\"\n * await db.user.findMany({ where: { name: { similarTo: \"jon\" } } });\n */\nexport function trgm(options: TrgmOptions = {}): VibeExtension {\n const threshold = options.threshold ?? DEFAULT_THRESHOLD;\n\n return defineExtension({\n name: \"trgm\",\n version: VERSION,\n options: { ...options },\n requires: { postgresql: { extension: \"pg_trgm\" }, pglite: { extension: \"pg_trgm\" } },\n\n schema: {\n modelAttributes: [\n {\n name: \"trgm\",\n parse: ({ args, fieldNames }) => {\n const fields = args.fields ?? [];\n if (fields.length === 0) {\n return { error: \"@@trgm requires a field list, e.g. @@trgm([name])\" };\n }\n const missing = fields.find((name) => !fieldNames.includes(name));\n if (missing !== undefined) {\n return { error: `@@trgm references unknown field \"${missing}\"` };\n }\n return { declaration: { fields } };\n },\n },\n ],\n indexKinds: [\"gin_trgm\"],\n },\n\n migrate: { artifacts: trgmArtifacts },\n\n sql: {\n whereOperators: [\n {\n name: \"similarTo\",\n compile: ({ column, operand, dialect, options: extOptions }) => {\n if (dialect !== \"postgres\") {\n throw unsupportedCapability({\n dialect,\n feature: 'the \"similarTo\" operator (pg_trgm)',\n hint: \"pg_trgm exists on postgres/pglite only\",\n });\n }\n const text =\n typeof operand === \"string\"\n ? operand\n : typeof operand === \"object\" && operand !== null && \"text\" in operand\n ? (operand as { text: unknown }).text\n : undefined;\n if (typeof text !== \"string\") {\n return { error: 'expects a string or { text, threshold? }' };\n }\n const perUse =\n typeof operand === \"object\" && operand !== null && \"threshold\" in operand\n ? (operand as { threshold: unknown }).threshold\n : undefined;\n const configured = extOptions[\"threshold\"];\n const sessionThreshold = typeof configured === \"number\" ? configured : DEFAULT_THRESHOLD;\n const effective = typeof perUse === \"number\" ? perUse : sessionThreshold;\n // PG-1 (SQL review round 2): `%` prunes at the CONNECTION's\n // pg_trgm.similarity_threshold. onConnect SETs the configured\n // value only on connections `$connect` touches — POOLED\n // connections keep the server default 0.3 (live-proven dropped\n // rows: configured 0.2, similarity 0.273, `%` pruned at 0.3).\n // Gate the index route on effective >= max(configured, 0.3): a\n // connection whose actual GUC is LOWER than effective only widens\n // the prefilter (safe). A postgresql.conf value RAISED above the\n // effective threshold can still prune — the doctor's\n // similarity_threshold check surfaces exactly that (documented in\n // docs/dialect-notes.md).\n if (effective >= Math.max(sessionThreshold, DEFAULT_THRESHOLD)) {\n // M8: `%` walks the gin_trgm_ops index; similarity() enforces\n // the exact bound.\n return {\n fragment: {\n parts: [\n \"(\",\n { ident: column },\n \" % \",\n { param: text },\n \" AND similarity(\",\n { ident: column },\n \", \",\n { param: text },\n \") >= \",\n { param: effective },\n \")\",\n ],\n },\n };\n }\n // Effective threshold below the `%` gate floor (max(configured,\n // 0.3)): on a pooled connection the GUC may still be 0.3, so `%`\n // would prune matches the caller asked for — honest full scan.\n return {\n fragment: {\n parts: [\n \"similarity(\",\n { ident: column },\n \", \",\n { param: text },\n \") >= \",\n { param: effective },\n ],\n },\n };\n },\n },\n ],\n },\n\n runtime: {\n onConnect: async ({ execute, dialect, options: extOptions }) => {\n // Session default for raw `%` queries; the operator itself never\n // depends on it. Only meaningful when explicitly configured.\n if (dialect !== \"postgres\" || typeof extOptions[\"threshold\"] !== \"number\") return;\n await execute({\n text: `SET pg_trgm.similarity_threshold = ${Number(threshold)}`,\n });\n },\n },\n\n doctor: async ({ execute, dialect }) => {\n if (dialect !== \"postgres\") {\n return { checks: [{ name: \"trgm dialect\", ok: false, note: \"postgres/pglite only\" }] };\n }\n const checks = [await pgExtensionCheck({ execute, name: \"pg_trgm\" })];\n // PG-1: the `%` route assumes the connection's GUC is ≤ the gate floor\n // (max(configured, 0.3)). A postgresql.conf value raised ABOVE it makes\n // `%` prune rows the similarity() bound would keep — surface it here.\n // current_setting(…, true) returns NULL instead of ERRORING when the\n // pg_trgm library is not session-loaded yet (SHOW errors there — a\n // false alarm on healthy setups, closure item 3); NULL means no value\n // is configured anywhere, so the 0.3 default (≤ floor) applies.\n const floor = Math.max(threshold, DEFAULT_THRESHOLD);\n try {\n const rows = await execute({\n text: `SELECT current_setting('pg_trgm.similarity_threshold', true) AS \"threshold\"`,\n });\n const raw = rows[0]?.[\"threshold\"];\n if (raw === undefined || raw === null || raw === \"\") {\n checks.push({\n name: \"trgm similarity_threshold\",\n ok: true,\n note: `not set in this session — the 0.3 default applies (≤ gate floor ${floor})`,\n });\n return { checks };\n }\n const server = Number(raw);\n checks.push(\n Number.isNaN(server)\n ? { name: \"trgm similarity_threshold\", ok: false, note: \"could not read pg_trgm.similarity_threshold\" }\n : server <= floor\n ? { name: \"trgm similarity_threshold\", ok: true, note: `server ${server} ≤ gate floor ${floor}` }\n : {\n name: \"trgm similarity_threshold\",\n ok: false,\n note: `server pg_trgm.similarity_threshold=${server} exceeds the %-gate floor ${floor} — indexed similarTo queries can drop rows; lower the GUC or raise the extension threshold`,\n },\n );\n } catch {\n checks.push({\n name: \"trgm similarity_threshold\",\n ok: false,\n note: \"reading pg_trgm.similarity_threshold failed\",\n });\n }\n return { checks };\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": "0582C70BE4C3EDA564756E2164756E21",
|
|
10
|
+
"names": []
|
|
11
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `vector` — pgvector similarity search (extensions.md §5).
|
|
3
|
+
*
|
|
4
|
+
* Schema: `embedding Vector(1536)` (field type) + `@@vector([embedding],
|
|
5
|
+
* metric: "cosine", index: "hnsw")` (index declaration). Client surface is a
|
|
6
|
+
* namespace (D9): `db.doc.vector.nearest({ by, to, take })` and
|
|
7
|
+
* `db.doc.vector.set({ where, by, to })`. Postgres AND pglite (pglite bundles
|
|
8
|
+
* pgvector); sqlite/mysql → VIBE_UNSUPPORTED_CAPABILITY at generate time.
|
|
9
|
+
*/
|
|
10
|
+
import type { VibeExtension } from "@vibeorm/schema";
|
|
11
|
+
import type { ExtensionMountContext } from "@vibeorm/runtime";
|
|
12
|
+
export type VectorOptions = Record<string, never>;
|
|
13
|
+
export type VectorMetric = "cosine" | "l2" | "ip";
|
|
14
|
+
/** One nearest-neighbour hit. */
|
|
15
|
+
export type VectorHit<T> = {
|
|
16
|
+
readonly row: T;
|
|
17
|
+
/** Distance under the query's metric — smaller is closer (ip is negated inner product). */
|
|
18
|
+
readonly distance: number;
|
|
19
|
+
};
|
|
20
|
+
type VectorColumnDecl = {
|
|
21
|
+
readonly column: string;
|
|
22
|
+
readonly dims: number;
|
|
23
|
+
};
|
|
24
|
+
/** Baked per-model config the generator writes into the client. */
|
|
25
|
+
export type VectorNamespaceConfig = {
|
|
26
|
+
readonly table: string;
|
|
27
|
+
/** Field name → column + dims. */
|
|
28
|
+
readonly columns: Readonly<Record<string, VectorColumnDecl>>;
|
|
29
|
+
/** Default metric from the schema declaration. */
|
|
30
|
+
readonly metric: VectorMetric;
|
|
31
|
+
};
|
|
32
|
+
export type VectorNearestArgs = {
|
|
33
|
+
readonly by: string;
|
|
34
|
+
readonly to: readonly number[];
|
|
35
|
+
readonly metric?: VectorMetric;
|
|
36
|
+
readonly take?: number;
|
|
37
|
+
readonly where?: Record<string, unknown>;
|
|
38
|
+
readonly maxDistance?: number;
|
|
39
|
+
};
|
|
40
|
+
export type VectorSetArgs = {
|
|
41
|
+
readonly where: Record<string, unknown>;
|
|
42
|
+
readonly by: string;
|
|
43
|
+
readonly to: readonly number[];
|
|
44
|
+
};
|
|
45
|
+
export type VectorNamespace = {
|
|
46
|
+
readonly nearest: (args: VectorNearestArgs) => Promise<VectorHit<Record<string, unknown>>[]>;
|
|
47
|
+
readonly set: (args: VectorSetArgs) => Promise<number>;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* The generated client's glue target: bakes per-model config at generate time,
|
|
51
|
+
* mounts as the `vector` namespace at runtime (D9).
|
|
52
|
+
*/
|
|
53
|
+
export declare function createVectorNamespace(config: VectorNamespaceConfig): (ctx: ExtensionMountContext) => VectorNamespace;
|
|
54
|
+
/**
|
|
55
|
+
* pgvector similarity search.
|
|
56
|
+
*
|
|
57
|
+
* @example schema
|
|
58
|
+
* // embedding Vector(1536)
|
|
59
|
+
* // @@vector([embedding], metric: "cosine", index: "hnsw")
|
|
60
|
+
* const near = await db.doc.vector.nearest({ by: "embedding", to: queryVec, take: 10 });
|
|
61
|
+
*/
|
|
62
|
+
export declare function vector(options?: VectorOptions): VibeExtension;
|
|
63
|
+
export {};
|
|
64
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/vector/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,KAAK,EAGV,aAAa,EACd,MAAM,iBAAiB,CAAC;AAEzB,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AAY9D,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;AAElD,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,IAAI,GAAG,IAAI,CAAC;AAElD,iCAAiC;AACjC,MAAM,MAAM,SAAS,CAAC,CAAC,IAAI;IACzB,QAAQ,CAAC,GAAG,EAAE,CAAC,CAAC;IAChB,2FAA2F;IAC3F,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B,CAAC;AAsBF,KAAK,gBAAgB,GAAG;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAqF3E,mEAAmE;AACnE,MAAM,MAAM,qBAAqB,GAAG;IAClC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,kCAAkC;IAClC,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAC;IAC7D,kDAAkD;IAClD,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;CAC/B,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,EAAE,SAAS,MAAM,EAAE,CAAC;IAC/B,QAAQ,CAAC,MAAM,CAAC,EAAE,YAAY,CAAC;IAC/B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACzC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACxC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,EAAE,SAAS,MAAM,EAAE,CAAC;CAChC,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,iBAAiB,KAAK,OAAO,CAAC,SAAS,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC;IAC7F,QAAQ,CAAC,GAAG,EAAE,CAAC,IAAI,EAAE,aAAa,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;CACxD,CAAC;AAgCF;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,qBAAqB,GAC5B,CAAC,GAAG,EAAE,qBAAqB,KAAK,eAAe,CAiDjD;AAID;;;;;;;GAOG;AACH,wBAAgB,MAAM,CAAC,OAAO,GAAE,aAAkB,GAAG,aAAa,CAoHjE"}
|