@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,663 @@
1
+ /**
2
+ * Edge case integration tests for pgvector.
3
+ *
4
+ * Tests boundary conditions, error handling, special values,
5
+ * and security considerations.
6
+ */
7
+ import { describe, it, expect, beforeAll, afterAll, beforeEach } from "vitest";
8
+ import { Pool } from "pg";
9
+ import { PGSearchIndex } from "../../search.js";
10
+ const TEST_DB_URL = process.env.KERNL_PG_TEST_URL;
11
+ const SCHEMA = "kernl_edge_integration_test";
12
+ describe.sequential("pgvector edge cases integration tests", () => {
13
+ if (!TEST_DB_URL) {
14
+ it.skip("requires KERNL_PG_TEST_URL environment variable", () => { });
15
+ return;
16
+ }
17
+ let pool;
18
+ let pgvec;
19
+ beforeAll(async () => {
20
+ pool = new Pool({ connectionString: TEST_DB_URL });
21
+ await pool.query(`CREATE EXTENSION IF NOT EXISTS vector`);
22
+ await pool.query(`DROP SCHEMA IF EXISTS "${SCHEMA}" CASCADE`);
23
+ await pool.query(`CREATE SCHEMA "${SCHEMA}"`);
24
+ pgvec = new PGSearchIndex({ pool });
25
+ }, 30000);
26
+ afterAll(async () => {
27
+ await pool.query(`DROP SCHEMA IF EXISTS "${SCHEMA}" CASCADE`);
28
+ await pool.end();
29
+ });
30
+ // ============================================================
31
+ // SPECIAL STRING VALUES
32
+ // ============================================================
33
+ describe("special string values", () => {
34
+ let handle;
35
+ beforeAll(async () => {
36
+ await pgvec.createIndex({
37
+ id: "string_edge",
38
+ schema: {
39
+ id: { type: "string", pk: true },
40
+ title: { type: "string" },
41
+ content: { type: "string" },
42
+ embedding: { type: "vector", dimensions: 4 },
43
+ },
44
+ providerOptions: { schema: SCHEMA },
45
+ });
46
+ handle = pgvec.index("string_edge");
47
+ });
48
+ beforeEach(async () => {
49
+ await pool.query(`DELETE FROM "${SCHEMA}"."string_edge"`);
50
+ });
51
+ it("handles SQL injection attempt in value", async () => {
52
+ await handle.upsert({
53
+ id: "injection-1",
54
+ title: "'; DROP TABLE string_edge; --",
55
+ content: "Content",
56
+ embedding: [0.1, 0.1, 0.1, 0.1],
57
+ });
58
+ // Table should still exist and be queryable
59
+ const hits = await handle.query({
60
+ filter: { id: "injection-1" },
61
+ topK: 1,
62
+ });
63
+ expect(hits).toHaveLength(1);
64
+ expect(hits[0].document?.title).toBe("'; DROP TABLE string_edge; --");
65
+ });
66
+ it("handles SQL injection attempt in filter", async () => {
67
+ await handle.upsert({
68
+ id: "safe-doc",
69
+ title: "Safe Title",
70
+ content: "Content",
71
+ embedding: [0.1, 0.1, 0.1, 0.1],
72
+ });
73
+ const hits = await handle.query({
74
+ filter: { title: "'; DROP TABLE string_edge; --" },
75
+ topK: 10,
76
+ });
77
+ // Should return no results, not crash
78
+ expect(hits).toHaveLength(0);
79
+ });
80
+ it("handles quotes in strings", async () => {
81
+ await handle.upsert({
82
+ id: "quotes",
83
+ title: 'He said "Hello"',
84
+ content: "It's a test",
85
+ embedding: [0.1, 0.1, 0.1, 0.1],
86
+ });
87
+ const hits = await handle.query({
88
+ filter: { id: "quotes" },
89
+ topK: 1,
90
+ });
91
+ expect(hits[0].document?.title).toBe('He said "Hello"');
92
+ expect(hits[0].document?.content).toBe("It's a test");
93
+ });
94
+ it("handles backslashes", async () => {
95
+ await handle.upsert({
96
+ id: "backslash",
97
+ title: "Path\\to\\file",
98
+ content: "C:\\Windows\\System32",
99
+ embedding: [0.1, 0.1, 0.1, 0.1],
100
+ });
101
+ const hits = await handle.query({
102
+ filter: { id: "backslash" },
103
+ topK: 1,
104
+ });
105
+ expect(hits[0].document?.title).toBe("Path\\to\\file");
106
+ });
107
+ it("handles newlines and tabs", async () => {
108
+ await handle.upsert({
109
+ id: "whitespace",
110
+ title: "Line1\nLine2",
111
+ content: "Col1\tCol2\tCol3",
112
+ embedding: [0.1, 0.1, 0.1, 0.1],
113
+ });
114
+ const hits = await handle.query({
115
+ filter: { id: "whitespace" },
116
+ topK: 1,
117
+ });
118
+ expect(hits[0].document?.title).toBe("Line1\nLine2");
119
+ expect(hits[0].document?.content).toBe("Col1\tCol2\tCol3");
120
+ });
121
+ it("handles null byte attempt", async () => {
122
+ // Postgres doesn't allow null bytes in text
123
+ await expect(handle.upsert({
124
+ id: "nullbyte",
125
+ title: "Has\x00null",
126
+ content: "Content",
127
+ embedding: [0.1, 0.1, 0.1, 0.1],
128
+ })).rejects.toThrow();
129
+ });
130
+ it("handles unicode edge cases", async () => {
131
+ await handle.upsert({
132
+ id: "unicode-edge",
133
+ title: "\u0000\u0001\uFFFF", // Postgres will reject null byte
134
+ content: "Normal",
135
+ embedding: [0.1, 0.1, 0.1, 0.1],
136
+ }).catch(() => {
137
+ // Expected to fail with null byte
138
+ });
139
+ // Test without null byte
140
+ await handle.upsert({
141
+ id: "unicode-edge-2",
142
+ title: "\u0001\uFFFE\uFFFF",
143
+ content: "Unicode test",
144
+ embedding: [0.1, 0.1, 0.1, 0.1],
145
+ });
146
+ const hits = await handle.query({
147
+ filter: { id: "unicode-edge-2" },
148
+ topK: 1,
149
+ });
150
+ expect(hits).toHaveLength(1);
151
+ });
152
+ it("handles very long strings", async () => {
153
+ const longString = "x".repeat(100000);
154
+ await handle.upsert({
155
+ id: "long",
156
+ title: longString,
157
+ content: "Short",
158
+ embedding: [0.1, 0.1, 0.1, 0.1],
159
+ });
160
+ const hits = await handle.query({
161
+ filter: { id: "long" },
162
+ topK: 1,
163
+ });
164
+ expect(hits[0].document?.title).toHaveLength(100000);
165
+ });
166
+ it("handles LIKE pattern characters in $contains", async () => {
167
+ await handle.upsert({
168
+ id: "pattern-chars",
169
+ title: "100% match_test",
170
+ content: "Content",
171
+ embedding: [0.1, 0.1, 0.1, 0.1],
172
+ });
173
+ // Should not interpret % and _ as wildcards
174
+ const hits = await handle.query({
175
+ filter: { title: { $contains: "100%" } },
176
+ topK: 10,
177
+ });
178
+ expect(hits).toHaveLength(1);
179
+ });
180
+ });
181
+ // ============================================================
182
+ // NUMERIC EDGE CASES
183
+ // ============================================================
184
+ describe("numeric edge cases", () => {
185
+ let handle;
186
+ beforeAll(async () => {
187
+ await pgvec.createIndex({
188
+ id: "numeric_edge",
189
+ schema: {
190
+ id: { type: "string", pk: true },
191
+ int_val: { type: "int" },
192
+ float_val: { type: "float" },
193
+ embedding: { type: "vector", dimensions: 4 },
194
+ },
195
+ providerOptions: { schema: SCHEMA },
196
+ });
197
+ handle = pgvec.index("numeric_edge");
198
+ });
199
+ beforeEach(async () => {
200
+ await pool.query(`DELETE FROM "${SCHEMA}"."numeric_edge"`);
201
+ });
202
+ it("handles integer zero", async () => {
203
+ await handle.upsert({
204
+ id: "zero",
205
+ int_val: 0,
206
+ float_val: 0.0,
207
+ embedding: [0.1, 0.1, 0.1, 0.1],
208
+ });
209
+ const hits = await handle.query({
210
+ filter: { int_val: 0 },
211
+ topK: 1,
212
+ });
213
+ expect(hits).toHaveLength(1);
214
+ expect(hits[0].document?.int_val).toBe(0);
215
+ });
216
+ it("handles negative numbers", async () => {
217
+ await handle.upsert({
218
+ id: "negative",
219
+ int_val: -999,
220
+ float_val: -123.456,
221
+ embedding: [0.1, 0.1, 0.1, 0.1],
222
+ });
223
+ const hits = await handle.query({
224
+ filter: { int_val: { $lt: 0 } },
225
+ topK: 1,
226
+ });
227
+ expect(hits).toHaveLength(1);
228
+ expect(hits[0].document?.int_val).toBe(-999);
229
+ });
230
+ it("handles large integers", async () => {
231
+ // PostgreSQL integer max is 2147483647
232
+ await handle.upsert({
233
+ id: "large-int",
234
+ int_val: 2147483647,
235
+ float_val: 0,
236
+ embedding: [0.1, 0.1, 0.1, 0.1],
237
+ });
238
+ const hits = await handle.query({
239
+ filter: { int_val: 2147483647 },
240
+ topK: 1,
241
+ });
242
+ expect(hits).toHaveLength(1);
243
+ expect(hits[0].document?.int_val).toBe(2147483647);
244
+ });
245
+ it("handles float precision", async () => {
246
+ await handle.upsert({
247
+ id: "precise",
248
+ int_val: 0,
249
+ float_val: 0.123456789012345,
250
+ embedding: [0.1, 0.1, 0.1, 0.1],
251
+ });
252
+ const hits = await handle.query({
253
+ filter: { id: "precise" },
254
+ topK: 1,
255
+ });
256
+ // Double precision maintains about 15 significant digits
257
+ expect(hits[0].document?.float_val).toBeCloseTo(0.123456789012345, 10);
258
+ });
259
+ it("handles special float values", async () => {
260
+ // Infinity and NaN are not valid JSON, so they shouldn't be used
261
+ // But very small and very large floats should work
262
+ await handle.upsert({
263
+ id: "extreme-float",
264
+ int_val: 0,
265
+ float_val: 1e308, // Near max double
266
+ embedding: [0.1, 0.1, 0.1, 0.1],
267
+ });
268
+ const hits = await handle.query({
269
+ filter: { id: "extreme-float" },
270
+ topK: 1,
271
+ });
272
+ expect(hits[0].document?.float_val).toBe(1e308);
273
+ });
274
+ });
275
+ // ============================================================
276
+ // VECTOR EDGE CASES
277
+ // ============================================================
278
+ describe("vector edge cases", () => {
279
+ let handle;
280
+ beforeAll(async () => {
281
+ await pgvec.createIndex({
282
+ id: "vector_edge",
283
+ schema: {
284
+ id: { type: "string", pk: true },
285
+ embedding: { type: "vector", dimensions: 4, similarity: "cosine" },
286
+ },
287
+ providerOptions: { schema: SCHEMA },
288
+ });
289
+ handle = pgvec.index("vector_edge");
290
+ });
291
+ beforeEach(async () => {
292
+ await pool.query(`DELETE FROM "${SCHEMA}"."vector_edge"`);
293
+ });
294
+ it("handles zero vector", async () => {
295
+ await handle.upsert({
296
+ id: "zero-vec",
297
+ embedding: [0, 0, 0, 0],
298
+ });
299
+ // Querying with zero vector - cosine similarity undefined
300
+ // pgvector returns NaN for cosine of zero vectors
301
+ const hits = await handle.query({
302
+ filter: { id: "zero-vec" },
303
+ topK: 1,
304
+ });
305
+ expect(hits).toHaveLength(1);
306
+ });
307
+ it("handles negative vector components", async () => {
308
+ await handle.upsert({
309
+ id: "negative-vec",
310
+ embedding: [-1, -0.5, 0.5, 1],
311
+ });
312
+ const hits = await handle.query({
313
+ query: [{ embedding: [-1, -0.5, 0.5, 1] }],
314
+ topK: 1,
315
+ });
316
+ expect(hits[0].id).toBe("negative-vec");
317
+ expect(hits[0].score).toBeGreaterThan(0.99);
318
+ });
319
+ it("handles very small vector components", async () => {
320
+ await handle.upsert({
321
+ id: "small-vec",
322
+ embedding: [1e-10, 1e-10, 1e-10, 1e-10],
323
+ });
324
+ const hits = await handle.query({
325
+ filter: { id: "small-vec" },
326
+ topK: 1,
327
+ });
328
+ expect(hits).toHaveLength(1);
329
+ });
330
+ it("handles very large vector components", async () => {
331
+ await handle.upsert({
332
+ id: "large-vec",
333
+ embedding: [1e10, 1e10, 1e10, 1e10],
334
+ });
335
+ const hits = await handle.query({
336
+ query: [{ embedding: [1e10, 1e10, 1e10, 1e10] }],
337
+ topK: 1,
338
+ });
339
+ expect(hits[0].id).toBe("large-vec");
340
+ });
341
+ it("rejects wrong dimension vector", async () => {
342
+ await expect(handle.upsert({
343
+ id: "wrong-dim",
344
+ embedding: [1, 2, 3], // 3 dimensions instead of 4
345
+ })).rejects.toThrow();
346
+ });
347
+ it("handles high-dimensional vectors (1536)", async () => {
348
+ await pgvec.createIndex({
349
+ id: "high_dim_edge",
350
+ schema: {
351
+ id: { type: "string", pk: true },
352
+ embedding: { type: "vector", dimensions: 1536 },
353
+ },
354
+ providerOptions: { schema: SCHEMA },
355
+ });
356
+ const highHandle = pgvec.index("high_dim_edge");
357
+ const vec = new Array(1536).fill(0).map((_, i) => Math.sin(i));
358
+ await highHandle.upsert({ id: "high-1", embedding: vec });
359
+ const hits = await highHandle.query({
360
+ query: [{ embedding: vec }],
361
+ topK: 1,
362
+ });
363
+ expect(hits[0].id).toBe("high-1");
364
+ expect(hits[0].score).toBeGreaterThan(0.99);
365
+ });
366
+ });
367
+ // ============================================================
368
+ // NULL HANDLING
369
+ // ============================================================
370
+ describe("null handling", () => {
371
+ let handle;
372
+ beforeAll(async () => {
373
+ await pgvec.createIndex({
374
+ id: "null_edge",
375
+ schema: {
376
+ id: { type: "string", pk: true },
377
+ required_field: { type: "string" },
378
+ optional_field: { type: "string" },
379
+ embedding: { type: "vector", dimensions: 4 },
380
+ },
381
+ providerOptions: { schema: SCHEMA },
382
+ });
383
+ handle = pgvec.index("null_edge");
384
+ });
385
+ beforeEach(async () => {
386
+ await pool.query(`DELETE FROM "${SCHEMA}"."null_edge"`);
387
+ });
388
+ it("stores null in optional field", async () => {
389
+ await handle.upsert({
390
+ id: "null-test",
391
+ required_field: "present",
392
+ optional_field: null,
393
+ embedding: [0.1, 0.1, 0.1, 0.1],
394
+ });
395
+ const hits = await handle.query({
396
+ filter: { optional_field: null },
397
+ topK: 1,
398
+ });
399
+ expect(hits).toHaveLength(1);
400
+ expect(hits[0].document?.optional_field).toBeNull();
401
+ });
402
+ it("$exists works with null values", async () => {
403
+ await handle.upsert([
404
+ {
405
+ id: "has-value",
406
+ required_field: "yes",
407
+ optional_field: "present",
408
+ embedding: [0.1, 0.1, 0.1, 0.1],
409
+ },
410
+ {
411
+ id: "has-null",
412
+ required_field: "yes",
413
+ optional_field: null,
414
+ embedding: [0.2, 0.2, 0.2, 0.2],
415
+ },
416
+ ]);
417
+ const existsTrue = await handle.query({
418
+ filter: { optional_field: { $exists: true } },
419
+ topK: 10,
420
+ });
421
+ const existsFalse = await handle.query({
422
+ filter: { optional_field: { $exists: false } },
423
+ topK: 10,
424
+ });
425
+ expect(existsTrue).toHaveLength(1);
426
+ expect(existsTrue[0].id).toBe("has-value");
427
+ expect(existsFalse).toHaveLength(1);
428
+ expect(existsFalse[0].id).toBe("has-null");
429
+ });
430
+ it("patches field to null", async () => {
431
+ await handle.upsert({
432
+ id: "patch-null",
433
+ required_field: "yes",
434
+ optional_field: "will be nulled",
435
+ embedding: [0.1, 0.1, 0.1, 0.1],
436
+ });
437
+ await handle.patch({
438
+ id: "patch-null",
439
+ optional_field: null,
440
+ });
441
+ const hits = await handle.query({
442
+ filter: { id: "patch-null" },
443
+ topK: 1,
444
+ });
445
+ expect(hits[0].document?.optional_field).toBeNull();
446
+ });
447
+ });
448
+ // ============================================================
449
+ // ID EDGE CASES
450
+ // ============================================================
451
+ describe("id edge cases", () => {
452
+ let handle;
453
+ beforeAll(async () => {
454
+ await pgvec.createIndex({
455
+ id: "id_edge",
456
+ schema: {
457
+ id: { type: "string", pk: true },
458
+ name: { type: "string" },
459
+ embedding: { type: "vector", dimensions: 4 },
460
+ },
461
+ providerOptions: { schema: SCHEMA },
462
+ });
463
+ handle = pgvec.index("id_edge");
464
+ });
465
+ beforeEach(async () => {
466
+ await pool.query(`DELETE FROM "${SCHEMA}"."id_edge"`);
467
+ });
468
+ it("handles UUID-style ids", async () => {
469
+ const uuid = "550e8400-e29b-41d4-a716-446655440000";
470
+ await handle.upsert({
471
+ id: uuid,
472
+ name: "UUID Doc",
473
+ embedding: [0.1, 0.1, 0.1, 0.1],
474
+ });
475
+ const hits = await handle.query({
476
+ filter: { id: uuid },
477
+ topK: 1,
478
+ });
479
+ expect(hits[0].id).toBe(uuid);
480
+ });
481
+ it("handles very long ids", async () => {
482
+ const longId = "x".repeat(255);
483
+ await handle.upsert({
484
+ id: longId,
485
+ name: "Long ID Doc",
486
+ embedding: [0.1, 0.1, 0.1, 0.1],
487
+ });
488
+ const hits = await handle.query({
489
+ filter: { id: longId },
490
+ topK: 1,
491
+ });
492
+ expect(hits[0].id).toBe(longId);
493
+ });
494
+ it("handles ids with special characters", async () => {
495
+ const specialId = "doc/with:special@chars#and?query=params";
496
+ await handle.upsert({
497
+ id: specialId,
498
+ name: "Special ID Doc",
499
+ embedding: [0.1, 0.1, 0.1, 0.1],
500
+ });
501
+ const hits = await handle.query({
502
+ filter: { id: specialId },
503
+ topK: 1,
504
+ });
505
+ expect(hits[0].id).toBe(specialId);
506
+ });
507
+ it("handles numeric string ids", async () => {
508
+ await handle.upsert({
509
+ id: "12345",
510
+ name: "Numeric ID",
511
+ embedding: [0.1, 0.1, 0.1, 0.1],
512
+ });
513
+ const hits = await handle.query({
514
+ filter: { id: "12345" },
515
+ topK: 1,
516
+ });
517
+ expect(hits[0].id).toBe("12345");
518
+ });
519
+ });
520
+ // ============================================================
521
+ // ERROR HANDLING
522
+ // ============================================================
523
+ describe("error handling", () => {
524
+ it("throws on non-existent table query", async () => {
525
+ const badHandle = pgvec.index("nonexistent_table");
526
+ await expect(badHandle.query({
527
+ query: [{ embedding: [0.1, 0.1, 0.1, 0.1] }],
528
+ topK: 10,
529
+ })).rejects.toThrow();
530
+ });
531
+ it("throws on non-existent column in filter", async () => {
532
+ await pgvec.createIndex({
533
+ id: "error_test",
534
+ schema: {
535
+ id: { type: "string", pk: true },
536
+ name: { type: "string" },
537
+ embedding: { type: "vector", dimensions: 4 },
538
+ },
539
+ providerOptions: { schema: SCHEMA },
540
+ });
541
+ const handle = pgvec.index("error_test");
542
+ await expect(handle.query({
543
+ filter: { nonexistent_column: "value" },
544
+ topK: 10,
545
+ })).rejects.toThrow();
546
+ });
547
+ it("throws on deleteIndex for non-bound index", async () => {
548
+ await expect(pgvec.deleteIndex("not_bound")).rejects.toThrow('Index "not_bound" not bound');
549
+ });
550
+ it("throws on describeIndex for non-bound index", async () => {
551
+ await expect(pgvec.describeIndex("not_bound")).rejects.toThrow('Index "not_bound" not bound');
552
+ });
553
+ it("throws on createIndex without primary key", async () => {
554
+ await expect(pgvec.createIndex({
555
+ id: "no_pk",
556
+ schema: {
557
+ name: { type: "string" },
558
+ embedding: { type: "vector", dimensions: 4 },
559
+ },
560
+ providerOptions: { schema: SCHEMA },
561
+ })).rejects.toThrow("schema must have a field with pk: true");
562
+ });
563
+ });
564
+ // ============================================================
565
+ // CONCURRENT OPERATIONS
566
+ // ============================================================
567
+ describe("concurrent operations", () => {
568
+ let handle;
569
+ beforeAll(async () => {
570
+ await pgvec.createIndex({
571
+ id: "concurrent_test",
572
+ schema: {
573
+ id: { type: "string", pk: true },
574
+ counter: { type: "int" },
575
+ embedding: { type: "vector", dimensions: 4 },
576
+ },
577
+ providerOptions: { schema: SCHEMA },
578
+ });
579
+ handle = pgvec.index("concurrent_test");
580
+ });
581
+ beforeEach(async () => {
582
+ await pool.query(`DELETE FROM "${SCHEMA}"."concurrent_test"`);
583
+ });
584
+ it("handles concurrent upserts to same document", async () => {
585
+ // Insert initial doc
586
+ await handle.upsert({
587
+ id: "concurrent-1",
588
+ counter: 0,
589
+ embedding: [0.1, 0.1, 0.1, 0.1],
590
+ });
591
+ // Concurrent updates
592
+ const updates = Array.from({ length: 10 }, (_, i) => handle.upsert({
593
+ id: "concurrent-1",
594
+ counter: i + 1,
595
+ embedding: [0.1, 0.1, 0.1, 0.1],
596
+ }));
597
+ await Promise.all(updates);
598
+ // Should have completed without errors
599
+ const hits = await handle.query({
600
+ filter: { id: "concurrent-1" },
601
+ topK: 1,
602
+ });
603
+ expect(hits).toHaveLength(1);
604
+ // Counter should be one of the values
605
+ expect(hits[0].document?.counter).toBeGreaterThanOrEqual(1);
606
+ expect(hits[0].document?.counter).toBeLessThanOrEqual(10);
607
+ });
608
+ it("handles concurrent upserts to different documents", async () => {
609
+ const upserts = Array.from({ length: 50 }, (_, i) => handle.upsert({
610
+ id: `parallel-${i}`,
611
+ counter: i,
612
+ embedding: [i / 50, (50 - i) / 50, 0.5, 0.5],
613
+ }));
614
+ await Promise.all(upserts);
615
+ // Increase ef_search to allow HNSW to explore more candidates
616
+ // Default is 40, which limits results to ~40 even with topK=100
617
+ await pool.query("SET hnsw.ef_search = 200");
618
+ const hits = await handle.query({
619
+ query: [{ embedding: [0.5, 0.5, 0.5, 0.5] }],
620
+ topK: 100,
621
+ });
622
+ expect(hits).toHaveLength(50);
623
+ });
624
+ it("handles query during upsert", async () => {
625
+ // Start upserting
626
+ const upsertPromise = handle.upsert(Array.from({ length: 100 }, (_, i) => ({
627
+ id: `during-${i}`,
628
+ counter: i,
629
+ embedding: [0.1, 0.1, 0.1, 0.1],
630
+ })));
631
+ // Query while upserting
632
+ const queryPromise = handle.query({
633
+ query: [{ embedding: [0.1, 0.1, 0.1, 0.1] }],
634
+ topK: 1000,
635
+ });
636
+ const [, hits] = await Promise.all([upsertPromise, queryPromise]);
637
+ // Query should succeed (may see partial or full results)
638
+ expect(hits).toBeDefined();
639
+ });
640
+ });
641
+ // ============================================================
642
+ // SCHEMA NAME EDGE CASES
643
+ // ============================================================
644
+ describe("schema name handling", () => {
645
+ it("creates index in custom schema", async () => {
646
+ await pool.query(`CREATE SCHEMA IF NOT EXISTS "custom_schema"`);
647
+ await pgvec.createIndex({
648
+ id: "custom_schema_test",
649
+ schema: {
650
+ id: { type: "string", pk: true },
651
+ embedding: { type: "vector", dimensions: 4 },
652
+ },
653
+ providerOptions: { schema: "custom_schema" },
654
+ });
655
+ // Verify table is in correct schema
656
+ const result = await pool.query(`SELECT 1 FROM information_schema.tables
657
+ WHERE table_schema = 'custom_schema' AND table_name = 'custom_schema_test'`);
658
+ expect(result.rows).toHaveLength(1);
659
+ await pgvec.deleteIndex("custom_schema_test");
660
+ await pool.query(`DROP SCHEMA IF EXISTS "custom_schema" CASCADE`);
661
+ });
662
+ });
663
+ });
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Comprehensive filter integration tests for pgvector.
3
+ *
4
+ * Tests all filter operators against real PostgreSQL with a
5
+ * deterministic dataset.
6
+ */
7
+ export {};
8
+ //# sourceMappingURL=filters.integration.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"filters.integration.test.d.ts","sourceRoot":"","sources":["../../../../src/pgvector/__tests__/integration/filters.integration.test.ts"],"names":[],"mappings":"AAAA;;;;;GAKG"}