@mastra/libsql 0.0.4-alpha.4 → 0.0.5-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,58 +1,93 @@
1
1
  import type { InValue } from '@libsql/client';
2
+ import { parseFieldKey } from '@mastra/core/utils';
2
3
  import type {
3
- VectorFilter,
4
4
  BasicOperator,
5
5
  NumericOperator,
6
6
  ArrayOperator,
7
7
  ElementOperator,
8
8
  LogicalOperator,
9
- RegexOperator,
9
+ VectorFilter,
10
10
  } from '@mastra/core/vector/filter';
11
11
 
12
- export type OperatorType =
12
+ type OperatorType =
13
13
  | BasicOperator
14
14
  | NumericOperator
15
15
  | ArrayOperator
16
16
  | ElementOperator
17
17
  | LogicalOperator
18
18
  | '$contains'
19
- | Exclude<RegexOperator, '$options'>;
19
+ | '$size';
20
20
 
21
21
  type FilterOperator = {
22
22
  sql: string;
23
23
  needsValue: boolean;
24
- transformValue?: (value: any) => any;
24
+ transformValue?: () => any;
25
25
  };
26
26
 
27
27
  type OperatorFn = (key: string, value?: any) => FilterOperator;
28
28
 
29
29
  // Helper functions to create operators
30
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
- });
31
+ return (key: string, value: any): FilterOperator => {
32
+ const jsonPathKey = parseJsonPathKey(key);
33
+ return {
34
+ sql: `CASE
35
+ WHEN ? IS NULL THEN json_extract(metadata, '$."${jsonPathKey}"') IS ${symbol === '=' ? '' : 'NOT'} NULL
36
+ ELSE json_extract(metadata, '$."${jsonPathKey}"') ${symbol} ?
37
+ END`,
38
+ needsValue: true,
39
+ transformValue: () => {
40
+ // Return the values directly, not in an object
41
+ return [value, value];
42
+ },
43
+ };
44
+ };
42
45
  };
43
46
  const createNumericOperator = (symbol: string) => {
44
- return (key: string): FilterOperator => ({
45
- sql: `CAST(json_extract(metadata, '$."${handleKey(key)}"') AS NUMERIC) ${symbol} ?`,
46
- needsValue: true,
47
- });
47
+ return (key: string): FilterOperator => {
48
+ const jsonPathKey = parseJsonPathKey(key);
49
+ return {
50
+ sql: `CAST(json_extract(metadata, '$."${jsonPathKey}"') AS NUMERIC) ${symbol} ?`,
51
+ needsValue: true,
52
+ };
53
+ };
48
54
  };
49
55
 
50
56
  const validateJsonArray = (key: string) =>
51
- `json_valid(json_extract(metadata, '$."${handleKey(key)}"'))
52
- AND json_type(json_extract(metadata, '$."${handleKey(key)}"')) = 'array'`;
57
+ `json_valid(json_extract(metadata, '$."${key}"'))
58
+ AND json_type(json_extract(metadata, '$."${key}"')) = 'array'`;
59
+
60
+ const pattern = /json_extract\(metadata, '\$\."[^"]*"(\."[^"]*")*'\)/g;
61
+
62
+ function buildElemMatchConditions(value: any) {
63
+ const conditions = Object.entries(value).map(([field, fieldValue]) => {
64
+ if (field.startsWith('$')) {
65
+ // Direct operators on array elements ($in, $gt, etc)
66
+ const { sql, values } = buildCondition('elem.value', { [field]: fieldValue }, '');
67
+ // Replace the metadata path with elem.value
68
+ const elemSql = sql.replace(pattern, 'elem.value');
69
+ return { sql: elemSql, values };
70
+ } else if (typeof fieldValue === 'object' && !Array.isArray(fieldValue)) {
71
+ // Nested field with operators (count: { $gt: 20 })
72
+ const { sql, values } = buildCondition(field, fieldValue, '');
73
+ // Replace the field path with elem.value path
74
+ const elemSql = sql.replace(pattern, `json_extract(elem.value, '$."${field}"')`);
75
+ return { sql: elemSql, values };
76
+ } else {
77
+ const parsedFieldKey = parseFieldKey(field);
78
+ // Simple field equality (warehouse: 'A')
79
+ return {
80
+ sql: `json_extract(elem.value, '$."${parsedFieldKey}"') = ?`,
81
+ values: [fieldValue],
82
+ };
83
+ }
84
+ });
85
+
86
+ return conditions;
87
+ }
53
88
 
54
89
  // Define all filter operators
55
- export const FILTER_OPERATORS: Record<string, OperatorFn> = {
90
+ const FILTER_OPERATORS: Record<OperatorType, OperatorFn> = {
56
91
  $eq: createBasicOperator('='),
57
92
  $ne: createBasicOperator('!='),
58
93
  $gt: createNumericOperator('>'),
@@ -61,101 +96,121 @@ export const FILTER_OPERATORS: Record<string, OperatorFn> = {
61
96
  $lte: createNumericOperator('<='),
62
97
 
63
98
  // 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
- }
99
+ $in: (key: string, value: any) => {
100
+ const jsonPathKey = parseJsonPathKey(key);
101
+ const arr = Array.isArray(value) ? value : [value];
102
+ if (arr.length === 0) {
103
+ return { sql: '1 = 0', needsValue: true, transformValue: () => [] };
104
+ }
105
+ const paramPlaceholders = arr.map(() => '?').join(',');
106
+ return {
107
+ sql: `(
108
+ CASE
109
+ WHEN ${validateJsonArray(jsonPathKey)} THEN
110
+ EXISTS (
111
+ SELECT 1 FROM json_each(json_extract(metadata, '$."${jsonPathKey}"')) as elem
112
+ WHERE elem.value IN (SELECT value FROM json_each(?))
113
+ )
114
+ ELSE json_extract(metadata, '$."${jsonPathKey}"') IN (${paramPlaceholders})
115
+ END
116
+ )`,
117
+ needsValue: true,
118
+ transformValue: () => [JSON.stringify(arr), ...arr],
119
+ };
120
+ },
84
121
 
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
- )
122
+ $nin: (key: string, value: any) => {
123
+ const jsonPathKey = parseJsonPathKey(key);
124
+ const arr = Array.isArray(value) ? value : [value];
125
+ if (arr.length === 0) {
126
+ return { sql: '1 = 1', needsValue: true, transformValue: () => [] };
127
+ }
128
+ const paramPlaceholders = arr.map(() => '?').join(',');
129
+ return {
130
+ sql: `(
131
+ CASE
132
+ WHEN ${validateJsonArray(jsonPathKey)} THEN
133
+ NOT EXISTS (
134
+ SELECT 1 FROM json_each(json_extract(metadata, '$."${jsonPathKey}"')) as elem
135
+ WHERE elem.value IN (SELECT value FROM json_each(?))
136
+ )
137
+ ELSE json_extract(metadata, '$."${jsonPathKey}"') NOT IN (${paramPlaceholders})
138
+ END
139
+ )`,
140
+ needsValue: true,
141
+ transformValue: () => [JSON.stringify(arr), ...arr],
142
+ };
143
+ },
144
+ $all: (key: string, value: any) => {
145
+ const jsonPathKey = parseJsonPathKey(key);
146
+ let sql: string;
147
+ const arrayValue = Array.isArray(value) ? value : [value];
148
+
149
+ if (arrayValue.length === 0) {
150
+ // If the array is empty, always return false (no matches)
151
+ sql = '1 = 0';
152
+ } else {
153
+ sql = `(
154
+ CASE
155
+ WHEN ${validateJsonArray(jsonPathKey)} THEN
156
+ NOT EXISTS (
157
+ SELECT value
158
+ FROM json_each(?)
159
+ WHERE value NOT IN (
160
+ SELECT value
161
+ FROM json_each(json_extract(metadata, '$."${jsonPathKey}"'))
96
162
  )
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
- };
163
+ )
164
+ ELSE FALSE
165
+ END
166
+ )`;
167
+ }
168
+
169
+ return {
170
+ sql,
171
+ needsValue: true,
172
+ transformValue: () => {
173
+ if (arrayValue.length === 0) {
174
+ return [];
134
175
  }
135
- });
176
+ return [JSON.stringify(arrayValue)];
177
+ },
178
+ };
179
+ },
180
+ $elemMatch: (key: string, value: any) => {
181
+ const jsonPathKey = parseJsonPathKey(key);
182
+ if (typeof value !== 'object' || Array.isArray(value)) {
183
+ throw new Error('$elemMatch requires an object with conditions');
184
+ }
136
185
 
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
- }),
186
+ // For nested object conditions
187
+ const conditions = buildElemMatchConditions(value);
188
+
189
+ return {
190
+ sql: `(
191
+ CASE
192
+ WHEN ${validateJsonArray(jsonPathKey)} THEN
193
+ EXISTS (
194
+ SELECT 1
195
+ FROM json_each(json_extract(metadata, '$."${jsonPathKey}"')) as elem
196
+ WHERE ${conditions.map(c => c.sql).join(' AND ')}
197
+ )
198
+ ELSE FALSE
199
+ END
200
+ )`,
201
+ needsValue: true,
202
+ transformValue: () => conditions.flatMap(c => c.values),
203
+ };
204
+ },
153
205
 
154
206
  // Element Operators
155
- $exists: (key: string) => ({
156
- sql: `json_extract(metadata, '$."${handleKey(key)}"') IS NOT NULL`,
157
- needsValue: false,
158
- }),
207
+ $exists: (key: string) => {
208
+ const jsonPathKey = parseJsonPathKey(key);
209
+ return {
210
+ sql: `json_extract(metadata, '$."${jsonPathKey}"') IS NOT NULL`,
211
+ needsValue: false,
212
+ };
213
+ },
159
214
 
160
215
  // Logical Operators
161
216
  $and: (key: string) => ({
@@ -171,27 +226,30 @@ export const FILTER_OPERATORS: Record<string, OperatorFn> = {
171
226
  sql: `NOT (${key})`,
172
227
  needsValue: false,
173
228
  }),
174
- $size: (key: string, paramIndex: number) => ({
175
- sql: `(
229
+ $size: (key: string, paramIndex: number) => {
230
+ const jsonPathKey = parseJsonPathKey(key);
231
+ return {
232
+ sql: `(
176
233
  CASE
177
- WHEN json_type(json_extract(metadata, '$."${handleKey(key)}"')) = 'array' THEN
178
- json_array_length(json_extract(metadata, '$."${handleKey(key)}"')) = $${paramIndex}
234
+ WHEN json_type(json_extract(metadata, '$."${jsonPathKey}"')) = 'array' THEN
235
+ json_array_length(json_extract(metadata, '$."${jsonPathKey}"')) = $${paramIndex}
179
236
  ELSE FALSE
180
237
  END
181
238
  )`,
182
- needsValue: true,
183
- }),
239
+ needsValue: true,
240
+ };
241
+ },
184
242
  // /**
185
243
  // * Regex Operators
186
244
  // * Supports case insensitive and multiline
187
245
  // */
188
246
  // $regex: (key: string): FilterOperator => ({
189
- // sql: `json_extract(metadata, '$."${handleKey(key)}"') = ?`,
247
+ // sql: `json_extract(metadata, '$."${toJsonPathKey(key)}"') = ?`,
190
248
  // needsValue: true,
191
249
  // transformValue: (value: any) => {
192
250
  // const pattern = typeof value === 'object' ? value.$regex : value;
193
251
  // const options = typeof value === 'object' ? value.$options || '' : '';
194
- // let sql = `json_extract(metadata, '$."${handleKey(key)}"')`;
252
+ // let sql = `json_extract(metadata, '$."${toJsonPathKey(key)}"')`;
195
253
 
196
254
  // // Handle multiline
197
255
  // // if (options.includes('m')) {
@@ -247,63 +305,73 @@ export const FILTER_OPERATORS: Record<string, OperatorFn> = {
247
305
  // };
248
306
  // },
249
307
  // }),
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
- }
308
+ $contains: (key: string, value: any) => {
309
+ const jsonPathKey = parseJsonPathKey(key);
310
+ let sql;
311
+ if (Array.isArray(value)) {
312
+ sql = `(
313
+ SELECT ${validateJsonArray(jsonPathKey)}
314
+ AND EXISTS (
315
+ SELECT 1
316
+ FROM json_each(json_extract(metadata, '$."${jsonPathKey}"')) as m
317
+ WHERE m.value IN (SELECT value FROM json_each(?))
318
+ )
319
+ )`;
320
+ } else if (typeof value === 'string') {
321
+ sql = `lower(json_extract(metadata, '$."${jsonPathKey}"')) LIKE '%' || lower(?) || '%' ESCAPE '\\'`;
322
+ } else {
323
+ sql = `json_extract(metadata, '$."${jsonPathKey}"') = ?`;
324
+ }
325
+ return {
326
+ sql,
327
+ needsValue: true,
328
+ transformValue: () => {
329
+ if (Array.isArray(value)) {
330
+ return [JSON.stringify(value)];
284
331
  }
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
- }),
332
+ if (typeof value === 'object' && value !== null) {
333
+ return [JSON.stringify(value)];
334
+ }
335
+ if (typeof value === 'string') {
336
+ return [escapeLikePattern(value)];
337
+ }
338
+ return [value];
339
+ },
340
+ };
341
+ },
342
+ /**
343
+ * $objectContains: True JSON containment for advanced use (deep sub-object match).
344
+ * Usage: { field: { $objectContains: { ...subobject } } }
345
+ */
346
+ // $objectContains: (key: string) => ({
347
+ // sql: '', // Will be overridden by transformValue
348
+ // needsValue: true,
349
+ // transformValue: (value: any) => ({
350
+ // sql: `json_type(json_extract(metadata, '$."${toJsonPathKey(key)}"')) = 'object'
351
+ // AND json_patch(json_extract(metadata, '$."${toJsonPathKey(key)}"'), ?) = json_extract(metadata, '$."${toJsonPathKey(key)}"')`,
352
+ // values: [JSON.stringify(value)],
353
+ // }),
354
+ // }),
296
355
  };
297
356
 
298
- export interface FilterResult {
357
+ interface FilterResult {
299
358
  sql: string;
300
359
  values: InValue[];
301
360
  }
302
361
 
303
- export const handleKey = (key: string) => {
304
- return key.replace(/\./g, '"."');
362
+ function isFilterResult(obj: any): obj is FilterResult {
363
+ return obj && typeof obj === 'object' && typeof obj.sql === 'string' && Array.isArray(obj.values);
364
+ }
365
+
366
+ const parseJsonPathKey = (key: string) => {
367
+ const parsedKey = parseFieldKey(key);
368
+ return parsedKey.replace(/\./g, '"."');
305
369
  };
306
370
 
371
+ function escapeLikePattern(str: string): string {
372
+ return str.replace(/([%_\\])/g, '\\$1');
373
+ }
374
+
307
375
  export function buildFilterQuery(filter: VectorFilter): FilterResult {
308
376
  if (!filter) {
309
377
  return { sql: '', values: [] };
@@ -418,13 +486,13 @@ function handleOperator(key: string, value: any): FilterResult {
418
486
  operator === '$not'
419
487
  ? {
420
488
  sql: `NOT (${Object.entries(operatorValue as Record<string, any>)
421
- .map(([op, val]) => processOperator(key, op, val).sql)
489
+ .map(([op, val]) => processOperator(key, op as OperatorType, val).sql)
422
490
  .join(' AND ')})`,
423
491
  values: Object.entries(operatorValue as Record<string, any>).flatMap(
424
- ([op, val]) => processOperator(key, op, val).values,
492
+ ([op, val]) => processOperator(key, op as OperatorType, val).values,
425
493
  ),
426
494
  }
427
- : processOperator(key, operator, operatorValue),
495
+ : processOperator(key, operator as OperatorType, operatorValue),
428
496
  );
429
497
 
430
498
  return {
@@ -435,10 +503,10 @@ function handleOperator(key: string, value: any): FilterResult {
435
503
 
436
504
  // Handle single operator
437
505
  const [[operator, operatorValue] = []] = Object.entries(value);
438
- return processOperator(key, operator as string, operatorValue);
506
+ return processOperator(key, operator as OperatorType, operatorValue);
439
507
  }
440
508
 
441
- const processOperator = (key: string, operator: string, operatorValue: any): FilterResult => {
509
+ const processOperator = (key: string, operator: OperatorType, operatorValue: any): FilterResult => {
442
510
  if (!operator.startsWith('$') || !FILTER_OPERATORS[operator]) {
443
511
  throw new Error(`Invalid operator: ${operator}`);
444
512
  }
@@ -449,9 +517,9 @@ const processOperator = (key: string, operator: string, operatorValue: any): Fil
449
517
  return { sql: operatorResult.sql, values: [] };
450
518
  }
451
519
 
452
- const transformed = operatorResult.transformValue ? operatorResult.transformValue(operatorValue) : operatorValue;
520
+ const transformed = operatorResult.transformValue ? operatorResult.transformValue() : operatorValue;
453
521
 
454
- if (transformed && typeof transformed === 'object' && 'sql' in transformed) {
522
+ if (isFilterResult(transformed)) {
455
523
  return transformed;
456
524
  }
457
525