@mastra/pg 0.3.4 → 0.3.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,3 +1,4 @@
1
+ import { parseFieldKey } from '@mastra/core/utils';
1
2
  import type {
2
3
  BasicOperator,
3
4
  NumericOperator,
@@ -8,14 +9,15 @@ import type {
8
9
  VectorFilter,
9
10
  } from '@mastra/core/vector/filter';
10
11
 
11
- export type OperatorType =
12
+ type OperatorType =
12
13
  | BasicOperator
13
14
  | NumericOperator
14
15
  | ArrayOperator
15
16
  | ElementOperator
16
17
  | LogicalOperator
17
18
  | '$contains'
18
- | Exclude<RegexOperator, '$options'>;
19
+ | Exclude<RegexOperator, '$options'>
20
+ | '$size';
19
21
 
20
22
  type FilterOperator = {
21
23
  sql: string;
@@ -25,22 +27,27 @@ type FilterOperator = {
25
27
 
26
28
  type OperatorFn = (key: string, paramIndex: number, value?: any) => FilterOperator;
27
29
 
28
- // Helper functions to create operators
29
30
  const createBasicOperator = (symbol: string) => {
30
- return (key: string, paramIndex: number) => ({
31
- sql: `CASE
32
- WHEN $${paramIndex}::text IS NULL THEN metadata#>>'{${handleKey(key)}}' IS ${symbol === '=' ? '' : 'NOT'} NULL
33
- ELSE metadata#>>'{${handleKey(key)}}' ${symbol} $${paramIndex}::text
34
- END`,
35
- needsValue: true,
36
- });
31
+ return (key: string, paramIndex: number) => {
32
+ const jsonPathKey = parseJsonPathKey(key);
33
+ return {
34
+ sql: `CASE
35
+ WHEN $${paramIndex}::text IS NULL THEN metadata#>>'{${jsonPathKey}}' IS ${symbol === '=' ? '' : 'NOT'} NULL
36
+ ELSE metadata#>>'{${jsonPathKey}}' ${symbol} $${paramIndex}::text
37
+ END`,
38
+ needsValue: true,
39
+ };
40
+ };
37
41
  };
38
42
 
39
43
  const createNumericOperator = (symbol: string) => {
40
- return (key: string, paramIndex: number) => ({
41
- sql: `(metadata#>>'{${handleKey(key)}}')::numeric ${symbol} $${paramIndex}`,
42
- needsValue: true,
43
- });
44
+ return (key: string, paramIndex: number) => {
45
+ const jsonPathKey = parseJsonPathKey(key);
46
+ return {
47
+ sql: `(metadata#>>'{${jsonPathKey}}')::numeric ${symbol} $${paramIndex}`,
48
+ needsValue: true,
49
+ };
50
+ };
44
51
  };
45
52
 
46
53
  function buildElemMatchConditions(value: any, paramIndex: number): { sql: string; values: any[] } {
@@ -73,7 +80,7 @@ function buildElemMatchConditions(value: any, paramIndex: number): { sql: string
73
80
  paramValue = val;
74
81
  }
75
82
 
76
- const operatorFn = FILTER_OPERATORS[paramOperator as keyof typeof FILTER_OPERATORS];
83
+ const operatorFn = FILTER_OPERATORS[paramOperator as OperatorType];
77
84
  if (!operatorFn) {
78
85
  throw new Error(`Invalid operator: ${paramOperator}`);
79
86
  }
@@ -93,7 +100,7 @@ function buildElemMatchConditions(value: any, paramIndex: number): { sql: string
93
100
  }
94
101
 
95
102
  // Define all filter operators
96
- export const FILTER_OPERATORS: Record<string, OperatorFn> = {
103
+ const FILTER_OPERATORS: Record<OperatorType, OperatorFn> = {
97
104
  $eq: createBasicOperator('='),
98
105
  $ne: createBasicOperator('!='),
99
106
  $gt: createNumericOperator('>'),
@@ -102,46 +109,56 @@ export const FILTER_OPERATORS: Record<string, OperatorFn> = {
102
109
  $lte: createNumericOperator('<='),
103
110
 
104
111
  // Array Operators
105
- $in: (key, paramIndex) => ({
106
- sql: `(
107
- CASE
108
- WHEN jsonb_typeof(metadata->'${handleKey(key)}') = 'array' THEN
109
- EXISTS (
110
- SELECT 1 FROM jsonb_array_elements_text(metadata->'${handleKey(key)}') as elem
111
- WHERE elem = ANY($${paramIndex}::text[])
112
- )
113
- ELSE metadata#>>'{${handleKey(key)}}' = ANY($${paramIndex}::text[])
114
- END
115
- )`,
116
- needsValue: true,
117
- }),
118
- $nin: (key, paramIndex) => ({
119
- sql: `(
120
- CASE
121
- WHEN jsonb_typeof(metadata->'${handleKey(key)}') = 'array' THEN
122
- NOT EXISTS (
123
- SELECT 1 FROM jsonb_array_elements_text(metadata->'${handleKey(key)}') as elem
124
- WHERE elem = ANY($${paramIndex}::text[])
125
- )
126
- ELSE metadata#>>'{${handleKey(key)}}' != ALL($${paramIndex}::text[])
127
- END
128
- )`,
129
- needsValue: true,
130
- }),
131
- $all: (key, paramIndex) => ({
132
- sql: `CASE WHEN array_length($${paramIndex}::text[], 1) IS NULL THEN false
133
- ELSE (metadata#>'{${handleKey(key)}}')::jsonb ?& $${paramIndex}::text[] END`,
134
- needsValue: true,
135
- }),
112
+ $in: (key, paramIndex) => {
113
+ const jsonPathKey = parseJsonPathKey(key);
114
+ return {
115
+ sql: `(
116
+ CASE
117
+ WHEN jsonb_typeof(metadata->'${jsonPathKey}') = 'array' THEN
118
+ EXISTS (
119
+ SELECT 1 FROM jsonb_array_elements_text(metadata->'${jsonPathKey}') as elem
120
+ WHERE elem = ANY($${paramIndex}::text[])
121
+ )
122
+ ELSE metadata#>>'{${jsonPathKey}}' = ANY($${paramIndex}::text[])
123
+ END
124
+ )`,
125
+ needsValue: true,
126
+ };
127
+ },
128
+ $nin: (key, paramIndex) => {
129
+ const jsonPathKey = parseJsonPathKey(key);
130
+ return {
131
+ sql: `(
132
+ CASE
133
+ WHEN jsonb_typeof(metadata->'${jsonPathKey}') = 'array' THEN
134
+ NOT EXISTS (
135
+ SELECT 1 FROM jsonb_array_elements_text(metadata->'${jsonPathKey}') as elem
136
+ WHERE elem = ANY($${paramIndex}::text[])
137
+ )
138
+ ELSE metadata#>>'{${jsonPathKey}}' != ALL($${paramIndex}::text[])
139
+ END
140
+ )`,
141
+ needsValue: true,
142
+ };
143
+ },
144
+ $all: (key, paramIndex) => {
145
+ const jsonPathKey = parseJsonPathKey(key);
146
+ return {
147
+ sql: `CASE WHEN array_length($${paramIndex}::text[], 1) IS NULL THEN false
148
+ ELSE (metadata#>'{${jsonPathKey}}')::jsonb ?& $${paramIndex}::text[] END`,
149
+ needsValue: true,
150
+ };
151
+ },
136
152
  $elemMatch: (key: string, paramIndex: number, value: any): FilterOperator => {
137
153
  const { sql, values } = buildElemMatchConditions(value, paramIndex);
154
+ const jsonPathKey = parseJsonPathKey(key);
138
155
  return {
139
156
  sql: `(
140
157
  CASE
141
- WHEN jsonb_typeof(metadata->'${handleKey(key)}') = 'array' THEN
158
+ WHEN jsonb_typeof(metadata->'${jsonPathKey}') = 'array' THEN
142
159
  EXISTS (
143
160
  SELECT 1
144
- FROM jsonb_array_elements(metadata->'${handleKey(key)}') as elem
161
+ FROM jsonb_array_elements(metadata->'${jsonPathKey}') as elem
145
162
  WHERE ${sql}
146
163
  )
147
164
  ELSE FALSE
@@ -152,10 +169,13 @@ export const FILTER_OPERATORS: Record<string, OperatorFn> = {
152
169
  };
153
170
  },
154
171
  // Element Operators
155
- $exists: key => ({
156
- sql: `metadata ? '${key}'`,
157
- needsValue: false,
158
- }),
172
+ $exists: key => {
173
+ const jsonPathKey = parseJsonPathKey(key);
174
+ return {
175
+ sql: `metadata ? '${jsonPathKey}'`,
176
+ needsValue: false,
177
+ };
178
+ },
159
179
 
160
180
  // Logical Operators
161
181
  $and: key => ({ sql: `(${key})`, needsValue: false }),
@@ -164,24 +184,29 @@ export const FILTER_OPERATORS: Record<string, OperatorFn> = {
164
184
  $nor: key => ({ sql: `NOT (${key})`, needsValue: false }),
165
185
 
166
186
  // Regex Operators
167
- $regex: (key, paramIndex) => ({
168
- sql: `metadata#>>'{${handleKey(key)}}' ~ $${paramIndex}`,
169
- needsValue: true,
170
- }),
187
+ $regex: (key, paramIndex) => {
188
+ const jsonPathKey = parseJsonPathKey(key);
189
+ return {
190
+ sql: `metadata#>>'{${jsonPathKey}}' ~ $${paramIndex}`,
191
+ needsValue: true,
192
+ };
193
+ },
171
194
 
172
195
  $contains: (key, paramIndex, value: any) => {
196
+ const jsonPathKey = parseJsonPathKey(key);
173
197
  let sql;
174
198
  if (Array.isArray(value)) {
175
- sql = `(metadata->'${handleKey(key)}') ?& $${paramIndex}`;
199
+ sql = `(metadata->'${jsonPathKey}') ?& $${paramIndex}`;
176
200
  } else if (typeof value === 'string') {
177
- sql = `metadata->>'${handleKey(key)}' ILIKE '%' || $${paramIndex} || '%'`;
201
+ sql = `metadata->>'${jsonPathKey}' ILIKE '%' || $${paramIndex} || '%' ESCAPE '\\'`;
178
202
  } else {
179
- sql = `metadata->>'${handleKey(key)}' = $${paramIndex}`;
203
+ sql = `metadata->>'${jsonPathKey}' = $${paramIndex}`;
180
204
  }
181
205
  return {
182
206
  sql,
183
207
  needsValue: true,
184
- transformValue: () => (Array.isArray(value) ? value.map(String) : value),
208
+ transformValue: () =>
209
+ Array.isArray(value) ? value.map(String) : typeof value === 'string' ? escapeLikePattern(value) : value,
185
210
  };
186
211
  },
187
212
  /**
@@ -196,29 +221,37 @@ export const FILTER_OPERATORS: Record<string, OperatorFn> = {
196
221
  // return JSON.stringify(parts.reduceRight((value, key) => ({ [key]: value }), value));
197
222
  // },
198
223
  // }),
199
- $size: (key: string, paramIndex: number) => ({
200
- sql: `(
224
+ $size: (key: string, paramIndex: number) => {
225
+ const jsonPathKey = parseJsonPathKey(key);
226
+ return {
227
+ sql: `(
201
228
  CASE
202
- WHEN jsonb_typeof(metadata#>'{${handleKey(key)}}') = 'array' THEN
203
- jsonb_array_length(metadata#>'{${handleKey(key)}}') = $${paramIndex}
229
+ WHEN jsonb_typeof(metadata#>'{${jsonPathKey}}') = 'array' THEN
230
+ jsonb_array_length(metadata#>'{${jsonPathKey}}') = $${paramIndex}
204
231
  ELSE FALSE
205
232
  END
206
233
  )`,
207
- needsValue: true,
208
- }),
234
+ needsValue: true,
235
+ };
236
+ },
209
237
  };
210
238
 
211
- export interface FilterResult {
239
+ interface FilterResult {
212
240
  sql: string;
213
241
  values: any[];
214
242
  }
215
243
 
216
- export const handleKey = (key: string) => {
217
- return key.replace(/\./g, ',');
244
+ const parseJsonPathKey = (key: string) => {
245
+ const parsedKey = key !== '' ? parseFieldKey(key) : '';
246
+ return parsedKey.replace(/\./g, ',');
218
247
  };
219
248
 
220
- export function buildFilterQuery(filter: VectorFilter, minScore: number): FilterResult {
221
- const values = [minScore];
249
+ function escapeLikePattern(str: string): string {
250
+ return str.replace(/([%_\\])/g, '\\$1');
251
+ }
252
+
253
+ export function buildFilterQuery(filter: VectorFilter, minScore: number, topK: number): FilterResult {
254
+ const values = [minScore, topK];
222
255
 
223
256
  function buildCondition(key: string, value: any, parentPath: string): string {
224
257
  // Handle logical operators ($and/$or)
@@ -229,7 +262,7 @@ export function buildFilterQuery(filter: VectorFilter, minScore: number): Filter
229
262
  // If condition is not a FilterCondition object, assume it's an equality check
230
263
  if (!value || typeof value !== 'object') {
231
264
  values.push(value);
232
- return `metadata#>>'{${handleKey(key)}}' = $${values.length}`;
265
+ return `metadata#>>'{${parseJsonPathKey(key)}}' = $${values.length}`;
233
266
  }
234
267
 
235
268
  // Handle operator conditions
@@ -240,11 +273,11 @@ export function buildFilterQuery(filter: VectorFilter, minScore: number): Filter
240
273
  const entries = Object.entries(operatorValue as Record<string, unknown>);
241
274
  const conditions = entries
242
275
  .map(([nestedOp, nestedValue]) => {
243
- if (!FILTER_OPERATORS[nestedOp as keyof typeof FILTER_OPERATORS]) {
276
+ if (!FILTER_OPERATORS[nestedOp as OperatorType]) {
244
277
  throw new Error(`Invalid operator in $not condition: ${nestedOp}`);
245
278
  }
246
- const operatorFn = FILTER_OPERATORS[nestedOp]!;
247
- const operatorResult = operatorFn(key, values.length + 1);
279
+ const operatorFn = FILTER_OPERATORS[nestedOp as OperatorType]!;
280
+ const operatorResult = operatorFn(key, values.length + 1, nestedValue);
248
281
  if (operatorResult.needsValue) {
249
282
  values.push(nestedValue as number);
250
283
  }
@@ -254,7 +287,7 @@ export function buildFilterQuery(filter: VectorFilter, minScore: number): Filter
254
287
 
255
288
  return `NOT (${conditions})`;
256
289
  }
257
- const operatorFn = FILTER_OPERATORS[operator as string]!;
290
+ const operatorFn = FILTER_OPERATORS[operator as OperatorType]!;
258
291
  const operatorResult = operatorFn(key, values.length + 1, operatorValue);
259
292
  if (operatorResult.needsValue) {
260
293
  const transformedValue = operatorResult.transformValue ? operatorResult.transformValue() : operatorValue;
@@ -116,11 +116,11 @@ describe('PostgreSQL Index Performance', () => {
116
116
  vectorDB = new PGPerformanceVector(connectionString);
117
117
  });
118
118
  beforeEach(async () => {
119
- await vectorDB.deleteIndex(testIndexName);
119
+ await vectorDB.deleteIndex({ indexName: testIndexName });
120
120
  });
121
121
 
122
122
  afterEach(async () => {
123
- await vectorDB.deleteIndex(testIndexName);
123
+ await vectorDB.deleteIndex({ indexName: testIndexName });
124
124
  });
125
125
 
126
126
  afterAll(async () => {