@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,346 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { DOCUMENT, PATCH } from "../convert/document.js";
3
+ import { FILTER } from "../convert/filter.js";
4
+ import { QUERY, SEARCH_HIT } from "../convert/query.js";
5
+ import { SCALAR_TYPE, SIMILARITY, INDEX_SCHEMA } from "../convert/schema.js";
6
+ describe("convert", () => {
7
+ describe("SCALAR_TYPE", () => {
8
+ it("encodes kernl types to turbopuffer types", () => {
9
+ expect(SCALAR_TYPE.encode("string")).toBe("string");
10
+ expect(SCALAR_TYPE.encode("int")).toBe("int");
11
+ expect(SCALAR_TYPE.encode("float")).toBe("int"); // tpuf doesn't have float
12
+ expect(SCALAR_TYPE.encode("boolean")).toBe("bool");
13
+ expect(SCALAR_TYPE.encode("date")).toBe("datetime");
14
+ expect(SCALAR_TYPE.encode("string[]")).toBe("[]string");
15
+ expect(SCALAR_TYPE.encode("int[]")).toBe("[]int");
16
+ });
17
+ it("decodes turbopuffer types to kernl types", () => {
18
+ expect(SCALAR_TYPE.decode("string")).toBe("string");
19
+ expect(SCALAR_TYPE.decode("int")).toBe("int");
20
+ expect(SCALAR_TYPE.decode("bool")).toBe("boolean");
21
+ expect(SCALAR_TYPE.decode("datetime")).toBe("date");
22
+ expect(SCALAR_TYPE.decode("[]string")).toBe("string[]");
23
+ });
24
+ });
25
+ describe("SIMILARITY", () => {
26
+ it("encodes similarity to distance metric", () => {
27
+ expect(SIMILARITY.encode("cosine")).toBe("cosine_distance");
28
+ expect(SIMILARITY.encode("euclidean")).toBe("euclidean_squared");
29
+ expect(SIMILARITY.encode("dot_product")).toBe("cosine_distance");
30
+ expect(SIMILARITY.encode(undefined)).toBe("cosine_distance");
31
+ });
32
+ it("decodes distance metric to similarity", () => {
33
+ expect(SIMILARITY.decode("cosine_distance")).toBe("cosine");
34
+ expect(SIMILARITY.decode("euclidean_squared")).toBe("euclidean");
35
+ });
36
+ });
37
+ describe("INDEX_SCHEMA", () => {
38
+ it("encodes a schema with scalar fields", () => {
39
+ const result = INDEX_SCHEMA.encode({
40
+ title: { type: "string", filterable: true },
41
+ count: { type: "int" },
42
+ });
43
+ expect(result.title).toEqual({ type: "string", filterable: true });
44
+ expect(result.count).toEqual({ type: "int" });
45
+ });
46
+ it("encodes a schema with vector field", () => {
47
+ const result = INDEX_SCHEMA.encode({
48
+ vector: { type: "vector", dimensions: 384 },
49
+ });
50
+ expect(result.vector).toEqual({ type: "[384]f32", ann: true });
51
+ });
52
+ it("throws if vector field is not named vector", () => {
53
+ expect(() => INDEX_SCHEMA.encode({
54
+ embedding: { type: "vector", dimensions: 384 },
55
+ })).toThrow(/requires vector fields to be named "vector"/);
56
+ });
57
+ it("encodes fts fields", () => {
58
+ const result = INDEX_SCHEMA.encode({
59
+ content: { type: "string", fts: true },
60
+ });
61
+ expect(result.content).toEqual({
62
+ type: "string",
63
+ full_text_search: true,
64
+ });
65
+ });
66
+ });
67
+ describe("DOCUMENT", () => {
68
+ it("encodes flat document with scalar fields", () => {
69
+ const result = DOCUMENT.encode({
70
+ id: "doc-1",
71
+ title: "Hello",
72
+ count: 42,
73
+ active: true,
74
+ });
75
+ expect(result).toEqual({
76
+ id: "doc-1",
77
+ title: "Hello",
78
+ count: 42,
79
+ active: true,
80
+ });
81
+ });
82
+ it("encodes flat document with vector field", () => {
83
+ const result = DOCUMENT.encode({
84
+ id: "doc-2",
85
+ vector: [0.1, 0.2, 0.3],
86
+ });
87
+ expect(result).toEqual({
88
+ id: "doc-2",
89
+ vector: [0.1, 0.2, 0.3],
90
+ });
91
+ });
92
+ it("throws if id is missing", () => {
93
+ expect(() => DOCUMENT.encode({ title: "No ID" })).toThrow('Document must have a string "id" field');
94
+ });
95
+ });
96
+ describe("PATCH", () => {
97
+ it("encodes flat patch with field updates", () => {
98
+ const result = PATCH.encode({
99
+ id: "doc-1",
100
+ title: "Updated",
101
+ count: 100,
102
+ });
103
+ expect(result).toEqual({
104
+ id: "doc-1",
105
+ title: "Updated",
106
+ count: 100,
107
+ });
108
+ });
109
+ it("encodes null values to unset fields", () => {
110
+ const result = PATCH.encode({
111
+ id: "doc-1",
112
+ title: "Keep this",
113
+ description: null,
114
+ });
115
+ expect(result).toEqual({
116
+ id: "doc-1",
117
+ title: "Keep this",
118
+ description: null,
119
+ });
120
+ });
121
+ it("encodes vector updates", () => {
122
+ const result = PATCH.encode({
123
+ id: "doc-1",
124
+ vector: [0.5, 0.6],
125
+ });
126
+ expect(result).toEqual({
127
+ id: "doc-1",
128
+ vector: [0.5, 0.6],
129
+ });
130
+ });
131
+ it("throws if id is missing", () => {
132
+ expect(() => PATCH.encode({ title: "No ID" })).toThrow('Patch must have a string "id" field');
133
+ });
134
+ });
135
+ describe("FILTER", () => {
136
+ // Simple equality (shorthand)
137
+ it("encodes simple equality filter", () => {
138
+ expect(FILTER.encode({ f: "x" })).toEqual(["f", "Eq", "x"]);
139
+ expect(FILTER.encode({ f: 42 })).toEqual(["f", "Eq", 42]);
140
+ expect(FILTER.encode({ f: true })).toEqual(["f", "Eq", true]);
141
+ expect(FILTER.encode({ f: null })).toEqual(["f", "Eq", null]);
142
+ });
143
+ // Field operators
144
+ it("encodes $eq operator", () => {
145
+ expect(FILTER.encode({ f: { $eq: "x" } })).toEqual(["f", "Eq", "x"]);
146
+ });
147
+ it("encodes $neq operator", () => {
148
+ expect(FILTER.encode({ f: { $neq: "x" } })).toEqual(["f", "NotEq", "x"]);
149
+ });
150
+ it("encodes comparison operators", () => {
151
+ expect(FILTER.encode({ f: { $gt: 10 } })).toEqual(["f", "Gt", 10]);
152
+ expect(FILTER.encode({ f: { $gte: 10 } })).toEqual(["f", "Gte", 10]);
153
+ expect(FILTER.encode({ f: { $lt: 10 } })).toEqual(["f", "Lt", 10]);
154
+ expect(FILTER.encode({ f: { $lte: 10 } })).toEqual(["f", "Lte", 10]);
155
+ });
156
+ it("encodes set membership operators", () => {
157
+ expect(FILTER.encode({ f: { $in: ["a", "b"] } })).toEqual(["f", "In", ["a", "b"]]);
158
+ expect(FILTER.encode({ f: { $nin: ["a", "b"] } })).toEqual(["f", "NotIn", ["a", "b"]]);
159
+ });
160
+ it("encodes $contains operator", () => {
161
+ expect(FILTER.encode({ f: { $contains: "x" } })).toEqual(["f", "Contains", "x"]);
162
+ });
163
+ it("encodes string pattern operators as Glob", () => {
164
+ expect(FILTER.encode({ f: { $startsWith: "pre" } })).toEqual(["f", "Glob", "pre*"]);
165
+ expect(FILTER.encode({ f: { $endsWith: "suf" } })).toEqual(["f", "Glob", "*suf"]);
166
+ });
167
+ it("encodes $exists operator", () => {
168
+ expect(FILTER.encode({ f: { $exists: true } })).toEqual(["f", "NotEq", null]);
169
+ expect(FILTER.encode({ f: { $exists: false } })).toEqual(["f", "Eq", null]);
170
+ });
171
+ // Logical operators
172
+ it("encodes $and filter", () => {
173
+ const result = FILTER.encode({
174
+ $and: [{ a: 1 }, { b: 2 }],
175
+ });
176
+ expect(result).toEqual([
177
+ "And",
178
+ [
179
+ ["a", "Eq", 1],
180
+ ["b", "Eq", 2],
181
+ ],
182
+ ]);
183
+ });
184
+ it("encodes $or filter", () => {
185
+ const result = FILTER.encode({
186
+ $or: [{ status: "active" }, { status: "pending" }],
187
+ });
188
+ expect(result).toEqual([
189
+ "Or",
190
+ [
191
+ ["status", "Eq", "active"],
192
+ ["status", "Eq", "pending"],
193
+ ],
194
+ ]);
195
+ });
196
+ it("encodes $not filter", () => {
197
+ const result = FILTER.encode({
198
+ $not: { deleted: true },
199
+ });
200
+ expect(result).toEqual(["Not", ["deleted", "Eq", true]]);
201
+ });
202
+ it("encodes deeply nested filters", () => {
203
+ const result = FILTER.encode({
204
+ $and: [
205
+ { active: true },
206
+ {
207
+ $or: [
208
+ { role: "admin" },
209
+ {
210
+ $and: [{ role: "user" }, { verified: true }],
211
+ },
212
+ ],
213
+ },
214
+ ],
215
+ });
216
+ expect(result).toEqual([
217
+ "And",
218
+ [
219
+ ["active", "Eq", true],
220
+ [
221
+ "Or",
222
+ [
223
+ ["role", "Eq", "admin"],
224
+ [
225
+ "And",
226
+ [
227
+ ["role", "Eq", "user"],
228
+ ["verified", "Eq", true],
229
+ ],
230
+ ],
231
+ ],
232
+ ],
233
+ ],
234
+ ]);
235
+ });
236
+ it("encodes multiple fields as implicit AND", () => {
237
+ const result = FILTER.encode({ a: 1, b: 2 });
238
+ expect(result).toEqual([
239
+ "And",
240
+ [
241
+ ["a", "Eq", 1],
242
+ ["b", "Eq", 2],
243
+ ],
244
+ ]);
245
+ });
246
+ });
247
+ describe("QUERY", () => {
248
+ it("encodes vector search query", () => {
249
+ const result = QUERY.encode({
250
+ query: [{ vector: [0.1, 0.2, 0.3] }],
251
+ topK: 10,
252
+ });
253
+ expect(result.rank_by).toEqual(["vector", "ANN", [0.1, 0.2, 0.3]]);
254
+ expect(result.top_k).toBe(10);
255
+ });
256
+ it("encodes text search query", () => {
257
+ const result = QUERY.encode({
258
+ query: [{ content: "hello world" }],
259
+ topK: 5,
260
+ });
261
+ expect(result.rank_by).toEqual(["content", "BM25", "hello world"]);
262
+ expect(result.top_k).toBe(5);
263
+ });
264
+ it("encodes multi-field text search as Sum", () => {
265
+ const result = QUERY.encode({
266
+ query: [{ title: "search term" }, { body: "search term" }],
267
+ topK: 10,
268
+ });
269
+ expect(result.rank_by).toEqual([
270
+ "Sum",
271
+ [
272
+ ["title", "BM25", "search term"],
273
+ ["body", "BM25", "search term"],
274
+ ],
275
+ ]);
276
+ });
277
+ it("encodes filter", () => {
278
+ const result = QUERY.encode({
279
+ query: [{ vector: [0.1] }],
280
+ topK: 5,
281
+ filter: { category: { $eq: "news" } },
282
+ });
283
+ expect(result.filters).toEqual(["category", "Eq", "news"]);
284
+ });
285
+ it("encodes include fields array", () => {
286
+ const result = QUERY.encode({
287
+ query: [{ vector: [0.1] }],
288
+ topK: 5,
289
+ include: ["title", "content"],
290
+ });
291
+ expect(result.include_attributes).toEqual(["title", "content"]);
292
+ });
293
+ it("encodes include as boolean", () => {
294
+ const result = QUERY.encode({
295
+ query: [{ vector: [0.1] }],
296
+ topK: 5,
297
+ include: true,
298
+ });
299
+ expect(result.include_attributes).toBe(true);
300
+ });
301
+ });
302
+ describe("SEARCH_HIT", () => {
303
+ it("decodes row to search hit", () => {
304
+ const result = SEARCH_HIT.decode({
305
+ id: "doc-1",
306
+ $dist: 0.5,
307
+ title: "Test",
308
+ category: "news",
309
+ }, "my-index");
310
+ expect(result).toEqual({
311
+ id: "doc-1",
312
+ index: "my-index",
313
+ score: -0.5, // negated: distance → similarity
314
+ document: {
315
+ id: "doc-1",
316
+ title: "Test",
317
+ category: "news",
318
+ },
319
+ });
320
+ });
321
+ it("decodes row with vector", () => {
322
+ const result = SEARCH_HIT.decode({
323
+ id: "doc-2",
324
+ $dist: 0.1,
325
+ vector: [0.1, 0.2, 0.3],
326
+ }, "my-index");
327
+ expect(result.document?.id).toBe("doc-2");
328
+ expect(result.document?.vector).toEqual([0.1, 0.2, 0.3]);
329
+ });
330
+ it("handles missing score", () => {
331
+ const result = SEARCH_HIT.decode({
332
+ id: "doc-3",
333
+ }, "my-index");
334
+ expect(result.score).toBe(0);
335
+ expect(result.document).toEqual({ id: "doc-3" });
336
+ });
337
+ it("converts numeric id to string", () => {
338
+ const result = SEARCH_HIT.decode({
339
+ id: 123,
340
+ $dist: 0.1,
341
+ }, "my-index");
342
+ expect(result.id).toBe("123");
343
+ expect(result.document?.id).toBe(123); // document keeps original type
344
+ });
345
+ });
346
+ });
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Comprehensive unit tests for filter conversion.
3
+ *
4
+ * Tests all filter operators across scalar types, logical composition,
5
+ * edge cases, and error conditions.
6
+ */
7
+ export {};
8
+ //# sourceMappingURL=filter.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"filter.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/filter.test.ts"],"names":[],"mappings":"AAAA;;;;;GAKG"}