@mastra/pg 0.2.7-alpha.3 → 0.2.7-alpha.5

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.2.7-alpha.3 build /home/runner/work/mastra/mastra/stores/pg
2
+ > @mastra/pg@0.2.7-alpha.5 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 10020ms
9
+ TSC ⚡️ Build success in 10178ms
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 11079ms
16
+ DTS ⚡️ Build success in 10075ms
17
17
  CLI Cleaning output folder
18
18
  ESM Build start
19
19
  CJS Build start
20
- ESM dist/index.js 40.32 KB
21
- ESM ⚡️ Build success in 1253ms
22
- CJS dist/index.cjs 40.67 KB
23
- CJS ⚡️ Build success in 1258ms
20
+ ESM dist/index.js 40.60 KB
21
+ ESM ⚡️ Build success in 1332ms
22
+ CJS dist/index.cjs 41.03 KB
23
+ CJS ⚡️ Build success in 1332ms
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # @mastra/pg
2
2
 
3
+ ## 0.2.7-alpha.5
4
+
5
+ ### Patch Changes
6
+
7
+ - 93875ed: Improved the performance of Memory semantic recall by 2 to 3 times when using pg by making tweaks to @mastra/memory @mastra/core and @mastra/pg
8
+ - Updated dependencies [93875ed]
9
+ - @mastra/core@0.8.0-alpha.5
10
+
11
+ ## 0.2.7-alpha.4
12
+
13
+ ### Patch Changes
14
+
15
+ - Updated dependencies [d7e08e8]
16
+ - @mastra/core@0.8.0-alpha.4
17
+
3
18
  ## 0.2.7-alpha.3
4
19
 
5
20
  ### Patch Changes
@@ -200,12 +200,18 @@ declare interface PgQueryVectorParams extends QueryVectorParams {
200
200
 
201
201
  declare class PgVector extends MastraVector {
202
202
  private pool;
203
- private indexCache;
203
+ private describeIndexCache;
204
+ private createdIndexes;
205
+ private mutexesByName;
204
206
  constructor(connectionString: string);
207
+ private getMutexByName;
205
208
  transformFilter(filter?: VectorFilter): VectorFilter;
206
209
  getIndexInfo(indexName: string): Promise<PGIndexStats>;
207
210
  query(...args: ParamsToArgs<PgQueryVectorParams> | PgQueryVectorArgs): Promise<QueryResult[]>;
208
211
  upsert(...args: ParamsToArgs<UpsertVectorParams>): Promise<string[]>;
212
+ private hasher;
213
+ private getIndexCacheKey;
214
+ private cachedIndexExists;
209
215
  createIndex(...args: ParamsToArgs<PgCreateIndexParams> | PgCreateIndexArgs): Promise<void>;
210
216
  /**
211
217
  * @deprecated This function is deprecated. Use buildIndex instead
@@ -200,12 +200,18 @@ declare interface PgQueryVectorParams extends QueryVectorParams {
200
200
 
201
201
  declare class PgVector extends MastraVector {
202
202
  private pool;
203
- private indexCache;
203
+ private describeIndexCache;
204
+ private createdIndexes;
205
+ private mutexesByName;
204
206
  constructor(connectionString: string);
207
+ private getMutexByName;
205
208
  transformFilter(filter?: VectorFilter): VectorFilter;
206
209
  getIndexInfo(indexName: string): Promise<PGIndexStats>;
207
210
  query(...args: ParamsToArgs<PgQueryVectorParams> | PgQueryVectorArgs): Promise<QueryResult[]>;
208
211
  upsert(...args: ParamsToArgs<UpsertVectorParams>): Promise<string[]>;
212
+ private hasher;
213
+ private getIndexCacheKey;
214
+ private cachedIndexExists;
209
215
  createIndex(...args: ParamsToArgs<PgCreateIndexParams> | PgCreateIndexArgs): Promise<void>;
210
216
  /**
211
217
  * @deprecated This function is deprecated. Use buildIndex instead
package/dist/index.cjs CHANGED
@@ -1,8 +1,9 @@
1
1
  'use strict';
2
2
 
3
- var crypto$1 = require('crypto');
4
3
  var vector = require('@mastra/core/vector');
4
+ var asyncMutex = require('async-mutex');
5
5
  var pg = require('pg');
6
+ var xxhash = require('xxhash-wasm');
6
7
  var filter = require('@mastra/core/vector/filter');
7
8
  var storage = require('@mastra/core/storage');
8
9
  var pgPromise = require('pg-promise');
@@ -10,6 +11,7 @@ var pgPromise = require('pg-promise');
10
11
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
11
12
 
12
13
  var pg__default = /*#__PURE__*/_interopDefault(pg);
14
+ var xxhash__default = /*#__PURE__*/_interopDefault(xxhash);
13
15
  var pgPromise__default = /*#__PURE__*/_interopDefault(pgPromise);
14
16
 
15
17
  // src/vector/index.ts
@@ -294,7 +296,9 @@ function buildFilterQuery(filter, minScore) {
294
296
  // src/vector/index.ts
295
297
  var PgVector = class extends vector.MastraVector {
296
298
  pool;
297
- indexCache = /* @__PURE__ */ new Map();
299
+ describeIndexCache = /* @__PURE__ */ new Map();
300
+ createdIndexes = /* @__PURE__ */ new Map();
301
+ mutexesByName = /* @__PURE__ */ new Map();
298
302
  constructor(connectionString) {
299
303
  super();
300
304
  const basePool = new pg__default.default.Pool({
@@ -313,16 +317,33 @@ var PgVector = class extends vector.MastraVector {
313
317
  "vector.type": "postgres"
314
318
  }
315
319
  }) ?? basePool;
320
+ void (async () => {
321
+ const existingIndexes = await this.listIndexes();
322
+ void existingIndexes.map(async (indexName) => {
323
+ const info = await this.getIndexInfo(indexName);
324
+ const key = await this.getIndexCacheKey({
325
+ indexName,
326
+ metric: info.metric,
327
+ dimension: info.dimension,
328
+ type: info.type
329
+ });
330
+ this.createdIndexes.set(indexName, key);
331
+ });
332
+ })();
333
+ }
334
+ getMutexByName(indexName) {
335
+ if (!this.mutexesByName.has(indexName)) this.mutexesByName.set(indexName, new asyncMutex.Mutex());
336
+ return this.mutexesByName.get(indexName);
316
337
  }
317
338
  transformFilter(filter) {
318
339
  const translator = new PGFilterTranslator();
319
340
  return translator.translate(filter);
320
341
  }
321
342
  async getIndexInfo(indexName) {
322
- if (!this.indexCache.has(indexName)) {
323
- this.indexCache.set(indexName, await this.describeIndex(indexName));
343
+ if (!this.describeIndexCache.has(indexName)) {
344
+ this.describeIndexCache.set(indexName, await this.describeIndex(indexName));
324
345
  }
325
- return this.indexCache.get(indexName);
346
+ return this.describeIndexCache.get(indexName);
326
347
  }
327
348
  async query(...args) {
328
349
  const params = this.normalizeArgs("query", args, [
@@ -408,69 +429,77 @@ var PgVector = class extends vector.MastraVector {
408
429
  client.release();
409
430
  }
410
431
  }
432
+ hasher = xxhash__default.default();
433
+ async getIndexCacheKey(params) {
434
+ const input = params.indexName + params.dimension + params.metric + (params.type || "ivfflat");
435
+ return (await this.hasher).h32(input);
436
+ }
437
+ cachedIndexExists(indexName, newKey) {
438
+ const existingIndexCacheKey = this.createdIndexes.get(indexName);
439
+ return existingIndexCacheKey && existingIndexCacheKey === newKey;
440
+ }
411
441
  async createIndex(...args) {
412
442
  const params = this.normalizeArgs("createIndex", args, [
413
443
  "indexConfig",
414
444
  "buildIndex"
415
445
  ]);
416
446
  const { indexName, dimension, metric = "cosine", indexConfig = {}, buildIndex = true } = params;
417
- const client = await this.pool.connect();
418
- try {
419
- if (!indexName.match(/^[a-zA-Z_][a-zA-Z0-9_]*$/)) {
420
- throw new Error("Invalid index name format");
421
- }
422
- if (!Number.isInteger(dimension) || dimension <= 0) {
423
- throw new Error("Dimension must be a positive integer");
447
+ if (!indexName.match(/^[a-zA-Z_][a-zA-Z0-9_]*$/)) {
448
+ throw new Error("Invalid index name format");
449
+ }
450
+ if (!Number.isInteger(dimension) || dimension <= 0) {
451
+ throw new Error("Dimension must be a positive integer");
452
+ }
453
+ const indexCacheKey = await this.getIndexCacheKey({ indexName, dimension, type: indexConfig.type, metric });
454
+ if (this.cachedIndexExists(indexName, indexCacheKey)) {
455
+ return;
456
+ }
457
+ const mutex = this.getMutexByName(`create-${indexName}`);
458
+ await mutex.runExclusive(async () => {
459
+ if (this.cachedIndexExists(indexName, indexCacheKey)) {
460
+ return;
424
461
  }
425
- const extensionCheck = await client.query(`
462
+ const client = await this.pool.connect();
463
+ try {
464
+ const extensionCheck = await client.query(`
426
465
  SELECT EXISTS (
427
466
  SELECT 1 FROM pg_available_extensions WHERE name = 'vector'
428
467
  );
429
468
  `);
430
- if (!extensionCheck.rows[0].exists) {
431
- throw new Error("PostgreSQL vector extension is not available. Please install it first.");
432
- }
433
- const hash = crypto$1.createHash("sha256").update(indexName).digest("hex");
434
- const lockId = BigInt("0x" + hash.slice(0, 8)) % BigInt(2 ** 31);
435
- const acquired = await client.query("SELECT pg_try_advisory_lock($1)", [lockId]);
436
- if (!acquired.rows[0].pg_try_advisory_lock) {
437
- const exists = await client.query(
438
- `
439
- SELECT 1 FROM pg_class c
440
- JOIN pg_namespace n ON n.oid = c.relnamespace
441
- WHERE c.relname = $1
442
- AND n.nspname = 'public'
443
- `,
444
- [indexName]
445
- );
446
- if (exists.rows.length > 0) {
447
- console.log(`Table ${indexName} already exists, skipping creation`);
448
- return;
469
+ if (!extensionCheck.rows[0].exists) {
470
+ this.createdIndexes.delete(indexName);
471
+ throw new Error("PostgreSQL vector extension is not available. Please install it first.");
449
472
  }
450
- await client.query("SELECT pg_advisory_lock($1)", [lockId]);
451
- }
452
- try {
453
- await client.query("CREATE EXTENSION IF NOT EXISTS vector");
454
- await client.query(`
455
- CREATE TABLE IF NOT EXISTS ${indexName} (
456
- id SERIAL PRIMARY KEY,
457
- vector_id TEXT UNIQUE NOT NULL,
458
- embedding vector(${dimension}),
459
- metadata JSONB DEFAULT '{}'::jsonb
460
- );
461
- `);
473
+ try {
474
+ await client.query(`
475
+ DO $$
476
+ BEGIN
477
+ CREATE EXTENSION IF NOT EXISTS vector;
478
+
479
+ CREATE TABLE IF NOT EXISTS ${indexName} (
480
+ id SERIAL PRIMARY KEY,
481
+ vector_id TEXT UNIQUE NOT NULL,
482
+ embedding vector(${dimension}),
483
+ metadata JSONB DEFAULT '{}'::jsonb
484
+ );
485
+ END $$;
486
+ `);
487
+ this.createdIndexes.set(indexName, indexCacheKey);
488
+ } catch (e) {
489
+ this.createdIndexes.delete(indexName);
490
+ throw e;
491
+ }
492
+ if (buildIndex) {
493
+ await this.setupIndex({ indexName, metric, indexConfig }, client);
494
+ }
495
+ } catch (error) {
496
+ this.createdIndexes.delete(indexName);
497
+ console.error("Failed to create vector table:", error);
498
+ throw error;
462
499
  } finally {
463
- await client.query("SELECT pg_advisory_unlock($1)", [lockId]);
464
- }
465
- if (buildIndex) {
466
- await this.setupIndex({ indexName, metric, indexConfig }, client);
500
+ client.release();
467
501
  }
468
- } catch (error) {
469
- console.error("Failed to create vector table:", error);
470
- throw error;
471
- } finally {
472
- client.release();
473
- }
502
+ });
474
503
  }
475
504
  /**
476
505
  * @deprecated This function is deprecated. Use buildIndex instead
@@ -492,30 +521,13 @@ var PgVector = class extends vector.MastraVector {
492
521
  }
493
522
  }
494
523
  async setupIndex({ indexName, metric, indexConfig }, client) {
495
- const hash = crypto$1.createHash("sha256").update("build:" + indexName).digest("hex");
496
- const lockId = BigInt("0x" + hash.slice(0, 8)) % BigInt(2 ** 31);
497
- const acquired = await client.query("SELECT pg_try_advisory_lock($1)", [lockId]);
498
- if (!acquired.rows[0].pg_try_advisory_lock) {
499
- const exists = await client.query(
500
- `
501
- SELECT 1 FROM pg_class c
502
- JOIN pg_namespace n ON n.oid = c.relnamespace
503
- WHERE c.relname = $1
504
- AND n.nspname = 'public'
505
- `,
506
- [`${indexName}_vector_idx`]
507
- );
508
- if (exists.rows.length > 0) {
509
- console.log(`Index ${indexName}_vector_idx already exists, skipping creation`);
510
- this.indexCache.delete(indexName);
511
- return;
524
+ const mutex = this.getMutexByName(`build-${indexName}`);
525
+ await mutex.runExclusive(async () => {
526
+ if (this.createdIndexes.has(indexName)) {
527
+ await client.query(`DROP INDEX IF EXISTS ${indexName}_vector_idx`);
512
528
  }
513
- await client.query("SELECT pg_advisory_lock($1)", [lockId]);
514
- }
515
- try {
516
- await client.query(`DROP INDEX IF EXISTS ${indexName}_vector_idx`);
517
529
  if (indexConfig.type === "flat") {
518
- this.indexCache.delete(indexName);
530
+ this.describeIndexCache.delete(indexName);
519
531
  return;
520
532
  }
521
533
  const metricOp = metric === "cosine" ? "vector_cosine_ops" : metric === "euclidean" ? "vector_l2_ops" : "vector_ip_ops";
@@ -548,10 +560,7 @@ var PgVector = class extends vector.MastraVector {
548
560
  `;
549
561
  }
550
562
  await client.query(indexSQL);
551
- this.indexCache.delete(indexName);
552
- } finally {
553
- await client.query("SELECT pg_advisory_unlock($1)", [lockId]);
554
- }
563
+ });
555
564
  }
556
565
  async listIndexes() {
557
566
  const client = await this.pool.connect();
@@ -577,8 +586,7 @@ var PgVector = class extends vector.MastraVector {
577
586
  WHERE attrelid = $1::regclass
578
587
  AND attname = 'embedding';
579
588
  `;
580
- const countQuery = `
581
- SELECT COUNT(*) as count
589
+ const countQuery = ` SELECT COUNT(*) as count
582
590
  FROM ${indexName};
583
591
  `;
584
592
  const indexQuery = `
@@ -631,6 +639,7 @@ var PgVector = class extends vector.MastraVector {
631
639
  const client = await this.pool.connect();
632
640
  try {
633
641
  await client.query(`DROP TABLE IF EXISTS ${indexName} CASCADE`);
642
+ this.createdIndexes.delete(indexName);
634
643
  } catch (error) {
635
644
  await client.query("ROLLBACK");
636
645
  throw new Error(`Failed to delete vector table: ${error.message}`);
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
- import { createHash } from 'crypto';
2
1
  import { MastraVector } from '@mastra/core/vector';
2
+ import { Mutex } from 'async-mutex';
3
3
  import pg from 'pg';
4
+ import xxhash from 'xxhash-wasm';
4
5
  import { BaseFilterTranslator } from '@mastra/core/vector/filter';
5
6
  import { MastraStorage, TABLE_EVALS, TABLE_TRACES, TABLE_WORKFLOW_SNAPSHOT, TABLE_THREADS, TABLE_MESSAGES } from '@mastra/core/storage';
6
7
  import pgPromise from 'pg-promise';
@@ -287,7 +288,9 @@ function buildFilterQuery(filter, minScore) {
287
288
  // src/vector/index.ts
288
289
  var PgVector = class extends MastraVector {
289
290
  pool;
290
- indexCache = /* @__PURE__ */ new Map();
291
+ describeIndexCache = /* @__PURE__ */ new Map();
292
+ createdIndexes = /* @__PURE__ */ new Map();
293
+ mutexesByName = /* @__PURE__ */ new Map();
291
294
  constructor(connectionString) {
292
295
  super();
293
296
  const basePool = new pg.Pool({
@@ -306,16 +309,33 @@ var PgVector = class extends MastraVector {
306
309
  "vector.type": "postgres"
307
310
  }
308
311
  }) ?? basePool;
312
+ void (async () => {
313
+ const existingIndexes = await this.listIndexes();
314
+ void existingIndexes.map(async (indexName) => {
315
+ const info = await this.getIndexInfo(indexName);
316
+ const key = await this.getIndexCacheKey({
317
+ indexName,
318
+ metric: info.metric,
319
+ dimension: info.dimension,
320
+ type: info.type
321
+ });
322
+ this.createdIndexes.set(indexName, key);
323
+ });
324
+ })();
325
+ }
326
+ getMutexByName(indexName) {
327
+ if (!this.mutexesByName.has(indexName)) this.mutexesByName.set(indexName, new Mutex());
328
+ return this.mutexesByName.get(indexName);
309
329
  }
310
330
  transformFilter(filter) {
311
331
  const translator = new PGFilterTranslator();
312
332
  return translator.translate(filter);
313
333
  }
314
334
  async getIndexInfo(indexName) {
315
- if (!this.indexCache.has(indexName)) {
316
- this.indexCache.set(indexName, await this.describeIndex(indexName));
335
+ if (!this.describeIndexCache.has(indexName)) {
336
+ this.describeIndexCache.set(indexName, await this.describeIndex(indexName));
317
337
  }
318
- return this.indexCache.get(indexName);
338
+ return this.describeIndexCache.get(indexName);
319
339
  }
320
340
  async query(...args) {
321
341
  const params = this.normalizeArgs("query", args, [
@@ -401,69 +421,77 @@ var PgVector = class extends MastraVector {
401
421
  client.release();
402
422
  }
403
423
  }
424
+ hasher = xxhash();
425
+ async getIndexCacheKey(params) {
426
+ const input = params.indexName + params.dimension + params.metric + (params.type || "ivfflat");
427
+ return (await this.hasher).h32(input);
428
+ }
429
+ cachedIndexExists(indexName, newKey) {
430
+ const existingIndexCacheKey = this.createdIndexes.get(indexName);
431
+ return existingIndexCacheKey && existingIndexCacheKey === newKey;
432
+ }
404
433
  async createIndex(...args) {
405
434
  const params = this.normalizeArgs("createIndex", args, [
406
435
  "indexConfig",
407
436
  "buildIndex"
408
437
  ]);
409
438
  const { indexName, dimension, metric = "cosine", indexConfig = {}, buildIndex = true } = params;
410
- const client = await this.pool.connect();
411
- try {
412
- if (!indexName.match(/^[a-zA-Z_][a-zA-Z0-9_]*$/)) {
413
- throw new Error("Invalid index name format");
414
- }
415
- if (!Number.isInteger(dimension) || dimension <= 0) {
416
- throw new Error("Dimension must be a positive integer");
439
+ if (!indexName.match(/^[a-zA-Z_][a-zA-Z0-9_]*$/)) {
440
+ throw new Error("Invalid index name format");
441
+ }
442
+ if (!Number.isInteger(dimension) || dimension <= 0) {
443
+ throw new Error("Dimension must be a positive integer");
444
+ }
445
+ const indexCacheKey = await this.getIndexCacheKey({ indexName, dimension, type: indexConfig.type, metric });
446
+ if (this.cachedIndexExists(indexName, indexCacheKey)) {
447
+ return;
448
+ }
449
+ const mutex = this.getMutexByName(`create-${indexName}`);
450
+ await mutex.runExclusive(async () => {
451
+ if (this.cachedIndexExists(indexName, indexCacheKey)) {
452
+ return;
417
453
  }
418
- const extensionCheck = await client.query(`
454
+ const client = await this.pool.connect();
455
+ try {
456
+ const extensionCheck = await client.query(`
419
457
  SELECT EXISTS (
420
458
  SELECT 1 FROM pg_available_extensions WHERE name = 'vector'
421
459
  );
422
460
  `);
423
- if (!extensionCheck.rows[0].exists) {
424
- throw new Error("PostgreSQL vector extension is not available. Please install it first.");
425
- }
426
- const hash = createHash("sha256").update(indexName).digest("hex");
427
- const lockId = BigInt("0x" + hash.slice(0, 8)) % BigInt(2 ** 31);
428
- const acquired = await client.query("SELECT pg_try_advisory_lock($1)", [lockId]);
429
- if (!acquired.rows[0].pg_try_advisory_lock) {
430
- const exists = await client.query(
431
- `
432
- SELECT 1 FROM pg_class c
433
- JOIN pg_namespace n ON n.oid = c.relnamespace
434
- WHERE c.relname = $1
435
- AND n.nspname = 'public'
436
- `,
437
- [indexName]
438
- );
439
- if (exists.rows.length > 0) {
440
- console.log(`Table ${indexName} already exists, skipping creation`);
441
- return;
461
+ if (!extensionCheck.rows[0].exists) {
462
+ this.createdIndexes.delete(indexName);
463
+ throw new Error("PostgreSQL vector extension is not available. Please install it first.");
442
464
  }
443
- await client.query("SELECT pg_advisory_lock($1)", [lockId]);
444
- }
445
- try {
446
- await client.query("CREATE EXTENSION IF NOT EXISTS vector");
447
- await client.query(`
448
- CREATE TABLE IF NOT EXISTS ${indexName} (
449
- id SERIAL PRIMARY KEY,
450
- vector_id TEXT UNIQUE NOT NULL,
451
- embedding vector(${dimension}),
452
- metadata JSONB DEFAULT '{}'::jsonb
453
- );
454
- `);
465
+ try {
466
+ await client.query(`
467
+ DO $$
468
+ BEGIN
469
+ CREATE EXTENSION IF NOT EXISTS vector;
470
+
471
+ CREATE TABLE IF NOT EXISTS ${indexName} (
472
+ id SERIAL PRIMARY KEY,
473
+ vector_id TEXT UNIQUE NOT NULL,
474
+ embedding vector(${dimension}),
475
+ metadata JSONB DEFAULT '{}'::jsonb
476
+ );
477
+ END $$;
478
+ `);
479
+ this.createdIndexes.set(indexName, indexCacheKey);
480
+ } catch (e) {
481
+ this.createdIndexes.delete(indexName);
482
+ throw e;
483
+ }
484
+ if (buildIndex) {
485
+ await this.setupIndex({ indexName, metric, indexConfig }, client);
486
+ }
487
+ } catch (error) {
488
+ this.createdIndexes.delete(indexName);
489
+ console.error("Failed to create vector table:", error);
490
+ throw error;
455
491
  } finally {
456
- await client.query("SELECT pg_advisory_unlock($1)", [lockId]);
457
- }
458
- if (buildIndex) {
459
- await this.setupIndex({ indexName, metric, indexConfig }, client);
492
+ client.release();
460
493
  }
461
- } catch (error) {
462
- console.error("Failed to create vector table:", error);
463
- throw error;
464
- } finally {
465
- client.release();
466
- }
494
+ });
467
495
  }
468
496
  /**
469
497
  * @deprecated This function is deprecated. Use buildIndex instead
@@ -485,30 +513,13 @@ var PgVector = class extends MastraVector {
485
513
  }
486
514
  }
487
515
  async setupIndex({ indexName, metric, indexConfig }, client) {
488
- const hash = createHash("sha256").update("build:" + indexName).digest("hex");
489
- const lockId = BigInt("0x" + hash.slice(0, 8)) % BigInt(2 ** 31);
490
- const acquired = await client.query("SELECT pg_try_advisory_lock($1)", [lockId]);
491
- if (!acquired.rows[0].pg_try_advisory_lock) {
492
- const exists = await client.query(
493
- `
494
- SELECT 1 FROM pg_class c
495
- JOIN pg_namespace n ON n.oid = c.relnamespace
496
- WHERE c.relname = $1
497
- AND n.nspname = 'public'
498
- `,
499
- [`${indexName}_vector_idx`]
500
- );
501
- if (exists.rows.length > 0) {
502
- console.log(`Index ${indexName}_vector_idx already exists, skipping creation`);
503
- this.indexCache.delete(indexName);
504
- return;
516
+ const mutex = this.getMutexByName(`build-${indexName}`);
517
+ await mutex.runExclusive(async () => {
518
+ if (this.createdIndexes.has(indexName)) {
519
+ await client.query(`DROP INDEX IF EXISTS ${indexName}_vector_idx`);
505
520
  }
506
- await client.query("SELECT pg_advisory_lock($1)", [lockId]);
507
- }
508
- try {
509
- await client.query(`DROP INDEX IF EXISTS ${indexName}_vector_idx`);
510
521
  if (indexConfig.type === "flat") {
511
- this.indexCache.delete(indexName);
522
+ this.describeIndexCache.delete(indexName);
512
523
  return;
513
524
  }
514
525
  const metricOp = metric === "cosine" ? "vector_cosine_ops" : metric === "euclidean" ? "vector_l2_ops" : "vector_ip_ops";
@@ -541,10 +552,7 @@ var PgVector = class extends MastraVector {
541
552
  `;
542
553
  }
543
554
  await client.query(indexSQL);
544
- this.indexCache.delete(indexName);
545
- } finally {
546
- await client.query("SELECT pg_advisory_unlock($1)", [lockId]);
547
- }
555
+ });
548
556
  }
549
557
  async listIndexes() {
550
558
  const client = await this.pool.connect();
@@ -570,8 +578,7 @@ var PgVector = class extends MastraVector {
570
578
  WHERE attrelid = $1::regclass
571
579
  AND attname = 'embedding';
572
580
  `;
573
- const countQuery = `
574
- SELECT COUNT(*) as count
581
+ const countQuery = ` SELECT COUNT(*) as count
575
582
  FROM ${indexName};
576
583
  `;
577
584
  const indexQuery = `
@@ -624,6 +631,7 @@ var PgVector = class extends MastraVector {
624
631
  const client = await this.pool.connect();
625
632
  try {
626
633
  await client.query(`DROP TABLE IF EXISTS ${indexName} CASCADE`);
634
+ this.createdIndexes.delete(indexName);
627
635
  } catch (error) {
628
636
  await client.query("ROLLBACK");
629
637
  throw new Error(`Failed to delete vector table: ${error.message}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mastra/pg",
3
- "version": "0.2.7-alpha.3",
3
+ "version": "0.2.7-alpha.5",
4
4
  "description": "Postgres provider for Mastra - includes both vector and db storage capabilities",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -19,9 +19,11 @@
19
19
  "./package.json": "./package.json"
20
20
  },
21
21
  "dependencies": {
22
+ "async-mutex": "^0.5.0",
22
23
  "pg": "^8.13.3",
23
24
  "pg-promise": "^11.11.0",
24
- "@mastra/core": "^0.8.0-alpha.3"
25
+ "xxhash-wasm": "^1.1.0",
26
+ "@mastra/core": "^0.8.0-alpha.5"
25
27
  },
26
28
  "devDependencies": {
27
29
  "@microsoft/api-extractor": "^7.52.1",
@@ -1,4 +1,3 @@
1
- import { createHash } from 'crypto';
2
1
  import { MastraVector } from '@mastra/core/vector';
3
2
  import type {
4
3
  IndexStats,
@@ -11,7 +10,9 @@ import type {
11
10
  CreateIndexArgs,
12
11
  } from '@mastra/core/vector';
13
12
  import type { VectorFilter } from '@mastra/core/vector/filter';
13
+ import { Mutex } from 'async-mutex';
14
14
  import pg from 'pg';
15
+ import xxhash from 'xxhash-wasm';
15
16
 
16
17
  import { PGFilterTranslator } from './filter';
17
18
  import { buildFilterQuery } from './sql-builder';
@@ -60,7 +61,9 @@ type PgDefineIndexArgs = [string, 'cosine' | 'euclidean' | 'dotproduct', IndexCo
60
61
 
61
62
  export class PgVector extends MastraVector {
62
63
  private pool: pg.Pool;
63
- private indexCache: Map<string, PGIndexStats> = new Map();
64
+ private describeIndexCache: Map<string, PGIndexStats> = new Map();
65
+ private createdIndexes = new Map<string, number>();
66
+ private mutexesByName = new Map<string, Mutex>();
64
67
 
65
68
  constructor(connectionString: string) {
66
69
  super();
@@ -81,6 +84,26 @@ export class PgVector extends MastraVector {
81
84
  'vector.type': 'postgres',
82
85
  },
83
86
  }) ?? basePool;
87
+
88
+ void (async () => {
89
+ // warm the created indexes cache so we don't need to check if indexes exist every time
90
+ const existingIndexes = await this.listIndexes();
91
+ void existingIndexes.map(async indexName => {
92
+ const info = await this.getIndexInfo(indexName);
93
+ const key = await this.getIndexCacheKey({
94
+ indexName,
95
+ metric: info.metric,
96
+ dimension: info.dimension,
97
+ type: info.type,
98
+ });
99
+ this.createdIndexes.set(indexName, key);
100
+ });
101
+ })();
102
+ }
103
+
104
+ private getMutexByName(indexName: string) {
105
+ if (!this.mutexesByName.has(indexName)) this.mutexesByName.set(indexName, new Mutex());
106
+ return this.mutexesByName.get(indexName)!;
84
107
  }
85
108
 
86
109
  transformFilter(filter?: VectorFilter) {
@@ -89,10 +112,10 @@ export class PgVector extends MastraVector {
89
112
  }
90
113
 
91
114
  async getIndexInfo(indexName: string): Promise<PGIndexStats> {
92
- if (!this.indexCache.has(indexName)) {
93
- this.indexCache.set(indexName, await this.describeIndex(indexName));
115
+ if (!this.describeIndexCache.has(indexName)) {
116
+ this.describeIndexCache.set(indexName, await this.describeIndex(indexName));
94
117
  }
95
- return this.indexCache.get(indexName)!;
118
+ return this.describeIndexCache.get(indexName)!;
96
119
  }
97
120
 
98
121
  async query(...args: ParamsToArgs<PgQueryVectorParams> | PgQueryVectorArgs): Promise<QueryResult[]> {
@@ -197,6 +220,15 @@ export class PgVector extends MastraVector {
197
220
  }
198
221
  }
199
222
 
223
+ private hasher = xxhash();
224
+ private async getIndexCacheKey(params: CreateIndexParams & { type: IndexType | undefined }) {
225
+ const input = params.indexName + params.dimension + params.metric + (params.type || 'ivfflat'); // ivfflat is default
226
+ return (await this.hasher).h32(input);
227
+ }
228
+ private cachedIndexExists(indexName: string, newKey: number) {
229
+ const existingIndexCacheKey = this.createdIndexes.get(indexName);
230
+ return existingIndexCacheKey && existingIndexCacheKey === newKey;
231
+ }
200
232
  async createIndex(...args: ParamsToArgs<PgCreateIndexParams> | PgCreateIndexArgs): Promise<void> {
201
233
  const params = this.normalizeArgs<PgCreateIndexParams, PgCreateIndexArgs>('createIndex', args, [
202
234
  'indexConfig',
@@ -205,81 +237,74 @@ export class PgVector extends MastraVector {
205
237
 
206
238
  const { indexName, dimension, metric = 'cosine', indexConfig = {}, buildIndex = true } = params;
207
239
 
208
- const client = await this.pool.connect();
209
- try {
210
- // Validate inputs
211
- if (!indexName.match(/^[a-zA-Z_][a-zA-Z0-9_]*$/)) {
212
- throw new Error('Invalid index name format');
213
- }
214
- if (!Number.isInteger(dimension) || dimension <= 0) {
215
- throw new Error('Dimension must be a positive integer');
240
+ // Validate inputs
241
+ if (!indexName.match(/^[a-zA-Z_][a-zA-Z0-9_]*$/)) {
242
+ throw new Error('Invalid index name format');
243
+ }
244
+ if (!Number.isInteger(dimension) || dimension <= 0) {
245
+ throw new Error('Dimension must be a positive integer');
246
+ }
247
+
248
+ const indexCacheKey = await this.getIndexCacheKey({ indexName, dimension, type: indexConfig.type, metric });
249
+ if (this.cachedIndexExists(indexName, indexCacheKey)) {
250
+ // we already saw this index get created since the process started, no need to recreate it
251
+ return;
252
+ }
253
+
254
+ const mutex = this.getMutexByName(`create-${indexName}`);
255
+ // Use async-mutex instead of advisory lock for perf (over 2x as fast)
256
+ await mutex.runExclusive(async () => {
257
+ if (this.cachedIndexExists(indexName, indexCacheKey)) {
258
+ // this may have been created while we were waiting to acquire a lock
259
+ return;
216
260
  }
217
261
 
218
- // First check if vector extension is available
219
- const extensionCheck = await client.query(`
262
+ const client = await this.pool.connect();
263
+ try {
264
+ // First check if vector extension is available
265
+ const extensionCheck = await client.query(`
220
266
  SELECT EXISTS (
221
267
  SELECT 1 FROM pg_available_extensions WHERE name = 'vector'
222
268
  );
223
269
  `);
224
270
 
225
- if (!extensionCheck.rows[0].exists) {
226
- throw new Error('PostgreSQL vector extension is not available. Please install it first.');
227
- }
228
-
229
- // Get advisory lock using hash of index name
230
- const hash = createHash('sha256').update(indexName).digest('hex');
231
- const lockId = BigInt('0x' + hash.slice(0, 8)) % BigInt(2 ** 31); // Take first 8 chars and convert to number
232
- const acquired = await client.query('SELECT pg_try_advisory_lock($1)', [lockId]);
233
-
234
- if (!acquired.rows[0].pg_try_advisory_lock) {
235
- // Check if table already exists
236
- const exists = await client.query(
237
- `
238
- SELECT 1 FROM pg_class c
239
- JOIN pg_namespace n ON n.oid = c.relnamespace
240
- WHERE c.relname = $1
241
- AND n.nspname = 'public'
242
- `,
243
- [indexName],
244
- );
245
-
246
- if (exists.rows.length > 0) {
247
- // Table exists so return early
248
- console.log(`Table ${indexName} already exists, skipping creation`);
249
- return;
271
+ if (!extensionCheck.rows[0].exists) {
272
+ this.createdIndexes.delete(indexName);
273
+ throw new Error('PostgreSQL vector extension is not available. Please install it first.');
250
274
  }
251
275
 
252
- // Table doesn't exist, wait for lock
253
- await client.query('SELECT pg_advisory_lock($1)', [lockId]);
254
- }
255
-
256
- try {
257
276
  // Try to create extension
258
- await client.query('CREATE EXTENSION IF NOT EXISTS vector');
259
-
260
- // Create the table with explicit schema
261
- await client.query(`
262
- CREATE TABLE IF NOT EXISTS ${indexName} (
263
- id SERIAL PRIMARY KEY,
264
- vector_id TEXT UNIQUE NOT NULL,
265
- embedding vector(${dimension}),
266
- metadata JSONB DEFAULT '{}'::jsonb
267
- );
268
- `);
269
- } finally {
270
- // Always release lock
271
- await client.query('SELECT pg_advisory_unlock($1)', [lockId]);
272
- }
277
+ try {
278
+ await client.query(`
279
+ DO $$
280
+ BEGIN
281
+ CREATE EXTENSION IF NOT EXISTS vector;
282
+
283
+ CREATE TABLE IF NOT EXISTS ${indexName} (
284
+ id SERIAL PRIMARY KEY,
285
+ vector_id TEXT UNIQUE NOT NULL,
286
+ embedding vector(${dimension}),
287
+ metadata JSONB DEFAULT '{}'::jsonb
288
+ );
289
+ END $$;
290
+ `);
291
+ this.createdIndexes.set(indexName, indexCacheKey);
292
+ } catch (e) {
293
+ this.createdIndexes.delete(indexName);
294
+ throw e;
295
+ }
273
296
 
274
- if (buildIndex) {
275
- await this.setupIndex({ indexName, metric, indexConfig }, client);
297
+ if (buildIndex) {
298
+ await this.setupIndex({ indexName, metric, indexConfig }, client);
299
+ }
300
+ } catch (error: any) {
301
+ this.createdIndexes.delete(indexName);
302
+ console.error('Failed to create vector table:', error);
303
+ throw error;
304
+ } finally {
305
+ client.release();
276
306
  }
277
- } catch (error: any) {
278
- console.error('Failed to create vector table:', error);
279
- throw error;
280
- } finally {
281
- client.release();
282
- }
307
+ });
283
308
  }
284
309
 
285
310
  /**
@@ -310,40 +335,15 @@ export class PgVector extends MastraVector {
310
335
  }
311
336
 
312
337
  private async setupIndex({ indexName, metric, indexConfig }: PgDefineIndexParams, client: pg.PoolClient) {
313
- // Use a different hash prefix for buildIndex locks to avoid conflicts with createIndex
314
- const hash = createHash('sha256')
315
- .update('build:' + indexName)
316
- .digest('hex');
317
- const lockId = BigInt('0x' + hash.slice(0, 8)) % BigInt(2 ** 31);
318
- const acquired = await client.query('SELECT pg_try_advisory_lock($1)', [lockId]);
319
-
320
- if (!acquired.rows[0].pg_try_advisory_lock) {
321
- // Check if index already exists
322
- const exists = await client.query(
323
- `
324
- SELECT 1 FROM pg_class c
325
- JOIN pg_namespace n ON n.oid = c.relnamespace
326
- WHERE c.relname = $1
327
- AND n.nspname = 'public'
328
- `,
329
- [`${indexName}_vector_idx`],
330
- );
331
-
332
- if (exists.rows.length > 0) {
333
- console.log(`Index ${indexName}_vector_idx already exists, skipping creation`);
334
- this.indexCache.delete(indexName); // Still clear cache since we checked
335
- return;
338
+ const mutex = this.getMutexByName(`build-${indexName}`);
339
+ // Use async-mutex instead of advisory lock for perf (over 2x as fast)
340
+ await mutex.runExclusive(async () => {
341
+ if (this.createdIndexes.has(indexName)) {
342
+ await client.query(`DROP INDEX IF EXISTS ${indexName}_vector_idx`);
336
343
  }
337
344
 
338
- // Index doesn't exist, wait for lock
339
- await client.query('SELECT pg_advisory_lock($1)', [lockId]);
340
- }
341
-
342
- try {
343
- await client.query(`DROP INDEX IF EXISTS ${indexName}_vector_idx`);
344
-
345
345
  if (indexConfig.type === 'flat') {
346
- this.indexCache.delete(indexName);
346
+ this.describeIndexCache.delete(indexName);
347
347
  return;
348
348
  }
349
349
 
@@ -381,10 +381,7 @@ export class PgVector extends MastraVector {
381
381
  }
382
382
 
383
383
  await client.query(indexSQL);
384
- this.indexCache.delete(indexName);
385
- } finally {
386
- await client.query('SELECT pg_advisory_unlock($1)', [lockId]);
387
- }
384
+ });
388
385
  }
389
386
 
390
387
  async listIndexes(): Promise<string[]> {
@@ -416,8 +413,7 @@ export class PgVector extends MastraVector {
416
413
  `;
417
414
 
418
415
  // Get row count
419
- const countQuery = `
420
- SELECT COUNT(*) as count
416
+ const countQuery = ` SELECT COUNT(*) as count
421
417
  FROM ${indexName};
422
418
  `;
423
419
 
@@ -486,6 +482,7 @@ export class PgVector extends MastraVector {
486
482
  try {
487
483
  // Drop the table
488
484
  await client.query(`DROP TABLE IF EXISTS ${indexName} CASCADE`);
485
+ this.createdIndexes.delete(indexName);
489
486
  } catch (error: any) {
490
487
  await client.query('ROLLBACK');
491
488
  throw new Error(`Failed to delete vector table: ${error.message}`);