@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,548 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { sqlize } from "../query";
3
+ import { SQL_SELECT } from "../select";
4
+ import { SQL_WHERE } from "../where";
5
+ import { SQL_ORDER } from "../order";
6
+ import { SQL_LIMIT } from "../limit";
7
+
8
+ describe("sqlize", () => {
9
+ it("converts query with vector signal", () => {
10
+ const vector = [0.1, 0.2, 0.3];
11
+ const result = sqlize(
12
+ { query: [{ embedding: vector }] },
13
+ { pkey: "id", schema: "public", table: "docs" },
14
+ );
15
+
16
+ expect(result.select).toEqual({
17
+ pkey: "id",
18
+ signals: [{ embedding: vector }],
19
+ binding: undefined,
20
+ include: undefined,
21
+ });
22
+ expect(result.where).toEqual({ filter: undefined });
23
+ expect(result.order).toEqual({
24
+ signals: [{ embedding: vector }],
25
+ orderBy: undefined,
26
+ binding: undefined,
27
+ schema: "public",
28
+ table: "docs",
29
+ });
30
+ expect(result.limit).toEqual({ topK: 10, offset: 0 });
31
+ });
32
+
33
+ it("throws on multi-signal fusion (not supported by pgvector)", () => {
34
+ expect(() =>
35
+ sqlize(
36
+ {
37
+ max: [
38
+ { content: "search", weight: 0.3 },
39
+ { embedding: [0.1, 0.2], weight: 0.7 },
40
+ ],
41
+ },
42
+ { pkey: "doc_id", schema: "public", table: "docs" },
43
+ ),
44
+ ).toThrow("pgvector does not support multi-signal fusion");
45
+ });
46
+
47
+ it("converts query with filter", () => {
48
+ const result = sqlize(
49
+ {
50
+ query: [{ embedding: [0.1, 0.2] }],
51
+ filter: { status: "active", views: { $gt: 100 } },
52
+ },
53
+ { pkey: "id", schema: "public", table: "docs" },
54
+ );
55
+
56
+ expect(result.where.filter).toEqual({
57
+ status: "active",
58
+ views: { $gt: 100 },
59
+ });
60
+ });
61
+
62
+ it("converts query with orderBy", () => {
63
+ const result = sqlize(
64
+ {
65
+ filter: { status: "active" },
66
+ orderBy: { field: "created_at", direction: "desc" },
67
+ },
68
+ { pkey: "id", schema: "public", table: "docs" },
69
+ );
70
+
71
+ expect(result.order.orderBy).toEqual({
72
+ field: "created_at",
73
+ direction: "desc",
74
+ });
75
+ });
76
+
77
+ it("converts query with pagination", () => {
78
+ const result = sqlize(
79
+ {
80
+ query: [{ embedding: [0.1, 0.2] }],
81
+ topK: 25,
82
+ offset: 50,
83
+ },
84
+ { pkey: "id", schema: "public", table: "docs" },
85
+ );
86
+
87
+ expect(result.limit).toEqual({ topK: 25, offset: 50 });
88
+ });
89
+
90
+ it("uses default pagination values", () => {
91
+ const result = sqlize({ query: [{ embedding: [0.1, 0.2] }] }, { pkey: "id", schema: "public", table: "docs" });
92
+
93
+ expect(result.limit).toEqual({ topK: 10, offset: 0 });
94
+ });
95
+
96
+ it("passes binding through to all inputs", () => {
97
+ const binding = {
98
+ schema: "public",
99
+ table: "docs",
100
+ pkey: "id",
101
+ fields: {
102
+ embedding: {
103
+ column: "vec_col",
104
+ type: "vector" as const,
105
+ dimensions: 3,
106
+ similarity: "cosine" as const,
107
+ },
108
+ },
109
+ };
110
+
111
+ const result = sqlize(
112
+ { query: [{ embedding: [0.1, 0.2, 0.3] }] },
113
+ { pkey: "id", schema: "public", table: "docs", binding },
114
+ );
115
+
116
+ expect(result.select.binding).toBe(binding);
117
+ expect(result.order.binding).toBe(binding);
118
+ });
119
+
120
+ it("uses custom pkey", () => {
121
+ const result = sqlize(
122
+ { query: [{ embedding: [0.1, 0.2] }] },
123
+ { pkey: "document_id", schema: "public", table: "docs" },
124
+ );
125
+
126
+ expect(result.select.pkey).toBe("document_id");
127
+ });
128
+
129
+ it("handles empty query (filter-only)", () => {
130
+ const result = sqlize(
131
+ {
132
+ filter: { status: "active" },
133
+ orderBy: { field: "created_at", direction: "desc" },
134
+ topK: 100,
135
+ },
136
+ { pkey: "id", schema: "public", table: "docs" },
137
+ );
138
+
139
+ expect(result.select.signals).toEqual([]);
140
+ expect(result.order.signals).toEqual([]);
141
+ expect(result.where.filter).toEqual({ status: "active" });
142
+ });
143
+
144
+ describe("signal priority", () => {
145
+ it("prefers query over max when both present", () => {
146
+ const querySignals = [{ embedding: [0.1, 0.2, 0.3] }];
147
+ const maxSignals = [{ embedding: [0.4, 0.5, 0.6] }];
148
+
149
+ const result = sqlize(
150
+ { query: querySignals, max: maxSignals } as any,
151
+ { pkey: "id", schema: "public", table: "docs" },
152
+ );
153
+
154
+ // query takes precedence over max
155
+ expect(result.select.signals).toEqual(querySignals);
156
+ expect(result.order.signals).toEqual(querySignals);
157
+ });
158
+
159
+ it("uses max when query is undefined", () => {
160
+ const maxSignals = [{ embedding: [0.4, 0.5, 0.6] }];
161
+
162
+ const result = sqlize(
163
+ { max: maxSignals },
164
+ { pkey: "id", schema: "public", table: "docs" },
165
+ );
166
+
167
+ expect(result.select.signals).toEqual(maxSignals);
168
+ expect(result.order.signals).toEqual(maxSignals);
169
+ });
170
+
171
+ it("uses empty array when both query and max are undefined", () => {
172
+ const result = sqlize(
173
+ { filter: { status: "active" } },
174
+ { pkey: "id", schema: "public", table: "docs" },
175
+ );
176
+
177
+ expect(result.select.signals).toEqual([]);
178
+ expect(result.order.signals).toEqual([]);
179
+ });
180
+ });
181
+
182
+ describe("filter-only with binding", () => {
183
+ it("passes binding through for filter-only queries", () => {
184
+ const binding = {
185
+ schema: "public",
186
+ table: "docs",
187
+ pkey: "id",
188
+ fields: {
189
+ embedding: {
190
+ column: "vec_col",
191
+ type: "vector" as const,
192
+ dimensions: 3,
193
+ similarity: "cosine" as const,
194
+ },
195
+ },
196
+ };
197
+
198
+ const result = sqlize(
199
+ { filter: { status: "active" }, topK: 50 },
200
+ { pkey: "id", schema: "public", table: "docs", binding },
201
+ );
202
+
203
+ expect(result.select.binding).toBe(binding);
204
+ expect(result.order.binding).toBe(binding);
205
+ expect(result.select.signals).toEqual([]);
206
+ });
207
+ });
208
+
209
+ describe("orderBy-only queries", () => {
210
+ it("handles orderBy without query signals", () => {
211
+ const result = sqlize(
212
+ {
213
+ orderBy: { field: "created_at", direction: "asc" },
214
+ topK: 20,
215
+ },
216
+ { pkey: "id", schema: "public", table: "docs" },
217
+ );
218
+
219
+ expect(result.select.signals).toEqual([]);
220
+ expect(result.order.signals).toEqual([]);
221
+ expect(result.order.orderBy).toEqual({
222
+ field: "created_at",
223
+ direction: "asc",
224
+ });
225
+ expect(result.limit).toEqual({ topK: 20, offset: 0 });
226
+ });
227
+ });
228
+
229
+ describe("edge cases", () => {
230
+ it("handles query with empty signals array", () => {
231
+ const result = sqlize(
232
+ { query: [] },
233
+ { pkey: "id", schema: "public", table: "docs" },
234
+ );
235
+
236
+ expect(result.select.signals).toEqual([]);
237
+ expect(result.order.signals).toEqual([]);
238
+ });
239
+
240
+ it("handles all options together", () => {
241
+ const binding = {
242
+ schema: "public",
243
+ table: "docs",
244
+ pkey: "id",
245
+ fields: {
246
+ embedding: {
247
+ column: "vec_col",
248
+ type: "vector" as const,
249
+ dimensions: 3,
250
+ similarity: "euclidean" as const,
251
+ },
252
+ },
253
+ };
254
+
255
+ const result = sqlize(
256
+ {
257
+ query: [{ embedding: [0.1, 0.2, 0.3] }],
258
+ filter: { status: "active", views: { $gt: 100 } },
259
+ orderBy: { field: "created_at", direction: "desc" },
260
+ topK: 50,
261
+ offset: 25,
262
+ },
263
+ { pkey: "doc_id", schema: "public", table: "documents", binding },
264
+ );
265
+
266
+ expect(result.select.pkey).toBe("doc_id");
267
+ expect(result.select.signals).toHaveLength(1);
268
+ expect(result.select.binding).toBe(binding);
269
+ expect(result.where.filter).toEqual({
270
+ status: "active",
271
+ views: { $gt: 100 },
272
+ });
273
+ expect(result.order.orderBy).toEqual({
274
+ field: "created_at",
275
+ direction: "desc",
276
+ });
277
+ expect(result.order.binding).toBe(binding);
278
+ expect(result.limit).toEqual({ topK: 50, offset: 25 });
279
+ });
280
+
281
+ it("handles undefined filter", () => {
282
+ const result = sqlize(
283
+ { query: [{ embedding: [0.1, 0.2] }] },
284
+ { pkey: "id", schema: "public", table: "docs" },
285
+ );
286
+
287
+ expect(result.where.filter).toBeUndefined();
288
+ });
289
+
290
+ it("handles undefined orderBy", () => {
291
+ const result = sqlize(
292
+ { query: [{ embedding: [0.1, 0.2] }] },
293
+ { pkey: "id", schema: "public", table: "docs" },
294
+ );
295
+
296
+ expect(result.order.orderBy).toBeUndefined();
297
+ });
298
+ });
299
+ });
300
+
301
+ /**
302
+ * Full pipeline integration tests.
303
+ * These tests verify that sqlize → all codecs → assembled SQL works correctly.
304
+ */
305
+ describe("full pipeline integration", () => {
306
+ it("assembles complete SQL with vector search, filter, and pagination", () => {
307
+ const vector = [0.1, 0.2, 0.3];
308
+ const binding = {
309
+ schema: "public",
310
+ table: "documents",
311
+ pkey: "id",
312
+ fields: {
313
+ embedding: {
314
+ column: "vec_col",
315
+ type: "vector" as const,
316
+ dimensions: 3,
317
+ similarity: "cosine" as const,
318
+ },
319
+ },
320
+ };
321
+
322
+ // Step 1: sqlize the query
323
+ const query = sqlize(
324
+ {
325
+ query: [{ embedding: vector }],
326
+ filter: { status: "active", views: { $gt: 100 } },
327
+ topK: 25,
328
+ offset: 50,
329
+ },
330
+ { pkey: "doc_id", schema: "public", table: "documents", binding },
331
+ );
332
+
333
+ // Step 2: encode each clause (include: false to exclude extra columns)
334
+ const select = SQL_SELECT.encode({ ...query.select, include: false });
335
+ const whereStartIdx = 1 + select.params.length;
336
+ const where = SQL_WHERE.encode({ ...query.where, startIdx: whereStartIdx });
337
+ const order = SQL_ORDER.encode(query.order);
338
+ const limitStartIdx = whereStartIdx + where.params.length;
339
+ const limit = SQL_LIMIT.encode({ ...query.limit, startIdx: limitStartIdx });
340
+
341
+ // Step 3: verify individual clauses
342
+ expect(select.sql).toBe(
343
+ '"doc_id" as id, 1 - ("vec_col" <=> $1::vector) as score',
344
+ );
345
+ expect(select.params).toEqual([JSON.stringify(vector)]);
346
+
347
+ expect(where.sql).toBe('"status" = $2 AND "views" > $3');
348
+ expect(where.params).toEqual(["active", 100]);
349
+
350
+ expect(order.sql).toBe('"vec_col" <=> $1::vector');
351
+
352
+ expect(limit.sql).toBe("LIMIT $4 OFFSET $5");
353
+ expect(limit.params).toEqual([25, 50]);
354
+
355
+ // Step 4: verify assembled params array
356
+ const allParams = [...select.params, ...where.params, ...limit.params];
357
+ expect(allParams).toEqual([
358
+ JSON.stringify(vector), // $1
359
+ "active", // $2
360
+ 100, // $3
361
+ 25, // $4
362
+ 50, // $5
363
+ ]);
364
+
365
+ // Step 5: verify param indices are correct
366
+ expect(whereStartIdx).toBe(2);
367
+ expect(limitStartIdx).toBe(4);
368
+ });
369
+
370
+ it("assembles SQL with filter-only query (no vector)", () => {
371
+ const query = sqlize(
372
+ {
373
+ filter: {
374
+ status: "published",
375
+ $or: [{ featured: true }, { views: { $gte: 1000 } }],
376
+ },
377
+ orderBy: { field: "created_at", direction: "desc" },
378
+ topK: 10,
379
+ },
380
+ { pkey: "id", schema: "public", table: "docs" },
381
+ );
382
+
383
+ const select = SQL_SELECT.encode(query.select);
384
+ const whereStartIdx = 1 + select.params.length;
385
+ const where = SQL_WHERE.encode({ ...query.where, startIdx: whereStartIdx });
386
+ const order = SQL_ORDER.encode(query.order);
387
+ const limitStartIdx = whereStartIdx + where.params.length;
388
+ const limit = SQL_LIMIT.encode({ ...query.limit, startIdx: limitStartIdx });
389
+
390
+ // No vector signal, so no params from SELECT
391
+ expect(select.sql).toBe('"id" as id, 1 as score');
392
+ expect(select.params).toEqual([]);
393
+
394
+ // WHERE starts at $1 since SELECT has no params
395
+ expect(where.sql).toBe(
396
+ '"status" = $1 AND (("featured" = $2) OR ("views" >= $3))',
397
+ );
398
+ expect(where.params).toEqual(["published", true, 1000]);
399
+
400
+ // ORDER BY uses explicit field (table-qualified to avoid ambiguity)
401
+ expect(order.sql).toBe('"public"."docs"."created_at" DESC');
402
+
403
+ // LIMIT starts at $4
404
+ expect(limit.sql).toBe("LIMIT $4");
405
+ expect(limit.params).toEqual([10]);
406
+ });
407
+
408
+ it("assembles SQL with complex nested filter", () => {
409
+ const vector = [0.5, 0.5];
410
+
411
+ const query = sqlize(
412
+ {
413
+ query: [{ embedding: vector }],
414
+ filter: {
415
+ $and: [
416
+ { type: "article" },
417
+ {
418
+ $or: [
419
+ { status: "published" },
420
+ { $and: [{ status: "draft" }, { author_id: 123 }] },
421
+ ],
422
+ },
423
+ ],
424
+ deleted_at: null,
425
+ },
426
+ topK: 5,
427
+ },
428
+ { pkey: "id", schema: "public", table: "docs" },
429
+ );
430
+
431
+ const select = SQL_SELECT.encode(query.select);
432
+ const whereStartIdx = 1 + select.params.length;
433
+ const where = SQL_WHERE.encode({ ...query.where, startIdx: whereStartIdx });
434
+ const limitStartIdx = whereStartIdx + where.params.length;
435
+ const limit = SQL_LIMIT.encode({ ...query.limit, startIdx: limitStartIdx });
436
+
437
+ // SELECT uses $1
438
+ expect(select.params).toHaveLength(1);
439
+
440
+ // WHERE uses $2-$6
441
+ expect(where.sql).toBe(
442
+ '(("type" = $2) AND ((("status" = $3) OR ((("status" = $4) AND ("author_id" = $5)))))) AND "deleted_at" IS NULL',
443
+ );
444
+ expect(where.params).toEqual(["article", "published", "draft", 123]);
445
+
446
+ // LIMIT uses $6
447
+ expect(limit.sql).toBe("LIMIT $6");
448
+ expect(limit.params).toEqual([5]);
449
+ });
450
+
451
+ it("handles euclidean distance correctly through pipeline", () => {
452
+ const vector = [1.0, 2.0, 3.0];
453
+ const binding = {
454
+ schema: "public",
455
+ table: "docs",
456
+ pkey: "id",
457
+ fields: {
458
+ embedding: {
459
+ column: "embedding",
460
+ type: "vector" as const,
461
+ dimensions: 3,
462
+ similarity: "euclidean" as const,
463
+ },
464
+ },
465
+ };
466
+
467
+ const query = sqlize(
468
+ { query: [{ embedding: vector }], topK: 10 },
469
+ { pkey: "id", schema: "public", table: "docs", binding },
470
+ );
471
+
472
+ const select = SQL_SELECT.encode({ ...query.select, include: false });
473
+ const order = SQL_ORDER.encode(query.order);
474
+
475
+ // Euclidean uses <-> operator
476
+ expect(select.sql).toBe(
477
+ '"id" as id, 1 / (1 + ("embedding" <-> $1::vector)) as score',
478
+ );
479
+ expect(order.sql).toBe('"embedding" <-> $1::vector');
480
+ });
481
+
482
+ it("handles dot product correctly through pipeline", () => {
483
+ const vector = [1.0, 2.0, 3.0];
484
+ const binding = {
485
+ schema: "public",
486
+ table: "docs",
487
+ pkey: "id",
488
+ fields: {
489
+ embedding: {
490
+ column: "embedding",
491
+ type: "vector" as const,
492
+ dimensions: 3,
493
+ similarity: "dot_product" as const,
494
+ },
495
+ },
496
+ };
497
+
498
+ const query = sqlize(
499
+ { query: [{ embedding: vector }], topK: 10 },
500
+ { pkey: "id", schema: "public", table: "docs", binding },
501
+ );
502
+
503
+ const select = SQL_SELECT.encode({ ...query.select, include: false });
504
+ const order = SQL_ORDER.encode(query.order);
505
+
506
+ // Dot product uses <#> operator
507
+ expect(select.sql).toBe(
508
+ '"id" as id, -("embedding" <#> $1::vector) as score',
509
+ );
510
+ expect(order.sql).toBe('"embedding" <#> $1::vector');
511
+ });
512
+
513
+ it("guards against parameter index drift", () => {
514
+ // This test ensures that when we chain all codecs together,
515
+ // the parameter indices don't drift or overlap
516
+
517
+ const query = sqlize(
518
+ {
519
+ query: [{ embedding: [0.1, 0.2] }],
520
+ filter: { a: 1, b: 2, c: 3 },
521
+ topK: 10,
522
+ offset: 20,
523
+ },
524
+ { pkey: "id", schema: "public", table: "docs" },
525
+ );
526
+
527
+ const select = SQL_SELECT.encode(query.select);
528
+ const whereStartIdx = 1 + select.params.length;
529
+ const where = SQL_WHERE.encode({ ...query.where, startIdx: whereStartIdx });
530
+ const limitStartIdx = whereStartIdx + where.params.length;
531
+ const limit = SQL_LIMIT.encode({ ...query.limit, startIdx: limitStartIdx });
532
+
533
+ // Verify no gaps in indices
534
+ // SELECT: $1 (vector)
535
+ // WHERE: $2, $3, $4 (a, b, c)
536
+ // LIMIT: $5, $6 (topK, offset)
537
+
538
+ expect(select.params).toHaveLength(1); // $1
539
+ expect(whereStartIdx).toBe(2);
540
+ expect(where.params).toHaveLength(3); // $2, $3, $4
541
+ expect(limitStartIdx).toBe(5);
542
+ expect(limit.params).toHaveLength(2); // $5, $6
543
+
544
+ // Total params should be 6
545
+ const allParams = [...select.params, ...where.params, ...limit.params];
546
+ expect(allParams).toHaveLength(6);
547
+ });
548
+ });