@mastra/libsql 0.0.1-alpha.1

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.
@@ -0,0 +1,462 @@
1
+ import type { InValue } from '@libsql/client';
2
+ import type {
3
+ VectorFilter,
4
+ BasicOperator,
5
+ NumericOperator,
6
+ ArrayOperator,
7
+ ElementOperator,
8
+ LogicalOperator,
9
+ RegexOperator,
10
+ } from '@mastra/core/vector/filter';
11
+
12
+ export type OperatorType =
13
+ | BasicOperator
14
+ | NumericOperator
15
+ | ArrayOperator
16
+ | ElementOperator
17
+ | LogicalOperator
18
+ | '$contains'
19
+ | Exclude<RegexOperator, '$options'>;
20
+
21
+ type FilterOperator = {
22
+ sql: string;
23
+ needsValue: boolean;
24
+ transformValue?: (value: any) => any;
25
+ };
26
+
27
+ type OperatorFn = (key: string, value?: any) => FilterOperator;
28
+
29
+ // Helper functions to create operators
30
+ const createBasicOperator = (symbol: string) => {
31
+ return (key: string): FilterOperator => ({
32
+ sql: `CASE
33
+ WHEN ? IS NULL THEN json_extract(metadata, '$."${handleKey(key)}"') IS ${symbol === '=' ? '' : 'NOT'} NULL
34
+ ELSE json_extract(metadata, '$."${handleKey(key)}"') ${symbol} ?
35
+ END`,
36
+ needsValue: true,
37
+ transformValue: (value: any) => {
38
+ // Return the values directly, not in an object
39
+ return [value, value];
40
+ },
41
+ });
42
+ };
43
+ const createNumericOperator = (symbol: string) => {
44
+ return (key: string): FilterOperator => ({
45
+ sql: `CAST(json_extract(metadata, '$."${handleKey(key)}"') AS NUMERIC) ${symbol} ?`,
46
+ needsValue: true,
47
+ });
48
+ };
49
+
50
+ const validateJsonArray = (key: string) =>
51
+ `json_valid(json_extract(metadata, '$."${handleKey(key)}"'))
52
+ AND json_type(json_extract(metadata, '$."${handleKey(key)}"')) = 'array'`;
53
+
54
+ // Define all filter operators
55
+ export const FILTER_OPERATORS: Record<string, OperatorFn> = {
56
+ $eq: createBasicOperator('='),
57
+ $ne: createBasicOperator('!='),
58
+ $gt: createNumericOperator('>'),
59
+ $gte: createNumericOperator('>='),
60
+ $lt: createNumericOperator('<'),
61
+ $lte: createNumericOperator('<='),
62
+
63
+ // Array Operators
64
+ $in: (key: string, value: any) => ({
65
+ sql: `json_extract(metadata, '$."${handleKey(key)}"') IN (${value.map(() => '?').join(',')})`,
66
+ needsValue: true,
67
+ }),
68
+
69
+ $nin: (key: string, value: any) => ({
70
+ sql: `json_extract(metadata, '$."${handleKey(key)}"') NOT IN (${value.map(() => '?').join(',')})`,
71
+ needsValue: true,
72
+ }),
73
+ $all: (key: string) => ({
74
+ sql: `json_extract(metadata, '$."${handleKey(key)}"') = ?`,
75
+ needsValue: true,
76
+ transformValue: (value: any) => {
77
+ const arrayValue = Array.isArray(value) ? value : [value];
78
+ if (arrayValue.length === 0) {
79
+ return {
80
+ sql: '1 = 0',
81
+ values: [],
82
+ };
83
+ }
84
+
85
+ return {
86
+ sql: `(
87
+ CASE
88
+ WHEN ${validateJsonArray(key)} THEN
89
+ NOT EXISTS (
90
+ SELECT value
91
+ FROM json_each(?)
92
+ WHERE value NOT IN (
93
+ SELECT value
94
+ FROM json_each(json_extract(metadata, '$."${handleKey(key)}"'))
95
+ )
96
+ )
97
+ ELSE FALSE
98
+ END
99
+ )`,
100
+ values: [JSON.stringify(arrayValue)],
101
+ };
102
+ },
103
+ }),
104
+ $elemMatch: (key: string) => ({
105
+ sql: `json_extract(metadata, '$."${handleKey(key)}"') = ?`,
106
+ needsValue: true,
107
+ transformValue: (value: any) => {
108
+ if (typeof value !== 'object' || Array.isArray(value)) {
109
+ throw new Error('$elemMatch requires an object with conditions');
110
+ }
111
+
112
+ // For nested object conditions
113
+ const conditions = Object.entries(value).map(([field, fieldValue]) => {
114
+ if (field.startsWith('$')) {
115
+ // Direct operators on array elements ($in, $gt, etc)
116
+ const { sql, values } = buildCondition('elem.value', { [field]: fieldValue }, '');
117
+ // Replace the metadata path with elem.value
118
+ const pattern = /json_extract\(metadata, '\$\."[^"]*"(\."[^"]*")*'\)/g;
119
+ const elemSql = sql.replace(pattern, 'elem.value');
120
+ return { sql: elemSql, values };
121
+ } else if (typeof fieldValue === 'object' && !Array.isArray(fieldValue)) {
122
+ // Nested field with operators (count: { $gt: 20 })
123
+ const { sql, values } = buildCondition(field, fieldValue, '');
124
+ // Replace the field path with elem.value path
125
+ const pattern = /json_extract\(metadata, '\$\."[^"]*"(\."[^"]*")*'\)/g;
126
+ const elemSql = sql.replace(pattern, `json_extract(elem.value, '$."${field}"')`);
127
+ return { sql: elemSql, values };
128
+ } else {
129
+ // Simple field equality (warehouse: 'A')
130
+ return {
131
+ sql: `json_extract(elem.value, '$."${field}"') = ?`,
132
+ values: [fieldValue],
133
+ };
134
+ }
135
+ });
136
+
137
+ return {
138
+ sql: `(
139
+ CASE
140
+ WHEN ${validateJsonArray(key)} THEN
141
+ EXISTS (
142
+ SELECT 1
143
+ FROM json_each(json_extract(metadata, '$."${handleKey(key)}"')) as elem
144
+ WHERE ${conditions.map(c => c.sql).join(' AND ')}
145
+ )
146
+ ELSE FALSE
147
+ END
148
+ )`,
149
+ values: conditions.flatMap(c => c.values),
150
+ };
151
+ },
152
+ }),
153
+
154
+ // Element Operators
155
+ $exists: (key: string) => ({
156
+ sql: `json_extract(metadata, '$."${handleKey(key)}"') IS NOT NULL`,
157
+ needsValue: false,
158
+ }),
159
+
160
+ // Logical Operators
161
+ $and: (key: string) => ({
162
+ sql: `(${key})`,
163
+ needsValue: false,
164
+ }),
165
+ $or: (key: string) => ({
166
+ sql: `(${key})`,
167
+ needsValue: false,
168
+ }),
169
+ $not: key => ({ sql: `NOT (${key})`, needsValue: false }),
170
+ $nor: (key: string) => ({
171
+ sql: `NOT (${key})`,
172
+ needsValue: false,
173
+ }),
174
+ $size: (key: string, paramIndex: number) => ({
175
+ sql: `(
176
+ CASE
177
+ WHEN json_type(json_extract(metadata, '$."${handleKey(key)}"')) = 'array' THEN
178
+ json_array_length(json_extract(metadata, '$."${handleKey(key)}"')) = $${paramIndex}
179
+ ELSE FALSE
180
+ END
181
+ )`,
182
+ needsValue: true,
183
+ }),
184
+ // /**
185
+ // * Regex Operators
186
+ // * Supports case insensitive and multiline
187
+ // */
188
+ // $regex: (key: string): FilterOperator => ({
189
+ // sql: `json_extract(metadata, '$."${handleKey(key)}"') = ?`,
190
+ // needsValue: true,
191
+ // transformValue: (value: any) => {
192
+ // const pattern = typeof value === 'object' ? value.$regex : value;
193
+ // const options = typeof value === 'object' ? value.$options || '' : '';
194
+ // let sql = `json_extract(metadata, '$."${handleKey(key)}"')`;
195
+
196
+ // // Handle multiline
197
+ // // if (options.includes('m')) {
198
+ // // sql = `REPLACE(${sql}, CHAR(10), '\n')`;
199
+ // // }
200
+
201
+ // // let finalPattern = pattern;
202
+ // // if (options) {
203
+ // // finalPattern = `(\\?${options})${pattern}`;
204
+ // // }
205
+
206
+ // // // Handle case insensitivity
207
+ // // if (options.includes('i')) {
208
+ // // sql = `LOWER(${sql}) REGEXP LOWER(?)`;
209
+ // // } else {
210
+ // // sql = `${sql} REGEXP ?`;
211
+ // // }
212
+
213
+ // if (options.includes('m')) {
214
+ // sql = `EXISTS (
215
+ // SELECT 1
216
+ // FROM json_each(
217
+ // json_array(
218
+ // ${sql},
219
+ // REPLACE(${sql}, CHAR(10), CHAR(13))
220
+ // )
221
+ // ) as lines
222
+ // WHERE lines.value REGEXP ?
223
+ // )`;
224
+ // } else {
225
+ // sql = `${sql} REGEXP ?`;
226
+ // }
227
+
228
+ // // Handle case insensitivity
229
+ // if (options.includes('i')) {
230
+ // sql = sql.replace('REGEXP ?', 'REGEXP LOWER(?)');
231
+ // sql = sql.replace('value REGEXP', 'LOWER(value) REGEXP');
232
+ // }
233
+
234
+ // // Handle extended - allows whitespace and comments in pattern
235
+ // if (options.includes('x')) {
236
+ // // Remove whitespace and comments from pattern
237
+ // const cleanPattern = pattern.replace(/\s+|#.*$/gm, '');
238
+ // return {
239
+ // sql,
240
+ // values: [cleanPattern],
241
+ // };
242
+ // }
243
+
244
+ // return {
245
+ // sql,
246
+ // values: [pattern],
247
+ // };
248
+ // },
249
+ // }),
250
+ $contains: (key: string) => ({
251
+ sql: `json_extract(metadata, '$."${handleKey(key)}"') = ?`,
252
+ needsValue: true,
253
+ transformValue: (value: any) => {
254
+ // Array containment
255
+ if (Array.isArray(value)) {
256
+ return {
257
+ sql: `(
258
+ SELECT ${validateJsonArray(key)}
259
+ AND EXISTS (
260
+ SELECT 1
261
+ FROM json_each(json_extract(metadata, '$."${handleKey(key)}"')) as m
262
+ WHERE m.value IN (SELECT value FROM json_each(?))
263
+ )
264
+ )`,
265
+ values: [JSON.stringify(value)],
266
+ };
267
+ }
268
+
269
+ // Nested object traversal
270
+ if (value && typeof value === 'object') {
271
+ const paths: string[] = [];
272
+ const values: any[] = [];
273
+
274
+ function traverse(obj: any, path: string[] = []) {
275
+ for (const [k, v] of Object.entries(obj)) {
276
+ const currentPath = [...path, k];
277
+ if (v && typeof v === 'object' && !Array.isArray(v)) {
278
+ traverse(v, currentPath);
279
+ } else {
280
+ paths.push(currentPath.join('.'));
281
+ values.push(v);
282
+ }
283
+ }
284
+ }
285
+
286
+ traverse(value);
287
+ return {
288
+ sql: `(${paths.map(path => `json_extract(metadata, '$."${handleKey(key)}"."${path}"') = ?`).join(' AND ')})`,
289
+ values,
290
+ };
291
+ }
292
+
293
+ return value;
294
+ },
295
+ }),
296
+ };
297
+
298
+ export interface FilterResult {
299
+ sql: string;
300
+ values: InValue[];
301
+ }
302
+
303
+ export const handleKey = (key: string) => {
304
+ return key.replace(/\./g, '"."');
305
+ };
306
+
307
+ export function buildFilterQuery(filter: VectorFilter): FilterResult {
308
+ if (!filter) {
309
+ return { sql: '', values: [] };
310
+ }
311
+
312
+ const values: InValue[] = [];
313
+ const conditions = Object.entries(filter)
314
+ .map(([key, value]) => {
315
+ const condition = buildCondition(key, value, '');
316
+ values.push(...condition.values);
317
+ return condition.sql;
318
+ })
319
+ .join(' AND ');
320
+
321
+ return {
322
+ sql: conditions ? `WHERE ${conditions}` : '',
323
+ values,
324
+ };
325
+ }
326
+
327
+ function buildCondition(key: string, value: any, parentPath: string): FilterResult {
328
+ // Handle logical operators ($and/$or)
329
+ if (['$and', '$or', '$not', '$nor'].includes(key)) {
330
+ return handleLogicalOperator(key as '$and' | '$or' | '$not' | '$nor', value, parentPath);
331
+ }
332
+
333
+ // If condition is not a FilterCondition object, assume it's an equality check
334
+ if (!value || typeof value !== 'object') {
335
+ return {
336
+ sql: `json_extract(metadata, '$."${key.replace(/\./g, '"."')}"') = ?`,
337
+ values: [value],
338
+ };
339
+ }
340
+
341
+ //TODO: Add regex support
342
+ // if ('$regex' in value) {
343
+ // return handleRegexOperator(key, value);
344
+ // }
345
+
346
+ // Handle operator conditions
347
+ return handleOperator(key, value);
348
+ }
349
+
350
+ // function handleRegexOperator(key: string, value: any): FilterResult {
351
+ // const operatorFn = FILTER_OPERATORS['$regex']!;
352
+ // const operatorResult = operatorFn(key, value);
353
+ // const transformed = operatorResult.transformValue ? operatorResult.transformValue(value) : value;
354
+
355
+ // return {
356
+ // sql: transformed.sql,
357
+ // values: transformed.values,
358
+ // };
359
+ // }
360
+
361
+ function handleLogicalOperator(
362
+ key: '$and' | '$or' | '$not' | '$nor',
363
+ value: VectorFilter[] | VectorFilter,
364
+ parentPath: string,
365
+ ): FilterResult {
366
+ // Handle empty conditions
367
+ if (!value || value.length === 0) {
368
+ switch (key) {
369
+ case '$and':
370
+ case '$nor':
371
+ return { sql: 'true', values: [] };
372
+ case '$or':
373
+ return { sql: 'false', values: [] };
374
+ case '$not':
375
+ throw new Error('$not operator cannot be empty');
376
+ default:
377
+ return { sql: 'true', values: [] };
378
+ }
379
+ }
380
+
381
+ if (key === '$not') {
382
+ // For top-level $not
383
+ const entries = Object.entries(value);
384
+ const conditions = entries.map(([fieldKey, fieldValue]) => buildCondition(fieldKey, fieldValue, key));
385
+ return {
386
+ sql: `NOT (${conditions.map(c => c.sql).join(' AND ')})`,
387
+ values: conditions.flatMap(c => c.values),
388
+ };
389
+ }
390
+
391
+ const values: InValue[] = [];
392
+ const joinOperator = key === '$or' || key === '$nor' ? 'OR' : 'AND';
393
+ const conditions = Array.isArray(value)
394
+ ? value.map(f => {
395
+ const entries = Object.entries(f);
396
+ return entries.map(([k, v]) => buildCondition(k, v, key));
397
+ })
398
+ : [buildCondition(key, value, parentPath)];
399
+
400
+ const joined = conditions
401
+ .flat()
402
+ .map(c => {
403
+ values.push(...c.values);
404
+ return c.sql;
405
+ })
406
+ .join(` ${joinOperator} `);
407
+
408
+ return {
409
+ sql: key === '$nor' ? `NOT (${joined})` : `(${joined})`,
410
+ values,
411
+ };
412
+ }
413
+
414
+ function handleOperator(key: string, value: any): FilterResult {
415
+ if (typeof value === 'object' && !Array.isArray(value)) {
416
+ const entries = Object.entries(value);
417
+ const results = entries.map(([operator, operatorValue]) =>
418
+ operator === '$not'
419
+ ? {
420
+ sql: `NOT (${Object.entries(operatorValue as Record<string, any>)
421
+ .map(([op, val]) => processOperator(key, op, val).sql)
422
+ .join(' AND ')})`,
423
+ values: Object.entries(operatorValue as Record<string, any>).flatMap(
424
+ ([op, val]) => processOperator(key, op, val).values,
425
+ ),
426
+ }
427
+ : processOperator(key, operator, operatorValue),
428
+ );
429
+
430
+ return {
431
+ sql: `(${results.map(r => r.sql).join(' AND ')})`,
432
+ values: results.flatMap(r => r.values),
433
+ };
434
+ }
435
+
436
+ // Handle single operator
437
+ const [[operator, operatorValue] = []] = Object.entries(value);
438
+ return processOperator(key, operator as string, operatorValue);
439
+ }
440
+
441
+ const processOperator = (key: string, operator: string, operatorValue: any): FilterResult => {
442
+ if (!operator.startsWith('$') || !FILTER_OPERATORS[operator]) {
443
+ throw new Error(`Invalid operator: ${operator}`);
444
+ }
445
+ const operatorFn = FILTER_OPERATORS[operator]!;
446
+ const operatorResult = operatorFn(key, operatorValue);
447
+
448
+ if (!operatorResult.needsValue) {
449
+ return { sql: operatorResult.sql, values: [] };
450
+ }
451
+
452
+ const transformed = operatorResult.transformValue ? operatorResult.transformValue(operatorValue) : operatorValue;
453
+
454
+ if (transformed && typeof transformed === 'object' && 'sql' in transformed) {
455
+ return transformed;
456
+ }
457
+
458
+ return {
459
+ sql: operatorResult.sql,
460
+ values: Array.isArray(transformed) ? transformed : [transformed],
461
+ };
462
+ };
package/tsconfig.json ADDED
@@ -0,0 +1,5 @@
1
+ {
2
+ "extends": "../../tsconfig.node.json",
3
+ "include": ["src/**/*"],
4
+ "exclude": ["node_modules", "**/*.test.ts"]
5
+ }
@@ -0,0 +1,11 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ environment: 'node',
6
+ include: ['src/**/*.test.ts'],
7
+ coverage: {
8
+ reporter: ['text', 'json', 'html'],
9
+ },
10
+ },
11
+ });