agentfootprint 9.2.0 → 9.3.0
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/adapters/memory/pgVector.js +731 -0
- package/dist/adapters/memory/pgVector.js.map +1 -0
- package/dist/adapters/memory/s3Vectors.js +628 -0
- package/dist/adapters/memory/s3Vectors.js.map +1 -0
- package/dist/adapters/memory/sqliteVector.js +16 -79
- package/dist/adapters/memory/sqliteVector.js.map +1 -1
- package/dist/embedders/index.js +240 -76
- package/dist/embedders/index.js.map +1 -1
- package/dist/esm/adapters/memory/pgVector.d.ts +243 -0
- package/dist/esm/adapters/memory/pgVector.js +726 -0
- package/dist/esm/adapters/memory/pgVector.js.map +1 -0
- package/dist/esm/adapters/memory/s3Vectors.d.ts +208 -0
- package/dist/esm/adapters/memory/s3Vectors.js +624 -0
- package/dist/esm/adapters/memory/s3Vectors.js.map +1 -0
- package/dist/esm/adapters/memory/sqliteVector.d.ts +5 -27
- package/dist/esm/adapters/memory/sqliteVector.js +10 -73
- package/dist/esm/adapters/memory/sqliteVector.js.map +1 -1
- package/dist/esm/embedders/index.d.ts +99 -19
- package/dist/esm/embedders/index.js +240 -76
- package/dist/esm/embedders/index.js.map +1 -1
- package/dist/esm/lib/embedderMismatch.d.ts +67 -0
- package/dist/esm/lib/embedderMismatch.js +96 -0
- package/dist/esm/lib/embedderMismatch.js.map +1 -0
- package/dist/esm/lib/rag/defineRAG.d.ts +28 -6
- package/dist/esm/lib/rag/defineRAG.js +39 -5
- package/dist/esm/lib/rag/defineRAG.js.map +1 -1
- package/dist/esm/memory/define.js +31 -1
- package/dist/esm/memory/define.js.map +1 -1
- package/dist/esm/memory/define.types.d.ts +13 -2
- package/dist/esm/memory/define.types.js.map +1 -1
- package/dist/esm/memory/embedding/loadRelevant.d.ts +10 -2
- package/dist/esm/memory/embedding/loadRelevant.js +36 -18
- package/dist/esm/memory/embedding/loadRelevant.js.map +1 -1
- package/dist/esm/memory/pipeline/semantic.d.ts +13 -2
- package/dist/esm/memory/pipeline/semantic.js +11 -5
- package/dist/esm/memory/pipeline/semantic.js.map +1 -1
- package/dist/esm/memory/store/capability.d.ts +25 -6
- package/dist/esm/memory/store/capability.js +68 -3
- package/dist/esm/memory/store/capability.js.map +1 -1
- package/dist/esm/memory/store/index.d.ts +1 -0
- package/dist/esm/memory/store/index.js +4 -0
- package/dist/esm/memory/store/index.js.map +1 -1
- package/dist/esm/memory/store/types.d.ts +40 -0
- package/dist/esm/memory-providers.d.ts +7 -4
- package/dist/esm/memory-providers.js +18 -4
- package/dist/esm/memory-providers.js.map +1 -1
- package/dist/lib/embedderMismatch.js +103 -0
- package/dist/lib/embedderMismatch.js.map +1 -0
- package/dist/lib/rag/defineRAG.js +39 -5
- package/dist/lib/rag/defineRAG.js.map +1 -1
- package/dist/memory/define.js +31 -1
- package/dist/memory/define.js.map +1 -1
- package/dist/memory/define.types.js.map +1 -1
- package/dist/memory/embedding/loadRelevant.js +36 -18
- package/dist/memory/embedding/loadRelevant.js.map +1 -1
- package/dist/memory/pipeline/semantic.js +11 -5
- package/dist/memory/pipeline/semantic.js.map +1 -1
- package/dist/memory/store/capability.js +70 -4
- package/dist/memory/store/capability.js.map +1 -1
- package/dist/memory/store/index.js +7 -1
- package/dist/memory/store/index.js.map +1 -1
- package/dist/memory-providers.js +22 -5
- package/dist/memory-providers.js.map +1 -1
- package/dist/types/adapters/memory/pgVector.d.ts +244 -0
- package/dist/types/adapters/memory/pgVector.d.ts.map +1 -0
- package/dist/types/adapters/memory/s3Vectors.d.ts +209 -0
- package/dist/types/adapters/memory/s3Vectors.d.ts.map +1 -0
- package/dist/types/adapters/memory/sqliteVector.d.ts +5 -27
- package/dist/types/adapters/memory/sqliteVector.d.ts.map +1 -1
- package/dist/types/embedders/index.d.ts +99 -19
- package/dist/types/embedders/index.d.ts.map +1 -1
- package/dist/types/lib/embedderMismatch.d.ts +68 -0
- package/dist/types/lib/embedderMismatch.d.ts.map +1 -0
- package/dist/types/lib/rag/defineRAG.d.ts +28 -6
- package/dist/types/lib/rag/defineRAG.d.ts.map +1 -1
- package/dist/types/memory/define.d.ts.map +1 -1
- package/dist/types/memory/define.types.d.ts +13 -2
- package/dist/types/memory/define.types.d.ts.map +1 -1
- package/dist/types/memory/embedding/loadRelevant.d.ts +10 -2
- package/dist/types/memory/embedding/loadRelevant.d.ts.map +1 -1
- package/dist/types/memory/pipeline/semantic.d.ts +13 -2
- package/dist/types/memory/pipeline/semantic.d.ts.map +1 -1
- package/dist/types/memory/store/capability.d.ts +25 -6
- package/dist/types/memory/store/capability.d.ts.map +1 -1
- package/dist/types/memory/store/index.d.ts +1 -0
- package/dist/types/memory/store/index.d.ts.map +1 -1
- package/dist/types/memory/store/types.d.ts +40 -0
- package/dist/types/memory/store/types.d.ts.map +1 -1
- package/dist/types/memory-providers.d.ts +7 -4
- package/dist/types/memory-providers.d.ts.map +1 -1
- package/package.json +9 -1
|
@@ -0,0 +1,731 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* adapters/memory/pgVector — the target our own port has named since 2.x.
|
|
4
|
+
*
|
|
5
|
+
* `MemoryStore`'s docstring has listed the backends it was designed for from
|
|
6
|
+
* the beginning — *"Every storage backend (InMemory, Redis, DynamoDB,
|
|
7
|
+
* **Postgres**, Bedrock AgentCore) implements this interface"* — and named the
|
|
8
|
+
* query, twice, in the places an implementer would look: *"**Postgres**:
|
|
9
|
+
* multi-row INSERT … ON CONFLICT DO UPDATE"* for `putMany`, *"**pgvector**:
|
|
10
|
+
* `ORDER BY embedding <=> query LIMIT k`"* for `search`. Every one of those
|
|
11
|
+
* sentences was true about the design and false about the shipped package.
|
|
12
|
+
* This is the adapter that makes them the same sentence.
|
|
13
|
+
*
|
|
14
|
+
* It matters more than one row in a table of backends. Postgres is the database
|
|
15
|
+
* most teams already run, `pgvector` is an extension away, and a corpus that
|
|
16
|
+
* lives beside the application's own data inherits its backups, its failover,
|
|
17
|
+
* its access control and its migrations. `sqliteVectorStore` is one machine;
|
|
18
|
+
* `s3VectorsStore` is serverless and eventually consistent; this is the one for
|
|
19
|
+
* a fleet that already has a database.
|
|
20
|
+
*
|
|
21
|
+
* ── The table, and why this does not create it ──────────────────────────────
|
|
22
|
+
* A `vector(N)` column fixes N at creation, and N is a fact about your
|
|
23
|
+
* embedder. Creating the table implicitly would pick that number — and the
|
|
24
|
+
* index type, and the operator class — on your behalf, in a migration you never
|
|
25
|
+
* reviewed, in a database whose DDL is usually somebody's job. So the schema is
|
|
26
|
+
* yours to run, and this store REFUSES a table that is missing rather than
|
|
27
|
+
* silently answering "no matches" against nothing:
|
|
28
|
+
*
|
|
29
|
+
* ```sql
|
|
30
|
+
* CREATE EXTENSION IF NOT EXISTS vector;
|
|
31
|
+
*
|
|
32
|
+
* -- 1024 = your embedder's dimensions. bedrockEmbedder() default: 1024.
|
|
33
|
+
* -- openaiEmbedder() default: 1536. staticEmbedder(): 256.
|
|
34
|
+
* CREATE TABLE af_vectors (
|
|
35
|
+
* namespace TEXT NOT NULL,
|
|
36
|
+
* id TEXT NOT NULL,
|
|
37
|
+
* value JSONB NOT NULL,
|
|
38
|
+
* metadata JSONB,
|
|
39
|
+
* embedding vector(1024),
|
|
40
|
+
* embedder_fp TEXT,
|
|
41
|
+
* version INTEGER NOT NULL,
|
|
42
|
+
* created_at BIGINT NOT NULL,
|
|
43
|
+
* updated_at BIGINT NOT NULL,
|
|
44
|
+
* last_accessed_at BIGINT NOT NULL,
|
|
45
|
+
* access_count INTEGER NOT NULL,
|
|
46
|
+
* ttl BIGINT,
|
|
47
|
+
* tier TEXT,
|
|
48
|
+
* source JSONB,
|
|
49
|
+
* embedding_model TEXT,
|
|
50
|
+
* PRIMARY KEY (namespace, id)
|
|
51
|
+
* );
|
|
52
|
+
*
|
|
53
|
+
* -- Cosine, because that is the score this port reports and every threshold
|
|
54
|
+
* -- in this library is calibrated on. Match the operator class to the metric.
|
|
55
|
+
* CREATE INDEX af_vectors_hnsw ON af_vectors
|
|
56
|
+
* USING hnsw (embedding vector_cosine_ops);
|
|
57
|
+
* CREATE INDEX af_vectors_ns ON af_vectors (namespace);
|
|
58
|
+
*
|
|
59
|
+
* -- Recognition (`seen`/`recordSignature`), usefulness feedback, and the
|
|
60
|
+
* -- per-namespace embedder fingerprint. Small, and each one is a port method
|
|
61
|
+
* -- that would otherwise have to be refused.
|
|
62
|
+
* CREATE TABLE af_signatures (
|
|
63
|
+
* namespace TEXT NOT NULL, signature TEXT NOT NULL,
|
|
64
|
+
* PRIMARY KEY (namespace, signature)
|
|
65
|
+
* );
|
|
66
|
+
* CREATE TABLE af_feedback (
|
|
67
|
+
* namespace TEXT NOT NULL, id TEXT NOT NULL,
|
|
68
|
+
* total DOUBLE PRECISION NOT NULL, count INTEGER NOT NULL,
|
|
69
|
+
* PRIMARY KEY (namespace, id)
|
|
70
|
+
* );
|
|
71
|
+
* CREATE TABLE af_index_meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);
|
|
72
|
+
* ```
|
|
73
|
+
*
|
|
74
|
+
* Every table and column name above is an OPTION with that value as its
|
|
75
|
+
* default, so this drops into a schema that already has naming conventions —
|
|
76
|
+
* see {@link PgVectorStoreOptions}. Identifiers are validated and quoted; a
|
|
77
|
+
* name that is not a plain SQL identifier is refused rather than interpolated.
|
|
78
|
+
*
|
|
79
|
+
* ── Cosine, and only cosine ─────────────────────────────────────────────────
|
|
80
|
+
* `search` is `1 - (embedding <=> $query::vector)` — pgvector's cosine
|
|
81
|
+
* DISTANCE, converted to the cosine SIMILARITY the port reports and
|
|
82
|
+
* `defineRAG`'s 0.7 default is calibrated on. `<->` (L2) and `<#>` (inner
|
|
83
|
+
* product) are deliberately not options: their ranges are not that range, and a
|
|
84
|
+
* number that reads like a cosine and is not one is the failure mode the whole
|
|
85
|
+
* fingerprint machinery exists to prevent. Build the HNSW index with
|
|
86
|
+
* `vector_cosine_ops` so the operator and the index agree — with the wrong
|
|
87
|
+
* operator class the query still returns the right answer, slowly, by scanning.
|
|
88
|
+
*
|
|
89
|
+
* ── One statement at a time, on purpose ─────────────────────────────────────
|
|
90
|
+
* The client here is anything with `query()` — a `pg.Pool` is the expected one,
|
|
91
|
+
* and a Pool hands each `query()` its own connection. `BEGIN` on one and the
|
|
92
|
+
* next statement on another is a transaction that silently is not one, so this
|
|
93
|
+
* adapter never writes multi-statement transactions. Everything that must be
|
|
94
|
+
* atomic is ONE statement: `putMany` is one multi-row upsert, `putIfVersion` is
|
|
95
|
+
* one conditional upsert, `forget` is one statement with CTEs across all four
|
|
96
|
+
* tables. That is a constraint that made the code better.
|
|
97
|
+
*
|
|
98
|
+
* ── Lazy peer dependency ────────────────────────────────────────────────────
|
|
99
|
+
* `pg` is an OPTIONAL peer dependency, required at construction time. Importing
|
|
100
|
+
* `agentfootprint/memory` costs nothing for consumers who never build one of
|
|
101
|
+
* these. Pass `client` to reuse the pool your app already has — which is the
|
|
102
|
+
* recommended shape, because a second pool to the same database is a second set
|
|
103
|
+
* of connections nobody counted.
|
|
104
|
+
*/
|
|
105
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
106
|
+
exports.pgVectorStore = exports.PgVectorSchemaError = void 0;
|
|
107
|
+
const index_js_1 = require("../../memory/identity/index.js");
|
|
108
|
+
const lazyRequire_js_1 = require("../../lib/lazyRequire.js");
|
|
109
|
+
const embedderMismatch_js_1 = require("../../lib/embedderMismatch.js");
|
|
110
|
+
/**
|
|
111
|
+
* Raised when the database is reachable but its schema is not this store's.
|
|
112
|
+
*
|
|
113
|
+
* The law `sqliteVectorStore` states for a file, one backend over: **an
|
|
114
|
+
* unreadable index and an empty one are different facts, and only one of them
|
|
115
|
+
* is safe to answer with "no matches".** A store that treated a missing table
|
|
116
|
+
* as an empty corpus would answer every question from the model's own weights
|
|
117
|
+
* and log nothing.
|
|
118
|
+
*/
|
|
119
|
+
class PgVectorSchemaError extends Error {
|
|
120
|
+
code = 'ERR_PGVECTOR_SCHEMA';
|
|
121
|
+
/** The schema-qualified table that could not be used. */
|
|
122
|
+
table;
|
|
123
|
+
/** Columns this store needs and did not find. Empty when the table is absent entirely. */
|
|
124
|
+
missingColumns;
|
|
125
|
+
constructor(table, missingColumns, detail) {
|
|
126
|
+
super(`[memory] the pgVectorStore table ${table} cannot be used: ${detail}\n` +
|
|
127
|
+
` A missing table and an empty corpus are different facts, and only one of them is ` +
|
|
128
|
+
`safe to answer with "no matches" — so this refuses rather than quietly answering ` +
|
|
129
|
+
`every question from the model alone.\n` +
|
|
130
|
+
` Fix: run the CREATE TABLE from the pgVectorStore docs (it also needs ` +
|
|
131
|
+
`CREATE EXTENSION vector), or point \`table\`/\`columns\` at the schema you have.`);
|
|
132
|
+
this.name = 'PgVectorSchemaError';
|
|
133
|
+
this.table = table;
|
|
134
|
+
this.missingColumns = missingColumns;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
exports.PgVectorSchemaError = PgVectorSchemaError;
|
|
138
|
+
const DEFAULT_COLUMNS = {
|
|
139
|
+
namespace: 'namespace',
|
|
140
|
+
id: 'id',
|
|
141
|
+
value: 'value',
|
|
142
|
+
metadata: 'metadata',
|
|
143
|
+
embedding: 'embedding',
|
|
144
|
+
embedderFp: 'embedder_fp',
|
|
145
|
+
version: 'version',
|
|
146
|
+
createdAt: 'created_at',
|
|
147
|
+
updatedAt: 'updated_at',
|
|
148
|
+
lastAccessedAt: 'last_accessed_at',
|
|
149
|
+
accessCount: 'access_count',
|
|
150
|
+
ttl: 'ttl',
|
|
151
|
+
tier: 'tier',
|
|
152
|
+
source: 'source',
|
|
153
|
+
embeddingModel: 'embedding_model',
|
|
154
|
+
};
|
|
155
|
+
/**
|
|
156
|
+
* Open a `MemoryStore` over an existing Postgres + pgvector table.
|
|
157
|
+
*
|
|
158
|
+
* @throws when `pg` is absent and no `client` was passed.
|
|
159
|
+
* @throws PgVectorSchemaError on the first call, when the table or a column it
|
|
160
|
+
* needs is not there.
|
|
161
|
+
* @throws EmbedderMismatchError from `put`/`putMany`/`search` when a vector
|
|
162
|
+
* meets a namespace built by a different embedder.
|
|
163
|
+
*
|
|
164
|
+
* @example A corpus beside the application's own data
|
|
165
|
+
* ```ts
|
|
166
|
+
* import { Pool } from 'pg';
|
|
167
|
+
* import { defineRAG, indexDocuments } from 'agentfootprint';
|
|
168
|
+
* import { pgVectorStore } from 'agentfootprint/memory';
|
|
169
|
+
* import { openaiEmbedder } from 'agentfootprint/providers';
|
|
170
|
+
*
|
|
171
|
+
* const store = pgVectorStore({ client: new Pool({ connectionString: process.env.DATABASE_URL }) });
|
|
172
|
+
* const embedder = openaiEmbedder();
|
|
173
|
+
*
|
|
174
|
+
* await indexDocuments(store, embedder, docs, { embedderId: embedder.id });
|
|
175
|
+
*
|
|
176
|
+
* const agent = Agent.create({ provider })
|
|
177
|
+
* .rag(defineRAG({ id: 'docs', store, embedder, embedderId: embedder.id }))
|
|
178
|
+
* .build();
|
|
179
|
+
* ```
|
|
180
|
+
*/
|
|
181
|
+
function pgVectorStore(options = {}) {
|
|
182
|
+
const schema = ident(options.schema ?? 'public', 'schema');
|
|
183
|
+
const vectorsTable = qualified(schema, ident(options.table ?? 'af_vectors', 'table'));
|
|
184
|
+
const signaturesTable = qualified(schema, ident(options.signaturesTable ?? 'af_signatures', 'signaturesTable'));
|
|
185
|
+
const feedbackTable = qualified(schema, ident(options.feedbackTable ?? 'af_feedback', 'feedbackTable'));
|
|
186
|
+
const metaTable = qualified(schema, ident(options.metaTable ?? 'af_index_meta', 'metaTable'));
|
|
187
|
+
const rawTableName = options.table ?? 'af_vectors';
|
|
188
|
+
const rawSchemaName = options.schema ?? 'public';
|
|
189
|
+
const col = resolveColumns(options.columns);
|
|
190
|
+
const batchSize = Math.max(1, Math.floor(options.batchSize ?? 500));
|
|
191
|
+
/** The 15 columns a row is written with and read back through, in one order. */
|
|
192
|
+
const WRITE_COLUMNS = [
|
|
193
|
+
col.namespace,
|
|
194
|
+
col.id,
|
|
195
|
+
col.value,
|
|
196
|
+
col.metadata,
|
|
197
|
+
col.embedding,
|
|
198
|
+
col.embedderFp,
|
|
199
|
+
col.version,
|
|
200
|
+
col.createdAt,
|
|
201
|
+
col.updatedAt,
|
|
202
|
+
col.lastAccessedAt,
|
|
203
|
+
col.accessCount,
|
|
204
|
+
col.ttl,
|
|
205
|
+
col.tier,
|
|
206
|
+
col.source,
|
|
207
|
+
col.embeddingModel,
|
|
208
|
+
];
|
|
209
|
+
const READ_COLUMNS = [
|
|
210
|
+
col.id,
|
|
211
|
+
col.value,
|
|
212
|
+
col.metadata,
|
|
213
|
+
col.version,
|
|
214
|
+
col.createdAt,
|
|
215
|
+
col.updatedAt,
|
|
216
|
+
col.lastAccessedAt,
|
|
217
|
+
col.accessCount,
|
|
218
|
+
col.ttl,
|
|
219
|
+
col.tier,
|
|
220
|
+
col.source,
|
|
221
|
+
col.embeddingModel,
|
|
222
|
+
]
|
|
223
|
+
.map(quote)
|
|
224
|
+
.join(', ');
|
|
225
|
+
let owned = false;
|
|
226
|
+
let client;
|
|
227
|
+
const connect = () => {
|
|
228
|
+
if (client)
|
|
229
|
+
return client;
|
|
230
|
+
if (options._client) {
|
|
231
|
+
client = options._client;
|
|
232
|
+
return client;
|
|
233
|
+
}
|
|
234
|
+
if (options.client) {
|
|
235
|
+
client = options.client;
|
|
236
|
+
return client;
|
|
237
|
+
}
|
|
238
|
+
const pg = options._pg ?? loadPg();
|
|
239
|
+
if (typeof pg.Pool !== 'function') {
|
|
240
|
+
throw new Error('pgVectorStore: `pg` is installed but `Pool` was not found. Update the driver, or ' +
|
|
241
|
+
'pass `client` with a pre-built pool.');
|
|
242
|
+
}
|
|
243
|
+
if (!options.connectionString) {
|
|
244
|
+
throw new Error('pgVectorStore: pass `client` (recommended — reuse the pool your app already has) ' +
|
|
245
|
+
'or `connectionString` for this store to build one.');
|
|
246
|
+
}
|
|
247
|
+
client = new pg.Pool({ connectionString: options.connectionString });
|
|
248
|
+
owned = true;
|
|
249
|
+
return client;
|
|
250
|
+
};
|
|
251
|
+
let closed = false;
|
|
252
|
+
const open = (verb) => {
|
|
253
|
+
if (closed) {
|
|
254
|
+
throw new Error(`[memory] the pgVectorStore for ${vectorsTable} is closed, so it cannot ${verb}. ` +
|
|
255
|
+
`close() is final by design — reopening the pool behind you would hide a ` +
|
|
256
|
+
`shutdown-ordering bug rather than surface it. Build a new store if you need one ` +
|
|
257
|
+
`after closing this.`);
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
/**
|
|
261
|
+
* Verify the schema ONCE, on the first call, and refuse by name.
|
|
262
|
+
*
|
|
263
|
+
* Not at construction: building a store must not require a live connection
|
|
264
|
+
* (a module that connects on import is a module that fails at import). Not on
|
|
265
|
+
* every call either — the answer cannot change under a running process
|
|
266
|
+
* without a migration, and paying an `information_schema` read per retrieval
|
|
267
|
+
* would be a tax on the hot path for a fact checked at boot.
|
|
268
|
+
*/
|
|
269
|
+
let verified;
|
|
270
|
+
const ensureSchema = () => {
|
|
271
|
+
return (verified ??= (async () => {
|
|
272
|
+
const db = connect();
|
|
273
|
+
const result = await db.query(`SELECT column_name FROM information_schema.columns ` +
|
|
274
|
+
`WHERE table_schema = $1 AND table_name = $2`, [rawSchemaName, rawTableName]);
|
|
275
|
+
const found = new Set(result.rows.map((r) => String(r['column_name'])));
|
|
276
|
+
if (found.size === 0) {
|
|
277
|
+
throw new PgVectorSchemaError(vectorsTable, [], `no such table (looked in schema '${rawSchemaName}').`);
|
|
278
|
+
}
|
|
279
|
+
const missing = WRITE_COLUMNS.filter((name) => !found.has(name));
|
|
280
|
+
if (missing.length > 0) {
|
|
281
|
+
throw new PgVectorSchemaError(vectorsTable, missing, `it exists but is missing ${missing.join(', ')} — this is a table of that name ` +
|
|
282
|
+
`belonging to something else, or a schema from a different release.`);
|
|
283
|
+
}
|
|
284
|
+
})());
|
|
285
|
+
};
|
|
286
|
+
const query = async (text, params) => {
|
|
287
|
+
await ensureSchema();
|
|
288
|
+
const result = await connect().query(text, params);
|
|
289
|
+
return result.rows ?? [];
|
|
290
|
+
};
|
|
291
|
+
// ── Fingerprints ──────────────────────────────────────────────────
|
|
292
|
+
const fingerprints = new Map();
|
|
293
|
+
const readFingerprint = async (ns) => {
|
|
294
|
+
const held = fingerprints.get(ns);
|
|
295
|
+
if (held !== undefined)
|
|
296
|
+
return held;
|
|
297
|
+
const rows = await query(`SELECT "value" FROM ${metaTable} WHERE "key" = $1`, [`fp:${ns}`]);
|
|
298
|
+
const value = rows[0]?.['value'];
|
|
299
|
+
if (typeof value !== 'string')
|
|
300
|
+
return undefined;
|
|
301
|
+
fingerprints.set(ns, value);
|
|
302
|
+
return value;
|
|
303
|
+
};
|
|
304
|
+
const recordFingerprint = async (ns, fp) => {
|
|
305
|
+
await query(`INSERT INTO ${metaTable} ("key", "value") VALUES ($1, $2) ` +
|
|
306
|
+
`ON CONFLICT ("key") DO UPDATE SET "value" = EXCLUDED."value"`, [`fp:${ns}`, fp]);
|
|
307
|
+
fingerprints.set(ns, fp);
|
|
308
|
+
};
|
|
309
|
+
/** The `sqliteVectorStore` rule, one backend over: dimensions always decide. */
|
|
310
|
+
const reconcile = async (ns, incoming, operation) => {
|
|
311
|
+
const storedText = await readFingerprint(ns);
|
|
312
|
+
if (storedText === undefined) {
|
|
313
|
+
if (operation === 'write to')
|
|
314
|
+
await recordFingerprint(ns, (0, embedderMismatch_js_1.fingerprintText)(incoming));
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const stored = (0, embedderMismatch_js_1.parseFingerprint)(storedText);
|
|
318
|
+
const conflict = (0, embedderMismatch_js_1.fingerprintConflict)(stored, incoming);
|
|
319
|
+
if (conflict !== null) {
|
|
320
|
+
throw new embedderMismatch_js_1.EmbedderMismatchError(ns, storedText, (0, embedderMismatch_js_1.fingerprintText)(incoming), conflict, operation, 'point this store at a different table');
|
|
321
|
+
}
|
|
322
|
+
if (operation === 'write to' && stored.id === undefined && incoming.id !== undefined) {
|
|
323
|
+
await recordFingerprint(ns, (0, embedderMismatch_js_1.fingerprintText)(incoming));
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
/** The 15 bound values one row is written with, in `WRITE_COLUMNS` order. */
|
|
327
|
+
const rowValues = (ns, entry) => {
|
|
328
|
+
const embedding = entry.embedding;
|
|
329
|
+
const fp = embedding && embedding.length > 0
|
|
330
|
+
? (0, embedderMismatch_js_1.fingerprintText)({
|
|
331
|
+
...(entry.embeddingModel !== undefined && { id: entry.embeddingModel }),
|
|
332
|
+
dims: embedding.length,
|
|
333
|
+
})
|
|
334
|
+
: null;
|
|
335
|
+
return [
|
|
336
|
+
ns,
|
|
337
|
+
entry.id,
|
|
338
|
+
JSON.stringify(entry.value ?? null),
|
|
339
|
+
entry.metadata === undefined ? null : JSON.stringify(entry.metadata),
|
|
340
|
+
// pgvector reads its own literal form, `'[1,2,3]'`, cast to `vector`.
|
|
341
|
+
embedding && embedding.length > 0 ? `[${embedding.join(',')}]` : null,
|
|
342
|
+
fp,
|
|
343
|
+
entry.version,
|
|
344
|
+
entry.createdAt,
|
|
345
|
+
entry.updatedAt,
|
|
346
|
+
entry.lastAccessedAt,
|
|
347
|
+
entry.accessCount,
|
|
348
|
+
entry.ttl ?? null,
|
|
349
|
+
entry.tier ?? null,
|
|
350
|
+
entry.source === undefined ? null : JSON.stringify(entry.source),
|
|
351
|
+
entry.embeddingModel ?? null,
|
|
352
|
+
];
|
|
353
|
+
};
|
|
354
|
+
/**
|
|
355
|
+
* The SQL type of each bound value, in `WRITE_COLUMNS` order.
|
|
356
|
+
*
|
|
357
|
+
* Every parameter is cast EXPLICITLY rather than left to inference. In an
|
|
358
|
+
* `INSERT … VALUES` Postgres can infer from the target column, but in the
|
|
359
|
+
* `INSERT … SELECT` form `putIfVersion` needs it cannot, and an uncast
|
|
360
|
+
* parameter arrives as text — which fails on the first integer column with
|
|
361
|
+
* an error about a type nobody wrote. One list, both statements, no
|
|
362
|
+
* inference to reason about.
|
|
363
|
+
*/
|
|
364
|
+
const CASTS = [
|
|
365
|
+
'text', // namespace
|
|
366
|
+
'text', // id
|
|
367
|
+
'jsonb', // value
|
|
368
|
+
'jsonb', // metadata
|
|
369
|
+
'vector', // embedding
|
|
370
|
+
'text', // embedder_fp
|
|
371
|
+
'int', // version
|
|
372
|
+
'bigint', // created_at
|
|
373
|
+
'bigint', // updated_at
|
|
374
|
+
'bigint', // last_accessed_at
|
|
375
|
+
'int', // access_count
|
|
376
|
+
'bigint', // ttl
|
|
377
|
+
'text', // tier
|
|
378
|
+
'jsonb', // source
|
|
379
|
+
'text', // embedding_model
|
|
380
|
+
];
|
|
381
|
+
/** `$1::text, $2::text, $3::jsonb, …` for one row, offset into the batch. */
|
|
382
|
+
const rowParams = (offset) => CASTS.map((cast, i) => `$${offset + i + 1}::${cast}`).join(', ');
|
|
383
|
+
const upsertSet = WRITE_COLUMNS.slice(2)
|
|
384
|
+
.map((name) => `${quote(name)} = EXCLUDED.${quote(name)}`)
|
|
385
|
+
.join(', ');
|
|
386
|
+
const insertColumns = WRITE_COLUMNS.map(quote).join(', ');
|
|
387
|
+
const conflictTarget = `(${quote(col.namespace)}, ${quote(col.id)})`;
|
|
388
|
+
const store = {
|
|
389
|
+
// Vectors in, ranked vectors out — `ORDER BY embedding <=> $query` ranks
|
|
390
|
+
// the embeddings this store was handed.
|
|
391
|
+
supportsVectorSearch: true,
|
|
392
|
+
ranksBy: 'vector',
|
|
393
|
+
async fingerprintOf(identity) {
|
|
394
|
+
open('report a fingerprint');
|
|
395
|
+
return readFingerprint((0, index_js_1.identityNamespace)(identity));
|
|
396
|
+
},
|
|
397
|
+
async get(identity, id) {
|
|
398
|
+
open('read an entry');
|
|
399
|
+
const ns = (0, index_js_1.identityNamespace)(identity);
|
|
400
|
+
const rows = await query(`SELECT ${READ_COLUMNS} FROM ${vectorsTable} ` +
|
|
401
|
+
`WHERE ${quote(col.namespace)} = $1 AND ${quote(col.id)} = $2`, [ns, id]);
|
|
402
|
+
const row = rows[0];
|
|
403
|
+
if (row === undefined)
|
|
404
|
+
return null;
|
|
405
|
+
const entry = rowToEntry(row, col);
|
|
406
|
+
if (entry.ttl !== undefined && entry.ttl <= Date.now())
|
|
407
|
+
return null;
|
|
408
|
+
// Decay signals, the same side effect the port documents for `get`.
|
|
409
|
+
await query(`UPDATE ${vectorsTable} SET ${quote(col.lastAccessedAt)} = $1, ` +
|
|
410
|
+
`${quote(col.accessCount)} = ${quote(col.accessCount)} + 1 ` +
|
|
411
|
+
`WHERE ${quote(col.namespace)} = $2 AND ${quote(col.id)} = $3`, [Date.now(), ns, id]);
|
|
412
|
+
return entry;
|
|
413
|
+
},
|
|
414
|
+
async put(identity, entry) {
|
|
415
|
+
await this.putMany(identity, [entry]);
|
|
416
|
+
},
|
|
417
|
+
async putMany(identity, entries) {
|
|
418
|
+
open('write entries');
|
|
419
|
+
// The port requires an empty batch to be a no-op — callers rely on it to
|
|
420
|
+
// skip a round-trip on a turn that produced nothing.
|
|
421
|
+
if (entries.length === 0)
|
|
422
|
+
return;
|
|
423
|
+
const ns = (0, index_js_1.identityNamespace)(identity);
|
|
424
|
+
for (const entry of entries) {
|
|
425
|
+
const embedding = entry.embedding;
|
|
426
|
+
if (embedding && embedding.length > 0) {
|
|
427
|
+
await reconcile(ns, {
|
|
428
|
+
...(entry.embeddingModel !== undefined && { id: entry.embeddingModel }),
|
|
429
|
+
dims: embedding.length,
|
|
430
|
+
}, 'write to');
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
// ONE multi-row upsert per chunk — the statement the port's own docstring
|
|
434
|
+
// has named for this backend since 2.x. One statement is one implicit
|
|
435
|
+
// transaction, so a chunk lands whole or not at all: a HALF-INDEXED
|
|
436
|
+
// corpus keeps answering and quietly cannot see what did not land.
|
|
437
|
+
for (let i = 0; i < entries.length; i += batchSize) {
|
|
438
|
+
const chunk = entries.slice(i, i + batchSize);
|
|
439
|
+
const params = [];
|
|
440
|
+
const tuples = [];
|
|
441
|
+
for (const entry of chunk) {
|
|
442
|
+
tuples.push(`(${rowParams(params.length)})`);
|
|
443
|
+
params.push(...rowValues(ns, entry));
|
|
444
|
+
}
|
|
445
|
+
await query(`INSERT INTO ${vectorsTable} (${insertColumns}) VALUES ${tuples.join(', ')} ` +
|
|
446
|
+
`ON CONFLICT ${conflictTarget} DO UPDATE SET ${upsertSet}`, params);
|
|
447
|
+
}
|
|
448
|
+
},
|
|
449
|
+
async putIfVersion(identity, entry, expectedVersion) {
|
|
450
|
+
open('write an entry');
|
|
451
|
+
const ns = (0, index_js_1.identityNamespace)(identity);
|
|
452
|
+
const embedding = entry.embedding;
|
|
453
|
+
if (embedding && embedding.length > 0) {
|
|
454
|
+
await reconcile(ns, {
|
|
455
|
+
...(entry.embeddingModel !== undefined && { id: entry.embeddingModel }),
|
|
456
|
+
dims: embedding.length,
|
|
457
|
+
}, 'write to');
|
|
458
|
+
}
|
|
459
|
+
// ONE statement, so the check and the write cannot be separated by
|
|
460
|
+
// another writer — which is the entire point of a compare-and-set, and
|
|
461
|
+
// is why this is not a SELECT followed by an UPDATE (a Pool would happily
|
|
462
|
+
// run those two on different connections).
|
|
463
|
+
//
|
|
464
|
+
// The `WHERE` on the source SELECT is what makes the absent-row case
|
|
465
|
+
// correct: with `expectedVersion !== 0` and no existing row, no source
|
|
466
|
+
// row is produced, nothing is inserted, and nothing comes back. With a
|
|
467
|
+
// row present, the source row IS produced, the conflict fires, and the
|
|
468
|
+
// `DO UPDATE … WHERE version = expected` decides.
|
|
469
|
+
const params = rowValues(ns, entry);
|
|
470
|
+
const expectedParam = `$${params.length + 1}`;
|
|
471
|
+
params.push(expectedVersion);
|
|
472
|
+
const rows = await query(`INSERT INTO ${vectorsTable} AS af_target (${insertColumns}) ` +
|
|
473
|
+
`SELECT ${rowParams(0)} ` +
|
|
474
|
+
`WHERE ${expectedParam}::int = 0 OR EXISTS (SELECT 1 FROM ${vectorsTable} AS af_probe ` +
|
|
475
|
+
`WHERE af_probe.${quote(col.namespace)} = $1 AND af_probe.${quote(col.id)} = $2) ` +
|
|
476
|
+
`ON CONFLICT ${conflictTarget} DO UPDATE SET ${upsertSet} ` +
|
|
477
|
+
`WHERE af_target.${quote(col.version)} = ${expectedParam}::int ` +
|
|
478
|
+
`RETURNING af_target.${quote(col.version)}`, params);
|
|
479
|
+
if (rows.length > 0)
|
|
480
|
+
return { applied: true };
|
|
481
|
+
// Advisory, and read AFTER the failed attempt rather than inside it:
|
|
482
|
+
// the port describes `currentVersion` as what the caller decides a retry
|
|
483
|
+
// against, not as a value it may assume is still current.
|
|
484
|
+
const current = await query(`SELECT ${quote(col.version)} FROM ${vectorsTable} ` +
|
|
485
|
+
`WHERE ${quote(col.namespace)} = $1 AND ${quote(col.id)} = $2`, [ns, entry.id]);
|
|
486
|
+
const version = current[0]?.[col.version];
|
|
487
|
+
return version === undefined || version === null
|
|
488
|
+
? { applied: false }
|
|
489
|
+
: { applied: false, currentVersion: num(version) };
|
|
490
|
+
},
|
|
491
|
+
async list(identity, listOptions) {
|
|
492
|
+
open('list entries');
|
|
493
|
+
const ns = (0, index_js_1.identityNamespace)(identity);
|
|
494
|
+
const limit = Math.max(1, Math.floor(listOptions?.limit ?? 100));
|
|
495
|
+
const after = listOptions?.cursor ?? '';
|
|
496
|
+
// Keyset pagination on the primary key: stable under concurrent writes,
|
|
497
|
+
// unlike OFFSET, and the cursor is the last id rather than a position.
|
|
498
|
+
const rows = await query(`SELECT ${READ_COLUMNS} FROM ${vectorsTable} ` +
|
|
499
|
+
`WHERE ${quote(col.namespace)} = $1 AND ${quote(col.id)} > $2 ` +
|
|
500
|
+
`ORDER BY ${quote(col.id)} LIMIT $3`, [ns, after, limit + 1]);
|
|
501
|
+
const tierFilter = listOptions?.tiers ? new Set(listOptions.tiers) : undefined;
|
|
502
|
+
const page = [];
|
|
503
|
+
let cursor;
|
|
504
|
+
for (const row of rows) {
|
|
505
|
+
if (page.length === limit) {
|
|
506
|
+
// The extra row only ever exists to prove there IS a next page.
|
|
507
|
+
cursor = page[page.length - 1]?.id;
|
|
508
|
+
break;
|
|
509
|
+
}
|
|
510
|
+
const entry = rowToEntry(row, col);
|
|
511
|
+
if (entry.ttl !== undefined && entry.ttl <= Date.now())
|
|
512
|
+
continue;
|
|
513
|
+
if (tierFilter && (entry.tier === undefined || !tierFilter.has(entry.tier)))
|
|
514
|
+
continue;
|
|
515
|
+
page.push(entry);
|
|
516
|
+
}
|
|
517
|
+
return { entries: page, ...(cursor !== undefined && { cursor }) };
|
|
518
|
+
},
|
|
519
|
+
async delete(identity, id) {
|
|
520
|
+
open('delete an entry');
|
|
521
|
+
await query(`DELETE FROM ${vectorsTable} WHERE ${quote(col.namespace)} = $1 AND ${quote(col.id)} = $2`, [(0, index_js_1.identityNamespace)(identity), id]);
|
|
522
|
+
},
|
|
523
|
+
async seen(identity, signature) {
|
|
524
|
+
open('check a signature');
|
|
525
|
+
const rows = await query(`SELECT 1 FROM ${signaturesTable} WHERE "namespace" = $1 AND "signature" = $2`, [(0, index_js_1.identityNamespace)(identity), signature]);
|
|
526
|
+
return rows.length > 0;
|
|
527
|
+
},
|
|
528
|
+
async recordSignature(identity, signature) {
|
|
529
|
+
open('record a signature');
|
|
530
|
+
await query(`INSERT INTO ${signaturesTable} ("namespace", "signature") VALUES ($1, $2) ` +
|
|
531
|
+
`ON CONFLICT ("namespace", "signature") DO NOTHING`, [(0, index_js_1.identityNamespace)(identity), signature]);
|
|
532
|
+
},
|
|
533
|
+
async feedback(identity, id, usefulness) {
|
|
534
|
+
open('record feedback');
|
|
535
|
+
// Non-finite values poison the aggregate; the port says adapters must
|
|
536
|
+
// reject them, and clamp the rest.
|
|
537
|
+
if (!Number.isFinite(usefulness))
|
|
538
|
+
return;
|
|
539
|
+
const clamped = Math.max(-1, Math.min(1, usefulness));
|
|
540
|
+
await query(`INSERT INTO ${feedbackTable} AS af_fb ("namespace", "id", "total", "count") ` +
|
|
541
|
+
`VALUES ($1::text, $2::text, $3::double precision, 1) ` +
|
|
542
|
+
`ON CONFLICT ("namespace", "id") DO UPDATE SET ` +
|
|
543
|
+
`"total" = af_fb."total" + EXCLUDED."total", "count" = af_fb."count" + 1`, [(0, index_js_1.identityNamespace)(identity), id, clamped]);
|
|
544
|
+
},
|
|
545
|
+
async getFeedback(identity, id) {
|
|
546
|
+
open('read feedback');
|
|
547
|
+
const rows = await query(`SELECT "total", "count" FROM ${feedbackTable} WHERE "namespace" = $1 AND "id" = $2`, [(0, index_js_1.identityNamespace)(identity), id]);
|
|
548
|
+
const row = rows[0];
|
|
549
|
+
const count = row === undefined ? 0 : num(row['count']);
|
|
550
|
+
if (count === 0)
|
|
551
|
+
return null;
|
|
552
|
+
return { average: num(row?.['total']) / count, count };
|
|
553
|
+
},
|
|
554
|
+
async forget(identity) {
|
|
555
|
+
open('forget a namespace');
|
|
556
|
+
const ns = (0, index_js_1.identityNamespace)(identity);
|
|
557
|
+
// ONE statement across all four tables. A GDPR erasure that removed the
|
|
558
|
+
// vectors and left the signatures behind would be a deletion that is not
|
|
559
|
+
// one, and a Pool cannot be trusted to keep four statements on one
|
|
560
|
+
// connection — so they are one statement, and therefore one transaction.
|
|
561
|
+
await query(`WITH cleared_vectors AS (DELETE FROM ${vectorsTable} WHERE ${quote(col.namespace)} = $1), ` +
|
|
562
|
+
`cleared_signatures AS (DELETE FROM ${signaturesTable} WHERE "namespace" = $1), ` +
|
|
563
|
+
`cleared_feedback AS (DELETE FROM ${feedbackTable} WHERE "namespace" = $1) ` +
|
|
564
|
+
`DELETE FROM ${metaTable} WHERE "key" = $2`, [ns, `fp:${ns}`]);
|
|
565
|
+
fingerprints.delete(ns);
|
|
566
|
+
},
|
|
567
|
+
async search(identity, queryVector, searchOptions) {
|
|
568
|
+
open('search');
|
|
569
|
+
const ns = (0, index_js_1.identityNamespace)(identity);
|
|
570
|
+
const k = Math.max(1, Math.floor(searchOptions?.k ?? 10));
|
|
571
|
+
if (queryVector.length === 0)
|
|
572
|
+
return [];
|
|
573
|
+
// pgvector's cosine distance is undefined for a zero vector (it divides
|
|
574
|
+
// by the norm) and comes back as NaN, which sorts unpredictably. An
|
|
575
|
+
// unscoreable query is empty rather than arbitrary.
|
|
576
|
+
if (queryVector.every((v) => v === 0))
|
|
577
|
+
return [];
|
|
578
|
+
// Refused BEFORE the scan, on both halves of the fingerprint. A swapped
|
|
579
|
+
// embedder must not score against the old vectors — the numbers come
|
|
580
|
+
// back in the same range as real ones and no threshold separates them.
|
|
581
|
+
await reconcile(ns, {
|
|
582
|
+
...(searchOptions?.embedderId !== undefined && { id: searchOptions.embedderId }),
|
|
583
|
+
dims: queryVector.length,
|
|
584
|
+
}, 'search');
|
|
585
|
+
const params = [ns, `[${queryVector.join(',')}]`];
|
|
586
|
+
const where = [
|
|
587
|
+
`${quote(col.namespace)} = $1`,
|
|
588
|
+
`${quote(col.embedding)} IS NOT NULL`,
|
|
589
|
+
`(${quote(col.ttl)} IS NULL OR ${quote(col.ttl)} > ${bind(params, Date.now())})`,
|
|
590
|
+
];
|
|
591
|
+
const tiers = searchOptions?.tiers;
|
|
592
|
+
if (tiers && tiers.length > 0) {
|
|
593
|
+
where.push(`${quote(col.tier)} = ANY(${bind(params, [...tiers])}::text[])`);
|
|
594
|
+
}
|
|
595
|
+
if (searchOptions?.embedderId !== undefined) {
|
|
596
|
+
// A row that never named its embedder is not evidence of a different
|
|
597
|
+
// one — the same asymmetry the fingerprint rule uses.
|
|
598
|
+
const p = bind(params, searchOptions.embedderId);
|
|
599
|
+
where.push(`(${quote(col.embeddingModel)} IS NULL OR ${quote(col.embeddingModel)} = ${p})`);
|
|
600
|
+
}
|
|
601
|
+
if (searchOptions?.minScore !== undefined) {
|
|
602
|
+
const p = bind(params, searchOptions.minScore);
|
|
603
|
+
where.push(`1 - (${quote(col.embedding)} <=> $2::vector) >= ${p}`);
|
|
604
|
+
}
|
|
605
|
+
const limitParam = bind(params, k);
|
|
606
|
+
// The query the port's docstring has named for this backend since 2.x —
|
|
607
|
+
// `ORDER BY embedding <=> query LIMIT k` — with the distance turned into
|
|
608
|
+
// the cosine SIMILARITY the port reports.
|
|
609
|
+
const rows = await query(`SELECT ${READ_COLUMNS}, 1 - (${quote(col.embedding)} <=> $2::vector) AS af_score ` +
|
|
610
|
+
`FROM ${vectorsTable} WHERE ${where.join(' AND ')} ` +
|
|
611
|
+
`ORDER BY ${quote(col.embedding)} <=> $2::vector, ${quote(col.id)} ` +
|
|
612
|
+
`LIMIT ${limitParam}`, params);
|
|
613
|
+
return rows.map((row) => ({
|
|
614
|
+
entry: rowToEntry(row, col),
|
|
615
|
+
score: num(row['af_score']),
|
|
616
|
+
}));
|
|
617
|
+
},
|
|
618
|
+
async close() {
|
|
619
|
+
if (closed)
|
|
620
|
+
return;
|
|
621
|
+
closed = true;
|
|
622
|
+
if (owned)
|
|
623
|
+
await client?.end?.();
|
|
624
|
+
},
|
|
625
|
+
};
|
|
626
|
+
return store;
|
|
627
|
+
}
|
|
628
|
+
exports.pgVectorStore = pgVectorStore;
|
|
629
|
+
// ─── Identifiers ─────────────────────────────────────────────────────
|
|
630
|
+
/**
|
|
631
|
+
* Validate a caller-supplied identifier, or refuse it.
|
|
632
|
+
*
|
|
633
|
+
* Table and column names are the one part of a query that cannot be a bound
|
|
634
|
+
* parameter — they are interpolated into the SQL text. So they are checked
|
|
635
|
+
* against a plain-identifier shape and refused otherwise, rather than escaped
|
|
636
|
+
* and hoped for. A store configured from an environment variable is one
|
|
637
|
+
* `DB_TABLE` away from being an injection point, and "we quoted it" is not the
|
|
638
|
+
* answer that lets you stop thinking about it.
|
|
639
|
+
*/
|
|
640
|
+
function ident(name, option) {
|
|
641
|
+
if (!/^[A-Za-z_][A-Za-z0-9_$]*$/.test(name)) {
|
|
642
|
+
throw new TypeError(`pgVectorStore: \`${option}\` must be a plain SQL identifier — letters, digits and ` +
|
|
643
|
+
`underscores, not starting with a digit. Received ${JSON.stringify(name)}.\n` +
|
|
644
|
+
` Table and column names cannot be bound parameters; they are interpolated into the ` +
|
|
645
|
+
`statement, so anything else is refused rather than escaped and hoped for.`);
|
|
646
|
+
}
|
|
647
|
+
return name;
|
|
648
|
+
}
|
|
649
|
+
function quote(name) {
|
|
650
|
+
return `"${name}"`;
|
|
651
|
+
}
|
|
652
|
+
function qualified(schema, table) {
|
|
653
|
+
return `${quote(schema)}.${quote(table)}`;
|
|
654
|
+
}
|
|
655
|
+
function resolveColumns(overrides) {
|
|
656
|
+
const out = { ...DEFAULT_COLUMNS };
|
|
657
|
+
for (const key of Object.keys(DEFAULT_COLUMNS)) {
|
|
658
|
+
const value = overrides?.[key];
|
|
659
|
+
if (value !== undefined)
|
|
660
|
+
out[key] = ident(value, `columns.${key}`);
|
|
661
|
+
}
|
|
662
|
+
return out;
|
|
663
|
+
}
|
|
664
|
+
/** Append a value to the parameter list and return its `$n` placeholder. */
|
|
665
|
+
function bind(params, value) {
|
|
666
|
+
params.push(value);
|
|
667
|
+
return `$${params.length}`;
|
|
668
|
+
}
|
|
669
|
+
// ─── Row mapping ─────────────────────────────────────────────────────
|
|
670
|
+
/**
|
|
671
|
+
* `BIGINT` comes back from `pg` as a STRING by default (it does not fit in a
|
|
672
|
+
* JavaScript number in general), and every timestamp here is one. Coerced in
|
|
673
|
+
* one place, so a `createdAt` never reaches a consumer as `'1754640000000'`.
|
|
674
|
+
*/
|
|
675
|
+
function num(value) {
|
|
676
|
+
if (typeof value === 'number')
|
|
677
|
+
return value;
|
|
678
|
+
if (typeof value === 'string') {
|
|
679
|
+
const n = Number(value);
|
|
680
|
+
return Number.isFinite(n) ? n : 0;
|
|
681
|
+
}
|
|
682
|
+
return 0;
|
|
683
|
+
}
|
|
684
|
+
/** `JSONB` arrives already parsed from `pg`; a mock or a `TEXT` column may not. */
|
|
685
|
+
function json(value) {
|
|
686
|
+
if (typeof value !== 'string')
|
|
687
|
+
return value;
|
|
688
|
+
try {
|
|
689
|
+
return JSON.parse(value);
|
|
690
|
+
}
|
|
691
|
+
catch {
|
|
692
|
+
return value;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
function rowToEntry(row, col) {
|
|
696
|
+
const metadata = row[col.metadata];
|
|
697
|
+
const source = row[col.source];
|
|
698
|
+
const ttl = row[col.ttl];
|
|
699
|
+
const tier = row[col.tier];
|
|
700
|
+
const model = row[col.embeddingModel];
|
|
701
|
+
return {
|
|
702
|
+
id: String(row[col.id]),
|
|
703
|
+
value: json(row[col.value]),
|
|
704
|
+
...(metadata !== null &&
|
|
705
|
+
metadata !== undefined && { metadata: json(metadata) }),
|
|
706
|
+
version: num(row[col.version]),
|
|
707
|
+
createdAt: num(row[col.createdAt]),
|
|
708
|
+
updatedAt: num(row[col.updatedAt]),
|
|
709
|
+
lastAccessedAt: num(row[col.lastAccessedAt]),
|
|
710
|
+
accessCount: num(row[col.accessCount]),
|
|
711
|
+
...(ttl !== null && ttl !== undefined && { ttl: num(ttl) }),
|
|
712
|
+
...(typeof tier === 'string' && { tier: tier }),
|
|
713
|
+
...(source !== null &&
|
|
714
|
+
source !== undefined && { source: json(source) }),
|
|
715
|
+
...(typeof model === 'string' && { embeddingModel: model }),
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
// ─── Driver loading ──────────────────────────────────────────────────
|
|
719
|
+
function loadPg() {
|
|
720
|
+
try {
|
|
721
|
+
return (0, lazyRequire_js_1.lazyRequire)('pg');
|
|
722
|
+
}
|
|
723
|
+
catch {
|
|
724
|
+
throw new Error('pgVectorStore requires the `pg` peer dependency.\n' +
|
|
725
|
+
' Install: npm install pg\n' +
|
|
726
|
+
' And in the database: CREATE EXTENSION IF NOT EXISTS vector;\n' +
|
|
727
|
+
' Or pass `client` with a pre-built pool — recommended, so one pool serves the ' +
|
|
728
|
+
'whole app.');
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
//# sourceMappingURL=pgVector.js.map
|