@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.
Files changed (153) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-check-types.log +36 -0
  3. package/CHANGELOG.md +32 -0
  4. package/README.md +124 -0
  5. package/dist/__tests__/integration.test.js +2 -2
  6. package/dist/__tests__/memory-integration.test.d.ts +2 -0
  7. package/dist/__tests__/memory-integration.test.d.ts.map +1 -0
  8. package/dist/__tests__/memory-integration.test.js +287 -0
  9. package/dist/__tests__/memory.test.d.ts +2 -0
  10. package/dist/__tests__/memory.test.d.ts.map +1 -0
  11. package/dist/__tests__/memory.test.js +357 -0
  12. package/dist/index.d.ts +5 -3
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +5 -3
  15. package/dist/memory/sql.d.ts +30 -0
  16. package/dist/memory/sql.d.ts.map +1 -0
  17. package/dist/memory/sql.js +100 -0
  18. package/dist/memory/store.d.ts +41 -0
  19. package/dist/memory/store.d.ts.map +1 -0
  20. package/dist/memory/store.js +114 -0
  21. package/dist/migrations.d.ts +1 -1
  22. package/dist/migrations.d.ts.map +1 -1
  23. package/dist/migrations.js +9 -3
  24. package/dist/pgvector/__tests__/handle.test.d.ts +2 -0
  25. package/dist/pgvector/__tests__/handle.test.d.ts.map +1 -0
  26. package/dist/pgvector/__tests__/handle.test.js +277 -0
  27. package/dist/pgvector/__tests__/hit.test.d.ts +2 -0
  28. package/dist/pgvector/__tests__/hit.test.d.ts.map +1 -0
  29. package/dist/pgvector/__tests__/hit.test.js +134 -0
  30. package/dist/pgvector/__tests__/integration/document.integration.test.d.ts +7 -0
  31. package/dist/pgvector/__tests__/integration/document.integration.test.d.ts.map +1 -0
  32. package/dist/pgvector/__tests__/integration/document.integration.test.js +587 -0
  33. package/dist/pgvector/__tests__/integration/edge.integration.test.d.ts +8 -0
  34. package/dist/pgvector/__tests__/integration/edge.integration.test.d.ts.map +1 -0
  35. package/dist/pgvector/__tests__/integration/edge.integration.test.js +663 -0
  36. package/dist/pgvector/__tests__/integration/filters.integration.test.d.ts +8 -0
  37. package/dist/pgvector/__tests__/integration/filters.integration.test.d.ts.map +1 -0
  38. package/dist/pgvector/__tests__/integration/filters.integration.test.js +609 -0
  39. package/dist/pgvector/__tests__/integration/lifecycle.integration.test.d.ts +8 -0
  40. package/dist/pgvector/__tests__/integration/lifecycle.integration.test.d.ts.map +1 -0
  41. package/dist/pgvector/__tests__/integration/lifecycle.integration.test.js +449 -0
  42. package/dist/pgvector/__tests__/integration/query.integration.test.d.ts +8 -0
  43. package/dist/pgvector/__tests__/integration/query.integration.test.d.ts.map +1 -0
  44. package/dist/pgvector/__tests__/integration/query.integration.test.js +544 -0
  45. package/dist/pgvector/__tests__/search.test.d.ts +2 -0
  46. package/dist/pgvector/__tests__/search.test.d.ts.map +1 -0
  47. package/dist/pgvector/__tests__/search.test.js +279 -0
  48. package/dist/pgvector/handle.d.ts +60 -0
  49. package/dist/pgvector/handle.d.ts.map +1 -0
  50. package/dist/pgvector/handle.js +213 -0
  51. package/dist/pgvector/hit.d.ts +10 -0
  52. package/dist/pgvector/hit.d.ts.map +1 -0
  53. package/dist/pgvector/hit.js +44 -0
  54. package/dist/pgvector/index.d.ts +7 -0
  55. package/dist/pgvector/index.d.ts.map +1 -0
  56. package/dist/pgvector/index.js +5 -0
  57. package/dist/pgvector/search.d.ts +60 -0
  58. package/dist/pgvector/search.d.ts.map +1 -0
  59. package/dist/pgvector/search.js +227 -0
  60. package/dist/pgvector/sql/__tests__/limit.test.d.ts +2 -0
  61. package/dist/pgvector/sql/__tests__/limit.test.d.ts.map +1 -0
  62. package/dist/pgvector/sql/__tests__/limit.test.js +161 -0
  63. package/dist/pgvector/sql/__tests__/order.test.d.ts +2 -0
  64. package/dist/pgvector/sql/__tests__/order.test.d.ts.map +1 -0
  65. package/dist/pgvector/sql/__tests__/order.test.js +218 -0
  66. package/dist/pgvector/sql/__tests__/query.test.d.ts +2 -0
  67. package/dist/pgvector/sql/__tests__/query.test.d.ts.map +1 -0
  68. package/dist/pgvector/sql/__tests__/query.test.js +392 -0
  69. package/dist/pgvector/sql/__tests__/select.test.d.ts +2 -0
  70. package/dist/pgvector/sql/__tests__/select.test.d.ts.map +1 -0
  71. package/dist/pgvector/sql/__tests__/select.test.js +293 -0
  72. package/dist/pgvector/sql/__tests__/where.test.d.ts +2 -0
  73. package/dist/pgvector/sql/__tests__/where.test.d.ts.map +1 -0
  74. package/dist/pgvector/sql/__tests__/where.test.js +488 -0
  75. package/dist/pgvector/sql/index.d.ts +7 -0
  76. package/dist/pgvector/sql/index.d.ts.map +1 -0
  77. package/dist/pgvector/sql/index.js +6 -0
  78. package/dist/pgvector/sql/limit.d.ts +8 -0
  79. package/dist/pgvector/sql/limit.d.ts.map +1 -0
  80. package/dist/pgvector/sql/limit.js +20 -0
  81. package/dist/pgvector/sql/order.d.ts +9 -0
  82. package/dist/pgvector/sql/order.d.ts.map +1 -0
  83. package/dist/pgvector/sql/order.js +47 -0
  84. package/dist/pgvector/sql/query.d.ts +46 -0
  85. package/dist/pgvector/sql/query.d.ts.map +1 -0
  86. package/dist/pgvector/sql/query.js +54 -0
  87. package/dist/pgvector/sql/schema.d.ts +16 -0
  88. package/dist/pgvector/sql/schema.d.ts.map +1 -0
  89. package/dist/pgvector/sql/schema.js +47 -0
  90. package/dist/pgvector/sql/select.d.ts +11 -0
  91. package/dist/pgvector/sql/select.d.ts.map +1 -0
  92. package/dist/pgvector/sql/select.js +87 -0
  93. package/dist/pgvector/sql/where.d.ts +8 -0
  94. package/dist/pgvector/sql/where.d.ts.map +1 -0
  95. package/dist/pgvector/sql/where.js +137 -0
  96. package/dist/pgvector/types.d.ts +20 -0
  97. package/dist/pgvector/types.d.ts.map +1 -0
  98. package/dist/pgvector/types.js +1 -0
  99. package/dist/pgvector/utils.d.ts +18 -0
  100. package/dist/pgvector/utils.d.ts.map +1 -0
  101. package/dist/pgvector/utils.js +22 -0
  102. package/dist/postgres.d.ts +19 -26
  103. package/dist/postgres.d.ts.map +1 -1
  104. package/dist/postgres.js +15 -27
  105. package/dist/storage.d.ts +48 -0
  106. package/dist/storage.d.ts.map +1 -1
  107. package/dist/storage.js +32 -9
  108. package/dist/thread/sql.d.ts +38 -0
  109. package/dist/thread/sql.d.ts.map +1 -0
  110. package/dist/thread/sql.js +112 -0
  111. package/dist/thread/store.d.ts +2 -2
  112. package/dist/thread/store.d.ts.map +1 -1
  113. package/dist/thread/store.js +32 -102
  114. package/package.json +7 -4
  115. package/src/__tests__/integration.test.ts +15 -17
  116. package/src/__tests__/memory-integration.test.ts +355 -0
  117. package/src/__tests__/memory.test.ts +428 -0
  118. package/src/index.ts +19 -3
  119. package/src/memory/sql.ts +141 -0
  120. package/src/memory/store.ts +166 -0
  121. package/src/migrations.ts +13 -3
  122. package/src/pgvector/README.md +50 -0
  123. package/src/pgvector/__tests__/handle.test.ts +335 -0
  124. package/src/pgvector/__tests__/hit.test.ts +165 -0
  125. package/src/pgvector/__tests__/integration/document.integration.test.ts +717 -0
  126. package/src/pgvector/__tests__/integration/edge.integration.test.ts +835 -0
  127. package/src/pgvector/__tests__/integration/filters.integration.test.ts +721 -0
  128. package/src/pgvector/__tests__/integration/lifecycle.integration.test.ts +570 -0
  129. package/src/pgvector/__tests__/integration/query.integration.test.ts +667 -0
  130. package/src/pgvector/__tests__/search.test.ts +366 -0
  131. package/src/pgvector/handle.ts +285 -0
  132. package/src/pgvector/hit.ts +56 -0
  133. package/src/pgvector/index.ts +7 -0
  134. package/src/pgvector/search.ts +330 -0
  135. package/src/pgvector/sql/__tests__/limit.test.ts +180 -0
  136. package/src/pgvector/sql/__tests__/order.test.ts +248 -0
  137. package/src/pgvector/sql/__tests__/query.test.ts +548 -0
  138. package/src/pgvector/sql/__tests__/select.test.ts +367 -0
  139. package/src/pgvector/sql/__tests__/where.test.ts +554 -0
  140. package/src/pgvector/sql/index.ts +14 -0
  141. package/src/pgvector/sql/limit.ts +29 -0
  142. package/src/pgvector/sql/order.ts +55 -0
  143. package/src/pgvector/sql/query.ts +112 -0
  144. package/src/pgvector/sql/schema.ts +61 -0
  145. package/src/pgvector/sql/select.ts +100 -0
  146. package/src/pgvector/sql/where.ts +152 -0
  147. package/src/pgvector/types.ts +21 -0
  148. package/src/pgvector/utils.ts +24 -0
  149. package/src/postgres.ts +31 -33
  150. package/src/storage.ts +77 -9
  151. package/src/thread/sql.ts +159 -0
  152. package/src/thread/store.ts +40 -127
  153. 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
+ });