@kernl-sdk/pg 0.1.11 → 0.1.12
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/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-check-types.log +36 -0
- package/CHANGELOG.md +32 -0
- package/README.md +124 -0
- package/dist/__tests__/integration.test.js +2 -2
- package/dist/__tests__/memory-integration.test.d.ts +2 -0
- package/dist/__tests__/memory-integration.test.d.ts.map +1 -0
- package/dist/__tests__/memory-integration.test.js +287 -0
- package/dist/__tests__/memory.test.d.ts +2 -0
- package/dist/__tests__/memory.test.d.ts.map +1 -0
- package/dist/__tests__/memory.test.js +357 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -3
- package/dist/memory/sql.d.ts +30 -0
- package/dist/memory/sql.d.ts.map +1 -0
- package/dist/memory/sql.js +100 -0
- package/dist/memory/store.d.ts +41 -0
- package/dist/memory/store.d.ts.map +1 -0
- package/dist/memory/store.js +114 -0
- package/dist/migrations.d.ts +1 -1
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +9 -3
- package/dist/pgvector/__tests__/handle.test.d.ts +2 -0
- package/dist/pgvector/__tests__/handle.test.d.ts.map +1 -0
- package/dist/pgvector/__tests__/handle.test.js +277 -0
- package/dist/pgvector/__tests__/hit.test.d.ts +2 -0
- package/dist/pgvector/__tests__/hit.test.d.ts.map +1 -0
- package/dist/pgvector/__tests__/hit.test.js +134 -0
- package/dist/pgvector/__tests__/integration/document.integration.test.d.ts +7 -0
- package/dist/pgvector/__tests__/integration/document.integration.test.d.ts.map +1 -0
- package/dist/pgvector/__tests__/integration/document.integration.test.js +587 -0
- package/dist/pgvector/__tests__/integration/edge.integration.test.d.ts +8 -0
- package/dist/pgvector/__tests__/integration/edge.integration.test.d.ts.map +1 -0
- package/dist/pgvector/__tests__/integration/edge.integration.test.js +663 -0
- package/dist/pgvector/__tests__/integration/filters.integration.test.d.ts +8 -0
- package/dist/pgvector/__tests__/integration/filters.integration.test.d.ts.map +1 -0
- package/dist/pgvector/__tests__/integration/filters.integration.test.js +609 -0
- package/dist/pgvector/__tests__/integration/lifecycle.integration.test.d.ts +8 -0
- package/dist/pgvector/__tests__/integration/lifecycle.integration.test.d.ts.map +1 -0
- package/dist/pgvector/__tests__/integration/lifecycle.integration.test.js +449 -0
- package/dist/pgvector/__tests__/integration/query.integration.test.d.ts +8 -0
- package/dist/pgvector/__tests__/integration/query.integration.test.d.ts.map +1 -0
- package/dist/pgvector/__tests__/integration/query.integration.test.js +544 -0
- package/dist/pgvector/__tests__/search.test.d.ts +2 -0
- package/dist/pgvector/__tests__/search.test.d.ts.map +1 -0
- package/dist/pgvector/__tests__/search.test.js +279 -0
- package/dist/pgvector/handle.d.ts +60 -0
- package/dist/pgvector/handle.d.ts.map +1 -0
- package/dist/pgvector/handle.js +213 -0
- package/dist/pgvector/hit.d.ts +10 -0
- package/dist/pgvector/hit.d.ts.map +1 -0
- package/dist/pgvector/hit.js +44 -0
- package/dist/pgvector/index.d.ts +7 -0
- package/dist/pgvector/index.d.ts.map +1 -0
- package/dist/pgvector/index.js +5 -0
- package/dist/pgvector/search.d.ts +60 -0
- package/dist/pgvector/search.d.ts.map +1 -0
- package/dist/pgvector/search.js +227 -0
- package/dist/pgvector/sql/__tests__/limit.test.d.ts +2 -0
- package/dist/pgvector/sql/__tests__/limit.test.d.ts.map +1 -0
- package/dist/pgvector/sql/__tests__/limit.test.js +161 -0
- package/dist/pgvector/sql/__tests__/order.test.d.ts +2 -0
- package/dist/pgvector/sql/__tests__/order.test.d.ts.map +1 -0
- package/dist/pgvector/sql/__tests__/order.test.js +218 -0
- package/dist/pgvector/sql/__tests__/query.test.d.ts +2 -0
- package/dist/pgvector/sql/__tests__/query.test.d.ts.map +1 -0
- package/dist/pgvector/sql/__tests__/query.test.js +392 -0
- package/dist/pgvector/sql/__tests__/select.test.d.ts +2 -0
- package/dist/pgvector/sql/__tests__/select.test.d.ts.map +1 -0
- package/dist/pgvector/sql/__tests__/select.test.js +293 -0
- package/dist/pgvector/sql/__tests__/where.test.d.ts +2 -0
- package/dist/pgvector/sql/__tests__/where.test.d.ts.map +1 -0
- package/dist/pgvector/sql/__tests__/where.test.js +488 -0
- package/dist/pgvector/sql/index.d.ts +7 -0
- package/dist/pgvector/sql/index.d.ts.map +1 -0
- package/dist/pgvector/sql/index.js +6 -0
- package/dist/pgvector/sql/limit.d.ts +8 -0
- package/dist/pgvector/sql/limit.d.ts.map +1 -0
- package/dist/pgvector/sql/limit.js +20 -0
- package/dist/pgvector/sql/order.d.ts +9 -0
- package/dist/pgvector/sql/order.d.ts.map +1 -0
- package/dist/pgvector/sql/order.js +47 -0
- package/dist/pgvector/sql/query.d.ts +46 -0
- package/dist/pgvector/sql/query.d.ts.map +1 -0
- package/dist/pgvector/sql/query.js +54 -0
- package/dist/pgvector/sql/schema.d.ts +16 -0
- package/dist/pgvector/sql/schema.d.ts.map +1 -0
- package/dist/pgvector/sql/schema.js +47 -0
- package/dist/pgvector/sql/select.d.ts +11 -0
- package/dist/pgvector/sql/select.d.ts.map +1 -0
- package/dist/pgvector/sql/select.js +87 -0
- package/dist/pgvector/sql/where.d.ts +8 -0
- package/dist/pgvector/sql/where.d.ts.map +1 -0
- package/dist/pgvector/sql/where.js +137 -0
- package/dist/pgvector/types.d.ts +20 -0
- package/dist/pgvector/types.d.ts.map +1 -0
- package/dist/pgvector/types.js +1 -0
- package/dist/pgvector/utils.d.ts +18 -0
- package/dist/pgvector/utils.d.ts.map +1 -0
- package/dist/pgvector/utils.js +22 -0
- package/dist/postgres.d.ts +19 -26
- package/dist/postgres.d.ts.map +1 -1
- package/dist/postgres.js +15 -27
- package/dist/storage.d.ts +48 -0
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +32 -9
- package/dist/thread/sql.d.ts +38 -0
- package/dist/thread/sql.d.ts.map +1 -0
- package/dist/thread/sql.js +112 -0
- package/dist/thread/store.d.ts +2 -2
- package/dist/thread/store.d.ts.map +1 -1
- package/dist/thread/store.js +32 -102
- package/package.json +7 -4
- package/src/__tests__/integration.test.ts +15 -17
- package/src/__tests__/memory-integration.test.ts +355 -0
- package/src/__tests__/memory.test.ts +428 -0
- package/src/index.ts +19 -3
- package/src/memory/sql.ts +141 -0
- package/src/memory/store.ts +166 -0
- package/src/migrations.ts +13 -3
- package/src/pgvector/README.md +50 -0
- package/src/pgvector/__tests__/handle.test.ts +335 -0
- package/src/pgvector/__tests__/hit.test.ts +165 -0
- package/src/pgvector/__tests__/integration/document.integration.test.ts +717 -0
- package/src/pgvector/__tests__/integration/edge.integration.test.ts +835 -0
- package/src/pgvector/__tests__/integration/filters.integration.test.ts +721 -0
- package/src/pgvector/__tests__/integration/lifecycle.integration.test.ts +570 -0
- package/src/pgvector/__tests__/integration/query.integration.test.ts +667 -0
- package/src/pgvector/__tests__/search.test.ts +366 -0
- package/src/pgvector/handle.ts +285 -0
- package/src/pgvector/hit.ts +56 -0
- package/src/pgvector/index.ts +7 -0
- package/src/pgvector/search.ts +330 -0
- package/src/pgvector/sql/__tests__/limit.test.ts +180 -0
- package/src/pgvector/sql/__tests__/order.test.ts +248 -0
- package/src/pgvector/sql/__tests__/query.test.ts +548 -0
- package/src/pgvector/sql/__tests__/select.test.ts +367 -0
- package/src/pgvector/sql/__tests__/where.test.ts +554 -0
- package/src/pgvector/sql/index.ts +14 -0
- package/src/pgvector/sql/limit.ts +29 -0
- package/src/pgvector/sql/order.ts +55 -0
- package/src/pgvector/sql/query.ts +112 -0
- package/src/pgvector/sql/schema.ts +61 -0
- package/src/pgvector/sql/select.ts +100 -0
- package/src/pgvector/sql/where.ts +152 -0
- package/src/pgvector/types.ts +21 -0
- package/src/pgvector/utils.ts +24 -0
- package/src/postgres.ts +31 -33
- package/src/storage.ts +77 -9
- package/src/thread/sql.ts +159 -0
- package/src/thread/store.ts +40 -127
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,717 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Document operations integration tests for pgvector.
|
|
3
|
+
*
|
|
4
|
+
* Tests upsert, patch, and delete operations against real PostgreSQL.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect, beforeAll, afterAll, beforeEach } from "vitest";
|
|
8
|
+
import { Pool } from "pg";
|
|
9
|
+
|
|
10
|
+
import { PGSearchIndex } from "../../search";
|
|
11
|
+
import type { IndexHandle } from "@kernl-sdk/retrieval";
|
|
12
|
+
|
|
13
|
+
const TEST_DB_URL = process.env.KERNL_PG_TEST_URL;
|
|
14
|
+
const SCHEMA = "kernl_document_integration_test";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Test document type.
|
|
18
|
+
*/
|
|
19
|
+
interface TestDoc {
|
|
20
|
+
id: string;
|
|
21
|
+
title: string;
|
|
22
|
+
content: string;
|
|
23
|
+
views: number;
|
|
24
|
+
published: boolean;
|
|
25
|
+
embedding: number[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe.sequential("pgvector document operations integration tests", () => {
|
|
29
|
+
if (!TEST_DB_URL) {
|
|
30
|
+
it.skip("requires KERNL_PG_TEST_URL environment variable", () => {});
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let pool: Pool;
|
|
35
|
+
let pgvec: PGSearchIndex;
|
|
36
|
+
let handle: IndexHandle<TestDoc>;
|
|
37
|
+
const testIndexId = "document_test_docs";
|
|
38
|
+
|
|
39
|
+
beforeAll(async () => {
|
|
40
|
+
pool = new Pool({ connectionString: TEST_DB_URL });
|
|
41
|
+
|
|
42
|
+
await pool.query(`CREATE EXTENSION IF NOT EXISTS vector`);
|
|
43
|
+
await pool.query(`DROP SCHEMA IF EXISTS "${SCHEMA}" CASCADE`);
|
|
44
|
+
await pool.query(`CREATE SCHEMA "${SCHEMA}"`);
|
|
45
|
+
|
|
46
|
+
pgvec = new PGSearchIndex({ pool });
|
|
47
|
+
|
|
48
|
+
await pgvec.createIndex({
|
|
49
|
+
id: testIndexId,
|
|
50
|
+
schema: {
|
|
51
|
+
id: { type: "string", pk: true },
|
|
52
|
+
title: { type: "string" },
|
|
53
|
+
content: { type: "string" },
|
|
54
|
+
views: { type: "int" },
|
|
55
|
+
published: { type: "boolean" },
|
|
56
|
+
embedding: { type: "vector", dimensions: 4, similarity: "cosine" },
|
|
57
|
+
},
|
|
58
|
+
providerOptions: { schema: SCHEMA },
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
handle = pgvec.index<TestDoc>(testIndexId);
|
|
62
|
+
}, 30000);
|
|
63
|
+
|
|
64
|
+
afterAll(async () => {
|
|
65
|
+
await pool.query(`DROP SCHEMA IF EXISTS "${SCHEMA}" CASCADE`);
|
|
66
|
+
await pool.end();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
beforeEach(async () => {
|
|
70
|
+
// Clear table between tests
|
|
71
|
+
await pool.query(`DELETE FROM "${SCHEMA}"."${testIndexId}"`);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// ============================================================
|
|
75
|
+
// UPSERT OPERATIONS
|
|
76
|
+
// ============================================================
|
|
77
|
+
|
|
78
|
+
describe("upsert", () => {
|
|
79
|
+
it("inserts a single document", async () => {
|
|
80
|
+
const result = await handle.upsert({
|
|
81
|
+
id: "doc-1",
|
|
82
|
+
title: "Hello World",
|
|
83
|
+
content: "First document",
|
|
84
|
+
views: 100,
|
|
85
|
+
published: true,
|
|
86
|
+
embedding: [0.1, 0.2, 0.3, 0.4],
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
expect(result.count).toBe(1);
|
|
90
|
+
expect(result.inserted).toBe(1);
|
|
91
|
+
expect(result.updated).toBe(0);
|
|
92
|
+
|
|
93
|
+
// Verify document was inserted
|
|
94
|
+
const hits = await handle.query({
|
|
95
|
+
query: [{ embedding: [0.1, 0.2, 0.3, 0.4] }],
|
|
96
|
+
topK: 1,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
expect(hits).toHaveLength(1);
|
|
100
|
+
expect(hits[0].id).toBe("doc-1");
|
|
101
|
+
expect(hits[0].document?.title).toBe("Hello World");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("inserts multiple documents", async () => {
|
|
105
|
+
const result = await handle.upsert([
|
|
106
|
+
{
|
|
107
|
+
id: "doc-1",
|
|
108
|
+
title: "First",
|
|
109
|
+
content: "Content 1",
|
|
110
|
+
views: 10,
|
|
111
|
+
published: true,
|
|
112
|
+
embedding: [1, 0, 0, 0],
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
id: "doc-2",
|
|
116
|
+
title: "Second",
|
|
117
|
+
content: "Content 2",
|
|
118
|
+
views: 20,
|
|
119
|
+
published: false,
|
|
120
|
+
embedding: [0, 1, 0, 0],
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
id: "doc-3",
|
|
124
|
+
title: "Third",
|
|
125
|
+
content: "Content 3",
|
|
126
|
+
views: 30,
|
|
127
|
+
published: true,
|
|
128
|
+
embedding: [0, 0, 1, 0],
|
|
129
|
+
},
|
|
130
|
+
]);
|
|
131
|
+
|
|
132
|
+
expect(result.count).toBe(3);
|
|
133
|
+
expect(result.inserted).toBe(3);
|
|
134
|
+
expect(result.updated).toBe(0);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("updates existing document on conflict", async () => {
|
|
138
|
+
// Insert
|
|
139
|
+
await handle.upsert({
|
|
140
|
+
id: "doc-1",
|
|
141
|
+
title: "Original Title",
|
|
142
|
+
content: "Original content",
|
|
143
|
+
views: 50,
|
|
144
|
+
published: false,
|
|
145
|
+
embedding: [0.1, 0.1, 0.1, 0.1],
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Upsert (update)
|
|
149
|
+
const result = await handle.upsert({
|
|
150
|
+
id: "doc-1",
|
|
151
|
+
title: "Updated Title",
|
|
152
|
+
content: "Updated content",
|
|
153
|
+
views: 100,
|
|
154
|
+
published: true,
|
|
155
|
+
embedding: [0.2, 0.2, 0.2, 0.2],
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
expect(result.count).toBe(1);
|
|
159
|
+
expect(result.inserted).toBe(0);
|
|
160
|
+
expect(result.updated).toBe(1);
|
|
161
|
+
|
|
162
|
+
// Verify update
|
|
163
|
+
const hits = await handle.query({
|
|
164
|
+
filter: { id: "doc-1" },
|
|
165
|
+
topK: 1,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
expect(hits[0].document?.title).toBe("Updated Title");
|
|
169
|
+
expect(hits[0].document?.views).toBe(100);
|
|
170
|
+
expect(hits[0].document?.published).toBe(true);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("handles mixed insert and update", async () => {
|
|
174
|
+
// Insert first doc
|
|
175
|
+
await handle.upsert({
|
|
176
|
+
id: "doc-1",
|
|
177
|
+
title: "Existing",
|
|
178
|
+
content: "Content",
|
|
179
|
+
views: 10,
|
|
180
|
+
published: true,
|
|
181
|
+
embedding: [0.1, 0.1, 0.1, 0.1],
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Upsert batch with existing and new docs
|
|
185
|
+
const result = await handle.upsert([
|
|
186
|
+
{
|
|
187
|
+
id: "doc-1",
|
|
188
|
+
title: "Updated",
|
|
189
|
+
content: "Updated content",
|
|
190
|
+
views: 20,
|
|
191
|
+
published: false,
|
|
192
|
+
embedding: [0.2, 0.2, 0.2, 0.2],
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
id: "doc-2",
|
|
196
|
+
title: "New Doc",
|
|
197
|
+
content: "New content",
|
|
198
|
+
views: 30,
|
|
199
|
+
published: true,
|
|
200
|
+
embedding: [0.3, 0.3, 0.3, 0.3],
|
|
201
|
+
},
|
|
202
|
+
]);
|
|
203
|
+
|
|
204
|
+
expect(result.count).toBe(2);
|
|
205
|
+
expect(result.inserted).toBe(1);
|
|
206
|
+
expect(result.updated).toBe(1);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("handles empty array", async () => {
|
|
210
|
+
const result = await handle.upsert([]);
|
|
211
|
+
|
|
212
|
+
expect(result.count).toBe(0);
|
|
213
|
+
expect(result.inserted).toBe(0);
|
|
214
|
+
expect(result.updated).toBe(0);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("throws on missing primary key", async () => {
|
|
218
|
+
await expect(
|
|
219
|
+
handle.upsert({
|
|
220
|
+
title: "No ID",
|
|
221
|
+
content: "Content",
|
|
222
|
+
views: 0,
|
|
223
|
+
published: false,
|
|
224
|
+
embedding: [0, 0, 0, 0],
|
|
225
|
+
} as any),
|
|
226
|
+
).rejects.toThrow();
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("handles large batch upsert", async () => {
|
|
230
|
+
const docs = Array.from({ length: 100 }, (_, i) => ({
|
|
231
|
+
id: `batch-doc-${i}`,
|
|
232
|
+
title: `Document ${i}`,
|
|
233
|
+
content: `Content for document ${i}`,
|
|
234
|
+
views: i * 10,
|
|
235
|
+
published: i % 2 === 0,
|
|
236
|
+
embedding: [i / 100, (100 - i) / 100, 0.5, 0.5],
|
|
237
|
+
}));
|
|
238
|
+
|
|
239
|
+
const result = await handle.upsert(docs);
|
|
240
|
+
|
|
241
|
+
expect(result.count).toBe(100);
|
|
242
|
+
expect(result.inserted).toBe(100);
|
|
243
|
+
|
|
244
|
+
// Verify count
|
|
245
|
+
const hits = await handle.query({
|
|
246
|
+
query: [{ embedding: [0.5, 0.5, 0.5, 0.5] }],
|
|
247
|
+
topK: 1000,
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
expect(hits).toHaveLength(100);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("preserves vector embeddings correctly", async () => {
|
|
254
|
+
const embedding = [0.123456789, -0.987654321, 0.5, -0.5];
|
|
255
|
+
|
|
256
|
+
await handle.upsert({
|
|
257
|
+
id: "vec-test",
|
|
258
|
+
title: "Vector Test",
|
|
259
|
+
content: "Content",
|
|
260
|
+
views: 0,
|
|
261
|
+
published: true,
|
|
262
|
+
embedding,
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
const hits = await handle.query({
|
|
266
|
+
filter: { id: "vec-test" },
|
|
267
|
+
topK: 1,
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// Vector values should be preserved (within floating point precision)
|
|
271
|
+
const stored = hits[0].document?.embedding;
|
|
272
|
+
expect(stored).toHaveLength(4);
|
|
273
|
+
expect(stored?.[0]).toBeCloseTo(0.123456789, 5);
|
|
274
|
+
expect(stored?.[1]).toBeCloseTo(-0.987654321, 5);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it("handles null values in optional fields", async () => {
|
|
278
|
+
// Create index with nullable field
|
|
279
|
+
await pgvec.createIndex({
|
|
280
|
+
id: "nullable_test",
|
|
281
|
+
schema: {
|
|
282
|
+
id: { type: "string", pk: true },
|
|
283
|
+
title: { type: "string" },
|
|
284
|
+
subtitle: { type: "string" },
|
|
285
|
+
embedding: { type: "vector", dimensions: 4 },
|
|
286
|
+
},
|
|
287
|
+
providerOptions: { schema: SCHEMA },
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
const nullHandle = pgvec.index("nullable_test");
|
|
291
|
+
|
|
292
|
+
await nullHandle.upsert({
|
|
293
|
+
id: "doc-null",
|
|
294
|
+
title: "Has Title",
|
|
295
|
+
subtitle: null,
|
|
296
|
+
embedding: [0.1, 0.1, 0.1, 0.1],
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
const hits = await nullHandle.query({
|
|
300
|
+
filter: { id: "doc-null" },
|
|
301
|
+
topK: 1,
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
expect(hits[0].document?.title).toBe("Has Title");
|
|
305
|
+
expect(hits[0].document?.subtitle).toBeNull();
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
// ============================================================
|
|
310
|
+
// PATCH OPERATIONS
|
|
311
|
+
// ============================================================
|
|
312
|
+
|
|
313
|
+
describe("patch", () => {
|
|
314
|
+
beforeEach(async () => {
|
|
315
|
+
// Insert test documents
|
|
316
|
+
await handle.upsert([
|
|
317
|
+
{
|
|
318
|
+
id: "patch-1",
|
|
319
|
+
title: "Original Title 1",
|
|
320
|
+
content: "Original Content 1",
|
|
321
|
+
views: 100,
|
|
322
|
+
published: true,
|
|
323
|
+
embedding: [0.1, 0.1, 0.1, 0.1],
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
id: "patch-2",
|
|
327
|
+
title: "Original Title 2",
|
|
328
|
+
content: "Original Content 2",
|
|
329
|
+
views: 200,
|
|
330
|
+
published: false,
|
|
331
|
+
embedding: [0.2, 0.2, 0.2, 0.2],
|
|
332
|
+
},
|
|
333
|
+
]);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it("patches single field", async () => {
|
|
337
|
+
const result = await handle.patch({
|
|
338
|
+
id: "patch-1",
|
|
339
|
+
title: "Updated Title",
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
expect(result.count).toBe(1);
|
|
343
|
+
|
|
344
|
+
const hits = await handle.query({
|
|
345
|
+
filter: { id: "patch-1" },
|
|
346
|
+
topK: 1,
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
expect(hits[0].document?.title).toBe("Updated Title");
|
|
350
|
+
expect(hits[0].document?.content).toBe("Original Content 1"); // unchanged
|
|
351
|
+
expect(hits[0].document?.views).toBe(100); // unchanged
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it("patches multiple fields", async () => {
|
|
355
|
+
const result = await handle.patch({
|
|
356
|
+
id: "patch-1",
|
|
357
|
+
title: "New Title",
|
|
358
|
+
views: 999,
|
|
359
|
+
published: false,
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
expect(result.count).toBe(1);
|
|
363
|
+
|
|
364
|
+
const hits = await handle.query({
|
|
365
|
+
filter: { id: "patch-1" },
|
|
366
|
+
topK: 1,
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
expect(hits[0].document?.title).toBe("New Title");
|
|
370
|
+
expect(hits[0].document?.views).toBe(999);
|
|
371
|
+
expect(hits[0].document?.published).toBe(false);
|
|
372
|
+
expect(hits[0].document?.content).toBe("Original Content 1"); // unchanged
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it("patches vector embedding", async () => {
|
|
376
|
+
const newEmbedding = [0.9, 0.9, 0.9, 0.9];
|
|
377
|
+
|
|
378
|
+
await handle.patch({
|
|
379
|
+
id: "patch-1",
|
|
380
|
+
embedding: newEmbedding,
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
const hits = await handle.query({
|
|
384
|
+
filter: { id: "patch-1" },
|
|
385
|
+
topK: 1,
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
const stored = hits[0].document?.embedding;
|
|
389
|
+
expect(stored?.[0]).toBeCloseTo(0.9, 5);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it("patches multiple documents", async () => {
|
|
393
|
+
const result = await handle.patch([
|
|
394
|
+
{ id: "patch-1", views: 1000 },
|
|
395
|
+
{ id: "patch-2", views: 2000 },
|
|
396
|
+
]);
|
|
397
|
+
|
|
398
|
+
expect(result.count).toBe(2);
|
|
399
|
+
|
|
400
|
+
const hits = await handle.query({
|
|
401
|
+
query: [{ embedding: [0.5, 0.5, 0.5, 0.5] }],
|
|
402
|
+
topK: 10,
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
const doc1 = hits.find((h) => h.id === "patch-1");
|
|
406
|
+
const doc2 = hits.find((h) => h.id === "patch-2");
|
|
407
|
+
|
|
408
|
+
expect(doc1?.document?.views).toBe(1000);
|
|
409
|
+
expect(doc2?.document?.views).toBe(2000);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it("sets field to null", async () => {
|
|
413
|
+
// First, create an index with nullable field and insert
|
|
414
|
+
await pgvec.createIndex({
|
|
415
|
+
id: "patch_null_test",
|
|
416
|
+
schema: {
|
|
417
|
+
id: { type: "string", pk: true },
|
|
418
|
+
title: { type: "string" },
|
|
419
|
+
subtitle: { type: "string" },
|
|
420
|
+
embedding: { type: "vector", dimensions: 4 },
|
|
421
|
+
},
|
|
422
|
+
providerOptions: { schema: SCHEMA },
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
const nullHandle = pgvec.index("patch_null_test");
|
|
426
|
+
|
|
427
|
+
await nullHandle.upsert({
|
|
428
|
+
id: "doc-1",
|
|
429
|
+
title: "Title",
|
|
430
|
+
subtitle: "Has Subtitle",
|
|
431
|
+
embedding: [0.1, 0.1, 0.1, 0.1],
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
await nullHandle.patch({
|
|
435
|
+
id: "doc-1",
|
|
436
|
+
subtitle: null,
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
const hits = await nullHandle.query({
|
|
440
|
+
filter: { id: "doc-1" },
|
|
441
|
+
topK: 1,
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
expect(hits[0].document?.subtitle).toBeNull();
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
it("handles empty patch array", async () => {
|
|
448
|
+
const result = await handle.patch([]);
|
|
449
|
+
expect(result.count).toBe(0);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it("returns count 0 for non-existent document", async () => {
|
|
453
|
+
const result = await handle.patch({
|
|
454
|
+
id: "nonexistent",
|
|
455
|
+
title: "Won't Work",
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
expect(result.count).toBe(0);
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
it("ignores undefined fields", async () => {
|
|
462
|
+
await handle.patch({
|
|
463
|
+
id: "patch-1",
|
|
464
|
+
title: "Updated",
|
|
465
|
+
content: undefined,
|
|
466
|
+
} as any);
|
|
467
|
+
|
|
468
|
+
const hits = await handle.query({
|
|
469
|
+
filter: { id: "patch-1" },
|
|
470
|
+
topK: 1,
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
expect(hits[0].document?.title).toBe("Updated");
|
|
474
|
+
expect(hits[0].document?.content).toBe("Original Content 1"); // unchanged
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
it("skips patch with no fields to update", async () => {
|
|
478
|
+
const result = await handle.patch({
|
|
479
|
+
id: "patch-1",
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
// No fields to update, so count should be 0
|
|
483
|
+
expect(result.count).toBe(0);
|
|
484
|
+
});
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
// ============================================================
|
|
488
|
+
// DELETE OPERATIONS
|
|
489
|
+
// ============================================================
|
|
490
|
+
|
|
491
|
+
describe("delete", () => {
|
|
492
|
+
beforeEach(async () => {
|
|
493
|
+
// Insert test documents
|
|
494
|
+
await handle.upsert([
|
|
495
|
+
{
|
|
496
|
+
id: "del-1",
|
|
497
|
+
title: "Delete Me 1",
|
|
498
|
+
content: "Content",
|
|
499
|
+
views: 10,
|
|
500
|
+
published: true,
|
|
501
|
+
embedding: [0.1, 0.1, 0.1, 0.1],
|
|
502
|
+
},
|
|
503
|
+
{
|
|
504
|
+
id: "del-2",
|
|
505
|
+
title: "Delete Me 2",
|
|
506
|
+
content: "Content",
|
|
507
|
+
views: 20,
|
|
508
|
+
published: false,
|
|
509
|
+
embedding: [0.2, 0.2, 0.2, 0.2],
|
|
510
|
+
},
|
|
511
|
+
{
|
|
512
|
+
id: "del-3",
|
|
513
|
+
title: "Delete Me 3",
|
|
514
|
+
content: "Content",
|
|
515
|
+
views: 30,
|
|
516
|
+
published: true,
|
|
517
|
+
embedding: [0.3, 0.3, 0.3, 0.3],
|
|
518
|
+
},
|
|
519
|
+
]);
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
it("deletes single document by id", async () => {
|
|
523
|
+
const result = await handle.delete("del-1");
|
|
524
|
+
|
|
525
|
+
expect(result.count).toBe(1);
|
|
526
|
+
|
|
527
|
+
// Verify deletion
|
|
528
|
+
const hits = await handle.query({
|
|
529
|
+
filter: { id: "del-1" },
|
|
530
|
+
topK: 1,
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
expect(hits).toHaveLength(0);
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
it("deletes multiple documents by id array", async () => {
|
|
537
|
+
const result = await handle.delete(["del-1", "del-2"]);
|
|
538
|
+
|
|
539
|
+
expect(result.count).toBe(2);
|
|
540
|
+
|
|
541
|
+
// Verify only del-3 remains
|
|
542
|
+
const hits = await handle.query({
|
|
543
|
+
query: [{ embedding: [0.5, 0.5, 0.5, 0.5] }],
|
|
544
|
+
topK: 10,
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
expect(hits).toHaveLength(1);
|
|
548
|
+
expect(hits[0].id).toBe("del-3");
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
it("returns count 0 for non-existent id", async () => {
|
|
552
|
+
const result = await handle.delete("nonexistent");
|
|
553
|
+
expect(result.count).toBe(0);
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
it("handles mixed existing and non-existent ids", async () => {
|
|
557
|
+
const result = await handle.delete(["del-1", "nonexistent", "del-2"]);
|
|
558
|
+
|
|
559
|
+
// Should only count the ones that existed
|
|
560
|
+
expect(result.count).toBe(2);
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
it("handles empty array", async () => {
|
|
564
|
+
const result = await handle.delete([]);
|
|
565
|
+
expect(result.count).toBe(0);
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
it("deletes all documents", async () => {
|
|
569
|
+
await handle.delete(["del-1", "del-2", "del-3"]);
|
|
570
|
+
|
|
571
|
+
const hits = await handle.query({
|
|
572
|
+
query: [{ embedding: [0.5, 0.5, 0.5, 0.5] }],
|
|
573
|
+
topK: 10,
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
expect(hits).toHaveLength(0);
|
|
577
|
+
});
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
// ============================================================
|
|
581
|
+
// DATA TYPE HANDLING
|
|
582
|
+
// ============================================================
|
|
583
|
+
|
|
584
|
+
describe("data type handling", () => {
|
|
585
|
+
it("handles integer zero correctly", async () => {
|
|
586
|
+
await handle.upsert({
|
|
587
|
+
id: "zero-test",
|
|
588
|
+
title: "Zero Views",
|
|
589
|
+
content: "Content",
|
|
590
|
+
views: 0,
|
|
591
|
+
published: false,
|
|
592
|
+
embedding: [0.1, 0.1, 0.1, 0.1],
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
const hits = await handle.query({
|
|
596
|
+
filter: { views: 0 },
|
|
597
|
+
topK: 1,
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
expect(hits).toHaveLength(1);
|
|
601
|
+
expect(hits[0].document?.views).toBe(0);
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
it("handles negative integers", async () => {
|
|
605
|
+
await handle.upsert({
|
|
606
|
+
id: "negative-test",
|
|
607
|
+
title: "Negative Views",
|
|
608
|
+
content: "Content",
|
|
609
|
+
views: -50,
|
|
610
|
+
published: false,
|
|
611
|
+
embedding: [0.1, 0.1, 0.1, 0.1],
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
const hits = await handle.query({
|
|
615
|
+
filter: { views: -50 },
|
|
616
|
+
topK: 1,
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
expect(hits).toHaveLength(1);
|
|
620
|
+
expect(hits[0].document?.views).toBe(-50);
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
it("handles boolean true and false", async () => {
|
|
624
|
+
await handle.upsert([
|
|
625
|
+
{
|
|
626
|
+
id: "bool-true",
|
|
627
|
+
title: "Published",
|
|
628
|
+
content: "Content",
|
|
629
|
+
views: 0,
|
|
630
|
+
published: true,
|
|
631
|
+
embedding: [0.1, 0.1, 0.1, 0.1],
|
|
632
|
+
},
|
|
633
|
+
{
|
|
634
|
+
id: "bool-false",
|
|
635
|
+
title: "Draft",
|
|
636
|
+
content: "Content",
|
|
637
|
+
views: 0,
|
|
638
|
+
published: false,
|
|
639
|
+
embedding: [0.2, 0.2, 0.2, 0.2],
|
|
640
|
+
},
|
|
641
|
+
]);
|
|
642
|
+
|
|
643
|
+
const trueHits = await handle.query({
|
|
644
|
+
filter: { published: true },
|
|
645
|
+
topK: 10,
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
const falseHits = await handle.query({
|
|
649
|
+
filter: { published: false },
|
|
650
|
+
topK: 10,
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
expect(trueHits).toHaveLength(1);
|
|
654
|
+
expect(trueHits[0].id).toBe("bool-true");
|
|
655
|
+
expect(falseHits).toHaveLength(1);
|
|
656
|
+
expect(falseHits[0].id).toBe("bool-false");
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
it("handles empty string", async () => {
|
|
660
|
+
await handle.upsert({
|
|
661
|
+
id: "empty-string",
|
|
662
|
+
title: "",
|
|
663
|
+
content: "Has content",
|
|
664
|
+
views: 0,
|
|
665
|
+
published: false,
|
|
666
|
+
embedding: [0.1, 0.1, 0.1, 0.1],
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
const hits = await handle.query({
|
|
670
|
+
filter: { title: "" },
|
|
671
|
+
topK: 1,
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
expect(hits).toHaveLength(1);
|
|
675
|
+
expect(hits[0].document?.title).toBe("");
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
it("handles unicode strings", async () => {
|
|
679
|
+
await handle.upsert({
|
|
680
|
+
id: "unicode-test",
|
|
681
|
+
title: "Hello 世界 🌍",
|
|
682
|
+
content: "Привет мир",
|
|
683
|
+
views: 0,
|
|
684
|
+
published: false,
|
|
685
|
+
embedding: [0.1, 0.1, 0.1, 0.1],
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
const hits = await handle.query({
|
|
689
|
+
filter: { id: "unicode-test" },
|
|
690
|
+
topK: 1,
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
expect(hits[0].document?.title).toBe("Hello 世界 🌍");
|
|
694
|
+
expect(hits[0].document?.content).toBe("Привет мир");
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
it("handles very long strings", async () => {
|
|
698
|
+
const longString = "a".repeat(10000);
|
|
699
|
+
|
|
700
|
+
await handle.upsert({
|
|
701
|
+
id: "long-string",
|
|
702
|
+
title: "Short",
|
|
703
|
+
content: longString,
|
|
704
|
+
views: 0,
|
|
705
|
+
published: false,
|
|
706
|
+
embedding: [0.1, 0.1, 0.1, 0.1],
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
const hits = await handle.query({
|
|
710
|
+
filter: { id: "long-string" },
|
|
711
|
+
topK: 1,
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
expect(hits[0].document?.content).toBe(longString);
|
|
715
|
+
});
|
|
716
|
+
});
|
|
717
|
+
});
|