@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,554 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { SQL_WHERE } from "../where";
3
+
4
+ describe("SQL_WHERE", () => {
5
+ describe("encode", () => {
6
+ it("returns empty sql when no filter", () => {
7
+ const result = SQL_WHERE.encode({ startIdx: 1 });
8
+ expect(result.sql).toBe("");
9
+ expect(result.params).toEqual([]);
10
+ });
11
+
12
+ it("returns empty sql when filter is undefined", () => {
13
+ const result = SQL_WHERE.encode({ filter: undefined, startIdx: 1 });
14
+ expect(result.sql).toBe("");
15
+ expect(result.params).toEqual([]);
16
+ });
17
+
18
+ describe("equality shorthand", () => {
19
+ it("handles string equality", () => {
20
+ const result = SQL_WHERE.encode({
21
+ filter: { status: "active" },
22
+ startIdx: 1,
23
+ });
24
+ expect(result.sql).toBe('"status" = $1');
25
+ expect(result.params).toEqual(["active"]);
26
+ });
27
+
28
+ it("handles number equality", () => {
29
+ const result = SQL_WHERE.encode({
30
+ filter: { count: 42 },
31
+ startIdx: 1,
32
+ });
33
+ expect(result.sql).toBe('"count" = $1');
34
+ expect(result.params).toEqual([42]);
35
+ });
36
+
37
+ it("handles boolean equality", () => {
38
+ const result = SQL_WHERE.encode({
39
+ filter: { active: true },
40
+ startIdx: 1,
41
+ });
42
+ expect(result.sql).toBe('"active" = $1');
43
+ expect(result.params).toEqual([true]);
44
+ });
45
+
46
+ it("handles null as IS NULL", () => {
47
+ const result = SQL_WHERE.encode({
48
+ filter: { deleted_at: null },
49
+ startIdx: 1,
50
+ });
51
+ expect(result.sql).toBe('"deleted_at" IS NULL');
52
+ expect(result.params).toEqual([]);
53
+ });
54
+
55
+ it("combines multiple fields with AND", () => {
56
+ const result = SQL_WHERE.encode({
57
+ filter: { status: "active", type: "doc" },
58
+ startIdx: 1,
59
+ });
60
+ expect(result.sql).toBe('"status" = $1 AND "type" = $2');
61
+ expect(result.params).toEqual(["active", "doc"]);
62
+ });
63
+
64
+ it("respects startIdx for parameter numbering", () => {
65
+ const result = SQL_WHERE.encode({
66
+ filter: { status: "active" },
67
+ startIdx: 5,
68
+ });
69
+ expect(result.sql).toBe('"status" = $5');
70
+ expect(result.params).toEqual(["active"]);
71
+ });
72
+ });
73
+
74
+ describe("comparison operators", () => {
75
+ it("handles $eq", () => {
76
+ const result = SQL_WHERE.encode({
77
+ filter: { count: { $eq: 10 } },
78
+ startIdx: 1,
79
+ });
80
+ expect(result.sql).toBe('"count" = $1');
81
+ expect(result.params).toEqual([10]);
82
+ });
83
+
84
+ it("handles $neq", () => {
85
+ const result = SQL_WHERE.encode({
86
+ filter: { status: { $neq: "deleted" } },
87
+ startIdx: 1,
88
+ });
89
+ expect(result.sql).toBe('"status" != $1');
90
+ expect(result.params).toEqual(["deleted"]);
91
+ });
92
+
93
+ it("handles $gt", () => {
94
+ const result = SQL_WHERE.encode({
95
+ filter: { views: { $gt: 100 } },
96
+ startIdx: 1,
97
+ });
98
+ expect(result.sql).toBe('"views" > $1');
99
+ expect(result.params).toEqual([100]);
100
+ });
101
+
102
+ it("handles $gte", () => {
103
+ const result = SQL_WHERE.encode({
104
+ filter: { views: { $gte: 100 } },
105
+ startIdx: 1,
106
+ });
107
+ expect(result.sql).toBe('"views" >= $1');
108
+ expect(result.params).toEqual([100]);
109
+ });
110
+
111
+ it("handles $lt", () => {
112
+ const result = SQL_WHERE.encode({
113
+ filter: { price: { $lt: 50 } },
114
+ startIdx: 1,
115
+ });
116
+ expect(result.sql).toBe('"price" < $1');
117
+ expect(result.params).toEqual([50]);
118
+ });
119
+
120
+ it("handles $lte", () => {
121
+ const result = SQL_WHERE.encode({
122
+ filter: { price: { $lte: 50 } },
123
+ startIdx: 1,
124
+ });
125
+ expect(result.sql).toBe('"price" <= $1');
126
+ expect(result.params).toEqual([50]);
127
+ });
128
+
129
+ it("handles multiple operators on same field", () => {
130
+ const result = SQL_WHERE.encode({
131
+ filter: { price: { $gte: 10, $lte: 100 } },
132
+ startIdx: 1,
133
+ });
134
+ expect(result.sql).toBe('"price" >= $1 AND "price" <= $2');
135
+ expect(result.params).toEqual([10, 100]);
136
+ });
137
+ });
138
+
139
+ describe("set operators", () => {
140
+ it("handles $in", () => {
141
+ const result = SQL_WHERE.encode({
142
+ filter: { status: { $in: ["active", "pending"] } },
143
+ startIdx: 1,
144
+ });
145
+ expect(result.sql).toBe('"status" = ANY($1)');
146
+ expect(result.params).toEqual([["active", "pending"]]);
147
+ });
148
+
149
+ it("handles $nin", () => {
150
+ const result = SQL_WHERE.encode({
151
+ filter: { status: { $nin: ["deleted", "archived"] } },
152
+ startIdx: 1,
153
+ });
154
+ expect(result.sql).toBe('"status" != ALL($1)');
155
+ expect(result.params).toEqual([["deleted", "archived"]]);
156
+ });
157
+ });
158
+
159
+ describe("string operators", () => {
160
+ it("handles $contains", () => {
161
+ const result = SQL_WHERE.encode({
162
+ filter: { title: { $contains: "hello" } },
163
+ startIdx: 1,
164
+ });
165
+ expect(result.sql).toBe('"title" ILIKE $1');
166
+ expect(result.params).toEqual(["%hello%"]);
167
+ });
168
+
169
+ it("handles $startsWith", () => {
170
+ const result = SQL_WHERE.encode({
171
+ filter: { name: { $startsWith: "Dr." } },
172
+ startIdx: 1,
173
+ });
174
+ expect(result.sql).toBe('"name" ILIKE $1');
175
+ expect(result.params).toEqual(["Dr.%"]);
176
+ });
177
+
178
+ it("handles $endsWith", () => {
179
+ const result = SQL_WHERE.encode({
180
+ filter: { email: { $endsWith: "@example.com" } },
181
+ startIdx: 1,
182
+ });
183
+ expect(result.sql).toBe('"email" ILIKE $1');
184
+ expect(result.params).toEqual(["%@example.com"]);
185
+ });
186
+ });
187
+
188
+ describe("existence operators", () => {
189
+ it("handles $exists: true", () => {
190
+ const result = SQL_WHERE.encode({
191
+ filter: { avatar: { $exists: true } },
192
+ startIdx: 1,
193
+ });
194
+ expect(result.sql).toBe('"avatar" IS NOT NULL');
195
+ expect(result.params).toEqual([]);
196
+ });
197
+
198
+ it("handles $exists: false", () => {
199
+ const result = SQL_WHERE.encode({
200
+ filter: { avatar: { $exists: false } },
201
+ startIdx: 1,
202
+ });
203
+ expect(result.sql).toBe('"avatar" IS NULL');
204
+ expect(result.params).toEqual([]);
205
+ });
206
+ });
207
+
208
+ describe("logical operators", () => {
209
+ it("handles $and", () => {
210
+ const result = SQL_WHERE.encode({
211
+ filter: {
212
+ $and: [{ status: "active" }, { views: { $gt: 100 } }],
213
+ },
214
+ startIdx: 1,
215
+ });
216
+ expect(result.sql).toBe('(("status" = $1) AND ("views" > $2))');
217
+ expect(result.params).toEqual(["active", 100]);
218
+ });
219
+
220
+ it("handles $or", () => {
221
+ const result = SQL_WHERE.encode({
222
+ filter: {
223
+ $or: [{ status: "draft" }, { status: "review" }],
224
+ },
225
+ startIdx: 1,
226
+ });
227
+ expect(result.sql).toBe('(("status" = $1) OR ("status" = $2))');
228
+ expect(result.params).toEqual(["draft", "review"]);
229
+ });
230
+
231
+ it("handles $not", () => {
232
+ const result = SQL_WHERE.encode({
233
+ filter: {
234
+ $not: { status: "deleted" },
235
+ },
236
+ startIdx: 1,
237
+ });
238
+ expect(result.sql).toBe('NOT ("status" = $1)');
239
+ expect(result.params).toEqual(["deleted"]);
240
+ });
241
+
242
+ it("handles nested logical operators", () => {
243
+ const result = SQL_WHERE.encode({
244
+ filter: {
245
+ $and: [
246
+ { type: "article" },
247
+ {
248
+ $or: [{ status: "published" }, { featured: true }],
249
+ },
250
+ ],
251
+ },
252
+ startIdx: 1,
253
+ });
254
+ expect(result.sql).toBe(
255
+ '(("type" = $1) AND ((("status" = $2) OR ("featured" = $3))))',
256
+ );
257
+ expect(result.params).toEqual(["article", "published", true]);
258
+ });
259
+ });
260
+
261
+ describe("complex filters", () => {
262
+ it("handles real-world filter", () => {
263
+ const result = SQL_WHERE.encode({
264
+ filter: {
265
+ published: true,
266
+ views: { $gte: 1000 },
267
+ tags: { $in: ["ai", "ml"] },
268
+ $or: [{ featured: true }, { category: "trending" }],
269
+ },
270
+ startIdx: 2, // simulate SELECT using $1
271
+ });
272
+
273
+ expect(result.sql).toBe(
274
+ '"published" = $2 AND "views" >= $3 AND "tags" = ANY($4) AND (("featured" = $5) OR ("category" = $6))',
275
+ );
276
+ expect(result.params).toEqual([
277
+ true,
278
+ 1000,
279
+ ["ai", "ml"],
280
+ true,
281
+ "trending",
282
+ ]);
283
+ });
284
+
285
+ it("skips undefined values", () => {
286
+ const result = SQL_WHERE.encode({
287
+ filter: { status: "active", deleted: undefined },
288
+ startIdx: 1,
289
+ });
290
+ expect(result.sql).toBe('"status" = $1');
291
+ expect(result.params).toEqual(["active"]);
292
+ });
293
+ });
294
+
295
+ describe("empty and weird filters", () => {
296
+ it("returns empty sql for empty filter object", () => {
297
+ const result = SQL_WHERE.encode({
298
+ filter: {},
299
+ startIdx: 1,
300
+ });
301
+ expect(result.sql).toBe("");
302
+ expect(result.params).toEqual([]);
303
+ });
304
+
305
+ it("skips field when value is undefined", () => {
306
+ const result = SQL_WHERE.encode({
307
+ filter: { field: undefined },
308
+ startIdx: 1,
309
+ });
310
+ expect(result.sql).toBe("");
311
+ expect(result.params).toEqual([]);
312
+ });
313
+
314
+ it("handles filter with all undefined values", () => {
315
+ const result = SQL_WHERE.encode({
316
+ filter: { a: undefined, b: undefined, c: undefined },
317
+ startIdx: 1,
318
+ });
319
+ expect(result.sql).toBe("");
320
+ expect(result.params).toEqual([]);
321
+ });
322
+ });
323
+
324
+ describe("null semantics", () => {
325
+ it("treats shorthand null as IS NULL", () => {
326
+ const result = SQL_WHERE.encode({
327
+ filter: { field: null },
328
+ startIdx: 1,
329
+ });
330
+ expect(result.sql).toBe('"field" IS NULL');
331
+ expect(result.params).toEqual([]);
332
+ });
333
+
334
+ it("treats $eq: null as parameterized equality", () => {
335
+ const result = SQL_WHERE.encode({
336
+ filter: { field: { $eq: null } },
337
+ startIdx: 1,
338
+ });
339
+ // $eq: null uses parameterized query (different from shorthand)
340
+ expect(result.sql).toBe('"field" = $1');
341
+ expect(result.params).toEqual([null]);
342
+ });
343
+
344
+ it("treats $neq: null as parameterized inequality", () => {
345
+ const result = SQL_WHERE.encode({
346
+ filter: { field: { $neq: null } },
347
+ startIdx: 1,
348
+ });
349
+ expect(result.sql).toBe('"field" != $1');
350
+ expect(result.params).toEqual([null]);
351
+ });
352
+ });
353
+
354
+ describe("empty $in/$nin arrays", () => {
355
+ it("handles $in with empty array", () => {
356
+ const result = SQL_WHERE.encode({
357
+ filter: { status: { $in: [] } },
358
+ startIdx: 1,
359
+ });
360
+ // Empty array is valid - will match no rows
361
+ expect(result.sql).toBe('"status" = ANY($1)');
362
+ expect(result.params).toEqual([[]]);
363
+ });
364
+
365
+ it("handles $nin with empty array", () => {
366
+ const result = SQL_WHERE.encode({
367
+ filter: { status: { $nin: [] } },
368
+ startIdx: 1,
369
+ });
370
+ // Empty array is valid - will match all rows
371
+ expect(result.sql).toBe('"status" != ALL($1)');
372
+ expect(result.params).toEqual([[]]);
373
+ });
374
+ });
375
+
376
+ describe("deep nesting and param indices", () => {
377
+ it("handles deeply nested $and/$or/$not with high startIdx", () => {
378
+ const result = SQL_WHERE.encode({
379
+ filter: {
380
+ $and: [
381
+ { a: 1 },
382
+ {
383
+ $or: [
384
+ { b: 2 },
385
+ {
386
+ $and: [
387
+ { c: 3 },
388
+ { $not: { d: 4 } },
389
+ ],
390
+ },
391
+ ],
392
+ },
393
+ { e: 5 },
394
+ ],
395
+ },
396
+ startIdx: 10, // simulate many prior params
397
+ });
398
+
399
+ expect(result.sql).toBe(
400
+ '(("a" = $10) AND ((("b" = $11) OR ((("c" = $12) AND (NOT ("d" = $13)))))) AND ("e" = $14))',
401
+ );
402
+ expect(result.params).toEqual([1, 2, 3, 4, 5]);
403
+ });
404
+
405
+ it("handles very large startIdx", () => {
406
+ const result = SQL_WHERE.encode({
407
+ filter: { field: "value" },
408
+ startIdx: 50,
409
+ });
410
+ expect(result.sql).toBe('"field" = $50');
411
+ expect(result.params).toEqual(["value"]);
412
+ });
413
+ });
414
+
415
+ describe("mixed logical + field ops", () => {
416
+ it("handles top-level field with $or", () => {
417
+ const result = SQL_WHERE.encode({
418
+ filter: {
419
+ type: "article",
420
+ $or: [{ status: "draft" }, { status: "review" }],
421
+ },
422
+ startIdx: 1,
423
+ });
424
+ expect(result.sql).toBe(
425
+ '"type" = $1 AND (("status" = $2) OR ("status" = $3))',
426
+ );
427
+ expect(result.params).toEqual(["article", "draft", "review"]);
428
+ });
429
+
430
+ it("handles $and and $or at same level", () => {
431
+ const result = SQL_WHERE.encode({
432
+ filter: {
433
+ $and: [{ a: 1 }, { b: 2 }],
434
+ $or: [{ c: 3 }, { d: 4 }],
435
+ },
436
+ startIdx: 1,
437
+ });
438
+ // Both are processed, joined by implicit AND
439
+ expect(result.sql).toBe(
440
+ '(("a" = $1) AND ("b" = $2)) AND (("c" = $3) OR ("d" = $4))',
441
+ );
442
+ expect(result.params).toEqual([1, 2, 3, 4]);
443
+ });
444
+
445
+ it("handles multiple fields with multiple $or clauses", () => {
446
+ const result = SQL_WHERE.encode({
447
+ filter: {
448
+ published: true,
449
+ category: "tech",
450
+ $or: [
451
+ { featured: true },
452
+ { views: { $gt: 1000 } },
453
+ ],
454
+ },
455
+ startIdx: 1,
456
+ });
457
+ expect(result.sql).toBe(
458
+ '"published" = $1 AND "category" = $2 AND (("featured" = $3) OR ("views" > $4))',
459
+ );
460
+ expect(result.params).toEqual([true, "tech", true, 1000]);
461
+ });
462
+ });
463
+
464
+ describe("operator edge cases", () => {
465
+ it("handles multiple operators with some undefined", () => {
466
+ const result = SQL_WHERE.encode({
467
+ filter: { field: { $eq: undefined, $gt: 10, $lt: undefined } },
468
+ startIdx: 1,
469
+ });
470
+ // Only $gt should generate SQL (others are undefined)
471
+ expect(result.sql).toBe('"field" > $1');
472
+ expect(result.params).toEqual([10]);
473
+ });
474
+
475
+ it("handles all operators undefined", () => {
476
+ const result = SQL_WHERE.encode({
477
+ filter: { field: { $eq: undefined, $gt: undefined } },
478
+ startIdx: 1,
479
+ });
480
+ // No conditions generated
481
+ expect(result.sql).toBe("");
482
+ expect(result.params).toEqual([]);
483
+ });
484
+ });
485
+
486
+ describe("field name handling", () => {
487
+ it("quotes field names with spaces", () => {
488
+ const result = SQL_WHERE.encode({
489
+ filter: { "my field": "value" },
490
+ startIdx: 1,
491
+ });
492
+ expect(result.sql).toBe('"my field" = $1');
493
+ expect(result.params).toEqual(["value"]);
494
+ });
495
+
496
+ it("quotes field names with special characters", () => {
497
+ const result = SQL_WHERE.encode({
498
+ filter: { "field-name": "value", field_name: "value2" },
499
+ startIdx: 1,
500
+ });
501
+ expect(result.sql).toBe('"field-name" = $1 AND "field_name" = $2');
502
+ expect(result.params).toEqual(["value", "value2"]);
503
+ });
504
+
505
+ it("handles values with SQL special characters (parameterized)", () => {
506
+ const result = SQL_WHERE.encode({
507
+ filter: { name: "'; DROP TABLE users; --" },
508
+ startIdx: 1,
509
+ });
510
+ // Value is parameterized, not interpolated - safe from injection
511
+ expect(result.sql).toBe('"name" = $1');
512
+ expect(result.params).toEqual(["'; DROP TABLE users; --"]);
513
+ });
514
+ });
515
+
516
+ describe("empty logical operators", () => {
517
+ it("handles empty $and array", () => {
518
+ const result = SQL_WHERE.encode({
519
+ filter: { $and: [] },
520
+ startIdx: 1,
521
+ });
522
+ expect(result.sql).toBe("");
523
+ expect(result.params).toEqual([]);
524
+ });
525
+
526
+ it("handles empty $or array", () => {
527
+ const result = SQL_WHERE.encode({
528
+ filter: { $or: [] },
529
+ startIdx: 1,
530
+ });
531
+ expect(result.sql).toBe("");
532
+ expect(result.params).toEqual([]);
533
+ });
534
+
535
+ it("handles $and with empty filter objects", () => {
536
+ const result = SQL_WHERE.encode({
537
+ filter: { $and: [{}, { status: "active" }, {}] },
538
+ startIdx: 1,
539
+ });
540
+ // Empty objects produce no SQL, only non-empty ones contribute
541
+ expect(result.sql).toBe('(("status" = $1))');
542
+ expect(result.params).toEqual(["active"]);
543
+ });
544
+ });
545
+ });
546
+
547
+ describe("decode", () => {
548
+ it("throws not implemented", () => {
549
+ expect(() => SQL_WHERE.decode({} as any)).toThrow(
550
+ "SQL_WHERE.decode not implemented",
551
+ );
552
+ });
553
+ });
554
+ });
@@ -0,0 +1,14 @@
1
+ export {
2
+ sqlize,
3
+ type SqlizeConfig,
4
+ type SqlizedQuery,
5
+ type SelectInput,
6
+ type WhereInput,
7
+ type OrderInput,
8
+ type LimitInput,
9
+ } from "./query";
10
+ export { SQL_SELECT, type SQLClause } from "./select";
11
+ export { SQL_WHERE } from "./where";
12
+ export { SQL_ORDER } from "./order";
13
+ export { SQL_LIMIT } from "./limit";
14
+ export { FIELD_TYPE, SIMILARITY } from "./schema";
@@ -0,0 +1,29 @@
1
+ import type { Codec } from "@kernl-sdk/shared/lib";
2
+
3
+ import type { LimitInput } from "./query";
4
+ import type { SQLClause } from "./select";
5
+
6
+ /**
7
+ * Codec for building sql LIMIT clause.
8
+ */
9
+ export const SQL_LIMIT: Codec<LimitInput, SQLClause> = {
10
+ encode({ topK, offset, startIdx }) {
11
+ const parts: string[] = [];
12
+ const params: unknown[] = [];
13
+ let idx = startIdx;
14
+
15
+ parts.push(`LIMIT $${idx++}`);
16
+ params.push(topK);
17
+
18
+ if (offset > 0) {
19
+ parts.push(`OFFSET $${idx++}`);
20
+ params.push(offset);
21
+ }
22
+
23
+ return { sql: parts.join(" "), params };
24
+ },
25
+
26
+ decode() {
27
+ throw new Error("SQL_LIMIT.decode not implemented");
28
+ },
29
+ };
@@ -0,0 +1,55 @@
1
+ import type { Codec } from "@kernl-sdk/shared/lib";
2
+ import type { OrderInput } from "./query";
3
+
4
+ /**
5
+ * pgvector distance operators by similarity metric.
6
+ */
7
+ const DISTANCE_OPS = {
8
+ cosine: "<=>",
9
+ euclidean: "<->",
10
+ dot_product: "<#>",
11
+ } as const;
12
+
13
+ /**
14
+ * Codec for building ORDER BY clause.
15
+ */
16
+ export const SQL_ORDER: Codec<OrderInput, { sql: string }> = {
17
+ encode({ signals, orderBy, binding, schema, table }) {
18
+ // explicit orderBy takes precedence
19
+ if (orderBy) {
20
+ const dir = (orderBy.direction ?? "desc").toUpperCase();
21
+ const col = binding?.fields[orderBy.field]?.column ?? orderBy.field;
22
+
23
+ // Qualify the column with table name to avoid ambiguity with score alias
24
+ if (schema && table) {
25
+ return { sql: `"${schema}"."${table}"."${col}" ${dir}` };
26
+ }
27
+ return { sql: `"${col}" ${dir}` };
28
+ }
29
+
30
+ // vector ordering from signals
31
+ const vsig = signals.find((s) => {
32
+ for (const [key, val] of Object.entries(s)) {
33
+ if (key !== "weight" && Array.isArray(val)) return true;
34
+ }
35
+ return false;
36
+ });
37
+
38
+ if (vsig) {
39
+ for (const [key, val] of Object.entries(vsig)) {
40
+ if (key !== "weight" && Array.isArray(val)) {
41
+ const col = binding?.fields[key]?.column ?? key;
42
+ const similarity = binding?.fields[key]?.similarity ?? "cosine";
43
+ const op = DISTANCE_OPS[similarity];
44
+ return { sql: `"${col}" ${op} $1::vector` };
45
+ }
46
+ }
47
+ }
48
+
49
+ return { sql: "score DESC" }; // default: score descending
50
+ },
51
+
52
+ decode() {
53
+ throw new Error("SQL_ORDER.decode not implemented");
54
+ },
55
+ };