@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,721 @@
1
+ /**
2
+ * Comprehensive filter integration tests for pgvector.
3
+ *
4
+ * Tests all filter operators against real PostgreSQL with a
5
+ * deterministic dataset.
6
+ */
7
+
8
+ import { describe, it, expect, beforeAll, afterAll } from "vitest";
9
+ import { Pool } from "pg";
10
+
11
+ import { PGSearchIndex } from "../../search";
12
+ import type { IndexHandle, Filter } from "@kernl-sdk/retrieval";
13
+
14
+ const TEST_DB_URL = process.env.KERNL_PG_TEST_URL;
15
+ const SCHEMA = "kernl_filter_integration_test";
16
+
17
+ /**
18
+ * Test document type.
19
+ */
20
+ interface TestDoc {
21
+ id: string;
22
+ num: number;
23
+ flag: boolean;
24
+ status: string;
25
+ name: string;
26
+ optionalField?: string | null;
27
+ embedding: number[];
28
+ }
29
+
30
+ /**
31
+ * Deterministic test dataset.
32
+ *
33
+ * 10 documents with varied field values for comprehensive filter testing.
34
+ */
35
+ const TEST_DOCS: TestDoc[] = [
36
+ {
37
+ id: "doc-01",
38
+ num: 10,
39
+ flag: true,
40
+ status: "active",
41
+ name: "alice_smith",
42
+ optionalField: "present",
43
+ embedding: [0.1, 0.0, 0.0, 0.0],
44
+ },
45
+ {
46
+ id: "doc-02",
47
+ num: 20,
48
+ flag: false,
49
+ status: "active",
50
+ name: "bob_jones",
51
+ optionalField: null,
52
+ embedding: [0.0, 0.1, 0.0, 0.0],
53
+ },
54
+ {
55
+ id: "doc-03",
56
+ num: 30,
57
+ flag: true,
58
+ status: "pending",
59
+ name: "charlie_smith",
60
+ optionalField: "also_present",
61
+ embedding: [0.0, 0.0, 0.1, 0.0],
62
+ },
63
+ {
64
+ id: "doc-04",
65
+ num: 40,
66
+ flag: false,
67
+ status: "pending",
68
+ name: "diana_brown",
69
+ optionalField: null,
70
+ embedding: [0.0, 0.0, 0.0, 0.1],
71
+ },
72
+ {
73
+ id: "doc-05",
74
+ num: 50,
75
+ flag: true,
76
+ status: "deleted",
77
+ name: "eve_johnson",
78
+ optionalField: "value",
79
+ embedding: [0.1, 0.1, 0.0, 0.0],
80
+ },
81
+ {
82
+ id: "doc-06",
83
+ num: 0,
84
+ flag: false,
85
+ status: "active",
86
+ name: "frank_miller",
87
+ optionalField: null,
88
+ embedding: [0.0, 0.1, 0.1, 0.0],
89
+ },
90
+ {
91
+ id: "doc-07",
92
+ num: -10,
93
+ flag: true,
94
+ status: "active",
95
+ name: "grace_wilson",
96
+ optionalField: "exists",
97
+ embedding: [0.0, 0.0, 0.1, 0.1],
98
+ },
99
+ {
100
+ id: "doc-08",
101
+ num: 100,
102
+ flag: false,
103
+ status: "deleted",
104
+ name: "henry_davis",
105
+ optionalField: null,
106
+ embedding: [0.1, 0.0, 0.1, 0.0],
107
+ },
108
+ {
109
+ id: "doc-09",
110
+ num: 25,
111
+ flag: true,
112
+ status: "pending",
113
+ name: "ivy_taylor",
114
+ optionalField: "set",
115
+ embedding: [0.0, 0.1, 0.0, 0.1],
116
+ },
117
+ {
118
+ id: "doc-10",
119
+ num: 35,
120
+ flag: false,
121
+ status: "active",
122
+ name: "jack_anderson",
123
+ optionalField: null,
124
+ embedding: [0.1, 0.0, 0.0, 0.1],
125
+ },
126
+ ];
127
+
128
+ // Standard query vector for all filter tests
129
+ const QUERY_VECTOR = [0.1, 0.1, 0.1, 0.1];
130
+
131
+ describe.sequential("pgvector filter integration tests", () => {
132
+ if (!TEST_DB_URL) {
133
+ it.skip("requires KERNL_PG_TEST_URL environment variable", () => {});
134
+ return;
135
+ }
136
+
137
+ let pool: Pool;
138
+ let pgvec: PGSearchIndex;
139
+ let handle: IndexHandle<TestDoc>;
140
+ const testIndexId = "filter_test_docs";
141
+
142
+ beforeAll(async () => {
143
+ pool = new Pool({ connectionString: TEST_DB_URL });
144
+
145
+ // Ensure pgvector extension exists
146
+ await pool.query(`CREATE EXTENSION IF NOT EXISTS vector`);
147
+
148
+ // Clean slate
149
+ await pool.query(`DROP SCHEMA IF EXISTS "${SCHEMA}" CASCADE`);
150
+ await pool.query(`CREATE SCHEMA "${SCHEMA}"`);
151
+
152
+ pgvec = new PGSearchIndex({ pool });
153
+
154
+ // Create index with schema
155
+ await pgvec.createIndex({
156
+ id: testIndexId,
157
+ schema: {
158
+ id: { type: "string", pk: true },
159
+ num: { type: "int" },
160
+ flag: { type: "boolean" },
161
+ status: { type: "string" },
162
+ name: { type: "string" },
163
+ optionalField: { type: "string" },
164
+ embedding: { type: "vector", dimensions: 4, similarity: "cosine" },
165
+ },
166
+ providerOptions: { schema: SCHEMA },
167
+ });
168
+
169
+ handle = pgvec.index<TestDoc>(testIndexId);
170
+
171
+ // Insert test documents
172
+ await handle.upsert(TEST_DOCS);
173
+ }, 30000);
174
+
175
+ afterAll(async () => {
176
+ await pool.query(`DROP SCHEMA IF EXISTS "${SCHEMA}" CASCADE`);
177
+ await pool.end();
178
+ });
179
+
180
+ /**
181
+ * Helper to query and return sorted IDs.
182
+ */
183
+ async function queryIds(filter: Filter): Promise<string[]> {
184
+ const hits = await handle.query({
185
+ query: [{ embedding: QUERY_VECTOR }],
186
+ topK: 100,
187
+ filter,
188
+ });
189
+ return hits.map((h) => h.id).sort();
190
+ }
191
+
192
+ // ============================================================
193
+ // EQUALITY OPERATORS
194
+ // ============================================================
195
+
196
+ describe("equality operators", () => {
197
+ it("$eq on string field (shorthand)", async () => {
198
+ const ids = await queryIds({ status: "active" });
199
+ expect(ids).toEqual(["doc-01", "doc-02", "doc-06", "doc-07", "doc-10"]);
200
+ });
201
+
202
+ it("$eq on string field (explicit)", async () => {
203
+ const ids = await queryIds({ status: { $eq: "active" } });
204
+ expect(ids).toEqual(["doc-01", "doc-02", "doc-06", "doc-07", "doc-10"]);
205
+ });
206
+
207
+ it("$eq on number field", async () => {
208
+ const ids = await queryIds({ num: 30 });
209
+ expect(ids).toEqual(["doc-03"]);
210
+ });
211
+
212
+ it("$eq on boolean true", async () => {
213
+ const ids = await queryIds({ flag: true });
214
+ expect(ids).toEqual(["doc-01", "doc-03", "doc-05", "doc-07", "doc-09"]);
215
+ });
216
+
217
+ it("$eq on boolean false", async () => {
218
+ const ids = await queryIds({ flag: false });
219
+ expect(ids).toEqual(["doc-02", "doc-04", "doc-06", "doc-08", "doc-10"]);
220
+ });
221
+
222
+ it("$eq on zero", async () => {
223
+ const ids = await queryIds({ num: 0 });
224
+ expect(ids).toEqual(["doc-06"]);
225
+ });
226
+
227
+ it("$eq on negative number", async () => {
228
+ const ids = await queryIds({ num: -10 });
229
+ expect(ids).toEqual(["doc-07"]);
230
+ });
231
+
232
+ it("$neq on string field", async () => {
233
+ const ids = await queryIds({ status: { $neq: "active" } });
234
+ expect(ids).toEqual(["doc-03", "doc-04", "doc-05", "doc-08", "doc-09"]);
235
+ });
236
+
237
+ it("$neq on boolean", async () => {
238
+ const ids = await queryIds({ flag: { $neq: true } });
239
+ expect(ids).toEqual(["doc-02", "doc-04", "doc-06", "doc-08", "doc-10"]);
240
+ });
241
+
242
+ it("$neq on number", async () => {
243
+ const ids = await queryIds({ num: { $neq: 10 } });
244
+ // All except doc-01
245
+ expect(ids).toEqual([
246
+ "doc-02",
247
+ "doc-03",
248
+ "doc-04",
249
+ "doc-05",
250
+ "doc-06",
251
+ "doc-07",
252
+ "doc-08",
253
+ "doc-09",
254
+ "doc-10",
255
+ ]);
256
+ });
257
+ });
258
+
259
+ // ============================================================
260
+ // COMPARISON OPERATORS
261
+ // ============================================================
262
+
263
+ describe("comparison operators", () => {
264
+ it("$gt on number", async () => {
265
+ const ids = await queryIds({ num: { $gt: 30 } });
266
+ expect(ids).toEqual(["doc-04", "doc-05", "doc-08", "doc-10"]);
267
+ });
268
+
269
+ it("$gte on number", async () => {
270
+ const ids = await queryIds({ num: { $gte: 30 } });
271
+ expect(ids).toEqual(["doc-03", "doc-04", "doc-05", "doc-08", "doc-10"]);
272
+ });
273
+
274
+ it("$lt on number", async () => {
275
+ const ids = await queryIds({ num: { $lt: 20 } });
276
+ expect(ids).toEqual(["doc-01", "doc-06", "doc-07"]);
277
+ });
278
+
279
+ it("$lte on number", async () => {
280
+ const ids = await queryIds({ num: { $lte: 20 } });
281
+ expect(ids).toEqual(["doc-01", "doc-02", "doc-06", "doc-07"]);
282
+ });
283
+
284
+ it("$gt with negative number", async () => {
285
+ const ids = await queryIds({ num: { $gt: -10 } });
286
+ expect(ids).toEqual([
287
+ "doc-01",
288
+ "doc-02",
289
+ "doc-03",
290
+ "doc-04",
291
+ "doc-05",
292
+ "doc-06",
293
+ "doc-08",
294
+ "doc-09",
295
+ "doc-10",
296
+ ]);
297
+ });
298
+
299
+ it("$lt with zero", async () => {
300
+ const ids = await queryIds({ num: { $lt: 0 } });
301
+ expect(ids).toEqual(["doc-07"]);
302
+ });
303
+
304
+ it("$lte with zero includes zero", async () => {
305
+ const ids = await queryIds({ num: { $lte: 0 } });
306
+ expect(ids).toEqual(["doc-06", "doc-07"]);
307
+ });
308
+
309
+ it("$gte with zero includes zero", async () => {
310
+ const ids = await queryIds({ num: { $gte: 0 } });
311
+ expect(ids).toEqual([
312
+ "doc-01",
313
+ "doc-02",
314
+ "doc-03",
315
+ "doc-04",
316
+ "doc-05",
317
+ "doc-06",
318
+ "doc-08",
319
+ "doc-09",
320
+ "doc-10",
321
+ ]);
322
+ });
323
+
324
+ it("range filter (gt + lt)", async () => {
325
+ const ids = await queryIds({ num: { $gt: 20, $lt: 40 } });
326
+ expect(ids).toEqual(["doc-03", "doc-09", "doc-10"]);
327
+ });
328
+
329
+ it("inclusive range (gte + lte)", async () => {
330
+ const ids = await queryIds({ num: { $gte: 20, $lte: 40 } });
331
+ expect(ids).toEqual(["doc-02", "doc-03", "doc-04", "doc-09", "doc-10"]);
332
+ });
333
+
334
+ it("empty range returns no results", async () => {
335
+ const ids = await queryIds({ num: { $gt: 100, $lt: 50 } });
336
+ expect(ids).toEqual([]);
337
+ });
338
+
339
+ it("single value range (gte = lte)", async () => {
340
+ const ids = await queryIds({ num: { $gte: 30, $lte: 30 } });
341
+ expect(ids).toEqual(["doc-03"]);
342
+ });
343
+ });
344
+
345
+ // ============================================================
346
+ // SET MEMBERSHIP OPERATORS
347
+ // ============================================================
348
+
349
+ describe("set membership operators", () => {
350
+ it("$in with string values", async () => {
351
+ const ids = await queryIds({ status: { $in: ["active", "pending"] } });
352
+ expect(ids).toEqual([
353
+ "doc-01",
354
+ "doc-02",
355
+ "doc-03",
356
+ "doc-04",
357
+ "doc-06",
358
+ "doc-07",
359
+ "doc-09",
360
+ "doc-10",
361
+ ]);
362
+ });
363
+
364
+ it("$in with single value", async () => {
365
+ const ids = await queryIds({ status: { $in: ["deleted"] } });
366
+ expect(ids).toEqual(["doc-05", "doc-08"]);
367
+ });
368
+
369
+ it("$in with number values", async () => {
370
+ const ids = await queryIds({ num: { $in: [10, 20, 30] } });
371
+ expect(ids).toEqual(["doc-01", "doc-02", "doc-03"]);
372
+ });
373
+
374
+ it("$in with empty array returns no results", async () => {
375
+ const ids = await queryIds({ status: { $in: [] } });
376
+ expect(ids).toEqual([]);
377
+ });
378
+
379
+ it("$in with non-matching values returns no results", async () => {
380
+ const ids = await queryIds({ status: { $in: ["nonexistent"] } });
381
+ expect(ids).toEqual([]);
382
+ });
383
+
384
+ it("$nin with string values", async () => {
385
+ const ids = await queryIds({ status: { $nin: ["deleted"] } });
386
+ expect(ids).toEqual([
387
+ "doc-01",
388
+ "doc-02",
389
+ "doc-03",
390
+ "doc-04",
391
+ "doc-06",
392
+ "doc-07",
393
+ "doc-09",
394
+ "doc-10",
395
+ ]);
396
+ });
397
+
398
+ it("$nin excludes multiple values", async () => {
399
+ const ids = await queryIds({ status: { $nin: ["active", "deleted"] } });
400
+ expect(ids).toEqual(["doc-03", "doc-04", "doc-09"]);
401
+ });
402
+
403
+ it("$nin with empty array returns all results", async () => {
404
+ const ids = await queryIds({ status: { $nin: [] } });
405
+ expect(ids).toHaveLength(10);
406
+ });
407
+
408
+ it("$nin with all values returns no results", async () => {
409
+ const ids = await queryIds({
410
+ status: { $nin: ["active", "pending", "deleted"] },
411
+ });
412
+ expect(ids).toEqual([]);
413
+ });
414
+ });
415
+
416
+ // ============================================================
417
+ // STRING PATTERN OPERATORS
418
+ // ============================================================
419
+
420
+ describe("string pattern operators", () => {
421
+ it("$contains matches substring", async () => {
422
+ const ids = await queryIds({ name: { $contains: "smith" } });
423
+ expect(ids).toEqual(["doc-01", "doc-03"]); // alice_smith, charlie_smith
424
+ });
425
+
426
+ it("$contains is case-insensitive", async () => {
427
+ const ids = await queryIds({ name: { $contains: "SMITH" } });
428
+ expect(ids).toEqual(["doc-01", "doc-03"]);
429
+ });
430
+
431
+ it("$contains with no matches returns empty", async () => {
432
+ const ids = await queryIds({ name: { $contains: "xyz" } });
433
+ expect(ids).toEqual([]);
434
+ });
435
+
436
+ it("$startsWith matches prefix", async () => {
437
+ const ids = await queryIds({ name: { $startsWith: "alice" } });
438
+ expect(ids).toEqual(["doc-01"]);
439
+ });
440
+
441
+ it("$startsWith is case-insensitive", async () => {
442
+ const ids = await queryIds({ name: { $startsWith: "ALICE" } });
443
+ expect(ids).toEqual(["doc-01"]);
444
+ });
445
+
446
+ it("$startsWith with common prefix", async () => {
447
+ const ids = await queryIds({ name: { $startsWith: "j" } });
448
+ expect(ids).toEqual(["doc-10"]); // jack_anderson
449
+ });
450
+
451
+ it("$endsWith matches suffix", async () => {
452
+ const ids = await queryIds({ name: { $endsWith: "_smith" } });
453
+ expect(ids).toEqual(["doc-01", "doc-03"]); // alice_smith, charlie_smith
454
+ });
455
+
456
+ it("$endsWith is case-insensitive", async () => {
457
+ const ids = await queryIds({ name: { $endsWith: "_SMITH" } });
458
+ expect(ids).toEqual(["doc-01", "doc-03"]);
459
+ });
460
+
461
+ it("$endsWith with different suffix", async () => {
462
+ const ids = await queryIds({ name: { $endsWith: "_jones" } });
463
+ expect(ids).toEqual(["doc-02"]); // bob_jones
464
+ });
465
+ });
466
+
467
+ // ============================================================
468
+ // EXISTENCE OPERATORS
469
+ // ============================================================
470
+
471
+ describe("existence operators", () => {
472
+ it("$exists: true finds docs with non-null field", async () => {
473
+ const ids = await queryIds({ optionalField: { $exists: true } });
474
+ // docs with optionalField set to a string value (not null)
475
+ expect(ids).toEqual(["doc-01", "doc-03", "doc-05", "doc-07", "doc-09"]);
476
+ });
477
+
478
+ it("$exists: false finds docs with null field", async () => {
479
+ const ids = await queryIds({ optionalField: { $exists: false } });
480
+ // docs where optionalField is null
481
+ expect(ids).toEqual(["doc-02", "doc-04", "doc-06", "doc-08", "doc-10"]);
482
+ });
483
+
484
+ it("$exists: true on always-present field returns all", async () => {
485
+ const ids = await queryIds({ status: { $exists: true } });
486
+ expect(ids).toHaveLength(10);
487
+ });
488
+ });
489
+
490
+ // ============================================================
491
+ // LOGICAL OPERATORS
492
+ // ============================================================
493
+
494
+ describe("logical operators", () => {
495
+ it("implicit AND with multiple fields", async () => {
496
+ const ids = await queryIds({ status: "active", flag: true });
497
+ expect(ids).toEqual(["doc-01", "doc-07"]);
498
+ });
499
+
500
+ it("$and with two conditions", async () => {
501
+ const ids = await queryIds({
502
+ $and: [{ status: "active" }, { num: { $gte: 0 } }],
503
+ });
504
+ expect(ids).toEqual(["doc-01", "doc-02", "doc-06", "doc-10"]);
505
+ });
506
+
507
+ it("$and with three conditions", async () => {
508
+ const ids = await queryIds({
509
+ $and: [{ status: "active" }, { flag: true }, { num: { $gt: 0 } }],
510
+ });
511
+ expect(ids).toEqual(["doc-01"]);
512
+ });
513
+
514
+ it("$or with two conditions", async () => {
515
+ const ids = await queryIds({
516
+ $or: [{ status: "deleted" }, { num: { $lt: 0 } }],
517
+ });
518
+ expect(ids).toEqual(["doc-05", "doc-07", "doc-08"]);
519
+ });
520
+
521
+ it("$or with equality on same field", async () => {
522
+ const ids = await queryIds({
523
+ $or: [{ num: 10 }, { num: 20 }, { num: 30 }],
524
+ });
525
+ expect(ids).toEqual(["doc-01", "doc-02", "doc-03"]);
526
+ });
527
+
528
+ it("$not with simple condition", async () => {
529
+ const ids = await queryIds({
530
+ $not: { status: "active" },
531
+ });
532
+ expect(ids).toEqual(["doc-03", "doc-04", "doc-05", "doc-08", "doc-09"]);
533
+ });
534
+
535
+ it("$not with comparison", async () => {
536
+ const ids = await queryIds({
537
+ $not: { num: { $gte: 30 } },
538
+ });
539
+ expect(ids).toEqual(["doc-01", "doc-02", "doc-06", "doc-07", "doc-09"]);
540
+ });
541
+
542
+ it("$not with boolean", async () => {
543
+ const ids = await queryIds({
544
+ $not: { flag: true },
545
+ });
546
+ expect(ids).toEqual(["doc-02", "doc-04", "doc-06", "doc-08", "doc-10"]);
547
+ });
548
+
549
+ it("AND of ORs", async () => {
550
+ const ids = await queryIds({
551
+ $and: [
552
+ { $or: [{ status: "active" }, { status: "pending" }] },
553
+ { $or: [{ flag: true }] },
554
+ ],
555
+ });
556
+ expect(ids).toEqual(["doc-01", "doc-03", "doc-07", "doc-09"]);
557
+ });
558
+
559
+ it("OR of ANDs", async () => {
560
+ const ids = await queryIds({
561
+ $or: [
562
+ { status: "active", flag: true },
563
+ { status: "deleted", flag: false },
564
+ ],
565
+ });
566
+ expect(ids).toEqual(["doc-01", "doc-07", "doc-08"]);
567
+ });
568
+
569
+ it("deeply nested filter", async () => {
570
+ const ids = await queryIds({
571
+ $and: [
572
+ { num: { $gte: 0 } },
573
+ {
574
+ $or: [
575
+ { status: "active", flag: true },
576
+ {
577
+ $and: [{ status: "pending" }, { num: { $gte: 25 } }],
578
+ },
579
+ ],
580
+ },
581
+ ],
582
+ });
583
+ // doc-01: active + flag=true + num >= 0
584
+ // doc-03: pending + num 30 >= 25
585
+ // doc-04: pending + num 40 >= 25
586
+ // doc-09: pending + num 25 >= 25
587
+ expect(ids).toEqual(["doc-01", "doc-03", "doc-04", "doc-09"]);
588
+ });
589
+
590
+ it("empty $and returns all results", async () => {
591
+ const ids = await queryIds({ $and: [] });
592
+ expect(ids).toHaveLength(10);
593
+ });
594
+
595
+ it("empty $or returns all results", async () => {
596
+ const ids = await queryIds({ $or: [] });
597
+ expect(ids).toHaveLength(10);
598
+ });
599
+
600
+ it("$and with single condition", async () => {
601
+ const ids = await queryIds({ $and: [{ status: "deleted" }] });
602
+ expect(ids).toEqual(["doc-05", "doc-08"]);
603
+ });
604
+
605
+ it("$or with single condition", async () => {
606
+ const ids = await queryIds({ $or: [{ status: "deleted" }] });
607
+ expect(ids).toEqual(["doc-05", "doc-08"]);
608
+ });
609
+ });
610
+
611
+ // ============================================================
612
+ // COMBINED FILTER + FIELD ASSERTIONS
613
+ // ============================================================
614
+
615
+ describe("filter result validation", () => {
616
+ it("filtered results have correct field values", async () => {
617
+ const hits = await handle.query({
618
+ query: [{ embedding: QUERY_VECTOR }],
619
+ topK: 100,
620
+ filter: { status: "pending" },
621
+ });
622
+
623
+ expect(hits.length).toBe(3);
624
+ for (const hit of hits) {
625
+ expect(hit.document?.status).toBe("pending");
626
+ }
627
+
628
+ const nums = hits
629
+ .map((h) => h.document?.num)
630
+ .sort((a, b) => (a ?? 0) - (b ?? 0));
631
+ expect(nums).toEqual([25, 30, 40]);
632
+ });
633
+
634
+ it("complex filter returns expected documents with correct data", async () => {
635
+ const hits = await handle.query({
636
+ query: [{ embedding: QUERY_VECTOR }],
637
+ topK: 100,
638
+ filter: {
639
+ $and: [{ num: { $gte: 20, $lte: 50 } }, { flag: true }],
640
+ },
641
+ });
642
+
643
+ expect(hits.length).toBe(3);
644
+ const names = hits.map((h) => h.document?.name).sort();
645
+ expect(names).toEqual(["charlie_smith", "eve_johnson", "ivy_taylor"]);
646
+
647
+ for (const hit of hits) {
648
+ expect(hit.document?.flag).toBe(true);
649
+ expect(hit.document?.num).toBeGreaterThanOrEqual(20);
650
+ expect(hit.document?.num).toBeLessThanOrEqual(50);
651
+ }
652
+ });
653
+
654
+ it("combining filter + topK limits correctly", async () => {
655
+ const hits = await handle.query({
656
+ query: [{ embedding: QUERY_VECTOR }],
657
+ topK: 2,
658
+ filter: { status: "active" },
659
+ });
660
+
661
+ expect(hits.length).toBe(2);
662
+ for (const hit of hits) {
663
+ expect(hit.document?.status).toBe("active");
664
+ }
665
+ });
666
+ });
667
+
668
+ // ============================================================
669
+ // NULL HANDLING
670
+ // ============================================================
671
+
672
+ describe("null handling", () => {
673
+ it("equality shorthand with null matches null values", async () => {
674
+ const ids = await queryIds({ optionalField: null });
675
+ expect(ids).toEqual(["doc-02", "doc-04", "doc-06", "doc-08", "doc-10"]);
676
+ });
677
+
678
+ it("$neq: null excludes null values", async () => {
679
+ const ids = await queryIds({ optionalField: { $neq: null } });
680
+ // This returns docs where optionalField IS NOT NULL
681
+ // Note: In SQL, `col != NULL` is always false, but our impl uses IS NOT NULL
682
+ // Actually looking at the code, $neq: null uses != $N with null param
683
+ // which in Postgres returns no rows. Let's see what behavior we get.
684
+ // If this fails, it reveals a semantic issue we should address.
685
+ expect(ids).toEqual([]);
686
+ });
687
+ });
688
+
689
+ // ============================================================
690
+ // EDGE CASES
691
+ // ============================================================
692
+
693
+ describe("edge cases", () => {
694
+ it("empty filter returns all results", async () => {
695
+ const ids = await queryIds({});
696
+ expect(ids).toHaveLength(10);
697
+ });
698
+
699
+ it("filter with undefined values is ignored", async () => {
700
+ const ids = await queryIds({ status: "active", bogus: undefined } as any);
701
+ expect(ids).toEqual(["doc-01", "doc-02", "doc-06", "doc-07", "doc-10"]);
702
+ });
703
+
704
+ it("multiple operators on same field all apply", async () => {
705
+ // num >= 10 AND num <= 30 AND num != 20
706
+ const ids = await queryIds({ num: { $gte: 10, $lte: 30, $neq: 20 } });
707
+ expect(ids).toEqual(["doc-01", "doc-03", "doc-09"]);
708
+ });
709
+
710
+ it("filter with special characters in value", async () => {
711
+ // No docs match this, but it shouldn't error
712
+ const ids = await queryIds({ name: "'; DROP TABLE users; --" });
713
+ expect(ids).toEqual([]);
714
+ });
715
+
716
+ it("filter on non-existent field returns empty", async () => {
717
+ // Postgres will error on non-existent column
718
+ await expect(queryIds({ nonexistent: "value" } as any)).rejects.toThrow();
719
+ });
720
+ });
721
+ });