@mastra/pg 0.3.0-alpha.6 → 0.3.0-alpha.8

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,23 +1,23 @@
1
1
 
2
- > @mastra/pg@0.3.0-alpha.6 build /home/runner/work/mastra/mastra/stores/pg
2
+ > @mastra/pg@0.3.0-alpha.8 build /home/runner/work/mastra/mastra/stores/pg
3
3
  > tsup src/index.ts --format esm,cjs --experimental-dts --clean --treeshake=smallest --splitting
4
4
 
5
5
  CLI Building entry: src/index.ts
6
6
  CLI Using tsconfig: tsconfig.json
7
7
  CLI tsup v8.4.0
8
8
  TSC Build start
9
- TSC ⚡️ Build success in 11111ms
9
+ TSC ⚡️ Build success in 11894ms
10
10
  DTS Build start
11
11
  CLI Target: es2022
12
12
  Analysis will use the bundled TypeScript version 5.8.2
13
13
  Writing package typings: /home/runner/work/mastra/mastra/stores/pg/dist/_tsup-dts-rollup.d.ts
14
14
  Analysis will use the bundled TypeScript version 5.8.2
15
15
  Writing package typings: /home/runner/work/mastra/mastra/stores/pg/dist/_tsup-dts-rollup.d.cts
16
- DTS ⚡️ Build success in 11009ms
16
+ DTS ⚡️ Build success in 12889ms
17
17
  CLI Cleaning output folder
18
18
  ESM Build start
19
19
  CJS Build start
20
- CJS dist/index.cjs 47.42 KB
21
- CJS ⚡️ Build success in 1596ms
22
- ESM dist/index.js 46.99 KB
23
- ESM ⚡️ Build success in 1596ms
20
+ ESM dist/index.js 48.21 KB
21
+ ESM ⚡️ Build success in 1550ms
22
+ CJS dist/index.cjs 48.63 KB
23
+ CJS ⚡️ Build success in 1553ms
package/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # @mastra/pg
2
2
 
3
+ ## 0.3.0-alpha.8
4
+
5
+ ### Patch Changes
6
+
7
+ - c0f22b4: [MASTRA-3130] Metadata Filter Update for PG and Libsql
8
+ - Updated dependencies [000a6d4]
9
+ - Updated dependencies [ed2f549]
10
+ - Updated dependencies [c0f22b4]
11
+ - Updated dependencies [0a033fa]
12
+ - Updated dependencies [9c26508]
13
+ - Updated dependencies [0f4eae3]
14
+ - Updated dependencies [16a8648]
15
+ - @mastra/core@0.9.0-alpha.8
16
+
17
+ ## 0.3.0-alpha.7
18
+
19
+ ### Patch Changes
20
+
21
+ - Updated dependencies [71d9444]
22
+ - @mastra/core@0.9.0-alpha.7
23
+
3
24
  ## 0.3.0-alpha.6
4
25
 
5
26
  ### Patch Changes
@@ -72,7 +72,7 @@ export declare const FILTER_OPERATORS: Record<string, OperatorFn>;
72
72
  declare type FilterOperator = {
73
73
  sql: string;
74
74
  needsValue: boolean;
75
- transformValue?: (value: any) => any;
75
+ transformValue?: () => any;
76
76
  };
77
77
 
78
78
  export declare interface FilterResult {
@@ -72,7 +72,7 @@ export declare const FILTER_OPERATORS: Record<string, OperatorFn>;
72
72
  declare type FilterOperator = {
73
73
  sql: string;
74
74
  needsValue: boolean;
75
- transformValue?: (value: any) => any;
75
+ transformValue?: () => any;
76
76
  };
77
77
 
78
78
  export declare interface FilterResult {
package/dist/index.cjs CHANGED
@@ -148,11 +148,29 @@ var FILTER_OPERATORS = {
148
148
  $lte: createNumericOperator("<="),
149
149
  // Array Operators
150
150
  $in: (key, paramIndex) => ({
151
- sql: `metadata#>>'{${handleKey(key)}}' = ANY($${paramIndex}::text[])`,
151
+ sql: `(
152
+ CASE
153
+ WHEN jsonb_typeof(metadata->'${handleKey(key)}') = 'array' THEN
154
+ EXISTS (
155
+ SELECT 1 FROM jsonb_array_elements_text(metadata->'${handleKey(key)}') as elem
156
+ WHERE elem = ANY($${paramIndex}::text[])
157
+ )
158
+ ELSE metadata#>>'{${handleKey(key)}}' = ANY($${paramIndex}::text[])
159
+ END
160
+ )`,
152
161
  needsValue: true
153
162
  }),
154
163
  $nin: (key, paramIndex) => ({
155
- sql: `metadata#>>'{${handleKey(key)}}' != ALL($${paramIndex}::text[])`,
164
+ sql: `(
165
+ CASE
166
+ WHEN jsonb_typeof(metadata->'${handleKey(key)}') = 'array' THEN
167
+ NOT EXISTS (
168
+ SELECT 1 FROM jsonb_array_elements_text(metadata->'${handleKey(key)}') as elem
169
+ WHERE elem = ANY($${paramIndex}::text[])
170
+ )
171
+ ELSE metadata#>>'{${handleKey(key)}}' != ALL($${paramIndex}::text[])
172
+ END
173
+ )`,
156
174
  needsValue: true
157
175
  }),
158
176
  $all: (key, paramIndex) => ({
@@ -193,14 +211,33 @@ var FILTER_OPERATORS = {
193
211
  sql: `metadata#>>'{${handleKey(key)}}' ~ $${paramIndex}`,
194
212
  needsValue: true
195
213
  }),
196
- $contains: (key, paramIndex) => ({
197
- sql: `metadata @> $${paramIndex}::jsonb`,
198
- needsValue: true,
199
- transformValue: (value) => {
200
- const parts = key.split(".");
201
- return JSON.stringify(parts.reduceRight((value2, key2) => ({ [key2]: value2 }), value));
214
+ $contains: (key, paramIndex, value) => {
215
+ let sql;
216
+ if (Array.isArray(value)) {
217
+ sql = `(metadata->'${handleKey(key)}') ?& $${paramIndex}`;
218
+ } else if (typeof value === "string") {
219
+ sql = `metadata->>'${handleKey(key)}' ILIKE '%' || $${paramIndex} || '%'`;
220
+ } else {
221
+ sql = `metadata->>'${handleKey(key)}' = $${paramIndex}`;
202
222
  }
203
- }),
223
+ return {
224
+ sql,
225
+ needsValue: true,
226
+ transformValue: () => Array.isArray(value) ? value.map(String) : value
227
+ };
228
+ },
229
+ /**
230
+ * $objectContains: Postgres-only operator for true JSONB object containment.
231
+ * Usage: { field: { $objectContains: { ...subobject } } }
232
+ */
233
+ // $objectContains: (key, paramIndex) => ({
234
+ // sql: `metadata @> $${paramIndex}::jsonb`,
235
+ // needsValue: true,
236
+ // transformValue: value => {
237
+ // const parts = key.split('.');
238
+ // return JSON.stringify(parts.reduceRight((value, key) => ({ [key]: value }), value));
239
+ // },
240
+ // }),
204
241
  $size: (key, paramIndex) => ({
205
242
  sql: `(
206
243
  CASE
@@ -244,7 +281,7 @@ function buildFilterQuery(filter, minScore) {
244
281
  const operatorFn = FILTER_OPERATORS[operator];
245
282
  const operatorResult = operatorFn(key, values.length + 1, operatorValue);
246
283
  if (operatorResult.needsValue) {
247
- const transformedValue = operatorResult.transformValue ? operatorResult.transformValue(operatorValue) : operatorValue;
284
+ const transformedValue = operatorResult.transformValue ? operatorResult.transformValue() : operatorValue;
248
285
  if (Array.isArray(transformedValue) && operator === "$elemMatch") {
249
286
  values.push(...transformedValue);
250
287
  } else {
package/dist/index.js CHANGED
@@ -140,11 +140,29 @@ var FILTER_OPERATORS = {
140
140
  $lte: createNumericOperator("<="),
141
141
  // Array Operators
142
142
  $in: (key, paramIndex) => ({
143
- sql: `metadata#>>'{${handleKey(key)}}' = ANY($${paramIndex}::text[])`,
143
+ sql: `(
144
+ CASE
145
+ WHEN jsonb_typeof(metadata->'${handleKey(key)}') = 'array' THEN
146
+ EXISTS (
147
+ SELECT 1 FROM jsonb_array_elements_text(metadata->'${handleKey(key)}') as elem
148
+ WHERE elem = ANY($${paramIndex}::text[])
149
+ )
150
+ ELSE metadata#>>'{${handleKey(key)}}' = ANY($${paramIndex}::text[])
151
+ END
152
+ )`,
144
153
  needsValue: true
145
154
  }),
146
155
  $nin: (key, paramIndex) => ({
147
- sql: `metadata#>>'{${handleKey(key)}}' != ALL($${paramIndex}::text[])`,
156
+ sql: `(
157
+ CASE
158
+ WHEN jsonb_typeof(metadata->'${handleKey(key)}') = 'array' THEN
159
+ NOT EXISTS (
160
+ SELECT 1 FROM jsonb_array_elements_text(metadata->'${handleKey(key)}') as elem
161
+ WHERE elem = ANY($${paramIndex}::text[])
162
+ )
163
+ ELSE metadata#>>'{${handleKey(key)}}' != ALL($${paramIndex}::text[])
164
+ END
165
+ )`,
148
166
  needsValue: true
149
167
  }),
150
168
  $all: (key, paramIndex) => ({
@@ -185,14 +203,33 @@ var FILTER_OPERATORS = {
185
203
  sql: `metadata#>>'{${handleKey(key)}}' ~ $${paramIndex}`,
186
204
  needsValue: true
187
205
  }),
188
- $contains: (key, paramIndex) => ({
189
- sql: `metadata @> $${paramIndex}::jsonb`,
190
- needsValue: true,
191
- transformValue: (value) => {
192
- const parts = key.split(".");
193
- return JSON.stringify(parts.reduceRight((value2, key2) => ({ [key2]: value2 }), value));
206
+ $contains: (key, paramIndex, value) => {
207
+ let sql;
208
+ if (Array.isArray(value)) {
209
+ sql = `(metadata->'${handleKey(key)}') ?& $${paramIndex}`;
210
+ } else if (typeof value === "string") {
211
+ sql = `metadata->>'${handleKey(key)}' ILIKE '%' || $${paramIndex} || '%'`;
212
+ } else {
213
+ sql = `metadata->>'${handleKey(key)}' = $${paramIndex}`;
194
214
  }
195
- }),
215
+ return {
216
+ sql,
217
+ needsValue: true,
218
+ transformValue: () => Array.isArray(value) ? value.map(String) : value
219
+ };
220
+ },
221
+ /**
222
+ * $objectContains: Postgres-only operator for true JSONB object containment.
223
+ * Usage: { field: { $objectContains: { ...subobject } } }
224
+ */
225
+ // $objectContains: (key, paramIndex) => ({
226
+ // sql: `metadata @> $${paramIndex}::jsonb`,
227
+ // needsValue: true,
228
+ // transformValue: value => {
229
+ // const parts = key.split('.');
230
+ // return JSON.stringify(parts.reduceRight((value, key) => ({ [key]: value }), value));
231
+ // },
232
+ // }),
196
233
  $size: (key, paramIndex) => ({
197
234
  sql: `(
198
235
  CASE
@@ -236,7 +273,7 @@ function buildFilterQuery(filter, minScore) {
236
273
  const operatorFn = FILTER_OPERATORS[operator];
237
274
  const operatorResult = operatorFn(key, values.length + 1, operatorValue);
238
275
  if (operatorResult.needsValue) {
239
- const transformedValue = operatorResult.transformValue ? operatorResult.transformValue(operatorValue) : operatorValue;
276
+ const transformedValue = operatorResult.transformValue ? operatorResult.transformValue() : operatorValue;
240
277
  if (Array.isArray(transformedValue) && operator === "$elemMatch") {
241
278
  values.push(...transformedValue);
242
279
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mastra/pg",
3
- "version": "0.3.0-alpha.6",
3
+ "version": "0.3.0-alpha.8",
4
4
  "description": "Postgres provider for Mastra - includes both vector and db storage capabilities",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -24,7 +24,7 @@
24
24
  "pg": "^8.13.3",
25
25
  "pg-promise": "^11.11.0",
26
26
  "xxhash-wasm": "^1.1.0",
27
- "@mastra/core": "^0.9.0-alpha.6"
27
+ "@mastra/core": "^0.9.0-alpha.8"
28
28
  },
29
29
  "devDependencies": {
30
30
  "@microsoft/api-extractor": "^7.52.1",
@@ -435,12 +435,7 @@ describe('PgVector', () => {
435
435
  });
436
436
 
437
437
  it('should handle filters correctly', async () => {
438
- const results = await vectorDB.query({
439
- indexName,
440
- queryVector: [1, 0, 0],
441
- topK: 10,
442
- filter: { type: 'a' },
443
- });
438
+ const results = await vectorDB.query({ indexName, queryVector: [1, 0, 0], topK: 10, filter: { type: 'a' } });
444
439
 
445
440
  expect(results).toHaveLength(1);
446
441
  results.forEach(result => {
@@ -606,7 +601,7 @@ describe('PgVector', () => {
606
601
 
607
602
  // Array Operator Tests
608
603
  describe('Array Operators', () => {
609
- it('should filter with $in operator', async () => {
604
+ it('should filter with $in operator for scalar field', async () => {
610
605
  const results = await vectorDB.query({
611
606
  indexName,
612
607
  queryVector: [1, 0, 0],
@@ -618,7 +613,25 @@ describe('PgVector', () => {
618
613
  });
619
614
  });
620
615
 
621
- it('should filter with $nin operator', async () => {
616
+ it('should filter with $in operator for array field', async () => {
617
+ // Insert a record with tags as array
618
+ await vectorDB.upsert({
619
+ indexName,
620
+ vectors: [[2, 0.2, 0]],
621
+ metadata: [{ tags: ['featured', 'sale', 'new'] }],
622
+ });
623
+ const results = await vectorDB.query({
624
+ indexName,
625
+ queryVector: [1, 0, 0],
626
+ filter: { tags: { $in: ['sale', 'clearance'] } },
627
+ });
628
+ expect(results.length).toBeGreaterThan(0);
629
+ results.forEach(result => {
630
+ expect(result.metadata?.tags.some((tag: string) => ['sale', 'clearance'].includes(tag))).toBe(true);
631
+ });
632
+ });
633
+
634
+ it('should filter with $nin operator for scalar field', async () => {
622
635
  const results = await vectorDB.query({
623
636
  indexName,
624
637
  queryVector: [1, 0, 0],
@@ -630,6 +643,24 @@ describe('PgVector', () => {
630
643
  });
631
644
  });
632
645
 
646
+ it('should filter with $nin operator for array field', async () => {
647
+ // Insert a record with tags as array
648
+ await vectorDB.upsert({
649
+ indexName,
650
+ vectors: [[2, 0.3, 0]],
651
+ metadata: [{ tags: ['clearance', 'used'] }],
652
+ });
653
+ const results = await vectorDB.query({
654
+ indexName,
655
+ queryVector: [1, 0, 0],
656
+ filter: { tags: { $nin: ['new', 'sale'] } },
657
+ });
658
+ expect(results.length).toBeGreaterThan(0);
659
+ results.forEach(result => {
660
+ expect(result.metadata?.tags.every((tag: string) => !['new', 'sale'].includes(tag))).toBe(true);
661
+ });
662
+ });
663
+
633
664
  it('should handle empty arrays in in/nin operators', async () => {
634
665
  // Should return no results for empty IN
635
666
  const resultsIn = await vectorDB.query({
@@ -660,6 +691,52 @@ describe('PgVector', () => {
660
691
  });
661
692
  });
662
693
 
694
+ it('should filter with $contains operator for string substring', async () => {
695
+ const results = await vectorDB.query({
696
+ indexName,
697
+ queryVector: [1, 0, 0],
698
+ filter: { category: { $contains: 'lectro' } },
699
+ });
700
+ expect(results.length).toBeGreaterThan(0);
701
+ results.forEach(result => {
702
+ expect(result.metadata?.category).toContain('lectro');
703
+ });
704
+ });
705
+
706
+ it('should not match deep object containment with $contains', async () => {
707
+ // Insert a record with a nested object
708
+ await vectorDB.upsert({
709
+ indexName,
710
+ vectors: [[1, 0.1, 0]],
711
+ metadata: [{ details: { color: 'red', size: 'large' }, category: 'clothing' }],
712
+ });
713
+ // $contains does NOT support deep object containment in Postgres
714
+ const results = await vectorDB.query({
715
+ indexName,
716
+ queryVector: [1, 0.1, 0],
717
+ filter: { details: { $contains: { color: 'red' } } },
718
+ });
719
+ expect(results.length).toBe(0);
720
+ });
721
+
722
+ it('should fallback to direct equality for non-array, non-string', async () => {
723
+ // Insert a record with a numeric field
724
+ await vectorDB.upsert({
725
+ indexName,
726
+ vectors: [[1, 0.2, 0]],
727
+ metadata: [{ price: 123 }],
728
+ });
729
+ const results = await vectorDB.query({
730
+ indexName,
731
+ queryVector: [1, 0, 0],
732
+ filter: { price: { $contains: 123 } },
733
+ });
734
+ expect(results.length).toBeGreaterThan(0);
735
+ results.forEach(result => {
736
+ expect(result.metadata?.price).toBe(123);
737
+ });
738
+ });
739
+
663
740
  it('should filter with $elemMatch operator', async () => {
664
741
  const results = await vectorDB.query({
665
742
  indexName,
@@ -810,29 +887,29 @@ describe('PgVector', () => {
810
887
  });
811
888
  });
812
889
 
813
- it('should filter with $contains operator for nested objects', async () => {
814
- // First insert a record with nested object
815
- await vectorDB.upsert({
816
- indexName,
817
- vectors: [[1, 0.1, 0]],
818
- metadata: [
819
- {
820
- details: { color: 'red', size: 'large' },
821
- category: 'clothing',
822
- },
823
- ],
824
- });
825
-
826
- const results = await vectorDB.query({
827
- indexName,
828
- queryVector: [1, 0.1, 0],
829
- filter: { details: { $contains: { color: 'red' } } },
830
- });
831
- expect(results.length).toBeGreaterThan(0);
832
- results.forEach(result => {
833
- expect(result.metadata?.details.color).toBe('red');
834
- });
835
- });
890
+ // it('should filter with $objectContains operator for nested objects', async () => {
891
+ // // First insert a record with nested object
892
+ // await vectorDB.upsert({
893
+ // indexName,
894
+ // vectors: [[1, 0.1, 0]],
895
+ // metadata: [
896
+ // {
897
+ // details: { color: 'red', size: 'large' },
898
+ // category: 'clothing',
899
+ // },
900
+ // ],
901
+ // });
902
+
903
+ // const results = await vectorDB.query({
904
+ // indexName,
905
+ // queryVector: [1, 0.1, 0],
906
+ // filter: { details: { $objectContains: { color: 'red' } } },
907
+ // });
908
+ // expect(results.length).toBeGreaterThan(0);
909
+ // results.forEach(result => {
910
+ // expect(result.metadata?.details.color).toBe('red');
911
+ // });
912
+ // });
836
913
 
837
914
  // String Pattern Tests
838
915
  it('should handle exact string matches', async () => {
@@ -1252,6 +1329,7 @@ describe('PgVector', () => {
1252
1329
  indexName,
1253
1330
  queryVector: [1, 0, 0],
1254
1331
  filter: { category: 'electronics' },
1332
+ includeVector: false,
1255
1333
  minScore: 0.9,
1256
1334
  });
1257
1335
  expect(results.length).toBeGreaterThan(0);
@@ -1403,7 +1481,8 @@ describe('PgVector', () => {
1403
1481
  indexName,
1404
1482
  queryVector: [1, 0, 0],
1405
1483
  filter: { category: 'electronics' },
1406
- minScore: 0.9, // minScore
1484
+ includeVector: false,
1485
+ minScore: 0.9,
1407
1486
  });
1408
1487
  expect(results.length).toBeGreaterThan(0);
1409
1488
  results.forEach(result => {
@@ -1457,7 +1536,11 @@ describe('PgVector', () => {
1457
1536
  const results = await vectorDB.query({
1458
1537
  indexName,
1459
1538
  queryVector: [1, 0, 0],
1460
- filter: { $and: [{ category: 'electronics' }], $or: [{ price: { $lt: 100 } }, { price: { $gt: 20 } }] },
1539
+ filter: {
1540
+ $and: [{ category: 'electronics' }],
1541
+ $or: [{ price: { $lt: 100 } }, { price: { $gt: 20 } }],
1542
+ $nor: [],
1543
+ },
1461
1544
  });
1462
1545
  expect(results.length).toBeGreaterThan(0);
1463
1546
  results.forEach(result => {
@@ -1477,13 +1560,7 @@ describe('PgVector', () => {
1477
1560
  const results = await vectorDB.query({
1478
1561
  indexName,
1479
1562
  queryVector: [1, 0, 0],
1480
- filter: {
1481
- tags: {
1482
- $elemMatch: {
1483
- $eq: 'value',
1484
- },
1485
- },
1486
- },
1563
+ filter: { tags: { $elemMatch: { $eq: 'value' } } },
1487
1564
  });
1488
1565
  expect(results).toHaveLength(0); // Should return no results for non-array field
1489
1566
  });
@@ -20,7 +20,7 @@ export type OperatorType =
20
20
  type FilterOperator = {
21
21
  sql: string;
22
22
  needsValue: boolean;
23
- transformValue?: (value: any) => any;
23
+ transformValue?: () => any;
24
24
  };
25
25
 
26
26
  type OperatorFn = (key: string, paramIndex: number, value?: any) => FilterOperator;
@@ -103,11 +103,29 @@ export const FILTER_OPERATORS: Record<string, OperatorFn> = {
103
103
 
104
104
  // Array Operators
105
105
  $in: (key, paramIndex) => ({
106
- sql: `metadata#>>'{${handleKey(key)}}' = ANY($${paramIndex}::text[])`,
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
+ )`,
107
116
  needsValue: true,
108
117
  }),
109
118
  $nin: (key, paramIndex) => ({
110
- sql: `metadata#>>'{${handleKey(key)}}' != ALL($${paramIndex}::text[])`,
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
+ )`,
111
129
  needsValue: true,
112
130
  }),
113
131
  $all: (key, paramIndex) => ({
@@ -151,14 +169,33 @@ export const FILTER_OPERATORS: Record<string, OperatorFn> = {
151
169
  needsValue: true,
152
170
  }),
153
171
 
154
- $contains: (key, paramIndex) => ({
155
- sql: `metadata @> $${paramIndex}::jsonb`,
156
- needsValue: true,
157
- transformValue: value => {
158
- const parts = key.split('.');
159
- return JSON.stringify(parts.reduceRight((value, key) => ({ [key]: value }), value));
160
- },
161
- }),
172
+ $contains: (key, paramIndex, value: any) => {
173
+ let sql;
174
+ if (Array.isArray(value)) {
175
+ sql = `(metadata->'${handleKey(key)}') ?& $${paramIndex}`;
176
+ } else if (typeof value === 'string') {
177
+ sql = `metadata->>'${handleKey(key)}' ILIKE '%' || $${paramIndex} || '%'`;
178
+ } else {
179
+ sql = `metadata->>'${handleKey(key)}' = $${paramIndex}`;
180
+ }
181
+ return {
182
+ sql,
183
+ needsValue: true,
184
+ transformValue: () => (Array.isArray(value) ? value.map(String) : value),
185
+ };
186
+ },
187
+ /**
188
+ * $objectContains: Postgres-only operator for true JSONB object containment.
189
+ * Usage: { field: { $objectContains: { ...subobject } } }
190
+ */
191
+ // $objectContains: (key, paramIndex) => ({
192
+ // sql: `metadata @> $${paramIndex}::jsonb`,
193
+ // needsValue: true,
194
+ // transformValue: value => {
195
+ // const parts = key.split('.');
196
+ // return JSON.stringify(parts.reduceRight((value, key) => ({ [key]: value }), value));
197
+ // },
198
+ // }),
162
199
  $size: (key: string, paramIndex: number) => ({
163
200
  sql: `(
164
201
  CASE
@@ -220,9 +257,7 @@ export function buildFilterQuery(filter: VectorFilter, minScore: number): Filter
220
257
  const operatorFn = FILTER_OPERATORS[operator as string]!;
221
258
  const operatorResult = operatorFn(key, values.length + 1, operatorValue);
222
259
  if (operatorResult.needsValue) {
223
- const transformedValue = operatorResult.transformValue
224
- ? operatorResult.transformValue(operatorValue)
225
- : operatorValue;
260
+ const transformedValue = operatorResult.transformValue ? operatorResult.transformValue() : operatorValue;
226
261
  if (Array.isArray(transformedValue) && operator === '$elemMatch') {
227
262
  values.push(...transformedValue);
228
263
  } else {