@kernl-sdk/turbopuffer 0.1.0

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 (91) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/.turbo/turbo-check-types.log +60 -0
  3. package/CHANGELOG.md +33 -0
  4. package/LICENSE +201 -0
  5. package/README.md +60 -0
  6. package/dist/__tests__/convert.test.d.ts +2 -0
  7. package/dist/__tests__/convert.test.d.ts.map +1 -0
  8. package/dist/__tests__/convert.test.js +346 -0
  9. package/dist/__tests__/filter.test.d.ts +8 -0
  10. package/dist/__tests__/filter.test.d.ts.map +1 -0
  11. package/dist/__tests__/filter.test.js +649 -0
  12. package/dist/__tests__/filters.integration.test.d.ts +8 -0
  13. package/dist/__tests__/filters.integration.test.d.ts.map +1 -0
  14. package/dist/__tests__/filters.integration.test.js +502 -0
  15. package/dist/__tests__/integration/filters.integration.test.d.ts +8 -0
  16. package/dist/__tests__/integration/filters.integration.test.d.ts.map +1 -0
  17. package/dist/__tests__/integration/filters.integration.test.js +475 -0
  18. package/dist/__tests__/integration/integration.test.d.ts +2 -0
  19. package/dist/__tests__/integration/integration.test.d.ts.map +1 -0
  20. package/dist/__tests__/integration/integration.test.js +329 -0
  21. package/dist/__tests__/integration/lifecycle.integration.test.d.ts +8 -0
  22. package/dist/__tests__/integration/lifecycle.integration.test.d.ts.map +1 -0
  23. package/dist/__tests__/integration/lifecycle.integration.test.js +370 -0
  24. package/dist/__tests__/integration/memory.integration.test.d.ts +2 -0
  25. package/dist/__tests__/integration/memory.integration.test.d.ts.map +1 -0
  26. package/dist/__tests__/integration/memory.integration.test.js +287 -0
  27. package/dist/__tests__/integration/query.integration.test.d.ts +8 -0
  28. package/dist/__tests__/integration/query.integration.test.d.ts.map +1 -0
  29. package/dist/__tests__/integration/query.integration.test.js +385 -0
  30. package/dist/__tests__/integration.test.d.ts +2 -0
  31. package/dist/__tests__/integration.test.d.ts.map +1 -0
  32. package/dist/__tests__/integration.test.js +343 -0
  33. package/dist/__tests__/lifecycle.integration.test.d.ts +8 -0
  34. package/dist/__tests__/lifecycle.integration.test.d.ts.map +1 -0
  35. package/dist/__tests__/lifecycle.integration.test.js +385 -0
  36. package/dist/__tests__/query.integration.test.d.ts +8 -0
  37. package/dist/__tests__/query.integration.test.d.ts.map +1 -0
  38. package/dist/__tests__/query.integration.test.js +423 -0
  39. package/dist/__tests__/query.test.d.ts +8 -0
  40. package/dist/__tests__/query.test.d.ts.map +1 -0
  41. package/dist/__tests__/query.test.js +472 -0
  42. package/dist/convert/document.d.ts +20 -0
  43. package/dist/convert/document.d.ts.map +1 -0
  44. package/dist/convert/document.js +72 -0
  45. package/dist/convert/filter.d.ts +15 -0
  46. package/dist/convert/filter.d.ts.map +1 -0
  47. package/dist/convert/filter.js +109 -0
  48. package/dist/convert/index.d.ts +8 -0
  49. package/dist/convert/index.d.ts.map +1 -0
  50. package/dist/convert/index.js +7 -0
  51. package/dist/convert/query.d.ts +22 -0
  52. package/dist/convert/query.d.ts.map +1 -0
  53. package/dist/convert/query.js +111 -0
  54. package/dist/convert/schema.d.ts +39 -0
  55. package/dist/convert/schema.d.ts.map +1 -0
  56. package/dist/convert/schema.js +124 -0
  57. package/dist/convert.d.ts +68 -0
  58. package/dist/convert.d.ts.map +1 -0
  59. package/dist/convert.js +333 -0
  60. package/dist/handle.d.ts +34 -0
  61. package/dist/handle.d.ts.map +1 -0
  62. package/dist/handle.js +72 -0
  63. package/dist/index.d.ts +27 -0
  64. package/dist/index.d.ts.map +1 -0
  65. package/dist/index.js +26 -0
  66. package/dist/search.d.ts +85 -0
  67. package/dist/search.d.ts.map +1 -0
  68. package/dist/search.js +167 -0
  69. package/dist/types.d.ts +14 -0
  70. package/dist/types.d.ts.map +1 -0
  71. package/dist/types.js +1 -0
  72. package/package.json +57 -0
  73. package/src/__tests__/convert.test.ts +425 -0
  74. package/src/__tests__/filter.test.ts +730 -0
  75. package/src/__tests__/integration/filters.integration.test.ts +558 -0
  76. package/src/__tests__/integration/integration.test.ts +399 -0
  77. package/src/__tests__/integration/lifecycle.integration.test.ts +464 -0
  78. package/src/__tests__/integration/memory.integration.test.ts +353 -0
  79. package/src/__tests__/integration/query.integration.test.ts +471 -0
  80. package/src/__tests__/query.test.ts +636 -0
  81. package/src/convert/document.ts +95 -0
  82. package/src/convert/filter.ts +123 -0
  83. package/src/convert/index.ts +8 -0
  84. package/src/convert/query.ts +151 -0
  85. package/src/convert/schema.ts +163 -0
  86. package/src/handle.ts +104 -0
  87. package/src/index.ts +31 -0
  88. package/src/search.ts +207 -0
  89. package/src/types.ts +14 -0
  90. package/tsconfig.json +13 -0
  91. package/vitest.config.ts +15 -0
@@ -0,0 +1,472 @@
1
+ /**
2
+ * Comprehensive unit tests for query conversion.
3
+ *
4
+ * Tests vector, text, hybrid queries, fusion modes, query options,
5
+ * and SearchHit decoding.
6
+ */
7
+ import { describe, it, expect } from "vitest";
8
+ import { QUERY, SEARCH_HIT } from "../convert/query.js";
9
+ describe("QUERY", () => {
10
+ // ============================================================
11
+ // VECTOR-ONLY QUERIES
12
+ // ============================================================
13
+ describe("vector-only queries", () => {
14
+ it("encodes single vector signal", () => {
15
+ const result = QUERY.encode({
16
+ query: [{ vector: [0.1, 0.2, 0.3] }],
17
+ });
18
+ expect(result.rank_by).toEqual(["vector", "ANN", [0.1, 0.2, 0.3]]);
19
+ });
20
+ it("encodes vector with many dimensions", () => {
21
+ const vec = new Array(384).fill(0.5);
22
+ const result = QUERY.encode({
23
+ query: [{ vector: vec }],
24
+ });
25
+ expect(result.rank_by).toEqual(["vector", "ANN", vec]);
26
+ });
27
+ it("encodes vector with zeros", () => {
28
+ const result = QUERY.encode({
29
+ query: [{ vector: [0, 0, 0] }],
30
+ });
31
+ expect(result.rank_by).toEqual(["vector", "ANN", [0, 0, 0]]);
32
+ });
33
+ it("encodes vector with negative values", () => {
34
+ const result = QUERY.encode({
35
+ query: [{ vector: [-0.5, 0.5, -1.0] }],
36
+ });
37
+ expect(result.rank_by).toEqual(["vector", "ANN", [-0.5, 0.5, -1.0]]);
38
+ });
39
+ it("throws error for multiple vector signals", () => {
40
+ expect(() => QUERY.encode({
41
+ query: [{ vector: [0.1, 0.2] }, { vector: [0.3, 0.4] }],
42
+ })).toThrow(/does not support multi-vector/);
43
+ });
44
+ it("encodes vector query with max fusion", () => {
45
+ const result = QUERY.encode({
46
+ max: [{ vector: [0.1, 0.2] }],
47
+ });
48
+ // Single signal, no fusion wrapper needed
49
+ expect(result.rank_by).toEqual(["vector", "ANN", [0.1, 0.2]]);
50
+ });
51
+ it("throws error for multiple vector signals with max fusion", () => {
52
+ expect(() => QUERY.encode({
53
+ max: [{ vector: [0.1, 0.2] }, { vector: [0.3, 0.4] }],
54
+ })).toThrow(/does not support multi-vector/);
55
+ });
56
+ });
57
+ // ============================================================
58
+ // TEXT-ONLY QUERIES (BM25)
59
+ // ============================================================
60
+ describe("text-only queries (BM25)", () => {
61
+ it("encodes single text field query", () => {
62
+ const result = QUERY.encode({
63
+ query: [{ content: "hello world" }],
64
+ });
65
+ expect(result.rank_by).toEqual(["content", "BM25", "hello world"]);
66
+ });
67
+ it("encodes text query on different field", () => {
68
+ const result = QUERY.encode({
69
+ query: [{ title: "search terms" }],
70
+ });
71
+ expect(result.rank_by).toEqual(["title", "BM25", "search terms"]);
72
+ });
73
+ it("encodes empty string text query", () => {
74
+ const result = QUERY.encode({
75
+ query: [{ content: "" }],
76
+ });
77
+ expect(result.rank_by).toEqual(["content", "BM25", ""]);
78
+ });
79
+ it("encodes text query with special characters", () => {
80
+ const result = QUERY.encode({
81
+ query: [{ content: "hello \"world\" & foo | bar" }],
82
+ });
83
+ expect(result.rank_by).toEqual([
84
+ "content",
85
+ "BM25",
86
+ "hello \"world\" & foo | bar",
87
+ ]);
88
+ });
89
+ it("encodes multi-field text query as Sum fusion", () => {
90
+ const result = QUERY.encode({
91
+ query: [{ title: "search" }, { body: "search" }],
92
+ });
93
+ expect(result.rank_by).toEqual([
94
+ "Sum",
95
+ [
96
+ ["title", "BM25", "search"],
97
+ ["body", "BM25", "search"],
98
+ ],
99
+ ]);
100
+ });
101
+ it("encodes three-field text query as Sum fusion", () => {
102
+ const result = QUERY.encode({
103
+ query: [
104
+ { title: "query" },
105
+ { description: "query" },
106
+ { content: "query" },
107
+ ],
108
+ });
109
+ expect(result.rank_by).toEqual([
110
+ "Sum",
111
+ [
112
+ ["title", "BM25", "query"],
113
+ ["description", "BM25", "query"],
114
+ ["content", "BM25", "query"],
115
+ ],
116
+ ]);
117
+ });
118
+ it("encodes multi-field text query with max fusion", () => {
119
+ const result = QUERY.encode({
120
+ max: [{ title: "search" }, { body: "search" }],
121
+ });
122
+ expect(result.rank_by).toEqual([
123
+ "Max",
124
+ [
125
+ ["title", "BM25", "search"],
126
+ ["body", "BM25", "search"],
127
+ ],
128
+ ]);
129
+ });
130
+ });
131
+ // ============================================================
132
+ // HYBRID QUERIES (VECTOR + TEXT) - NOT SUPPORTED
133
+ // ============================================================
134
+ describe("hybrid queries (vector + text)", () => {
135
+ it("throws error for vector + text fusion", () => {
136
+ expect(() => QUERY.encode({
137
+ query: [{ vector: [0.1, 0.2, 0.3] }, { content: "search terms" }],
138
+ })).toThrow(/does not support hybrid/);
139
+ });
140
+ it("throws error for vector + text max fusion", () => {
141
+ expect(() => QUERY.encode({
142
+ max: [{ vector: [0.1, 0.2, 0.3] }, { content: "search terms" }],
143
+ })).toThrow(/does not support hybrid/);
144
+ });
145
+ it("throws error for vector + multi-field text", () => {
146
+ expect(() => QUERY.encode({
147
+ query: [
148
+ { vector: [0.1, 0.2] },
149
+ { title: "query" },
150
+ { body: "query" },
151
+ ],
152
+ })).toThrow(/does not support hybrid/);
153
+ });
154
+ it("throws error for multi-vector fusion", () => {
155
+ expect(() => QUERY.encode({
156
+ query: [{ vector: [0.1, 0.2] }, { vector: [0.3, 0.4] }],
157
+ })).toThrow(/does not support multi-vector/);
158
+ });
159
+ });
160
+ // ============================================================
161
+ // QUERY OPTIONS
162
+ // ============================================================
163
+ describe("query options", () => {
164
+ describe("topK", () => {
165
+ it("encodes topK as top_k", () => {
166
+ const result = QUERY.encode({
167
+ query: [{ vector: [0.1] }],
168
+ topK: 10,
169
+ });
170
+ expect(result.top_k).toBe(10);
171
+ });
172
+ it("encodes topK of 1", () => {
173
+ const result = QUERY.encode({
174
+ query: [{ vector: [0.1] }],
175
+ topK: 1,
176
+ });
177
+ expect(result.top_k).toBe(1);
178
+ });
179
+ it("encodes large topK", () => {
180
+ const result = QUERY.encode({
181
+ query: [{ vector: [0.1] }],
182
+ topK: 10000,
183
+ });
184
+ expect(result.top_k).toBe(10000);
185
+ });
186
+ it("does not include top_k when undefined", () => {
187
+ const result = QUERY.encode({
188
+ query: [{ vector: [0.1] }],
189
+ });
190
+ expect(result.top_k).toBeUndefined();
191
+ });
192
+ });
193
+ describe("filter", () => {
194
+ it("encodes simple filter", () => {
195
+ const result = QUERY.encode({
196
+ query: [{ vector: [0.1] }],
197
+ filter: { status: "active" },
198
+ });
199
+ expect(result.filters).toEqual(["status", "Eq", "active"]);
200
+ });
201
+ it("encodes complex filter", () => {
202
+ const result = QUERY.encode({
203
+ query: [{ vector: [0.1] }],
204
+ filter: {
205
+ $and: [{ status: "active" }, { views: { $gte: 100 } }],
206
+ },
207
+ });
208
+ expect(result.filters).toEqual([
209
+ "And",
210
+ [
211
+ ["status", "Eq", "active"],
212
+ ["views", "Gte", 100],
213
+ ],
214
+ ]);
215
+ });
216
+ it("does not include filters when undefined", () => {
217
+ const result = QUERY.encode({
218
+ query: [{ vector: [0.1] }],
219
+ });
220
+ expect(result.filters).toBeUndefined();
221
+ });
222
+ });
223
+ describe("include", () => {
224
+ it("encodes include as boolean true", () => {
225
+ const result = QUERY.encode({
226
+ query: [{ vector: [0.1] }],
227
+ include: true,
228
+ });
229
+ expect(result.include_attributes).toBe(true);
230
+ });
231
+ it("encodes include as boolean false", () => {
232
+ const result = QUERY.encode({
233
+ query: [{ vector: [0.1] }],
234
+ include: false,
235
+ });
236
+ expect(result.include_attributes).toBe(false);
237
+ });
238
+ it("encodes include as string array", () => {
239
+ const result = QUERY.encode({
240
+ query: [{ vector: [0.1] }],
241
+ include: ["title", "content"],
242
+ });
243
+ expect(result.include_attributes).toEqual(["title", "content"]);
244
+ });
245
+ it("encodes include as single-element array", () => {
246
+ const result = QUERY.encode({
247
+ query: [{ vector: [0.1] }],
248
+ include: ["id"],
249
+ });
250
+ expect(result.include_attributes).toEqual(["id"]);
251
+ });
252
+ it("encodes include as empty array", () => {
253
+ const result = QUERY.encode({
254
+ query: [{ vector: [0.1] }],
255
+ include: [],
256
+ });
257
+ expect(result.include_attributes).toEqual([]);
258
+ });
259
+ it("does not include include_attributes when undefined", () => {
260
+ const result = QUERY.encode({
261
+ query: [{ vector: [0.1] }],
262
+ });
263
+ expect(result.include_attributes).toBeUndefined();
264
+ });
265
+ });
266
+ describe("combined options", () => {
267
+ it("encodes all options together", () => {
268
+ const result = QUERY.encode({
269
+ query: [{ vector: [0.1, 0.2] }],
270
+ topK: 20,
271
+ filter: { category: "news" },
272
+ include: ["title", "summary"],
273
+ });
274
+ expect(result.rank_by).toEqual(["vector", "ANN", [0.1, 0.2]]);
275
+ expect(result.top_k).toBe(20);
276
+ expect(result.filters).toEqual(["category", "Eq", "news"]);
277
+ expect(result.include_attributes).toEqual(["title", "summary"]);
278
+ });
279
+ });
280
+ });
281
+ // ============================================================
282
+ // FUSION MODE SELECTION
283
+ // ============================================================
284
+ describe("fusion mode selection", () => {
285
+ it("throws error when query has both vector and text", () => {
286
+ expect(() => QUERY.encode({
287
+ query: [{ vector: [0.1] }, { content: "search" }],
288
+ })).toThrow(/does not support hybrid/);
289
+ });
290
+ it("throws error when max has both vector and text", () => {
291
+ expect(() => QUERY.encode({
292
+ max: [{ vector: [0.1] }, { content: "search" }],
293
+ })).toThrow(/does not support hybrid/);
294
+ });
295
+ it("prefers query over max when both present", () => {
296
+ const result = QUERY.encode({
297
+ query: [{ vector: [0.1] }],
298
+ max: [{ content: "search" }],
299
+ });
300
+ // query takes precedence
301
+ expect(result.rank_by).toEqual(["vector", "ANN", [0.1]]);
302
+ });
303
+ });
304
+ // ============================================================
305
+ // ERROR CASES
306
+ // ============================================================
307
+ describe("error cases", () => {
308
+ it("throws when query array has only undefined values", () => {
309
+ expect(() => QUERY.encode({
310
+ query: [{ field: undefined }],
311
+ })).toThrow("No ranking signals provided");
312
+ });
313
+ it("returns params without rank_by when query is empty array", () => {
314
+ // Note: In practice, empty arrays are caught during normalization.
315
+ // Direct codec call with empty array returns params without rank_by.
316
+ const result = QUERY.encode({
317
+ query: [],
318
+ topK: 10,
319
+ });
320
+ expect(result.rank_by).toBeUndefined();
321
+ expect(result.top_k).toBe(10);
322
+ });
323
+ it("returns params without rank_by when no query or max", () => {
324
+ const result = QUERY.encode({
325
+ topK: 10,
326
+ filter: { status: "active" },
327
+ });
328
+ expect(result.rank_by).toBeUndefined();
329
+ expect(result.top_k).toBe(10);
330
+ expect(result.filters).toEqual(["status", "Eq", "active"]);
331
+ });
332
+ });
333
+ // ============================================================
334
+ // EDGE CASES
335
+ // ============================================================
336
+ describe("edge cases", () => {
337
+ it("handles signal with multiple fields in one object", () => {
338
+ // A signal object could technically have multiple fields
339
+ const result = QUERY.encode({
340
+ query: [{ title: "hello", body: "world" }],
341
+ });
342
+ // Both should be extracted as separate BM25 signals
343
+ expect(result.rank_by).toEqual([
344
+ "Sum",
345
+ [
346
+ ["title", "BM25", "hello"],
347
+ ["body", "BM25", "world"],
348
+ ],
349
+ ]);
350
+ });
351
+ it("throws error for mixed signal types in one object", () => {
352
+ // Vector and text in same signal object
353
+ expect(() => QUERY.encode({
354
+ query: [{ vector: [0.1], content: "search" }],
355
+ })).toThrow(/does not support hybrid/);
356
+ });
357
+ });
358
+ });
359
+ // ============================================================
360
+ // SEARCH_HIT CODEC
361
+ // ============================================================
362
+ describe("SEARCH_HIT", () => {
363
+ describe("decode", () => {
364
+ it("decodes basic row with id and score", () => {
365
+ const result = SEARCH_HIT.decode({ id: "doc-1", $dist: 0.5 }, "my-index");
366
+ expect(result).toEqual({
367
+ id: "doc-1",
368
+ index: "my-index",
369
+ score: -0.5, // negated: distance → similarity
370
+ document: { id: "doc-1" },
371
+ });
372
+ });
373
+ it("decodes row with additional attributes", () => {
374
+ const result = SEARCH_HIT.decode({
375
+ id: "doc-1",
376
+ $dist: 0.25,
377
+ title: "Hello World",
378
+ category: "greeting",
379
+ }, "my-index");
380
+ expect(result).toEqual({
381
+ id: "doc-1",
382
+ index: "my-index",
383
+ score: -0.25, // negated: distance → similarity
384
+ document: {
385
+ id: "doc-1",
386
+ title: "Hello World",
387
+ category: "greeting",
388
+ },
389
+ });
390
+ });
391
+ it("decodes row with vector attribute", () => {
392
+ const result = SEARCH_HIT.decode({
393
+ id: "doc-1",
394
+ $dist: 0.1,
395
+ vector: [0.1, 0.2, 0.3],
396
+ }, "my-index");
397
+ expect(result.document?.id).toBe("doc-1");
398
+ expect(result.document?.vector).toEqual([0.1, 0.2, 0.3]);
399
+ });
400
+ it("converts numeric id to string", () => {
401
+ const result = SEARCH_HIT.decode({ id: 12345, $dist: 0.5 }, "my-index");
402
+ expect(result.id).toBe("12345");
403
+ expect(typeof result.id).toBe("string");
404
+ expect(result.document?.id).toBe(12345); // document keeps original type
405
+ });
406
+ it("handles missing $dist (defaults to 0)", () => {
407
+ const result = SEARCH_HIT.decode({ id: "doc-1" }, "my-index");
408
+ expect(result.score).toBe(0);
409
+ expect(result.document).toEqual({ id: "doc-1" });
410
+ });
411
+ it("handles $dist of 0", () => {
412
+ const result = SEARCH_HIT.decode({ id: "doc-1", $dist: 0 }, "my-index");
413
+ expect(result.score).toBe(0);
414
+ });
415
+ it("handles negative $dist", () => {
416
+ const result = SEARCH_HIT.decode({ id: "doc-1", $dist: -0.5 }, "my-index");
417
+ expect(result.score).toBe(0.5); // negated: distance → similarity
418
+ });
419
+ it("handles large $dist values", () => {
420
+ const result = SEARCH_HIT.decode({ id: "doc-1", $dist: 999999.99 }, "my-index");
421
+ expect(result.score).toBe(-999999.99); // negated: distance → similarity
422
+ });
423
+ it("includes id in document even when no extra attributes", () => {
424
+ const result = SEARCH_HIT.decode({ id: "doc-1", $dist: 0.5 }, "my-index");
425
+ expect(result.document).toEqual({ id: "doc-1" });
426
+ });
427
+ it("preserves attribute types", () => {
428
+ const result = SEARCH_HIT.decode({
429
+ id: "doc-1",
430
+ $dist: 0.5,
431
+ count: 42,
432
+ enabled: true,
433
+ rating: 4.5,
434
+ tags: ["a", "b"],
435
+ meta: null,
436
+ }, "my-index");
437
+ expect(result.document).toEqual({
438
+ id: "doc-1",
439
+ count: 42,
440
+ enabled: true,
441
+ rating: 4.5,
442
+ tags: ["a", "b"],
443
+ meta: null,
444
+ });
445
+ });
446
+ it("handles empty string id", () => {
447
+ const result = SEARCH_HIT.decode({ id: "", $dist: 0.5 }, "my-index");
448
+ expect(result.id).toBe("");
449
+ });
450
+ it("handles special characters in attribute values", () => {
451
+ const result = SEARCH_HIT.decode({
452
+ id: "doc-1",
453
+ $dist: 0.5,
454
+ content: "Hello \"world\" & <script>",
455
+ }, "my-index");
456
+ expect(result.document?.content).toBe("Hello \"world\" & <script>");
457
+ });
458
+ });
459
+ describe("decode with typed document", () => {
460
+ it("returns typed document", () => {
461
+ const result = SEARCH_HIT.decode({
462
+ id: "doc-1",
463
+ $dist: 0.5,
464
+ title: "Test",
465
+ views: 100,
466
+ }, "my-index");
467
+ // TypeScript should recognize these as MyDoc fields
468
+ expect(result.document?.title).toBe("Test");
469
+ expect(result.document?.views).toBe(100);
470
+ });
471
+ });
472
+ });
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Document conversion codecs.
3
+ */
4
+ import type { Codec } from "@kernl-sdk/shared/lib";
5
+ import type { UnknownDocument } from "@kernl-sdk/retrieval";
6
+ import type { Row } from "@turbopuffer/turbopuffer/resources/namespaces";
7
+ /**
8
+ * Codec for converting documents to Turbopuffer Row.
9
+ *
10
+ * Documents must have an `id` field. All other fields become attributes.
11
+ * Undefined values are omitted.
12
+ */
13
+ export declare const DOCUMENT: Codec<UnknownDocument, Row>;
14
+ /**
15
+ * Codec for converting document patches to Turbopuffer Row.
16
+ *
17
+ * Similar to DOCUMENT but passes through null values to unset fields.
18
+ */
19
+ export declare const PATCH: Codec<UnknownDocument, Row>;
20
+ //# sourceMappingURL=document.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"document.d.ts","sourceRoot":"","sources":["../../src/convert/document.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAC;AACnD,OAAO,KAAK,EAGV,eAAe,EAChB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,+CAA+C,CAAC;AAEzE;;;;;GAKG;AACH,eAAO,MAAM,QAAQ,EAAE,KAAK,CAAC,eAAe,EAAE,GAAG,CAsBhD,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,KAAK,EAAE,KAAK,CAAC,eAAe,EAAE,GAAG,CAwB7C,CAAC"}
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Document conversion codecs.
3
+ */
4
+ /**
5
+ * Codec for converting documents to Turbopuffer Row.
6
+ *
7
+ * Documents must have an `id` field. All other fields become attributes.
8
+ * Undefined values are omitted.
9
+ */
10
+ export const DOCUMENT = {
11
+ encode: (doc) => {
12
+ const { id, ...fields } = doc;
13
+ if (typeof id !== "string") {
14
+ throw new Error('Document must have a string "id" field');
15
+ }
16
+ const row = { id };
17
+ for (const [key, val] of Object.entries(fields)) {
18
+ if (val !== undefined) {
19
+ row[key] = encodeFieldValue(val);
20
+ }
21
+ }
22
+ return row;
23
+ },
24
+ decode: (_row) => {
25
+ throw new Error("DOCUMENT.decode: not implemented");
26
+ },
27
+ };
28
+ /**
29
+ * Codec for converting document patches to Turbopuffer Row.
30
+ *
31
+ * Similar to DOCUMENT but passes through null values to unset fields.
32
+ */
33
+ export const PATCH = {
34
+ encode: (patch) => {
35
+ const { id, ...fields } = patch;
36
+ if (typeof id !== "string") {
37
+ throw new Error('Patch must have a string "id" field');
38
+ }
39
+ const row = { id };
40
+ for (const [key, val] of Object.entries(fields)) {
41
+ if (val === null) {
42
+ row[key] = null; // null unsets the field in Turbopuffer
43
+ }
44
+ else if (val !== undefined) {
45
+ row[key] = encodeFieldValue(val);
46
+ }
47
+ }
48
+ return row;
49
+ },
50
+ decode: (_row) => {
51
+ throw new Error("PATCH.decode: not implemented");
52
+ },
53
+ };
54
+ /**
55
+ * Check if a value is a DenseVector.
56
+ */
57
+ function isDenseVector(val) {
58
+ return (typeof val === "object" &&
59
+ val !== null &&
60
+ "kind" in val &&
61
+ val.kind === "vector");
62
+ }
63
+ /**
64
+ * Convert a FieldValue to Turbopuffer attribute value.
65
+ * Extracts vector values from DenseVector wrapper.
66
+ */
67
+ function encodeFieldValue(val) {
68
+ if (isDenseVector(val)) {
69
+ return val.values;
70
+ }
71
+ return val;
72
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Filter conversion codecs.
3
+ *
4
+ * Converts MongoDB-style filters to Turbopuffer filter format.
5
+ */
6
+ import type { Filter } from "@kernl-sdk/retrieval";
7
+ import type { Filter as TpufFilter } from "@turbopuffer/turbopuffer/resources/custom";
8
+ /**
9
+ * Codec for converting Filter to Turbopuffer Filter.
10
+ */
11
+ export declare const FILTER: {
12
+ encode: (filter: Filter) => TpufFilter;
13
+ decode: (_filter: TpufFilter) => Filter;
14
+ };
15
+ //# sourceMappingURL=filter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"filter.d.ts","sourceRoot":"","sources":["../../src/convert/filter.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAyB,MAAM,sBAAsB,CAAC;AAC1E,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,2CAA2C,CAAC;AAEtF;;GAEG;AACH,eAAO,MAAM,MAAM;qBACA,MAAM,KAAG,UAAU;sBAgDlB,UAAU,KAAG,MAAM;CAGtC,CAAC"}