@mastra/lance 0.2.9 → 0.2.11-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.
@@ -1,443 +0,0 @@
1
- import { BaseFilterTranslator } from '@mastra/core/vector/filter';
2
- import type {
3
- VectorFilter,
4
- OperatorValueMap,
5
- LogicalOperatorValueMap,
6
- BlacklistedRootOperators,
7
- } from '@mastra/core/vector/filter';
8
-
9
- type LanceOperatorValueMap = OperatorValueMap & {
10
- $like: string;
11
- $notLike: string;
12
- $contains: string;
13
- };
14
-
15
- type LanceBlacklisted = BlacklistedRootOperators | '$like' | '$notLike' | '$contains';
16
-
17
- export type LanceVectorFilter = VectorFilter<
18
- keyof LanceOperatorValueMap,
19
- LanceOperatorValueMap,
20
- LogicalOperatorValueMap,
21
- LanceBlacklisted
22
- >;
23
-
24
- export class LanceFilterTranslator extends BaseFilterTranslator<LanceVectorFilter, string> {
25
- translate(filter: LanceVectorFilter): string {
26
- if (!filter || Object.keys(filter).length === 0) {
27
- return '';
28
- }
29
-
30
- // Check for fields with periods that aren't nested at top level
31
- if (typeof filter === 'object' && filter !== null) {
32
- const keys = Object.keys(filter);
33
- for (const key of keys) {
34
- if (key.includes('.') && !this.isNormalNestedField(key)) {
35
- throw new Error(`Field names containing periods (.) are not supported: ${key}`);
36
- }
37
- }
38
- }
39
-
40
- return this.processFilter(filter);
41
- }
42
-
43
- private processFilter(filter: unknown, parentPath = ''): string {
44
- // Handle null case
45
- if (filter === null) {
46
- return `${parentPath} IS NULL`;
47
- }
48
-
49
- // Handle Date objects at top level
50
- if (filter instanceof Date) {
51
- return `${parentPath} = ${this.formatValue(filter)}`;
52
- }
53
-
54
- // Handle top-level operators
55
- if (typeof filter === 'object' && filter !== null) {
56
- const obj = filter as Record<string, unknown>;
57
- const keys = Object.keys(obj);
58
-
59
- // Handle logical operators at top level
60
- if (keys.length === 1 && this.isOperator(keys[0]!)) {
61
- const operator = keys[0]!;
62
- const operatorValue = obj[operator];
63
-
64
- if (this.isLogicalOperator(operator)) {
65
- if (operator === '$and' || operator === '$or') {
66
- return this.processLogicalOperator(operator, operatorValue as unknown[]);
67
- }
68
- throw new Error(BaseFilterTranslator.ErrorMessages.UNSUPPORTED_OPERATOR(operator));
69
- }
70
-
71
- throw new Error(BaseFilterTranslator.ErrorMessages.INVALID_TOP_LEVEL_OPERATOR(operator));
72
- }
73
-
74
- // Check for fields with periods that aren't nested
75
- for (const key of keys) {
76
- if (key.includes('.') && !this.isNormalNestedField(key)) {
77
- throw new Error(`Field names containing periods (.) are not supported: ${key}`);
78
- }
79
- }
80
-
81
- // Handle multiple fields (implicit AND)
82
- if (keys.length > 1) {
83
- const conditions = keys.map(key => {
84
- const value = obj[key];
85
- // Check if key is a nested path or a field
86
- if (this.isNestedObject(value) && !this.isDateObject(value)) {
87
- return this.processNestedObject(key, value);
88
- } else {
89
- return this.processField(key, value);
90
- }
91
- });
92
- return conditions.join(' AND ');
93
- }
94
-
95
- // Handle single field
96
- if (keys.length === 1) {
97
- const key = keys[0]!;
98
- const value = obj[key]!;
99
-
100
- if (this.isNestedObject(value) && !this.isDateObject(value)) {
101
- return this.processNestedObject(key!, value);
102
- } else {
103
- return this.processField(key!, value);
104
- }
105
- }
106
- }
107
-
108
- return '';
109
- }
110
-
111
- private processLogicalOperator(operator: string, conditions: unknown[]): string {
112
- if (!Array.isArray(conditions)) {
113
- throw new Error(`Logical operator ${operator} must have an array value`);
114
- }
115
-
116
- if (conditions.length === 0) {
117
- return operator === '$and' ? 'true' : 'false';
118
- }
119
-
120
- const sqlOperator = operator === '$and' ? 'AND' : 'OR';
121
-
122
- const processedConditions = conditions.map(condition => {
123
- if (typeof condition !== 'object' || condition === null) {
124
- throw new Error(BaseFilterTranslator.ErrorMessages.INVALID_LOGICAL_OPERATOR_CONTENT(operator));
125
- }
126
-
127
- // Check if condition is a nested logical operator
128
- const condObj = condition as Record<string, unknown>;
129
- const keys = Object.keys(condObj);
130
-
131
- if (keys.length === 1 && this.isOperator(keys[0]!)) {
132
- if (this.isLogicalOperator(keys[0])) {
133
- return `(${this.processLogicalOperator(keys[0], condObj[keys[0]] as unknown[])})`;
134
- } else {
135
- throw new Error(BaseFilterTranslator.ErrorMessages.UNSUPPORTED_OPERATOR(keys[0]));
136
- }
137
- }
138
-
139
- // Handle multiple fields within a logical condition (implicit AND)
140
- if (keys.length > 1) {
141
- return `(${this.processFilter(condition)})`;
142
- }
143
-
144
- return this.processFilter(condition);
145
- });
146
-
147
- return processedConditions.join(` ${sqlOperator} `);
148
- }
149
-
150
- private processNestedObject(path: string, value: unknown): string {
151
- if (typeof value !== 'object' || value === null) {
152
- throw new Error(`Expected object for nested path ${path}`);
153
- }
154
-
155
- const obj = value as Record<string, unknown>;
156
- const keys = Object.keys(obj);
157
-
158
- // Handle empty object
159
- if (keys.length === 0) {
160
- return `${path} = {}`;
161
- }
162
-
163
- // Handle operators on a field
164
- if (keys.every(k => this.isOperator(k))) {
165
- return this.processOperators(path, obj);
166
- }
167
-
168
- // Process each nested field and join with AND
169
- const conditions = keys.map(key => {
170
- const nestedPath = key.includes('.')
171
- ? `${path}.${key}` // Key already contains dots (pre-dotted path)
172
- : `${path}.${key}`; // Normal nested field
173
-
174
- if (this.isNestedObject(obj[key]) && !this.isDateObject(obj[key])) {
175
- return this.processNestedObject(nestedPath, obj[key]);
176
- } else {
177
- return this.processField(nestedPath, obj[key]);
178
- }
179
- });
180
-
181
- return conditions.join(' AND ');
182
- }
183
-
184
- private processField(field: string, value: unknown): string {
185
- // Check for illegal field names
186
- if (field.includes('.') && !this.isNormalNestedField(field)) {
187
- throw new Error(`Field names containing periods (.) are not supported: ${field}`);
188
- }
189
-
190
- // Escape field name if needed
191
- const escapedField = this.escapeFieldName(field);
192
-
193
- // Handle null value
194
- if (value === null) {
195
- return `${escapedField} IS NULL`;
196
- }
197
-
198
- // Handle Date objects properly
199
- if (value instanceof Date) {
200
- return `${escapedField} = ${this.formatValue(value)}`;
201
- }
202
-
203
- // Handle arrays (convert to IN)
204
- if (Array.isArray(value)) {
205
- if (value.length === 0) {
206
- return 'false'; // Empty array is usually false in SQL
207
- }
208
- const normalizedValues = this.normalizeArrayValues(value);
209
- return `${escapedField} IN (${this.formatArrayValues(normalizedValues)})`;
210
- }
211
-
212
- // Handle operator objects
213
- if (this.isOperatorObject(value)) {
214
- return this.processOperators(field, value as Record<string, unknown>);
215
- }
216
-
217
- // Handle basic values (normalize dates and other special values)
218
- return `${escapedField} = ${this.formatValue(this.normalizeComparisonValue(value))}`;
219
- }
220
-
221
- private processOperators(field: string, operators: Record<string, unknown>): string {
222
- const escapedField = this.escapeFieldName(field);
223
- const operatorKeys = Object.keys(operators);
224
-
225
- // Check for logical operators at field level
226
- if (operatorKeys.some(op => this.isLogicalOperator(op))) {
227
- const logicalOp = operatorKeys.find(op => this.isLogicalOperator(op)) || '';
228
- throw new Error(`Unsupported operator: ${logicalOp} cannot be used at field level`);
229
- }
230
-
231
- // Process each operator and join with AND
232
- return operatorKeys
233
- .map(op => {
234
- const value = operators[op];
235
-
236
- // Check if this is a supported operator
237
- if (!this.isFieldOperator(op) && !this.isCustomOperator(op)) {
238
- throw new Error(BaseFilterTranslator.ErrorMessages.UNSUPPORTED_OPERATOR(op));
239
- }
240
-
241
- switch (op) {
242
- case '$eq':
243
- if (value === null) {
244
- return `${escapedField} IS NULL`;
245
- }
246
- return `${escapedField} = ${this.formatValue(this.normalizeComparisonValue(value))}`;
247
- case '$ne':
248
- if (value === null) {
249
- return `${escapedField} IS NOT NULL`;
250
- }
251
- return `${escapedField} != ${this.formatValue(this.normalizeComparisonValue(value))}`;
252
- case '$gt':
253
- return `${escapedField} > ${this.formatValue(this.normalizeComparisonValue(value))}`;
254
- case '$gte':
255
- return `${escapedField} >= ${this.formatValue(this.normalizeComparisonValue(value))}`;
256
- case '$lt':
257
- return `${escapedField} < ${this.formatValue(this.normalizeComparisonValue(value))}`;
258
- case '$lte':
259
- return `${escapedField} <= ${this.formatValue(this.normalizeComparisonValue(value))}`;
260
- case '$in':
261
- if (!Array.isArray(value)) {
262
- throw new Error(`$in operator requires array value for field: ${field}`);
263
- }
264
- if (value.length === 0) {
265
- return 'false'; // Empty IN is false
266
- }
267
- const normalizedValues = this.normalizeArrayValues(value);
268
- return `${escapedField} IN (${this.formatArrayValues(normalizedValues)})`;
269
- case '$like':
270
- return `${escapedField} LIKE ${this.formatValue(value)}`;
271
- case '$notLike':
272
- return `${escapedField} NOT LIKE ${this.formatValue(value)}`;
273
- case '$regex':
274
- return `regexp_match(${escapedField}, ${this.formatValue(value)})`;
275
- default:
276
- throw new Error(BaseFilterTranslator.ErrorMessages.UNSUPPORTED_OPERATOR(op));
277
- }
278
- })
279
- .join(' AND ');
280
- }
281
-
282
- private formatValue(value: unknown): string {
283
- if (value === null) {
284
- return 'NULL';
285
- }
286
-
287
- if (typeof value === 'string') {
288
- // Escape single quotes in SQL strings by doubling them
289
- return `'${value.replace(/'/g, "''")}'`;
290
- }
291
-
292
- if (typeof value === 'number') {
293
- return value.toString();
294
- }
295
-
296
- if (typeof value === 'boolean') {
297
- return value ? 'true' : 'false';
298
- }
299
-
300
- if (value instanceof Date) {
301
- return `timestamp '${value.toISOString()}'`;
302
- }
303
-
304
- if (typeof value === 'object') {
305
- if (value instanceof Date) {
306
- return `timestamp '${value.toISOString()}'`;
307
- }
308
- return JSON.stringify(value);
309
- }
310
-
311
- return String(value);
312
- }
313
-
314
- private formatArrayValues(array: unknown[]): string {
315
- return array.map(item => this.formatValue(item)).join(', ');
316
- }
317
-
318
- normalizeArrayValues(array: unknown[]): unknown[] {
319
- return array.map(item => {
320
- if (item instanceof Date) {
321
- return item; // Keep Date objects as is to properly format them later
322
- }
323
- return this.normalizeComparisonValue(item);
324
- });
325
- }
326
-
327
- normalizeComparisonValue(value: unknown): unknown {
328
- // Date objects should be preserved as is, not converted to strings
329
- if (value instanceof Date) {
330
- return value;
331
- }
332
-
333
- return super.normalizeComparisonValue(value);
334
- }
335
-
336
- private isOperatorObject(value: unknown): boolean {
337
- if (typeof value !== 'object' || value === null) {
338
- return false;
339
- }
340
-
341
- const obj = value as Record<string, unknown>;
342
- const keys = Object.keys(obj);
343
-
344
- return keys.length > 0 && keys.some(key => this.isOperator(key));
345
- }
346
-
347
- private isNestedObject(value: unknown): boolean {
348
- return typeof value === 'object' && value !== null && !Array.isArray(value);
349
- }
350
-
351
- private isNormalNestedField(field: string): boolean {
352
- // Check if field is a proper nested field name
353
- const parts = field.split('.');
354
- // A valid nested field shouldn't have empty parts or start/end with a dot
355
- return !field.startsWith('.') && !field.endsWith('.') && parts.every(part => part.trim().length > 0);
356
- }
357
-
358
- private escapeFieldName(field: string): string {
359
- // If field contains special characters or is a SQL keyword, escape with backticks
360
- if (field.includes(' ') || field.includes('-') || /^[A-Z]+$/.test(field) || this.isSqlKeyword(field)) {
361
- // For nested fields, escape each part
362
- if (field.includes('.')) {
363
- return field
364
- .split('.')
365
- .map(part => `\`${part}\``)
366
- .join('.');
367
- }
368
- return `\`${field}\``;
369
- }
370
-
371
- return field;
372
- }
373
-
374
- private isSqlKeyword(str: string): boolean {
375
- // Common SQL keywords that might need escaping
376
- const sqlKeywords = [
377
- 'SELECT',
378
- 'FROM',
379
- 'WHERE',
380
- 'AND',
381
- 'OR',
382
- 'NOT',
383
- 'INSERT',
384
- 'UPDATE',
385
- 'DELETE',
386
- 'CREATE',
387
- 'ALTER',
388
- 'DROP',
389
- 'TABLE',
390
- 'VIEW',
391
- 'INDEX',
392
- 'JOIN',
393
- 'INNER',
394
- 'OUTER',
395
- 'LEFT',
396
- 'RIGHT',
397
- 'FULL',
398
- 'UNION',
399
- 'ALL',
400
- 'DISTINCT',
401
- 'AS',
402
- 'ON',
403
- 'BETWEEN',
404
- 'LIKE',
405
- 'IN',
406
- 'IS',
407
- 'NULL',
408
- 'TRUE',
409
- 'FALSE',
410
- 'ASC',
411
- 'DESC',
412
- 'GROUP',
413
- 'ORDER',
414
- 'BY',
415
- 'HAVING',
416
- 'LIMIT',
417
- 'OFFSET',
418
- 'CASE',
419
- 'WHEN',
420
- 'THEN',
421
- 'ELSE',
422
- 'END',
423
- 'CAST',
424
- 'CUBE',
425
- ];
426
-
427
- return sqlKeywords.includes(str.toUpperCase());
428
- }
429
-
430
- private isDateObject(value: unknown): boolean {
431
- return value instanceof Date;
432
- }
433
-
434
- /**
435
- * Override getSupportedOperators to add custom operators for LanceDB
436
- */
437
- protected override getSupportedOperators() {
438
- return {
439
- ...BaseFilterTranslator.DEFAULT_OPERATORS,
440
- custom: ['$like', '$notLike', '$regex'],
441
- };
442
- }
443
- }