@kernl-sdk/pg 0.1.10 → 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 +4 -5
  2. package/.turbo/turbo-check-types.log +36 -0
  3. package/CHANGELOG.md +41 -0
  4. package/README.md +124 -0
  5. package/dist/__tests__/integration.test.js +81 -1
  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 +62 -0
  106. package/dist/storage.d.ts.map +1 -1
  107. package/dist/storage.js +55 -10
  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 +7 -3
  112. package/dist/thread/store.d.ts.map +1 -1
  113. package/dist/thread/store.js +46 -105
  114. package/package.json +8 -5
  115. package/src/__tests__/integration.test.ts +114 -15
  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 +102 -11
  151. package/src/thread/sql.ts +159 -0
  152. package/src/thread/store.ts +58 -127
  153. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,570 @@
1
+ /**
2
+ * Index lifecycle integration tests for pgvector.
3
+ *
4
+ * Tests the full index lifecycle: create, bind, upsert, query,
5
+ * patch, delete documents, delete index.
6
+ */
7
+
8
+ import { describe, it, expect, beforeAll, afterAll } from "vitest";
9
+ import { Pool } from "pg";
10
+
11
+ import { PGSearchIndex } from "../../search";
12
+
13
+ const TEST_DB_URL = process.env.KERNL_PG_TEST_URL;
14
+ const SCHEMA = "kernl_lifecycle_integration_test";
15
+
16
+ describe.sequential("pgvector index lifecycle integration tests", () => {
17
+ if (!TEST_DB_URL) {
18
+ it.skip("requires KERNL_PG_TEST_URL environment variable", () => {});
19
+ return;
20
+ }
21
+
22
+ let pool: Pool;
23
+ let pgvec: PGSearchIndex;
24
+
25
+ beforeAll(async () => {
26
+ pool = new Pool({ connectionString: TEST_DB_URL });
27
+
28
+ await pool.query(`CREATE EXTENSION IF NOT EXISTS vector`);
29
+ await pool.query(`DROP SCHEMA IF EXISTS "${SCHEMA}" CASCADE`);
30
+ await pool.query(`CREATE SCHEMA "${SCHEMA}"`);
31
+
32
+ pgvec = new PGSearchIndex({ pool });
33
+ }, 30000);
34
+
35
+ afterAll(async () => {
36
+ await pool.query(`DROP SCHEMA IF EXISTS "${SCHEMA}" CASCADE`);
37
+ await pool.end();
38
+ });
39
+
40
+ // ============================================================
41
+ // FULL LIFECYCLE TEST
42
+ // ============================================================
43
+
44
+ describe("complete index lifecycle", () => {
45
+ const indexId = "lifecycle_test";
46
+
47
+ it("creates index with schema", async () => {
48
+ await pgvec.createIndex({
49
+ id: indexId,
50
+ schema: {
51
+ id: { type: "string", pk: true },
52
+ title: { type: "string" },
53
+ views: { type: "int" },
54
+ published: { type: "boolean" },
55
+ embedding: { type: "vector", dimensions: 4, similarity: "cosine" },
56
+ },
57
+ providerOptions: { schema: SCHEMA },
58
+ });
59
+
60
+ // Verify table was created
61
+ const tableCheck = await pool.query(
62
+ `SELECT 1 FROM information_schema.tables
63
+ WHERE table_schema = $1 AND table_name = $2`,
64
+ [SCHEMA, indexId],
65
+ );
66
+ expect(tableCheck.rows).toHaveLength(1);
67
+ });
68
+
69
+ it("index is immediately usable after creation", async () => {
70
+ const handle = pgvec.index(indexId);
71
+ expect(handle.id).toBe(indexId);
72
+ });
73
+
74
+ it("upserts documents", async () => {
75
+ const handle = pgvec.index(indexId);
76
+
77
+ const result = await handle.upsert([
78
+ {
79
+ id: "doc-1",
80
+ title: "First Document",
81
+ views: 100,
82
+ published: true,
83
+ embedding: [1, 0, 0, 0],
84
+ },
85
+ {
86
+ id: "doc-2",
87
+ title: "Second Document",
88
+ views: 200,
89
+ published: false,
90
+ embedding: [0, 1, 0, 0],
91
+ },
92
+ {
93
+ id: "doc-3",
94
+ title: "Third Document",
95
+ views: 50,
96
+ published: true,
97
+ embedding: [0, 0, 1, 0],
98
+ },
99
+ ]);
100
+
101
+ expect(result.count).toBe(3);
102
+ expect(result.inserted).toBe(3);
103
+ });
104
+
105
+ it("queries documents with vector search", async () => {
106
+ const handle = pgvec.index(indexId);
107
+
108
+ const hits = await handle.query({
109
+ query: [{ embedding: [1, 0, 0, 0] }],
110
+ topK: 10,
111
+ });
112
+
113
+ expect(hits).toHaveLength(3);
114
+ expect(hits[0].id).toBe("doc-1");
115
+ });
116
+
117
+ it("queries documents with filter", async () => {
118
+ const handle = pgvec.index(indexId);
119
+
120
+ const hits = await handle.query({
121
+ query: [{ embedding: [0.5, 0.5, 0.5, 0.5] }],
122
+ filter: { published: true },
123
+ topK: 10,
124
+ });
125
+
126
+ expect(hits).toHaveLength(2);
127
+ for (const hit of hits) {
128
+ expect(hit.document?.published).toBe(true);
129
+ }
130
+ });
131
+
132
+ it("patches documents", async () => {
133
+ const handle = pgvec.index(indexId);
134
+
135
+ await handle.patch({
136
+ id: "doc-1",
137
+ views: 500,
138
+ title: "Updated First Document",
139
+ });
140
+
141
+ const hits = await handle.query({
142
+ filter: { id: "doc-1" },
143
+ topK: 1,
144
+ });
145
+
146
+ expect(hits[0].document?.views).toBe(500);
147
+ expect(hits[0].document?.title).toBe("Updated First Document");
148
+ });
149
+
150
+ it("deletes documents", async () => {
151
+ const handle = pgvec.index(indexId);
152
+
153
+ await handle.delete("doc-3");
154
+
155
+ const hits = await handle.query({
156
+ query: [{ embedding: [0.5, 0.5, 0.5, 0.5] }],
157
+ topK: 10,
158
+ });
159
+
160
+ expect(hits).toHaveLength(2);
161
+ expect(hits.find((h) => h.id === "doc-3")).toBeUndefined();
162
+ });
163
+
164
+ it("describes index", async () => {
165
+ const stats = await pgvec.describeIndex(indexId);
166
+
167
+ expect(stats.id).toBe(indexId);
168
+ expect(stats.count).toBe(2);
169
+ expect(stats.dimensions).toBe(4);
170
+ expect(stats.similarity).toBe("cosine");
171
+ expect(stats.status).toBe("ready");
172
+ expect(stats.sizeb).toBeGreaterThan(0);
173
+ });
174
+
175
+ it("lists index", async () => {
176
+ const page = await pgvec.listIndexes({ prefix: "lifecycle" });
177
+
178
+ expect(page.data).toHaveLength(1);
179
+ expect(page.data[0].id).toBe(indexId);
180
+ expect(page.data[0].status).toBe("ready");
181
+ });
182
+
183
+ it("deletes index", async () => {
184
+ await pgvec.deleteIndex(indexId);
185
+
186
+ // Verify table was dropped
187
+ const tableCheck = await pool.query(
188
+ `SELECT 1 FROM information_schema.tables
189
+ WHERE table_schema = $1 AND table_name = $2`,
190
+ [SCHEMA, indexId],
191
+ );
192
+ expect(tableCheck.rows).toHaveLength(0);
193
+ });
194
+
195
+ it("throws on operations after delete", async () => {
196
+ await expect(pgvec.describeIndex(indexId)).rejects.toThrow(
197
+ `Index "${indexId}" not bound`,
198
+ );
199
+ });
200
+ });
201
+
202
+ // ============================================================
203
+ // BIND EXISTING TABLE
204
+ // ============================================================
205
+
206
+ describe("bindIndex", () => {
207
+ const tableId = "existing_table";
208
+
209
+ beforeAll(async () => {
210
+ // Create table manually
211
+ await pool.query(`
212
+ CREATE TABLE "${SCHEMA}"."${tableId}" (
213
+ doc_id TEXT PRIMARY KEY,
214
+ doc_title TEXT,
215
+ doc_count INTEGER,
216
+ doc_embedding vector(4)
217
+ )
218
+ `);
219
+
220
+ // Insert some data
221
+ await pool.query(`
222
+ INSERT INTO "${SCHEMA}"."${tableId}" (doc_id, doc_title, doc_count, doc_embedding)
223
+ VALUES
224
+ ('existing-1', 'Existing Doc 1', 10, '[0.1, 0.2, 0.3, 0.4]'),
225
+ ('existing-2', 'Existing Doc 2', 20, '[0.5, 0.6, 0.7, 0.8]')
226
+ `);
227
+ });
228
+
229
+ it("binds to existing table with custom field mapping", async () => {
230
+ await pgvec.bindIndex("custom_binding", {
231
+ schema: SCHEMA,
232
+ table: tableId,
233
+ pkey: "doc_id",
234
+ fields: {
235
+ id: { column: "doc_id", type: "string" },
236
+ title: { column: "doc_title", type: "string" },
237
+ count: { column: "doc_count", type: "int" },
238
+ embedding: {
239
+ column: "doc_embedding",
240
+ type: "vector",
241
+ dimensions: 4,
242
+ similarity: "cosine",
243
+ },
244
+ },
245
+ });
246
+
247
+ const handle = pgvec.index("custom_binding");
248
+ expect(handle.id).toBe("custom_binding");
249
+ });
250
+
251
+ it("queries bound table", async () => {
252
+ const handle = pgvec.index("custom_binding");
253
+
254
+ const hits = await handle.query({
255
+ query: [{ embedding: [0.1, 0.2, 0.3, 0.4] }],
256
+ topK: 10,
257
+ });
258
+
259
+ expect(hits).toHaveLength(2);
260
+ expect(hits[0].id).toBe("existing-1");
261
+ });
262
+
263
+ it("upserts to bound table", async () => {
264
+ const handle = pgvec.index("custom_binding");
265
+
266
+ await handle.upsert({
267
+ id: "new-doc",
268
+ title: "New Document",
269
+ count: 30,
270
+ embedding: [0.9, 0.9, 0.9, 0.9],
271
+ });
272
+
273
+ // Verify in raw SQL
274
+ const result = await pool.query(
275
+ `SELECT * FROM "${SCHEMA}"."${tableId}" WHERE doc_id = 'new-doc'`,
276
+ );
277
+
278
+ expect(result.rows).toHaveLength(1);
279
+ expect(result.rows[0].doc_title).toBe("New Document");
280
+ expect(result.rows[0].doc_count).toBe(30);
281
+ });
282
+
283
+ it("respects custom primary key", async () => {
284
+ const handle = pgvec.index("custom_binding");
285
+
286
+ // Update existing doc
287
+ await handle.upsert({
288
+ id: "existing-1",
289
+ title: "Updated Existing",
290
+ count: 100,
291
+ embedding: [0.1, 0.2, 0.3, 0.4],
292
+ });
293
+
294
+ const result = await pool.query(
295
+ `SELECT * FROM "${SCHEMA}"."${tableId}" WHERE doc_id = 'existing-1'`,
296
+ );
297
+
298
+ expect(result.rows[0].doc_title).toBe("Updated Existing");
299
+ expect(result.rows[0].doc_count).toBe(100);
300
+ });
301
+
302
+ it("rebinding updates configuration", async () => {
303
+ // Rebind with different field mapping
304
+ await pgvec.bindIndex("custom_binding", {
305
+ schema: SCHEMA,
306
+ table: tableId,
307
+ pkey: "doc_id",
308
+ fields: {
309
+ id: { column: "doc_id", type: "string" },
310
+ title: { column: "doc_title", type: "string" },
311
+ // count field omitted
312
+ embedding: {
313
+ column: "doc_embedding",
314
+ type: "vector",
315
+ dimensions: 4,
316
+ similarity: "cosine",
317
+ },
318
+ },
319
+ });
320
+
321
+ // Should still work
322
+ const handle = pgvec.index("custom_binding");
323
+ const hits = await handle.query({
324
+ query: [{ embedding: [0.5, 0.5, 0.5, 0.5] }],
325
+ topK: 1,
326
+ });
327
+
328
+ expect(hits).toHaveLength(1);
329
+ });
330
+ });
331
+
332
+ // ============================================================
333
+ // MULTIPLE INDEXES
334
+ // ============================================================
335
+
336
+ describe("multiple indexes", () => {
337
+ it("manages multiple indexes independently", async () => {
338
+ // Create two indexes
339
+ await pgvec.createIndex({
340
+ id: "multi_index_a",
341
+ schema: {
342
+ id: { type: "string", pk: true },
343
+ name: { type: "string" },
344
+ embedding: { type: "vector", dimensions: 4 },
345
+ },
346
+ providerOptions: { schema: SCHEMA },
347
+ });
348
+
349
+ await pgvec.createIndex({
350
+ id: "multi_index_b",
351
+ schema: {
352
+ id: { type: "string", pk: true },
353
+ title: { type: "string" },
354
+ embedding: { type: "vector", dimensions: 4 },
355
+ },
356
+ providerOptions: { schema: SCHEMA },
357
+ });
358
+
359
+ // Insert different data
360
+ const handleA = pgvec.index("multi_index_a");
361
+ const handleB = pgvec.index("multi_index_b");
362
+
363
+ await handleA.upsert({ id: "a-1", name: "Index A Doc", embedding: [1, 0, 0, 0] });
364
+ await handleB.upsert({ id: "b-1", title: "Index B Doc", embedding: [0, 1, 0, 0] });
365
+
366
+ // Query each
367
+ const hitsA = await handleA.query({
368
+ query: [{ embedding: [1, 0, 0, 0] }],
369
+ topK: 10,
370
+ });
371
+
372
+ const hitsB = await handleB.query({
373
+ query: [{ embedding: [0, 1, 0, 0] }],
374
+ topK: 10,
375
+ });
376
+
377
+ expect(hitsA).toHaveLength(1);
378
+ expect(hitsA[0].document?.name).toBe("Index A Doc");
379
+
380
+ expect(hitsB).toHaveLength(1);
381
+ expect(hitsB[0].document?.title).toBe("Index B Doc");
382
+
383
+ // Delete one doesn't affect the other
384
+ await pgvec.deleteIndex("multi_index_a");
385
+
386
+ const stillExists = await handleB.query({
387
+ query: [{ embedding: [0, 1, 0, 0] }],
388
+ topK: 10,
389
+ });
390
+ expect(stillExists).toHaveLength(1);
391
+
392
+ // Cleanup
393
+ await pgvec.deleteIndex("multi_index_b");
394
+ });
395
+
396
+ it("lists multiple indexes with pagination", async () => {
397
+ // Create several indexes
398
+ for (let i = 0; i < 5; i++) {
399
+ await pgvec.createIndex({
400
+ id: `paginated_${i}`,
401
+ schema: {
402
+ id: { type: "string", pk: true },
403
+ embedding: { type: "vector", dimensions: 4 },
404
+ },
405
+ providerOptions: { schema: SCHEMA },
406
+ });
407
+ }
408
+
409
+ // List with limit
410
+ const page1 = await pgvec.listIndexes({ prefix: "paginated_", limit: 2 });
411
+ expect(page1.data).toHaveLength(2);
412
+ expect(page1.last).toBe(false);
413
+
414
+ const page2 = await page1.next();
415
+ expect(page2).not.toBeNull();
416
+ expect(page2!.data).toHaveLength(2);
417
+
418
+ const page3 = await page2!.next();
419
+ expect(page3).not.toBeNull();
420
+ expect(page3!.data).toHaveLength(1);
421
+ expect(page3!.last).toBe(true);
422
+
423
+ // Cleanup
424
+ for (let i = 0; i < 5; i++) {
425
+ await pgvec.deleteIndex(`paginated_${i}`);
426
+ }
427
+ });
428
+ });
429
+
430
+ // ============================================================
431
+ // SCHEMA VARIATIONS
432
+ // ============================================================
433
+
434
+ describe("schema variations", () => {
435
+ it("creates index with all field types", async () => {
436
+ await pgvec.createIndex({
437
+ id: "all_types",
438
+ schema: {
439
+ id: { type: "string", pk: true },
440
+ str_field: { type: "string" },
441
+ int_field: { type: "int" },
442
+ float_field: { type: "float" },
443
+ bool_field: { type: "boolean" },
444
+ date_field: { type: "date" },
445
+ embedding: { type: "vector", dimensions: 4 },
446
+ },
447
+ providerOptions: { schema: SCHEMA },
448
+ });
449
+
450
+ // Verify column types
451
+ const result = await pool.query<{
452
+ column_name: string;
453
+ data_type: string;
454
+ }>(
455
+ `SELECT column_name, data_type
456
+ FROM information_schema.columns
457
+ WHERE table_schema = $1 AND table_name = $2
458
+ ORDER BY ordinal_position`,
459
+ [SCHEMA, "all_types"],
460
+ );
461
+
462
+ const columns = Object.fromEntries(
463
+ result.rows.map((r) => [r.column_name, r.data_type]),
464
+ );
465
+
466
+ expect(columns.str_field).toBe("text");
467
+ expect(columns.int_field).toBe("integer");
468
+ expect(columns.float_field).toBe("double precision");
469
+ expect(columns.bool_field).toBe("boolean");
470
+ expect(columns.date_field).toBe("timestamp with time zone");
471
+
472
+ await pgvec.deleteIndex("all_types");
473
+ });
474
+
475
+ it("creates index with high-dimensional vectors", async () => {
476
+ await pgvec.createIndex({
477
+ id: "high_dim",
478
+ schema: {
479
+ id: { type: "string", pk: true },
480
+ embedding: { type: "vector", dimensions: 1536, similarity: "cosine" },
481
+ },
482
+ providerOptions: { schema: SCHEMA },
483
+ });
484
+
485
+ const handle = pgvec.index("high_dim");
486
+ const vec = new Array(1536).fill(0.1);
487
+
488
+ await handle.upsert({ id: "high-1", embedding: vec });
489
+
490
+ const hits = await handle.query({
491
+ query: [{ embedding: vec }],
492
+ topK: 1,
493
+ });
494
+
495
+ expect(hits).toHaveLength(1);
496
+ expect(hits[0].document?.embedding).toHaveLength(1536);
497
+
498
+ await pgvec.deleteIndex("high_dim");
499
+ });
500
+
501
+ it("creates index with multiple vector fields", async () => {
502
+ await pgvec.createIndex({
503
+ id: "multi_vec",
504
+ schema: {
505
+ id: { type: "string", pk: true },
506
+ title_embedding: { type: "vector", dimensions: 4, similarity: "cosine" },
507
+ content_embedding: { type: "vector", dimensions: 4, similarity: "euclidean" },
508
+ },
509
+ providerOptions: { schema: SCHEMA },
510
+ });
511
+
512
+ const handle = pgvec.index("multi_vec");
513
+
514
+ await handle.upsert({
515
+ id: "mv-1",
516
+ title_embedding: [1, 0, 0, 0],
517
+ content_embedding: [0, 1, 0, 0],
518
+ });
519
+
520
+ // Query on first vector field
521
+ const hits = await handle.query({
522
+ query: [{ title_embedding: [1, 0, 0, 0] }],
523
+ topK: 1,
524
+ });
525
+
526
+ expect(hits).toHaveLength(1);
527
+
528
+ await pgvec.deleteIndex("multi_vec");
529
+ });
530
+ });
531
+
532
+ // ============================================================
533
+ // PERSISTENCE ACROSS INSTANCES
534
+ // ============================================================
535
+
536
+ describe("persistence", () => {
537
+ it("loads existing indexes on new instance", async () => {
538
+ // Create index with first instance
539
+ await pgvec.createIndex({
540
+ id: "persist_test",
541
+ schema: {
542
+ id: { type: "string", pk: true },
543
+ name: { type: "string" },
544
+ embedding: { type: "vector", dimensions: 4 },
545
+ },
546
+ providerOptions: { schema: SCHEMA },
547
+ });
548
+
549
+ const handle1 = pgvec.index("persist_test");
550
+ await handle1.upsert({ id: "p-1", name: "Persisted", embedding: [1, 0, 0, 0] });
551
+
552
+ // Create new instance
553
+ const pgvec2 = new PGSearchIndex({ pool });
554
+
555
+ // Should be able to access the index
556
+ const stats = await pgvec2.describeIndex("persist_test");
557
+ expect(stats.count).toBe(1);
558
+
559
+ const handle2 = pgvec2.index("persist_test");
560
+ const hits = await handle2.query({
561
+ query: [{ embedding: [1, 0, 0, 0] }],
562
+ topK: 1,
563
+ });
564
+
565
+ expect(hits[0].document?.name).toBe("Persisted");
566
+
567
+ await pgvec.deleteIndex("persist_test");
568
+ });
569
+ });
570
+ });