@workglow/postgres 0.2.31 → 0.2.33

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.
Files changed (30) hide show
  1. package/README.md +33 -0
  2. package/dist/job-queue/PostgresQueueStorage.d.ts +12 -19
  3. package/dist/job-queue/PostgresQueueStorage.d.ts.map +1 -1
  4. package/dist/job-queue/PostgresRateLimiterStorage.d.ts +9 -19
  5. package/dist/job-queue/PostgresRateLimiterStorage.d.ts.map +1 -1
  6. package/dist/job-queue/browser.js +331 -165
  7. package/dist/job-queue/browser.js.map +8 -5
  8. package/dist/job-queue/common.d.ts +3 -0
  9. package/dist/job-queue/common.d.ts.map +1 -1
  10. package/dist/job-queue/node.js +331 -165
  11. package/dist/job-queue/node.js.map +8 -5
  12. package/dist/migrations/PostgresMigrationRunner.d.ts +31 -0
  13. package/dist/migrations/PostgresMigrationRunner.d.ts.map +1 -0
  14. package/dist/migrations/common.d.ts +9 -0
  15. package/dist/migrations/common.d.ts.map +1 -0
  16. package/dist/migrations/postgresQueueMigrations.d.ts +18 -0
  17. package/dist/migrations/postgresQueueMigrations.d.ts.map +1 -0
  18. package/dist/migrations/postgresRateLimiterMigrations.d.ts +11 -0
  19. package/dist/migrations/postgresRateLimiterMigrations.d.ts.map +1 -0
  20. package/dist/storage/PostgresTabularStorage.d.ts +175 -10
  21. package/dist/storage/PostgresTabularStorage.d.ts.map +1 -1
  22. package/dist/storage/PostgresVectorStorage.d.ts +52 -2
  23. package/dist/storage/PostgresVectorStorage.d.ts.map +1 -1
  24. package/dist/storage/browser.js +454 -75
  25. package/dist/storage/browser.js.map +4 -4
  26. package/dist/storage/common.d.ts +1 -0
  27. package/dist/storage/common.d.ts.map +1 -1
  28. package/dist/storage/node.js +564 -75
  29. package/dist/storage/node.js.map +6 -5
  30. package/package.json +8 -8
@@ -38,26 +38,55 @@ import { createServiceToken as createServiceToken2 } from "@workglow/util";
38
38
  import { createServiceToken } from "@workglow/util";
39
39
  import {
40
40
  BaseSqlTabularStorage,
41
- isSearchCondition,
41
+ buildSearchWhere,
42
+ MIGRATIONS_TABLE,
43
+ PostgresDialect,
44
+ SqlTabularMigrationApplier,
42
45
  pickCoveringIndex
43
46
  } from "@workglow/storage";
44
47
  var POSTGRES_TABULAR_REPOSITORY = createServiceToken("storage.tabularRepository.postgres");
48
+ function assertPositiveInt(value, label) {
49
+ if (value === undefined)
50
+ return;
51
+ if (!Number.isFinite(value) || !Number.isInteger(value) || value <= 0) {
52
+ throw new Error(`VectorIndexOptions.${label} must be a positive integer; received ${String(value)}`);
53
+ }
54
+ }
45
55
 
46
56
  class PostgresTabularStorage extends BaseSqlTabularStorage {
47
57
  db;
48
- constructor(db, table = "tabular_store", schema, primaryKeyNames, indexes = [], clientProvidedKeys = "if-missing") {
49
- super(table, schema, primaryKeyNames, indexes, clientProvidedKeys);
58
+ constructor(db, table = "tabular_store", schema, primaryKeyNames, indexes = [], clientProvidedKeys = "if-missing", tabularMigrations) {
59
+ super(table, schema, primaryKeyNames, indexes, clientProvidedKeys, tabularMigrations, table);
50
60
  this.db = db;
51
61
  }
52
62
  async setupDatabase() {
63
+ if (this.tabularMigrations && this.tabularMigrations.length > 0) {
64
+ const exists = await this.tableExistsAsync();
65
+ await this.createTableAndIndexes();
66
+ await this.applyTabularMigrations({ freshTable: !exists });
67
+ if (exists) {
68
+ await this.createDeclaredIndexes();
69
+ }
70
+ return;
71
+ }
72
+ await this.createTableAndIndexes();
73
+ }
74
+ async tableExistsAsync() {
75
+ const r = await this.db.query(`SELECT 1 AS x FROM information_schema.tables WHERE table_name = $1 LIMIT 1`, [this.table]);
76
+ return r.rows.length > 0;
77
+ }
78
+ async createTableAndIndexes() {
53
79
  const sql = `
54
80
  CREATE TABLE IF NOT EXISTS "${this.table}" (
55
81
  ${this.constructPrimaryKeyColumns('"')} ${this.constructValueColumns('"')},
56
- PRIMARY KEY (${this.primaryKeyColumnList()})
82
+ PRIMARY KEY (${this.primaryKeyColumnList()})
57
83
  )
58
84
  `;
59
85
  await this.db.query(sql);
60
86
  await this.createVectorIndexes();
87
+ await this.createDeclaredIndexes();
88
+ }
89
+ async createDeclaredIndexes() {
61
90
  const pkColumns = this.primaryKeyColumns();
62
91
  const createdIndexes = new Set;
63
92
  for (const columns of this.indexes) {
@@ -81,6 +110,15 @@ class PostgresTabularStorage extends BaseSqlTabularStorage {
81
110
  }
82
111
  }
83
112
  }
113
+ async runMigrationDdl(sql) {
114
+ await this.db.query(sql);
115
+ }
116
+ async recordMigrationApplied(component, version, description) {
117
+ await this.db.query(`INSERT INTO ${MIGRATIONS_TABLE}(component, version, description) VALUES ($1, $2, $3)`, [component, version, description]);
118
+ }
119
+ getMigrationApplier() {
120
+ return new PostgresTabularMigrationApplierImpl(this);
121
+ }
84
122
  isVectorFormat(format) {
85
123
  if (!format)
86
124
  return false;
@@ -292,32 +330,73 @@ class PostgresTabularStorage extends BaseSqlTabularStorage {
292
330
  }
293
331
  return vectorColumns;
294
332
  }
333
+ getVectorIndexOptions() {
334
+ return {};
335
+ }
295
336
  async createVectorIndexes() {
296
337
  const vectorColumns = this.getVectorColumns();
297
338
  if (vectorColumns.length === 0) {
298
339
  return;
299
340
  }
341
+ const opts = this.getVectorIndexOptions();
342
+ if (opts.hnsw && opts.ivfflat) {
343
+ throw new Error("VectorIndexOptions: only one of `hnsw` or `ivfflat` may be set; received both.");
344
+ }
345
+ assertPositiveInt(opts.hnsw?.m, "hnsw.m");
346
+ assertPositiveInt(opts.hnsw?.efConstruction, "hnsw.efConstruction");
347
+ assertPositiveInt(opts.hnsw?.efSearch, "hnsw.efSearch");
348
+ assertPositiveInt(opts.ivfflat?.lists, "ivfflat.lists");
349
+ assertPositiveInt(opts.ivfflat?.probes, "ivfflat.probes");
300
350
  try {
301
351
  await this.db.query("CREATE EXTENSION IF NOT EXISTS vector");
302
352
  } catch (error) {
303
353
  console.warn("pgvector extension not available, vector columns will use TEXT fallback:", error);
304
354
  return;
305
355
  }
356
+ const distance = opts.distance ?? "cosine";
357
+ const opClass = distance === "l2" ? "vector_l2_ops" : distance === "ip" ? "vector_ip_ops" : "vector_cosine_ops";
358
+ const tableId = PostgresDialect.quoteId(this.table);
359
+ if (opts.ivfflat) {
360
+ const { lists } = opts.ivfflat;
361
+ for (const { column } of vectorColumns) {
362
+ const indexId = PostgresDialect.quoteId(`${this.table}_${column}_ivfflat_idx`);
363
+ const columnId = PostgresDialect.quoteId(column);
364
+ try {
365
+ await this.db.query(`
366
+ CREATE INDEX IF NOT EXISTS ${indexId}
367
+ ON ${tableId}
368
+ USING ivfflat (${columnId} ${opClass})
369
+ WITH (lists = ${lists})
370
+ `);
371
+ } catch (error) {
372
+ console.warn(`Failed to create IVFFlat index on ${column}:`, error);
373
+ }
374
+ }
375
+ return;
376
+ }
377
+ const hnsw = opts.hnsw ?? {};
378
+ const buildParams = [];
379
+ if (typeof hnsw.m === "number")
380
+ buildParams.push(`m = ${hnsw.m}`);
381
+ if (typeof hnsw.efConstruction === "number") {
382
+ buildParams.push(`ef_construction = ${hnsw.efConstruction}`);
383
+ }
384
+ const withClause = buildParams.length > 0 ? ` WITH (${buildParams.join(", ")})` : "";
306
385
  for (const { column } of vectorColumns) {
307
- const indexName = `${this.table}_${column}_hnsw_idx`;
386
+ const indexId = PostgresDialect.quoteId(`${this.table}_${column}_hnsw_idx`);
387
+ const columnId = PostgresDialect.quoteId(column);
308
388
  try {
309
389
  await this.db.query(`
310
- CREATE INDEX IF NOT EXISTS "${indexName}"
311
- ON "${this.table}"
312
- USING hnsw ("${column}" vector_cosine_ops)
390
+ CREATE INDEX IF NOT EXISTS ${indexId}
391
+ ON ${tableId}
392
+ USING hnsw (${columnId} ${opClass})${withClause}
313
393
  `);
314
394
  } catch (error) {
315
395
  console.warn(`Failed to create HNSW index on ${column}:`, error);
316
396
  }
317
397
  }
318
398
  }
319
- async put(entity) {
320
- const db = this.db;
399
+ buildPutSql(entity) {
321
400
  const columnsToInsert = [];
322
401
  const paramsToInsert = [];
323
402
  const pkColumns = this.primaryKeyColumns();
@@ -359,7 +438,7 @@ class PostgresTabularStorage extends BaseSqlTabularStorage {
359
438
  const placeholders = columnsToInsert.map((_, i) => `$${i + 1}`).join(", ");
360
439
  const conflictClause = valueColumns.length > 0 ? `
361
440
  ON CONFLICT (${this.primaryKeyColumnList('"')}) DO UPDATE
362
- SET
441
+ SET
363
442
  ${valueColumns.map((col) => {
364
443
  const colIdx = columnsToInsert.indexOf(String(col));
365
444
  return `"${col}" = $${colIdx + 1}`;
@@ -371,22 +450,177 @@ class PostgresTabularStorage extends BaseSqlTabularStorage {
371
450
  ${conflictClause}
372
451
  RETURNING *
373
452
  `;
374
- const params = paramsToInsert;
375
- const result = await db.query(sql, params);
376
- const updatedEntity = result.rows[0];
377
- const updatedRecord = updatedEntity;
453
+ return { sql, params: paramsToInsert };
454
+ }
455
+ hydrateRow(row) {
456
+ const entity = row;
457
+ const record = entity;
378
458
  for (const key in this.schema.properties) {
379
- updatedRecord[key] = this.sqlToJsValue(key, updatedRecord[key]);
459
+ record[key] = this.sqlToJsValue(key, record[key]);
460
+ }
461
+ return entity;
462
+ }
463
+ async acquireConnection() {
464
+ const supportsConnect = typeof this.db.connect === "function";
465
+ if (supportsConnect) {
466
+ return await this.db.connect();
467
+ }
468
+ const dbAny = this.db;
469
+ const ctorName = dbAny.constructor?.name;
470
+ const looksLikePGlite = typeof dbAny.exec === "function" && dbAny.waitReady !== undefined;
471
+ const looksLikePGLitePool = ctorName === "PGLitePool";
472
+ if (!looksLikePGlite && !looksLikePGLitePool) {
473
+ throw new Error(`PostgresTabularStorage.putBulk requires a pg.Pool with connect() or a known single-connection wrapper (PGLitePool, PGlite); got ${ctorName ?? typeof this.db}. A multi-connection pool without connect() would dispatch BEGIN and the bracketed INSERTs to different sessions, breaking atomicity.`);
380
474
  }
381
- this.events.emit("put", updatedEntity);
475
+ return {
476
+ query: this.db.query.bind(this.db),
477
+ release: () => {}
478
+ };
479
+ }
480
+ mutexChain = Promise.resolve();
481
+ get serializeOps() {
482
+ return typeof this.db.connect !== "function";
483
+ }
484
+ async mutex(fn) {
485
+ if (!this.serializeOps)
486
+ return fn();
487
+ const prev = this.mutexChain;
488
+ let release;
489
+ this.mutexChain = new Promise((resolve) => {
490
+ release = resolve;
491
+ });
492
+ await prev;
493
+ try {
494
+ return await fn();
495
+ } finally {
496
+ release();
497
+ }
498
+ }
499
+ inTransaction = false;
500
+ emitPut(entity) {
501
+ this.events.emit("put", entity);
502
+ }
503
+ async put(entity) {
504
+ return this.mutex(() => this._putInternal(entity));
505
+ }
506
+ async _putInternal(entity) {
507
+ const { sql, params } = this.buildPutSql(entity);
508
+ const result = await this.db.query(sql, params);
509
+ const updatedEntity = this.hydrateRow(result.rows[0]);
510
+ this.emitPut(updatedEntity);
382
511
  return updatedEntity;
383
512
  }
384
513
  async putBulk(entities) {
514
+ return this.mutex(() => this._putBulkInternal(entities));
515
+ }
516
+ async _putBulkInternal(entities) {
385
517
  if (entities.length === 0)
386
518
  return [];
387
- return await Promise.all(entities.map((entity) => this.put(entity)));
519
+ if (this.inTransaction) {
520
+ const updated = [];
521
+ for (const entity of entities) {
522
+ const { sql, params } = this.buildPutSql(entity);
523
+ const result = await this.db.query(sql, params);
524
+ updated.push(this.hydrateRow(result.rows[0]));
525
+ }
526
+ for (const entity of updated)
527
+ this.emitPut(entity);
528
+ return updated;
529
+ }
530
+ const conn = await this.acquireConnection();
531
+ const updatedEntities = [];
532
+ try {
533
+ await conn.query("BEGIN");
534
+ try {
535
+ for (const entity of entities) {
536
+ const { sql, params } = this.buildPutSql(entity);
537
+ const result = await conn.query(sql, params);
538
+ updatedEntities.push(this.hydrateRow(result.rows[0]));
539
+ }
540
+ await conn.query("COMMIT");
541
+ } catch (err) {
542
+ try {
543
+ await conn.query("ROLLBACK");
544
+ } catch {}
545
+ throw err;
546
+ }
547
+ } finally {
548
+ conn.release();
549
+ }
550
+ for (const entity of updatedEntities)
551
+ this.emitPut(entity);
552
+ return updatedEntities;
553
+ }
554
+ createTxView(txDb, deferredPutEvents) {
555
+ const target = this;
556
+ return new Proxy(target, {
557
+ get(t, prop, receiver) {
558
+ if (prop === "withTransaction") {
559
+ return () => {
560
+ throw new Error("PostgresTabularStorage.withTransaction does not support nesting. " + "Use SAVEPOINT directly or refactor to a single transaction.");
561
+ };
562
+ }
563
+ if (prop === "db")
564
+ return txDb;
565
+ if (prop === "inTransaction")
566
+ return true;
567
+ if (prop === "emitPut") {
568
+ return (entity) => deferredPutEvents.push(entity);
569
+ }
570
+ if (typeof prop === "string") {
571
+ const internal = t[`_${prop}Internal`];
572
+ if (typeof internal === "function") {
573
+ return (...args) => internal.apply(receiver, args);
574
+ }
575
+ }
576
+ const value = Reflect.get(t, prop, receiver);
577
+ return typeof value === "function" ? value.bind(receiver) : value;
578
+ }
579
+ });
580
+ }
581
+ async withTransaction(fn) {
582
+ const supportsConnect = typeof this.db.connect === "function";
583
+ if (supportsConnect) {
584
+ const client = await this.db.connect();
585
+ try {
586
+ return await this.runInTransaction(fn, { query: client.query.bind(client) });
587
+ } finally {
588
+ client.release();
589
+ }
590
+ }
591
+ if (this.inTransaction) {
592
+ throw new Error("PostgresTabularStorage.withTransaction does not support nesting. " + "Use SAVEPOINT directly or refactor to a single transaction.");
593
+ }
594
+ return this.mutex(async () => {
595
+ this.inTransaction = true;
596
+ try {
597
+ return await this.runInTransaction(fn, { query: this.db.query.bind(this.db) });
598
+ } finally {
599
+ this.inTransaction = false;
600
+ }
601
+ });
602
+ }
603
+ async runInTransaction(fn, txDb) {
604
+ const deferredPutEvents = [];
605
+ await txDb.query("BEGIN");
606
+ let result;
607
+ try {
608
+ result = await fn(this.createTxView(txDb, deferredPutEvents));
609
+ await txDb.query("COMMIT");
610
+ } catch (err) {
611
+ try {
612
+ await txDb.query("ROLLBACK");
613
+ } catch {}
614
+ throw err;
615
+ }
616
+ for (const entity of deferredPutEvents)
617
+ this.events.emit("put", entity);
618
+ return result;
388
619
  }
389
620
  async get(key) {
621
+ return this.mutex(() => this._getInternal(key));
622
+ }
623
+ async _getInternal(key) {
390
624
  const db = this.db;
391
625
  const whereClauses = this.primaryKeyColumns().map((discriminatorKey, i) => `"${discriminatorKey}" = $${i + 1}`).join(" AND ");
392
626
  const sql = `SELECT * FROM "${this.table}" WHERE ${whereClauses}`;
@@ -406,6 +640,9 @@ class PostgresTabularStorage extends BaseSqlTabularStorage {
406
640
  return val;
407
641
  }
408
642
  async delete(value) {
643
+ return this.mutex(() => this._deleteInternal(value));
644
+ }
645
+ async _deleteInternal(value) {
409
646
  const db = this.db;
410
647
  const { key } = this.separateKeyValueFromCombined(value);
411
648
  const whereClauses = this.primaryKeyColumns().map((key2, i) => `${key2} = $${i + 1}`).join(" AND ");
@@ -414,6 +651,9 @@ class PostgresTabularStorage extends BaseSqlTabularStorage {
414
651
  this.events.emit("delete", key);
415
652
  }
416
653
  async getAll(options) {
654
+ return this.mutex(() => this._getAllInternal(options));
655
+ }
656
+ async _getAllInternal(options) {
417
657
  this.validateGetAllOptions(options);
418
658
  const db = this.db;
419
659
  let sql = `SELECT * FROM "${this.table}"`;
@@ -443,18 +683,27 @@ class PostgresTabularStorage extends BaseSqlTabularStorage {
443
683
  return;
444
684
  }
445
685
  async deleteAll() {
686
+ return this.mutex(() => this._deleteAllInternal());
687
+ }
688
+ async _deleteAllInternal() {
446
689
  const db = this.db;
447
690
  await db.query(`DELETE FROM "${this.table}"`);
448
691
  this.events.emit("clearall");
449
692
  }
450
693
  async size() {
694
+ return this.mutex(() => this._sizeInternal());
695
+ }
696
+ async _sizeInternal() {
451
697
  const db = this.db;
452
698
  const result = await db.query(`SELECT COUNT(*) FROM "${this.table}"`);
453
699
  return parseInt(result.rows[0].count, 10);
454
700
  }
455
701
  async count(criteria) {
702
+ return this.mutex(() => this._countInternal(criteria));
703
+ }
704
+ async _countInternal(criteria) {
456
705
  if (!criteria || Object.keys(criteria).length === 0) {
457
- return await this.size();
706
+ return await this._sizeInternal();
458
707
  }
459
708
  this.validateQueryParams(criteria);
460
709
  const db = this.db;
@@ -463,6 +712,9 @@ class PostgresTabularStorage extends BaseSqlTabularStorage {
463
712
  return parseInt(result.rows[0].count, 10);
464
713
  }
465
714
  async getBulk(offset, limit) {
715
+ return this.mutex(() => this._getBulkInternal(offset, limit));
716
+ }
717
+ async _getBulkInternal(offset, limit) {
466
718
  const db = this.db;
467
719
  const orderByClause = this.primaryKeyColumns().map((col) => `"${String(col)}"`).join(", ");
468
720
  const result = await db.query(`SELECT * FROM "${this.table}" ORDER BY ${orderByClause} LIMIT $1 OFFSET $2`, [limit, offset]);
@@ -478,32 +730,52 @@ class PostgresTabularStorage extends BaseSqlTabularStorage {
478
730
  return result.rows;
479
731
  }
480
732
  buildDeleteSearchWhere(criteria) {
481
- const conditions = [];
482
- const params = [];
483
- let paramIndex = 1;
484
- for (const column of Object.keys(criteria)) {
485
- if (!(column in this.schema.properties)) {
486
- throw new Error(`Schema must have a ${String(column)} field to use deleteSearch`);
487
- }
488
- const criterion = criteria[column];
489
- let operator = "=";
490
- let value;
491
- if (isSearchCondition(criterion)) {
492
- operator = criterion.operator;
493
- value = criterion.value;
494
- } else {
495
- value = criterion;
496
- }
497
- conditions.push(`"${String(column)}" ${operator} $${paramIndex}`);
498
- params.push(this.jsToSqlValue(column, value));
499
- paramIndex++;
500
- }
733
+ const built = buildSearchWhere(PostgresDialect, criteria, this.schema.properties, (column, value) => this.jsToSqlValue(column, value));
734
+ return { whereClause: built.whereClause, params: built.params };
735
+ }
736
+ async getPage(request = {}) {
737
+ return this.mutex(() => this._getPageInternal(request));
738
+ }
739
+ async _getPageInternal(request) {
740
+ return this.runSqlPage(undefined, request, this.postgresDialect());
741
+ }
742
+ async queryPage(criteria, request = {}) {
743
+ return this.mutex(() => this._queryPageInternal(criteria, request));
744
+ }
745
+ async _queryPageInternal(criteria, request) {
746
+ this.validateQueryParams(criteria, undefined);
747
+ return this.runSqlPage(criteria, request, this.postgresDialect());
748
+ }
749
+ postgresDialect() {
750
+ return {
751
+ quote: '"',
752
+ placeholder: (index) => `$${index}`,
753
+ buildSearchWhere: (criteria, startIndex) => this.buildSearchWhereWithIndex(criteria, startIndex),
754
+ executeSelect: async (sql, params) => {
755
+ const result = await this.db.query(sql, params);
756
+ const rows = result.rows ?? [];
757
+ for (const row of rows) {
758
+ const record = row;
759
+ for (const k in this.schema.properties) {
760
+ record[k] = this.sqlToJsValue(k, record[k]);
761
+ }
762
+ }
763
+ return rows;
764
+ }
765
+ };
766
+ }
767
+ buildSearchWhereWithIndex(criteria, startIndex) {
768
+ const built = buildSearchWhere(PostgresDialect, criteria, this.schema.properties, (column, value) => this.jsToSqlValue(column, value), startIndex);
501
769
  return {
502
- whereClause: conditions.join(" AND "),
503
- params
770
+ whereClause: built.whereClause,
771
+ params: built.params,
772
+ nextIndex: startIndex + built.params.length
504
773
  };
505
774
  }
506
775
  async deleteSearch(criteria) {
776
+ return this.mutex(() => this._deleteSearchInternal(criteria));
777
+ }
778
+ async _deleteSearchInternal(criteria) {
507
779
  const criteriaKeys = Object.keys(criteria);
508
780
  if (criteriaKeys.length === 0) {
509
781
  return;
@@ -514,6 +786,9 @@ class PostgresTabularStorage extends BaseSqlTabularStorage {
514
786
  this.events.emit("delete", criteriaKeys[0]);
515
787
  }
516
788
  async query(criteria, options) {
789
+ return this.mutex(() => this._queryInternal(criteria, options));
790
+ }
791
+ async _queryInternal(criteria, options) {
517
792
  this.validateQueryParams(criteria, options);
518
793
  const db = this.db;
519
794
  let sql = `SELECT * FROM "${this.table}"`;
@@ -546,6 +821,9 @@ class PostgresTabularStorage extends BaseSqlTabularStorage {
546
821
  return;
547
822
  }
548
823
  async queryIndex(criteria, options) {
824
+ return this.mutex(() => this._queryIndexInternal(criteria, options));
825
+ }
826
+ async _queryIndexInternal(criteria, options) {
549
827
  this.validateSelect(options);
550
828
  this.validateQueryParams(criteria, options);
551
829
  const registered = this.indexes.map((cols2, i) => {
@@ -596,6 +874,51 @@ class PostgresTabularStorage extends BaseSqlTabularStorage {
596
874
  }
597
875
  }
598
876
 
877
+ class PostgresTabularMigrationApplierImpl extends SqlTabularMigrationApplier {
878
+ host;
879
+ constructor(host) {
880
+ super();
881
+ this.host = host;
882
+ }
883
+ dialectName() {
884
+ return "postgres";
885
+ }
886
+ table() {
887
+ return this.host.table;
888
+ }
889
+ storage() {
890
+ return this.host;
891
+ }
892
+ mapTypeToSQL(typeDef) {
893
+ return this.host.mapTypeToSQL(typeDef);
894
+ }
895
+ isNullableSchema(typeDef) {
896
+ return this.host.isNullable(typeDef);
897
+ }
898
+ async executeSql(sql) {
899
+ await this.host.db.query(sql);
900
+ }
901
+ async executeSqlTx(sql, tx) {
902
+ await tx.runMigrationDdl(sql);
903
+ }
904
+ async recordAppliedTx(component, version, description, tx) {
905
+ await tx.recordMigrationApplied(component, version, description ?? null);
906
+ }
907
+ async recordApplied(component, version, description) {
908
+ await this.host.db.query(`INSERT INTO ${MIGRATIONS_TABLE}(component, version, description) VALUES ($1, $2, $3)`, [component, version, description ?? null]);
909
+ }
910
+ async queryAppliedVersions(component) {
911
+ const r = await this.host.db.query(`SELECT version FROM ${MIGRATIONS_TABLE} WHERE component = $1`, [component]);
912
+ return new Set(r.rows.map((row) => Number(row.version)));
913
+ }
914
+ async probeTableExists() {
915
+ const r = await this.host.db.query(`SELECT 1 FROM information_schema.tables WHERE table_name = $1 LIMIT 1`, [
916
+ this.host.table
917
+ ]);
918
+ return r.rows.length > 0;
919
+ }
920
+ }
921
+
599
922
  // src/storage/PostgresKvStorage.ts
600
923
  import {
601
924
  DefaultKeyValueKey,
@@ -617,7 +940,12 @@ class PostgresKvStorage extends KvViaTabularStorage {
617
940
  }
618
941
  // src/storage/PostgresVectorStorage.ts
619
942
  import { cosineSimilarity } from "@workglow/util/schema";
620
- import { StorageValidationError, getMetadataProperty, getVectorProperty } from "@workglow/storage";
943
+ import {
944
+ PostgresDialect as PostgresDialect2,
945
+ StorageValidationError,
946
+ getMetadataProperty,
947
+ getVectorProperty
948
+ } from "@workglow/storage";
621
949
  var SAFE_IDENTIFIER_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
622
950
 
623
951
  class PostgresVectorStorage extends PostgresTabularStorage {
@@ -625,10 +953,12 @@ class PostgresVectorStorage extends PostgresTabularStorage {
625
953
  vectorCtor;
626
954
  vectorPropertyName;
627
955
  metadataPropertyName;
628
- constructor(db, table, schema, primaryKeyNames, indexes = [], dimensions, vectorCtor = Float32Array) {
956
+ indexOptions;
957
+ constructor(db, table, schema, primaryKeyNames, indexes = [], dimensions, vectorCtor = Float32Array, indexOptions = {}) {
629
958
  super(db, table, schema, primaryKeyNames, indexes);
630
959
  this.vectorDimensions = dimensions;
631
960
  this.vectorCtor = vectorCtor;
961
+ this.indexOptions = indexOptions;
632
962
  const vectorProp = getVectorProperty(schema);
633
963
  if (!vectorProp) {
634
964
  throw new Error("Schema must have a property with type array and format TypedArray");
@@ -639,20 +969,58 @@ class PostgresVectorStorage extends PostgresTabularStorage {
639
969
  getVectorDimensions() {
640
970
  return this.vectorDimensions;
641
971
  }
972
+ getVectorIndexOptions() {
973
+ return this.indexOptions;
974
+ }
975
+ get distance() {
976
+ return this.indexOptions.distance ?? "cosine";
977
+ }
978
+ get distanceOperator() {
979
+ switch (this.distance) {
980
+ case "l2":
981
+ return "<->";
982
+ case "ip":
983
+ return "<#>";
984
+ default:
985
+ return "<=>";
986
+ }
987
+ }
988
+ buildScoreExpr(vectorExpr, queryParam) {
989
+ const op = this.distanceOperator;
990
+ switch (this.distance) {
991
+ case "l2":
992
+ return `(1.0 / (1.0 + (${vectorExpr} ${op} ${queryParam}::vector)))`;
993
+ case "ip":
994
+ return `(-1.0 * (${vectorExpr} ${op} ${queryParam}::vector))`;
995
+ default:
996
+ return `(1 - (${vectorExpr} ${op} ${queryParam}::vector))`;
997
+ }
998
+ }
999
+ getQueryTuning() {
1000
+ return {
1001
+ efSearch: this.indexOptions.hnsw?.efSearch,
1002
+ probes: this.indexOptions.ivfflat?.probes
1003
+ };
1004
+ }
642
1005
  async similaritySearch(query, options = {}) {
643
1006
  const { topK = 10, filter, scoreThreshold = 0 } = options;
644
1007
  try {
645
1008
  const queryVector = `[${Array.from(query).join(",")}]`;
646
- const vectorCol = String(this.vectorPropertyName);
647
- const metadataCol = this.metadataPropertyName ? String(this.metadataPropertyName) : null;
1009
+ const vectorColRaw = String(this.vectorPropertyName);
1010
+ const vectorCol = PostgresDialect2.quoteId(vectorColRaw);
1011
+ const metadataColRaw = this.metadataPropertyName ? String(this.metadataPropertyName) : null;
1012
+ const metadataCol = metadataColRaw ? PostgresDialect2.quoteId(metadataColRaw) : null;
1013
+ const distOp = this.distanceOperator;
1014
+ const scoreExpr = this.buildScoreExpr(vectorCol, "$1");
648
1015
  let sql = `
649
- SELECT
1016
+ SELECT
650
1017
  *,
651
- 1 - (${vectorCol} <=> $1::vector) as score
1018
+ ${scoreExpr} as score
652
1019
  FROM "${this.table}"
653
1020
  `;
654
1021
  const params = [queryVector];
655
1022
  let paramIndex = 2;
1023
+ let hasWhere = false;
656
1024
  if (filter && Object.keys(filter).length > 0 && metadataCol) {
657
1025
  const conditions = [];
658
1026
  for (const [key, value] of Object.entries(filter)) {
@@ -663,21 +1031,22 @@ class PostgresVectorStorage extends PostgresTabularStorage {
663
1031
  params.push(String(value));
664
1032
  paramIndex++;
665
1033
  }
666
- sql += ` WHERE ${conditions.join(" AND ")}`;
667
- }
668
- if (scoreThreshold > 0) {
669
- sql += filter ? " AND" : " WHERE";
670
- sql += ` (1 - (${vectorCol} <=> $1::vector)) >= $${paramIndex}`;
671
- params.push(scoreThreshold);
672
- paramIndex++;
1034
+ if (conditions.length > 0) {
1035
+ sql += ` WHERE ${conditions.join(" AND ")}`;
1036
+ hasWhere = true;
1037
+ }
673
1038
  }
674
- sql += ` ORDER BY ${vectorCol} <=> $1::vector LIMIT $${paramIndex}`;
1039
+ sql += hasWhere ? " AND" : " WHERE";
1040
+ sql += ` ${scoreExpr} >= $${paramIndex}`;
1041
+ params.push(scoreThreshold);
1042
+ paramIndex++;
1043
+ sql += ` ORDER BY ${vectorCol} ${distOp} $1::vector LIMIT $${paramIndex}`;
675
1044
  params.push(topK);
676
1045
  const result = await this.db.query(sql, params);
677
1046
  const results = [];
678
1047
  for (const row of result.rows) {
679
1048
  const vectorResult = await this.db.query(`SELECT ${vectorCol}::text FROM "${this.table}" WHERE ${this.getPrimaryKeyWhereClause()}`, this.getPrimaryKeyValues(row));
680
- const vectorStr = vectorResult.rows[0]?.[vectorCol] || "[]";
1049
+ const vectorStr = vectorResult.rows[0]?.[vectorColRaw] || "[]";
681
1050
  const vectorArray = JSON.parse(vectorStr);
682
1051
  results.push({
683
1052
  ...row,
@@ -702,19 +1071,24 @@ class PostgresVectorStorage extends PostgresTabularStorage {
702
1071
  try {
703
1072
  const queryVector = `[${Array.from(query).join(",")}]`;
704
1073
  const tsQueryText = textQuery;
705
- const vectorCol = String(this.vectorPropertyName);
706
- const metadataCol = this.metadataPropertyName ? String(this.metadataPropertyName) : null;
1074
+ const vectorColRaw = String(this.vectorPropertyName);
1075
+ const vectorCol = PostgresDialect2.quoteId(vectorColRaw);
1076
+ const metadataColRaw = this.metadataPropertyName ? String(this.metadataPropertyName) : null;
1077
+ const metadataCol = metadataColRaw ? PostgresDialect2.quoteId(metadataColRaw) : null;
1078
+ const vectorScoreExpr = this.buildScoreExpr(vectorCol, "$1");
1079
+ const combinedScoreExpr = `(
1080
+ $2 * ${vectorScoreExpr} +
1081
+ $3 * ts_rank(to_tsvector('english', ${metadataCol || "''"}::text), plainto_tsquery('english', $4))
1082
+ )`;
707
1083
  let sql = `
708
- SELECT
1084
+ SELECT
709
1085
  *,
710
- (
711
- $2 * (1 - (${vectorCol} <=> $1::vector)) +
712
- $3 * ts_rank(to_tsvector('english', ${metadataCol || "''"}::text), plainto_tsquery('english', $4))
713
- ) as score
1086
+ ${combinedScoreExpr} as score
714
1087
  FROM "${this.table}"
715
1088
  `;
716
1089
  const params = [queryVector, vectorWeight, 1 - vectorWeight, tsQueryText];
717
1090
  let paramIndex = 5;
1091
+ let hasWhere = false;
718
1092
  if (filter && Object.keys(filter).length > 0 && metadataCol) {
719
1093
  const conditions = [];
720
1094
  for (const [key, value] of Object.entries(filter)) {
@@ -725,24 +1099,22 @@ class PostgresVectorStorage extends PostgresTabularStorage {
725
1099
  params.push(String(value));
726
1100
  paramIndex++;
727
1101
  }
728
- sql += ` WHERE ${conditions.join(" AND ")}`;
729
- }
730
- if (scoreThreshold > 0) {
731
- sql += filter ? " AND" : " WHERE";
732
- sql += ` (
733
- $2 * (1 - (${vectorCol} <=> $1::vector)) +
734
- $3 * ts_rank(to_tsvector('english', ${metadataCol || "''"}::text), plainto_tsquery('english', $4))
735
- ) >= $${paramIndex}`;
736
- params.push(scoreThreshold);
737
- paramIndex++;
1102
+ if (conditions.length > 0) {
1103
+ sql += ` WHERE ${conditions.join(" AND ")}`;
1104
+ hasWhere = true;
1105
+ }
738
1106
  }
1107
+ sql += hasWhere ? " AND" : " WHERE";
1108
+ sql += ` ${combinedScoreExpr} >= $${paramIndex}`;
1109
+ params.push(scoreThreshold);
1110
+ paramIndex++;
739
1111
  sql += ` ORDER BY score DESC LIMIT $${paramIndex}`;
740
1112
  params.push(topK);
741
1113
  const result = await this.db.query(sql, params);
742
1114
  const results = [];
743
1115
  for (const row of result.rows) {
744
1116
  const vectorResult = await this.db.query(`SELECT ${vectorCol}::text FROM "${this.table}" WHERE ${this.getPrimaryKeyWhereClause()}`, this.getPrimaryKeyValues(row));
745
- const vectorStr = vectorResult.rows[0]?.[vectorCol] || "[]";
1117
+ const vectorStr = vectorResult.rows[0]?.[vectorColRaw] || "[]";
746
1118
  const vectorArray = JSON.parse(vectorStr);
747
1119
  results.push({
748
1120
  ...row,
@@ -759,7 +1131,13 @@ class PostgresVectorStorage extends PostgresTabularStorage {
759
1131
  return this.hybridSearchFallback(query, options);
760
1132
  }
761
1133
  }
1134
+ assertFallbackSupportsDistance() {
1135
+ if (this.distance !== "cosine") {
1136
+ throw new Error(`PostgresVectorStorage: pgvector is unavailable and the in-memory ` + `fallback only supports cosine distance (configured: "${this.distance}"). ` + `Install pgvector or switch the storage to cosine distance.`);
1137
+ }
1138
+ }
762
1139
  async searchFallback(query, options) {
1140
+ this.assertFallbackSupportsDistance();
763
1141
  const { topK = 10, filter, scoreThreshold = 0 } = options;
764
1142
  const allRows = await this.getAll() || [];
765
1143
  const results = [];
@@ -779,6 +1157,7 @@ class PostgresVectorStorage extends PostgresTabularStorage {
779
1157
  return topResults;
780
1158
  }
781
1159
  async hybridSearchFallback(query, options) {
1160
+ this.assertFallbackSupportsDistance();
782
1161
  const { topK = 10, filter, scoreThreshold = 0, textQuery, vectorWeight = 0.7 } = options;
783
1162
  const allRows = await this.getAll() || [];
784
1163
  const results = [];
@@ -827,16 +1206,126 @@ class PostgresVectorStorage extends PostgresTabularStorage {
827
1206
  return true;
828
1207
  }
829
1208
  }
1209
+ // src/migrations/PostgresMigrationRunner.ts
1210
+ import {
1211
+ MIGRATIONS_TABLE as MIGRATIONS_TABLE2,
1212
+ sortMigrations
1213
+ } from "@workglow/storage";
1214
+
1215
+ class PostgresMigrationRunner {
1216
+ db;
1217
+ constructor(db) {
1218
+ this.db = db;
1219
+ }
1220
+ async ensureBookkeepingTable() {
1221
+ await this.db.query(`
1222
+ CREATE TABLE IF NOT EXISTS ${MIGRATIONS_TABLE2} (
1223
+ component TEXT NOT NULL,
1224
+ version INTEGER NOT NULL,
1225
+ description TEXT,
1226
+ applied_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
1227
+ PRIMARY KEY (component, version)
1228
+ )
1229
+ `);
1230
+ }
1231
+ async appliedVersions(component) {
1232
+ const result = await this.db.query(`SELECT version FROM ${MIGRATIONS_TABLE2} WHERE component = $1`, [component]);
1233
+ return new Set(result.rows.map((r) => Number(r.version)));
1234
+ }
1235
+ async acquireClient() {
1236
+ const pool = this.db;
1237
+ if (typeof pool.connect === "function") {
1238
+ const client = await pool.connect();
1239
+ return { client, release: () => client.release() };
1240
+ }
1241
+ return { client: this.db, release: () => {
1242
+ return;
1243
+ } };
1244
+ }
1245
+ async run(migrations, options = {}) {
1246
+ await this.ensureBookkeepingTable();
1247
+ const sorted = sortMigrations(migrations);
1248
+ const applied = [];
1249
+ const cache = new Map;
1250
+ const onProgress = options.onProgress;
1251
+ for (const m of sorted) {
1252
+ let seen = cache.get(m.component);
1253
+ if (!seen) {
1254
+ seen = await this.appliedVersions(m.component);
1255
+ cache.set(m.component, seen);
1256
+ }
1257
+ if (seen.has(m.version))
1258
+ continue;
1259
+ onProgress?.({
1260
+ component: m.component,
1261
+ version: m.version,
1262
+ phase: "starting",
1263
+ description: m.description
1264
+ });
1265
+ const { client, release } = await this.acquireClient();
1266
+ try {
1267
+ await client.query("BEGIN");
1268
+ await m.up(client, (fraction) => {
1269
+ onProgress?.({
1270
+ component: m.component,
1271
+ version: m.version,
1272
+ phase: "running",
1273
+ description: m.description,
1274
+ fraction
1275
+ });
1276
+ });
1277
+ await client.query(`INSERT INTO ${MIGRATIONS_TABLE2}(component, version, description) VALUES ($1, $2, $3)`, [m.component, m.version, m.description ?? null]);
1278
+ await client.query("COMMIT");
1279
+ seen.add(m.version);
1280
+ applied.push(m);
1281
+ onProgress?.({
1282
+ component: m.component,
1283
+ version: m.version,
1284
+ phase: "completed",
1285
+ description: m.description,
1286
+ fraction: 1
1287
+ });
1288
+ } catch (err) {
1289
+ await client.query("ROLLBACK").catch(() => {
1290
+ return;
1291
+ });
1292
+ if (err?.code === "23505") {
1293
+ seen.add(m.version);
1294
+ onProgress?.({
1295
+ component: m.component,
1296
+ version: m.version,
1297
+ phase: "completed",
1298
+ description: m.description,
1299
+ fraction: 1
1300
+ });
1301
+ continue;
1302
+ }
1303
+ onProgress?.({
1304
+ component: m.component,
1305
+ version: m.version,
1306
+ phase: "failed",
1307
+ description: m.description,
1308
+ error: err
1309
+ });
1310
+ throw err;
1311
+ } finally {
1312
+ release();
1313
+ }
1314
+ }
1315
+ return applied;
1316
+ }
1317
+ }
830
1318
  export {
831
1319
  loadPostgres,
832
1320
  getPostgres,
833
1321
  createPool,
834
1322
  PostgresVectorStorage,
835
1323
  PostgresTabularStorage,
1324
+ PostgresMigrationRunner,
836
1325
  PostgresKvStorage,
837
1326
  Postgres,
838
1327
  POSTGRES_TABULAR_REPOSITORY,
839
1328
  POSTGRES_KV_REPOSITORY
840
1329
  };
841
1330
 
842
- //# debugId=EAED4AF57300AAC364756E2164756E21
1331
+ //# debugId=1C3E72F1BE3B8A0064756E2164756E21