@mastra/lance 0.1.1-alpha.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.
- package/.turbo/turbo-build.log +23 -0
- package/CHANGELOG.md +13 -0
- package/LICENSE.md +46 -0
- package/README.md +263 -0
- package/dist/_tsup-dts-rollup.d.cts +369 -0
- package/dist/_tsup-dts-rollup.d.ts +369 -0
- package/dist/index.cjs +1564 -0
- package/dist/index.d.cts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1561 -0
- package/eslint.config.js +6 -0
- package/package.json +45 -0
- package/src/index.ts +2 -0
- package/src/storage/index.test.ts +1269 -0
- package/src/storage/index.ts +999 -0
- package/src/vector/filter.test.ts +295 -0
- package/src/vector/filter.ts +423 -0
- package/src/vector/index.test.ts +1493 -0
- package/src/vector/index.ts +700 -0
- package/src/vector/types.ts +16 -0
- package/tsconfig.json +5 -0
- package/vitest.config.ts +11 -0
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { LanceFilterTranslator } from './filter';
|
|
4
|
+
|
|
5
|
+
describe('LanceFilterTranslator', () => {
|
|
6
|
+
let translator: LanceFilterTranslator;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
translator = new LanceFilterTranslator();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
// Basic Filter Operations
|
|
13
|
+
describe('basic operations', () => {
|
|
14
|
+
it('handles empty filters', () => {
|
|
15
|
+
expect(translator.translate({})).toEqual('');
|
|
16
|
+
expect(translator.translate(null as any)).toEqual('');
|
|
17
|
+
expect(translator.translate(undefined as any)).toEqual('');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('translates equality operation', () => {
|
|
21
|
+
const filter = { field: 'value' };
|
|
22
|
+
expect(translator.translate(filter)).toEqual("field = 'value'");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('translates numeric equality operation', () => {
|
|
26
|
+
const filter = { count: 42 };
|
|
27
|
+
expect(translator.translate(filter)).toEqual('count = 42');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('translates boolean equality operation', () => {
|
|
31
|
+
const filter = { active: true };
|
|
32
|
+
expect(translator.translate(filter)).toEqual('active = true');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('combines multiple fields with AND', () => {
|
|
36
|
+
const filter = {
|
|
37
|
+
field1: 'value1',
|
|
38
|
+
field2: 'value2',
|
|
39
|
+
};
|
|
40
|
+
expect(translator.translate(filter)).toEqual("field1 = 'value1' AND field2 = 'value2'");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('handles comparison operators', () => {
|
|
44
|
+
const filter = {
|
|
45
|
+
price: { $gt: 100 },
|
|
46
|
+
};
|
|
47
|
+
expect(translator.translate(filter)).toEqual('price > 100');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('handles multiple operators on same field', () => {
|
|
51
|
+
const filter = {
|
|
52
|
+
price: { $gt: 100, $lt: 200 },
|
|
53
|
+
quantity: { $gte: 10, $lte: 20 },
|
|
54
|
+
};
|
|
55
|
+
expect(translator.translate(filter)).toEqual('price > 100 AND price < 200 AND quantity >= 10 AND quantity <= 20');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('handles date values', () => {
|
|
59
|
+
const date = new Date('2024-01-01');
|
|
60
|
+
const filter = { timestamp: date };
|
|
61
|
+
expect(translator.translate(filter)).toEqual(`timestamp = timestamp '${date.toISOString()}'`);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('handles date comparison operators', () => {
|
|
65
|
+
const date = new Date('2024-01-01');
|
|
66
|
+
const filter = { timestamp: { $gt: date } };
|
|
67
|
+
expect(translator.translate(filter)).toEqual(`timestamp > timestamp '${date.toISOString()}'`);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Array Operations
|
|
72
|
+
describe('array operations', () => {
|
|
73
|
+
it('translates arrays to IN operator', () => {
|
|
74
|
+
const filter = { tags: ['tag1', 'tag2'] };
|
|
75
|
+
expect(translator.translate(filter)).toEqual("tags IN ('tag1', 'tag2')");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('handles numeric arrays', () => {
|
|
79
|
+
const filter = { ids: [1, 2, 3] };
|
|
80
|
+
expect(translator.translate(filter)).toEqual('ids IN (1, 2, 3)');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('handles empty array values', () => {
|
|
84
|
+
const filter = { tags: [] };
|
|
85
|
+
expect(translator.translate(filter)).toEqual('false'); // Empty IN is usually false in SQL
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('handles explicit $in operator', () => {
|
|
89
|
+
const filter = { tags: { $in: ['tag1', 'tag2'] } };
|
|
90
|
+
expect(translator.translate(filter)).toEqual("tags IN ('tag1', 'tag2')");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('handles $in with mixed type values', () => {
|
|
94
|
+
const filter = { field: { $in: [1, 'two', true] } };
|
|
95
|
+
expect(translator.translate(filter)).toEqual("field IN (1, 'two', true)");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('handles $in with date values', () => {
|
|
99
|
+
const date = new Date('2024-01-01');
|
|
100
|
+
const filter = { field: { $in: [date] } };
|
|
101
|
+
expect(translator.translate(filter)).toEqual(`field IN (timestamp '${date.toISOString()}')`);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Logical Operators
|
|
106
|
+
describe('logical operators', () => {
|
|
107
|
+
it('handles $and operator', () => {
|
|
108
|
+
const filter = {
|
|
109
|
+
$and: [{ status: 'active' }, { age: { $gt: 25 } }],
|
|
110
|
+
};
|
|
111
|
+
expect(translator.translate(filter)).toEqual("status = 'active' AND age > 25");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('handles $or operator', () => {
|
|
115
|
+
const filter = {
|
|
116
|
+
$or: [{ status: 'active' }, { age: { $gt: 25 } }],
|
|
117
|
+
};
|
|
118
|
+
expect(translator.translate(filter)).toEqual("status = 'active' OR age > 25");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('handles nested logical operators', () => {
|
|
122
|
+
const filter = {
|
|
123
|
+
$and: [
|
|
124
|
+
{ status: 'active' },
|
|
125
|
+
{
|
|
126
|
+
$or: [{ category: { $in: ['A', 'B'] } }, { price: { $gt: 100 } }],
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
};
|
|
130
|
+
expect(translator.translate(filter)).toEqual("status = 'active' AND (category IN ('A', 'B') OR price > 100)");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('handles complex nested conditions', () => {
|
|
134
|
+
const filter = {
|
|
135
|
+
$or: [
|
|
136
|
+
{ age: { $gt: 25 } },
|
|
137
|
+
{
|
|
138
|
+
$and: [{ status: 'active' }, { theme: 'dark' }],
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
};
|
|
142
|
+
expect(translator.translate(filter)).toEqual("age > 25 OR (status = 'active' AND theme = 'dark')");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('handles $not operator with equality', () => {
|
|
146
|
+
const filter = { field: { $ne: 'value' } };
|
|
147
|
+
expect(translator.translate(filter)).toEqual("field != 'value'");
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('handles IS NULL conditions', () => {
|
|
151
|
+
const filter = { field: null };
|
|
152
|
+
expect(translator.translate(filter)).toEqual('field IS NULL');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('handles IS NOT NULL conditions', () => {
|
|
156
|
+
const filter = { field: { $ne: null } };
|
|
157
|
+
expect(translator.translate(filter)).toEqual('field IS NOT NULL');
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// Nested Objects and Fields
|
|
162
|
+
describe('nested objects and fields', () => {
|
|
163
|
+
it('converts nested objects to dot notation in SQL', () => {
|
|
164
|
+
const filter = {
|
|
165
|
+
user: {
|
|
166
|
+
profile: {
|
|
167
|
+
age: { $gt: 25 },
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
expect(translator.translate(filter)).toEqual('user.profile.age > 25');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('handles nested object equality', () => {
|
|
175
|
+
const filter = {
|
|
176
|
+
'user.profile.name': 'John',
|
|
177
|
+
};
|
|
178
|
+
expect(translator.translate(filter)).toEqual("user.profile.name = 'John'");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('handles mixed nesting patterns', () => {
|
|
182
|
+
const filter = {
|
|
183
|
+
user: {
|
|
184
|
+
'profile.age': { $gt: 25 },
|
|
185
|
+
name: 'John',
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
expect(translator.translate(filter)).toEqual("user.profile.age > 25 AND user.name = 'John'");
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// Special Operators
|
|
193
|
+
describe('special operators', () => {
|
|
194
|
+
it('handles LIKE operator', () => {
|
|
195
|
+
const filter = { name: { $like: '%John%' } };
|
|
196
|
+
expect(translator.translate(filter)).toEqual("name LIKE '%John%'");
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('handles NOT LIKE operator', () => {
|
|
200
|
+
const filter = { name: { $notLike: '%John%' } };
|
|
201
|
+
expect(translator.translate(filter)).toEqual("name NOT LIKE '%John%'");
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('handles regexp_match function', () => {
|
|
205
|
+
const filter = { name: { $regex: '^John' } };
|
|
206
|
+
expect(translator.translate(filter)).toEqual("regexp_match(name, '^John')");
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// Operator Validation
|
|
211
|
+
describe('operator validation', () => {
|
|
212
|
+
it('validates supported comparison operators', () => {
|
|
213
|
+
const supportedFilters = [
|
|
214
|
+
{ field: { $eq: 'value' } },
|
|
215
|
+
{ field: { $ne: 'value' } },
|
|
216
|
+
{ field: { $gt: 'value' } },
|
|
217
|
+
{ field: { $gte: 'value' } },
|
|
218
|
+
{ field: { $lt: 'value' } },
|
|
219
|
+
{ field: { $lte: 'value' } },
|
|
220
|
+
{ field: { $in: ['value'] } },
|
|
221
|
+
{ field: { $like: '%value%' } },
|
|
222
|
+
{ field: { $notLike: '%value%' } },
|
|
223
|
+
{ field: { $regex: 'pattern' } },
|
|
224
|
+
];
|
|
225
|
+
supportedFilters.forEach(filter => {
|
|
226
|
+
expect(() => translator.translate(filter)).not.toThrow();
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('throws error for unsupported operators', () => {
|
|
231
|
+
const unsupportedFilters = [
|
|
232
|
+
{ field: { $contains: 'value' } },
|
|
233
|
+
{ field: { $all: ['value'] } },
|
|
234
|
+
{ field: { $elemMatch: { $gt: 5 } } },
|
|
235
|
+
{ field: { $nor: [{ $eq: 'value' }] } },
|
|
236
|
+
{ field: { $type: 'string' } },
|
|
237
|
+
{ field: { $mod: [5, 0] } },
|
|
238
|
+
{ field: { $size: 3 } },
|
|
239
|
+
];
|
|
240
|
+
|
|
241
|
+
unsupportedFilters.forEach(filter => {
|
|
242
|
+
expect(() => translator.translate(filter)).toThrow(/Unsupported operator/);
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('throws error for invalid operators at top level', () => {
|
|
247
|
+
const invalidFilters = [{ $gt: 100 }, { $in: ['value1', 'value2'] }, { $like: '%pattern%' }];
|
|
248
|
+
|
|
249
|
+
invalidFilters.forEach(filter => {
|
|
250
|
+
expect(() => translator.translate(filter)).toThrow(/Invalid top-level operator/);
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('handles backtick escaping for special column names', () => {
|
|
255
|
+
const filter = {
|
|
256
|
+
CUBE: 10,
|
|
257
|
+
'Upper-Case-Name': 'Test',
|
|
258
|
+
'column name with space': 'value',
|
|
259
|
+
};
|
|
260
|
+
expect(translator.translate(filter)).toEqual(
|
|
261
|
+
"`CUBE` = 10 AND `Upper-Case-Name` = 'Test' AND `column name with space` = 'value'",
|
|
262
|
+
);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('throws error for field names with periods that are not nested fields', () => {
|
|
266
|
+
const filter = {
|
|
267
|
+
'field.with..period': 'value', // Using double dots to ensure it's invalid
|
|
268
|
+
};
|
|
269
|
+
expect(() => translator.translate(filter)).toThrow(/Field names containing periods/);
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// Type and value handling
|
|
274
|
+
describe('type handling', () => {
|
|
275
|
+
it('handles boolean values correctly', () => {
|
|
276
|
+
expect(translator.translate({ active: true })).toEqual('active = true');
|
|
277
|
+
expect(translator.translate({ active: false })).toEqual('active = false');
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('handles numeric types correctly', () => {
|
|
281
|
+
expect(translator.translate({ int: 42 })).toEqual('int = 42');
|
|
282
|
+
expect(translator.translate({ float: 3.14 })).toEqual('float = 3.14');
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('handles string values with proper quoting', () => {
|
|
286
|
+
expect(translator.translate({ name: 'John' })).toEqual("name = 'John'");
|
|
287
|
+
expect(translator.translate({ text: "O'Reilly" })).toEqual("text = 'O''Reilly'"); // SQL escaping
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('handles special SQL data types', () => {
|
|
291
|
+
const date = new Date('2024-01-01');
|
|
292
|
+
expect(translator.translate({ date_col: date })).toEqual(`date_col = timestamp '${date.toISOString()}'`);
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
});
|