@workglow/postgres 0.2.36 → 0.2.37

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 (36) hide show
  1. package/dist/job-queue/PostgresJobStore.d.ts +29 -0
  2. package/dist/job-queue/PostgresJobStore.d.ts.map +1 -0
  3. package/dist/job-queue/PostgresMessageQueue.d.ts +38 -0
  4. package/dist/job-queue/PostgresMessageQueue.d.ts.map +1 -0
  5. package/dist/job-queue/PostgresQueueStorage.d.ts +41 -10
  6. package/dist/job-queue/PostgresQueueStorage.d.ts.map +1 -1
  7. package/dist/job-queue/PostgresRateLimiterStorage.d.ts +1 -2
  8. package/dist/job-queue/PostgresRateLimiterStorage.d.ts.map +1 -1
  9. package/dist/job-queue/browser.js +401 -53
  10. package/dist/job-queue/browser.js.map +11 -8
  11. package/dist/job-queue/common.d.ts +3 -0
  12. package/dist/job-queue/common.d.ts.map +1 -1
  13. package/dist/job-queue/createPostgresQueue.d.ts +22 -0
  14. package/dist/job-queue/createPostgresQueue.d.ts.map +1 -0
  15. package/dist/job-queue/node.js +401 -53
  16. package/dist/job-queue/node.js.map +11 -8
  17. package/dist/migrations/PostgresMigrationRunner.d.ts +1 -1
  18. package/dist/migrations/PostgresMigrationRunner.d.ts.map +1 -1
  19. package/dist/migrations/postgresQueueMigrations.d.ts +9 -1
  20. package/dist/migrations/postgresQueueMigrations.d.ts.map +1 -1
  21. package/dist/migrations/postgresRateLimiterMigrations.d.ts +1 -1
  22. package/dist/migrations/postgresRateLimiterMigrations.d.ts.map +1 -1
  23. package/dist/storage/PostgresKvStorage.d.ts +1 -1
  24. package/dist/storage/PostgresKvStorage.d.ts.map +1 -1
  25. package/dist/storage/PostgresTabularStorage.d.ts +1 -1
  26. package/dist/storage/PostgresTabularStorage.d.ts.map +1 -1
  27. package/dist/storage/PostgresVectorStorage.d.ts +1 -1
  28. package/dist/storage/PostgresVectorStorage.d.ts.map +1 -1
  29. package/dist/storage/browser.js +28 -12
  30. package/dist/storage/browser.js.map +5 -5
  31. package/dist/storage/node.js +28 -12
  32. package/dist/storage/node.js.map +6 -6
  33. package/dist/text/PostgresFtsTextIndex.d.ts.map +1 -1
  34. package/dist/text/browser.js.map +2 -2
  35. package/dist/text/node.js.map +2 -2
  36. package/package.json +7 -7
@@ -1 +1 @@
1
- {"version":3,"file":"PostgresFtsTextIndex.d.ts","sourceRoot":"","sources":["../../src/text/PostgresFtsTextIndex.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,4BAA4B,CAAC;AACvD,OAAO,KAAK,EACV,UAAU,EACV,UAAU,EACV,iBAAiB,EACjB,gBAAgB,EACjB,MAAM,mBAAmB,CAAC;AAE3B;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,kCAAkC,EAAE,QAAQ,CACvD,MAAM,CAAC,MAAM,EAAE,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC,CAOtC,CAAC;AAEF,MAAM,WAAW,2BAA2B;IAC1C,wEAAwE;IACxE,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC;;;OAGG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC,CAAC,CAAC;CACzE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,qBAAa,oBAAqB,YAAW,UAAU;IACrD,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAO;IAC5B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA8B;IAatD,OAAO,CAAC,aAAa,CAKP;IAEd,YAAY,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,GAAE,2BAAgC,EAI/E;IAED,OAAO,KAAK,cAAc,GAEzB;IAED,OAAO,KAAK,YAAY,GAEvB;IAED;;;;OAIG;IACH,OAAO,KAAK,IAAI,GAGf;IAED;;;;OAIG;IACH,OAAO,KAAK,WAAW,GAQtB;IAED;;OAEG;IACG,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAYnC;IAEK,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CA4C3E;IAEK,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAG3C;IAEK,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAGnD;IAEK,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAI3B;IAEK,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC,CAK5B;IAEK,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,GAAE,iBAAsB,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAkBxF;IAEK,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAclC;IAEK,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAYnC;IAEK,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAQlC;IAED;;;;;;;OAOG;IACH,MAAM,IAAI;QAAE,IAAI,EAAE,cAAc,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAEhD;IAED,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAwB7B;YAYa,iBAAiB;CAqChC"}
1
+ {"version":3,"file":"PostgresFtsTextIndex.d.ts","sourceRoot":"","sources":["../../src/text/PostgresFtsTextIndex.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,4BAA4B,CAAC;AACvD,OAAO,KAAK,EACV,UAAU,EACV,UAAU,EACV,iBAAiB,EACjB,gBAAgB,EACjB,MAAM,mBAAmB,CAAC;AAE3B;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,kCAAkC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC,CAM9F,CAAC;AAEF,MAAM,WAAW,2BAA2B;IAC1C,wEAAwE;IACxE,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC;;;OAGG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC,CAAC,CAAC;CACzE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,qBAAa,oBAAqB,YAAW,UAAU;IACrD,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAO;IAC5B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA8B;IAatD,OAAO,CAAC,aAAa,CAKP;IAEd,YAAY,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,GAAE,2BAAgC,EAI/E;IAED,OAAO,KAAK,cAAc,GAEzB;IAED,OAAO,KAAK,YAAY,GAEvB;IAED;;;;OAIG;IACH,OAAO,KAAK,IAAI,GAGf;IAED;;;;OAIG;IACH,OAAO,KAAK,WAAW,GAQtB;IAED;;OAEG;IACG,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAYnC;IAEK,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CA0C3E;IAEK,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAG3C;IAEK,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAGnD;IAEK,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAI3B;IAEK,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC,CAK5B;IAEK,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,GAAE,iBAAsB,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAqBxF;IAEK,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAclC;IAEK,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAYnC;IAEK,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAQlC;IAED;;;;;;;OAOG;IACH,MAAM,IAAI;QAAE,IAAI,EAAE,cAAc,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAEhD;IAED,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAwB7B;YAYa,iBAAiB;CAqChC"}
@@ -2,9 +2,9 @@
2
2
  "version": 3,
3
3
  "sources": ["../../src/text/PostgresFtsTextIndex.ts"],
4
4
  "sourcesContent": [
5
- "/**\n * @license\n * Copyright 2026 Steven Roussey <sroussey@gmail.com>\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport type { Pool } from \"@workglow/postgres/storage\";\nimport type {\n ITextIndex,\n TextFields,\n TextSearchOptions,\n TextSearchResult,\n} from \"@workglow/storage\";\n\n/**\n * Per-field weights for {@link PostgresFtsTextIndex}. Postgres FTS supports\n * exactly four weight buckets — A (heaviest), B, C, D — and that maps\n * naturally onto the hierarchy of chunk fields:\n *\n * - `text` → A: direct chunk content\n * - `doc_title` → B: navigational\n * - `sectionTitles` → B: navigational\n * - `summary` → C: condensed body\n * - `parentSummaries` → D: inherited context\n *\n * Fields outside this map are silently ignored at index time (no FTS bucket\n * to assign). Callers can override the mapping via\n * {@link PostgresFtsTextIndexOptions.fieldWeights}.\n */\nexport const DEFAULT_POSTGRES_FTS_FIELD_WEIGHTS: Readonly<\n Record<string, \"A\" | \"B\" | \"C\" | \"D\">\n> = {\n text: \"A\",\n doc_title: \"B\",\n sectionTitles: \"B\",\n summary: \"C\",\n parentSummaries: \"D\",\n};\n\nexport interface PostgresFtsTextIndexOptions {\n /** Postgres text-search configuration name. Defaults to `\"english\"`. */\n readonly tsvectorConfig?: string;\n /**\n * Per-field FTS weight bucket. See {@link DEFAULT_POSTGRES_FTS_FIELD_WEIGHTS}\n * for the default mapping. Fields not listed are ignored at index time.\n */\n readonly fieldWeights?: Readonly<Record<string, \"A\" | \"B\" | \"C\" | \"D\">>;\n}\n\n/**\n * Postgres-native full-text index for {@link KnowledgeBase.hybridSearch} /\n * {@link KnowledgeBase.textSearch}. Stores postings on a single side table\n * keyed by `chunk_id`:\n *\n * ```sql\n * CREATE TABLE <table> (\n * chunk_id TEXT PRIMARY KEY,\n * doc_id TEXT NOT NULL,\n * tsv TSVECTOR NOT NULL\n * );\n * CREATE INDEX <table>_tsv_idx ON <table> USING GIN (tsv);\n * CREATE INDEX <table>_doc_idx ON <table> (doc_id);\n * ```\n *\n * Scoring is `ts_rank_cd(tsv, plainto_tsquery(...))`; unbounded above and\n * non-negative, suitable for RRF fusion without normalisation.\n *\n * Lifecycle: {@link setupDatabase} must be called before any other method.\n * {@link beginRebuild}/{@link commitRebuild}/{@link abortRebuild} let\n * `KnowledgeBase.reindexText` wrap a full rebuild in a database\n * transaction; on `abort` the table is rolled back to its pre-`begin`\n * contents.\n *\n * State is durable on the database side; {@link toJSON} / {@link fromJSON}\n * are intentional no-ops (the table itself is the snapshot).\n */\nexport class PostgresFtsTextIndex implements ITextIndex {\n private readonly pool: Pool;\n private readonly table: string;\n private readonly options: PostgresFtsTextIndexOptions;\n\n // Tx state for beginRebuild/commitRebuild/abortRebuild. Acquired on\n // beginRebuild; released on commit/abort. When set, all DML methods\n // (`clear`, `add`, `remove`, `removeByDocument`) route through the\n // dedicated client so the rebuild is one BEGIN…COMMIT block.\n //\n // We hold a pre-bound `query` reference alongside the client. `pg.PoolClient`'s\n // `query` method depends on `this` being the client instance; if we returned\n // the raw method reference from the `exec` getter and call sites invoked it\n // as a plain function (which they do), `this` would be undefined and real\n // pg drivers would throw. Binding once at acquisition keeps every `exec(...)`\n // call safe regardless of how it's invoked.\n private rebuildClient:\n | {\n boundQuery: Pool[\"query\"];\n release: () => void;\n }\n | undefined;\n\n constructor(pool: Pool, table: string, options: PostgresFtsTextIndexOptions = {}) {\n this.pool = pool;\n this.table = table;\n this.options = options;\n }\n\n private get tsvectorConfig(): string {\n return this.options.tsvectorConfig ?? \"english\";\n }\n\n private get fieldWeights(): Readonly<Record<string, \"A\" | \"B\" | \"C\" | \"D\">> {\n return this.options.fieldWeights ?? DEFAULT_POSTGRES_FTS_FIELD_WEIGHTS;\n }\n\n /**\n * Routes a query to the rebuild-bound client when a rebuild is open.\n * Returns a function that is safe to call without preserving `this` —\n * the underlying `pg.PoolClient.query` is bound at acquisition time.\n */\n private get exec(): Pool[\"query\"] {\n if (this.rebuildClient) return this.rebuildClient.boundQuery;\n return this.pool.query.bind(this.pool) as Pool[\"query\"];\n }\n\n /**\n * Identifier-quote a table name. The constructor accepts an arbitrary\n * string, so we whitelist it (alphanumerics + underscores) and reject\n * anything else before splicing into DDL.\n */\n private get quotedTable(): string {\n if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(this.table)) {\n throw new Error(\n `PostgresFtsTextIndex: refusing to use unsafe table name ${JSON.stringify(this.table)}; ` +\n `expected /^[A-Za-z_][A-Za-z0-9_]*$/.`\n );\n }\n return `\"${this.table}\"`;\n }\n\n /**\n * Create the postings table + GIN index. Idempotent.\n */\n async setupDatabase(): Promise<void> {\n const table = this.quotedTable;\n const exec = this.exec as (sql: string) => Promise<unknown>;\n await exec(\n `CREATE TABLE IF NOT EXISTS ${table} (\n chunk_id TEXT PRIMARY KEY,\n doc_id TEXT NOT NULL,\n tsv TSVECTOR NOT NULL\n )`\n );\n await exec(`CREATE INDEX IF NOT EXISTS \"${this.table}_tsv_idx\" ON ${table} USING GIN (tsv)`);\n await exec(`CREATE INDEX IF NOT EXISTS \"${this.table}_doc_idx\" ON ${table} (doc_id)`);\n }\n\n async add(chunkId: string, docId: string, fields: TextFields): Promise<void> {\n const weights = this.fieldWeights;\n const cfg = this.tsvectorConfig;\n\n const params: (string | null)[] = [];\n const tsvParts: string[] = [];\n params.push(cfg); // $1\n\n let nextParam = 2;\n for (const [field, weight] of Object.entries(weights)) {\n const raw = fields[field];\n if (raw === undefined) continue;\n const text = Array.isArray(raw) ? raw.join(\" \") : (raw as string);\n // Skip empty/whitespace-only — they'd contribute a zero-length tsvector\n // and waste a parameter slot.\n if (typeof text !== \"string\" || text.trim().length === 0) continue;\n params.push(text);\n const pIdx = nextParam++;\n tsvParts.push(\n `setweight(to_tsvector($1::regconfig, coalesce($${pIdx}, '')), '${weight}')`\n );\n }\n\n const table = this.quotedTable;\n\n if (tsvParts.length === 0) {\n // Nothing indexable on this chunk — drop any prior postings so an\n // update that clears text is reflected. Mirror BM25Index.add's behaviour.\n await this.remove(chunkId);\n return;\n }\n\n params.push(chunkId);\n const chunkIdParam = `$${nextParam++}`;\n params.push(docId);\n const docIdParam = `$${nextParam++}`;\n\n const tsvExpr = tsvParts.join(\" || \");\n const sql = `INSERT INTO ${table} (chunk_id, doc_id, tsv)\n VALUES (${chunkIdParam}, ${docIdParam}, ${tsvExpr})\n ON CONFLICT (chunk_id) DO UPDATE\n SET doc_id = EXCLUDED.doc_id,\n tsv = EXCLUDED.tsv`;\n await (this.exec as (sql: string, params: unknown[]) => Promise<unknown>)(sql, params);\n }\n\n async remove(chunkId: string): Promise<void> {\n const sql = `DELETE FROM ${this.quotedTable} WHERE chunk_id = $1`;\n await (this.exec as (sql: string, params: unknown[]) => Promise<unknown>)(sql, [chunkId]);\n }\n\n async removeByDocument(docId: string): Promise<void> {\n const sql = `DELETE FROM ${this.quotedTable} WHERE doc_id = $1`;\n await (this.exec as (sql: string, params: unknown[]) => Promise<unknown>)(sql, [docId]);\n }\n\n async clear(): Promise<void> {\n // TRUNCATE is faster than DELETE and friendly to the GIN index; we hold\n // the rebuild's transaction (when present) so it still rolls back on abort.\n await (this.exec as (sql: string) => Promise<unknown>)(`TRUNCATE TABLE ${this.quotedTable}`);\n }\n\n async size(): Promise<number> {\n const res = (await (this.exec as (sql: string) => Promise<{ rows: Array<{ n: number }> }>)(\n `SELECT count(*)::int AS n FROM ${this.quotedTable}`\n )) as { rows: Array<{ n: number }> };\n return res.rows[0]?.n ?? 0;\n }\n\n async search(query: string, options: TextSearchOptions = {}): Promise<TextSearchResult[]> {\n const topK = options.topK ?? 10;\n if (typeof query !== \"string\" || query.trim().length === 0) return [];\n const sql = `\n SELECT chunk_id, ts_rank_cd(tsv, plainto_tsquery($1::regconfig, $2)) AS score\n FROM ${this.quotedTable}\n WHERE tsv @@ plainto_tsquery($1::regconfig, $2)\n ORDER BY score DESC\n LIMIT $3\n `;\n const res = (await (\n this.exec as (sql: string, params: unknown[]) => Promise<{\n rows: Array<{ chunk_id: string; score: number }>;\n }>\n )(sql, [this.tsvectorConfig, query, topK])) as {\n rows: Array<{ chunk_id: string; score: number }>;\n };\n return res.rows.map((r) => ({ chunkId: r.chunk_id, score: Number(r.score) }));\n }\n\n async beginRebuild(): Promise<void> {\n if (this.rebuildClient) {\n throw new Error(\n `PostgresFtsTextIndex.beginRebuild on table \"${this.table}\" called while a rebuild was already open. Call commitRebuild() or abortRebuild() first.`\n );\n }\n this.rebuildClient = await this.acquireConnection();\n try {\n await this.rebuildClient.boundQuery(`BEGIN`);\n } catch (err) {\n this.rebuildClient.release();\n this.rebuildClient = undefined;\n throw err;\n }\n }\n\n async commitRebuild(): Promise<void> {\n if (!this.rebuildClient) {\n throw new Error(\n `PostgresFtsTextIndex.commitRebuild on table \"${this.table}\" called without an open rebuild.`\n );\n }\n try {\n await this.rebuildClient.boundQuery(`COMMIT`);\n } finally {\n this.rebuildClient.release();\n this.rebuildClient = undefined;\n }\n }\n\n async abortRebuild(): Promise<void> {\n if (!this.rebuildClient) return;\n try {\n await this.rebuildClient.boundQuery(`ROLLBACK`);\n } finally {\n this.rebuildClient.release();\n this.rebuildClient = undefined;\n }\n }\n\n /**\n * State lives server-side. `toJSON`/`fromJSON` are no-ops, mirrored on\n * {@link PostgresFtsTextIndex} so the {@link ITextIndex} contract still\n * round-trips for callers that snapshot/restore the index. Callers\n * relying on a real serialisation should use {@link beginRebuild} /\n * {@link commitRebuild} / {@link abortRebuild} for atomic mutations\n * instead.\n */\n toJSON(): { kind: \"postgres-fts\"; table: string } {\n return { kind: \"postgres-fts\", table: this.table };\n }\n\n fromJSON(state: unknown): void {\n if (\n !state ||\n typeof state !== \"object\" ||\n (state as { kind?: unknown }).kind !== \"postgres-fts\"\n ) {\n throw new Error(\n `PostgresFtsTextIndex.fromJSON: expected { kind: \"postgres-fts\" }, got ${JSON.stringify(\n state\n )}`\n );\n }\n // Symmetric with toJSON: if the snapshot carries a `table`, it must\n // match this instance's table. A snapshot from a different table\n // round-tripping through a mismatched instance would silently mask\n // bugs (the server-side data still lives in the original table).\n const snapshotTable = (state as { table?: unknown }).table;\n if (snapshotTable !== undefined && snapshotTable !== this.table) {\n throw new Error(\n `PostgresFtsTextIndex.fromJSON: snapshot table \"${String(snapshotTable)}\" does not match this index's table \"${this.table}\".`\n );\n }\n // No-op beyond validation: state is server-side. We accept the\n // snapshot for API parity.\n }\n\n /**\n * Mirror of `PostgresTabularStorage.acquireConnection`: prefer `pool.connect()`\n * when available (real `pg.Pool`); fall back to routing through `pool.query`\n * directly for single-connection wrappers (PGlite / PGLitePool). Failing\n * fast on anything else keeps BEGIN/COMMIT bracket sanity.\n *\n * Returns a pre-bound `boundQuery` rather than the raw `query` reference so\n * call sites invoking it as a plain function (`exec(sql, params)`) don't\n * lose the `pg.PoolClient` `this` binding.\n */\n private async acquireConnection(): Promise<{\n boundQuery: Pool[\"query\"];\n release: () => void;\n }> {\n const supportsConnect =\n typeof (this.pool as unknown as { connect?: unknown }).connect === \"function\";\n if (supportsConnect) {\n const client = await (\n this.pool as unknown as {\n connect: () => Promise<{ query: Pool[\"query\"]; release: () => void }>;\n }\n ).connect();\n return {\n boundQuery: client.query.bind(client) as Pool[\"query\"],\n release: () => client.release(),\n };\n }\n const poolAny = this.pool as unknown as {\n waitReady?: unknown;\n exec?: unknown;\n constructor?: { name?: string };\n };\n const ctorName = poolAny.constructor?.name;\n const looksLikePGlite = typeof poolAny.exec === \"function\" && poolAny.waitReady !== undefined;\n const looksLikePGLitePool = ctorName === \"PGLitePool\";\n if (!looksLikePGlite && !looksLikePGLitePool) {\n throw new Error(\n `PostgresFtsTextIndex requires a pg.Pool with connect() or a known single-connection wrapper (PGLitePool, PGlite); got ${\n ctorName ?? typeof this.pool\n }.`\n );\n }\n return {\n boundQuery: this.pool.query.bind(this.pool) as Pool[\"query\"],\n release: () => {},\n };\n }\n}\n"
5
+ "/**\n * @license\n * Copyright 2026 Steven Roussey <sroussey@gmail.com>\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport type { Pool } from \"@workglow/postgres/storage\";\nimport type {\n ITextIndex,\n TextFields,\n TextSearchOptions,\n TextSearchResult,\n} from \"@workglow/storage\";\n\n/**\n * Per-field weights for {@link PostgresFtsTextIndex}. Postgres FTS supports\n * exactly four weight buckets — A (heaviest), B, C, D — and that maps\n * naturally onto the hierarchy of chunk fields:\n *\n * - `text` → A: direct chunk content\n * - `doc_title` → B: navigational\n * - `sectionTitles` → B: navigational\n * - `summary` → C: condensed body\n * - `parentSummaries` → D: inherited context\n *\n * Fields outside this map are silently ignored at index time (no FTS bucket\n * to assign). Callers can override the mapping via\n * {@link PostgresFtsTextIndexOptions.fieldWeights}.\n */\nexport const DEFAULT_POSTGRES_FTS_FIELD_WEIGHTS: Readonly<Record<string, \"A\" | \"B\" | \"C\" | \"D\">> = {\n text: \"A\",\n doc_title: \"B\",\n sectionTitles: \"B\",\n summary: \"C\",\n parentSummaries: \"D\",\n};\n\nexport interface PostgresFtsTextIndexOptions {\n /** Postgres text-search configuration name. Defaults to `\"english\"`. */\n readonly tsvectorConfig?: string;\n /**\n * Per-field FTS weight bucket. See {@link DEFAULT_POSTGRES_FTS_FIELD_WEIGHTS}\n * for the default mapping. Fields not listed are ignored at index time.\n */\n readonly fieldWeights?: Readonly<Record<string, \"A\" | \"B\" | \"C\" | \"D\">>;\n}\n\n/**\n * Postgres-native full-text index for {@link KnowledgeBase.hybridSearch} /\n * {@link KnowledgeBase.textSearch}. Stores postings on a single side table\n * keyed by `chunk_id`:\n *\n * ```sql\n * CREATE TABLE <table> (\n * chunk_id TEXT PRIMARY KEY,\n * doc_id TEXT NOT NULL,\n * tsv TSVECTOR NOT NULL\n * );\n * CREATE INDEX <table>_tsv_idx ON <table> USING GIN (tsv);\n * CREATE INDEX <table>_doc_idx ON <table> (doc_id);\n * ```\n *\n * Scoring is `ts_rank_cd(tsv, plainto_tsquery(...))`; unbounded above and\n * non-negative, suitable for RRF fusion without normalisation.\n *\n * Lifecycle: {@link setupDatabase} must be called before any other method.\n * {@link beginRebuild}/{@link commitRebuild}/{@link abortRebuild} let\n * `KnowledgeBase.reindexText` wrap a full rebuild in a database\n * transaction; on `abort` the table is rolled back to its pre-`begin`\n * contents.\n *\n * State is durable on the database side; {@link toJSON} / {@link fromJSON}\n * are intentional no-ops (the table itself is the snapshot).\n */\nexport class PostgresFtsTextIndex implements ITextIndex {\n private readonly pool: Pool;\n private readonly table: string;\n private readonly options: PostgresFtsTextIndexOptions;\n\n // Tx state for beginRebuild/commitRebuild/abortRebuild. Acquired on\n // beginRebuild; released on commit/abort. When set, all DML methods\n // (`clear`, `add`, `remove`, `removeByDocument`) route through the\n // dedicated client so the rebuild is one BEGIN…COMMIT block.\n //\n // We hold a pre-bound `query` reference alongside the client. `pg.PoolClient`'s\n // `query` method depends on `this` being the client instance; if we returned\n // the raw method reference from the `exec` getter and call sites invoked it\n // as a plain function (which they do), `this` would be undefined and real\n // pg drivers would throw. Binding once at acquisition keeps every `exec(...)`\n // call safe regardless of how it's invoked.\n private rebuildClient:\n | {\n boundQuery: Pool[\"query\"];\n release: () => void;\n }\n | undefined;\n\n constructor(pool: Pool, table: string, options: PostgresFtsTextIndexOptions = {}) {\n this.pool = pool;\n this.table = table;\n this.options = options;\n }\n\n private get tsvectorConfig(): string {\n return this.options.tsvectorConfig ?? \"english\";\n }\n\n private get fieldWeights(): Readonly<Record<string, \"A\" | \"B\" | \"C\" | \"D\">> {\n return this.options.fieldWeights ?? DEFAULT_POSTGRES_FTS_FIELD_WEIGHTS;\n }\n\n /**\n * Routes a query to the rebuild-bound client when a rebuild is open.\n * Returns a function that is safe to call without preserving `this` —\n * the underlying `pg.PoolClient.query` is bound at acquisition time.\n */\n private get exec(): Pool[\"query\"] {\n if (this.rebuildClient) return this.rebuildClient.boundQuery;\n return this.pool.query.bind(this.pool) as Pool[\"query\"];\n }\n\n /**\n * Identifier-quote a table name. The constructor accepts an arbitrary\n * string, so we whitelist it (alphanumerics + underscores) and reject\n * anything else before splicing into DDL.\n */\n private get quotedTable(): string {\n if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(this.table)) {\n throw new Error(\n `PostgresFtsTextIndex: refusing to use unsafe table name ${JSON.stringify(this.table)}; ` +\n `expected /^[A-Za-z_][A-Za-z0-9_]*$/.`\n );\n }\n return `\"${this.table}\"`;\n }\n\n /**\n * Create the postings table + GIN index. Idempotent.\n */\n async setupDatabase(): Promise<void> {\n const table = this.quotedTable;\n const exec = this.exec as (sql: string) => Promise<unknown>;\n await exec(\n `CREATE TABLE IF NOT EXISTS ${table} (\n chunk_id TEXT PRIMARY KEY,\n doc_id TEXT NOT NULL,\n tsv TSVECTOR NOT NULL\n )`\n );\n await exec(`CREATE INDEX IF NOT EXISTS \"${this.table}_tsv_idx\" ON ${table} USING GIN (tsv)`);\n await exec(`CREATE INDEX IF NOT EXISTS \"${this.table}_doc_idx\" ON ${table} (doc_id)`);\n }\n\n async add(chunkId: string, docId: string, fields: TextFields): Promise<void> {\n const weights = this.fieldWeights;\n const cfg = this.tsvectorConfig;\n\n const params: (string | null)[] = [];\n const tsvParts: string[] = [];\n params.push(cfg); // $1\n\n let nextParam = 2;\n for (const [field, weight] of Object.entries(weights)) {\n const raw = fields[field];\n if (raw === undefined) continue;\n const text = Array.isArray(raw) ? raw.join(\" \") : (raw as string);\n // Skip empty/whitespace-only — they'd contribute a zero-length tsvector\n // and waste a parameter slot.\n if (typeof text !== \"string\" || text.trim().length === 0) continue;\n params.push(text);\n const pIdx = nextParam++;\n tsvParts.push(`setweight(to_tsvector($1::regconfig, coalesce($${pIdx}, '')), '${weight}')`);\n }\n\n const table = this.quotedTable;\n\n if (tsvParts.length === 0) {\n // Nothing indexable on this chunk — drop any prior postings so an\n // update that clears text is reflected. Mirror BM25Index.add's behaviour.\n await this.remove(chunkId);\n return;\n }\n\n params.push(chunkId);\n const chunkIdParam = `$${nextParam++}`;\n params.push(docId);\n const docIdParam = `$${nextParam++}`;\n\n const tsvExpr = tsvParts.join(\" || \");\n const sql = `INSERT INTO ${table} (chunk_id, doc_id, tsv)\n VALUES (${chunkIdParam}, ${docIdParam}, ${tsvExpr})\n ON CONFLICT (chunk_id) DO UPDATE\n SET doc_id = EXCLUDED.doc_id,\n tsv = EXCLUDED.tsv`;\n await (this.exec as (sql: string, params: unknown[]) => Promise<unknown>)(sql, params);\n }\n\n async remove(chunkId: string): Promise<void> {\n const sql = `DELETE FROM ${this.quotedTable} WHERE chunk_id = $1`;\n await (this.exec as (sql: string, params: unknown[]) => Promise<unknown>)(sql, [chunkId]);\n }\n\n async removeByDocument(docId: string): Promise<void> {\n const sql = `DELETE FROM ${this.quotedTable} WHERE doc_id = $1`;\n await (this.exec as (sql: string, params: unknown[]) => Promise<unknown>)(sql, [docId]);\n }\n\n async clear(): Promise<void> {\n // TRUNCATE is faster than DELETE and friendly to the GIN index; we hold\n // the rebuild's transaction (when present) so it still rolls back on abort.\n await (this.exec as (sql: string) => Promise<unknown>)(`TRUNCATE TABLE ${this.quotedTable}`);\n }\n\n async size(): Promise<number> {\n const res = (await (this.exec as (sql: string) => Promise<{ rows: Array<{ n: number }> }>)(\n `SELECT count(*)::int AS n FROM ${this.quotedTable}`\n )) as { rows: Array<{ n: number }> };\n return res.rows[0]?.n ?? 0;\n }\n\n async search(query: string, options: TextSearchOptions = {}): Promise<TextSearchResult[]> {\n const topK = options.topK ?? 10;\n if (typeof query !== \"string\" || query.trim().length === 0) return [];\n const sql = `\n SELECT chunk_id, ts_rank_cd(tsv, plainto_tsquery($1::regconfig, $2)) AS score\n FROM ${this.quotedTable}\n WHERE tsv @@ plainto_tsquery($1::regconfig, $2)\n ORDER BY score DESC\n LIMIT $3\n `;\n const res = (await (\n this.exec as (\n sql: string,\n params: unknown[]\n ) => Promise<{\n rows: Array<{ chunk_id: string; score: number }>;\n }>\n )(sql, [this.tsvectorConfig, query, topK])) as {\n rows: Array<{ chunk_id: string; score: number }>;\n };\n return res.rows.map((r) => ({ chunkId: r.chunk_id, score: Number(r.score) }));\n }\n\n async beginRebuild(): Promise<void> {\n if (this.rebuildClient) {\n throw new Error(\n `PostgresFtsTextIndex.beginRebuild on table \"${this.table}\" called while a rebuild was already open. Call commitRebuild() or abortRebuild() first.`\n );\n }\n this.rebuildClient = await this.acquireConnection();\n try {\n await this.rebuildClient.boundQuery(`BEGIN`);\n } catch (err) {\n this.rebuildClient.release();\n this.rebuildClient = undefined;\n throw err;\n }\n }\n\n async commitRebuild(): Promise<void> {\n if (!this.rebuildClient) {\n throw new Error(\n `PostgresFtsTextIndex.commitRebuild on table \"${this.table}\" called without an open rebuild.`\n );\n }\n try {\n await this.rebuildClient.boundQuery(`COMMIT`);\n } finally {\n this.rebuildClient.release();\n this.rebuildClient = undefined;\n }\n }\n\n async abortRebuild(): Promise<void> {\n if (!this.rebuildClient) return;\n try {\n await this.rebuildClient.boundQuery(`ROLLBACK`);\n } finally {\n this.rebuildClient.release();\n this.rebuildClient = undefined;\n }\n }\n\n /**\n * State lives server-side. `toJSON`/`fromJSON` are no-ops, mirrored on\n * {@link PostgresFtsTextIndex} so the {@link ITextIndex} contract still\n * round-trips for callers that snapshot/restore the index. Callers\n * relying on a real serialisation should use {@link beginRebuild} /\n * {@link commitRebuild} / {@link abortRebuild} for atomic mutations\n * instead.\n */\n toJSON(): { kind: \"postgres-fts\"; table: string } {\n return { kind: \"postgres-fts\", table: this.table };\n }\n\n fromJSON(state: unknown): void {\n if (\n !state ||\n typeof state !== \"object\" ||\n (state as { kind?: unknown }).kind !== \"postgres-fts\"\n ) {\n throw new Error(\n `PostgresFtsTextIndex.fromJSON: expected { kind: \"postgres-fts\" }, got ${JSON.stringify(\n state\n )}`\n );\n }\n // Symmetric with toJSON: if the snapshot carries a `table`, it must\n // match this instance's table. A snapshot from a different table\n // round-tripping through a mismatched instance would silently mask\n // bugs (the server-side data still lives in the original table).\n const snapshotTable = (state as { table?: unknown }).table;\n if (snapshotTable !== undefined && snapshotTable !== this.table) {\n throw new Error(\n `PostgresFtsTextIndex.fromJSON: snapshot table \"${String(snapshotTable)}\" does not match this index's table \"${this.table}\".`\n );\n }\n // No-op beyond validation: state is server-side. We accept the\n // snapshot for API parity.\n }\n\n /**\n * Mirror of `PostgresTabularStorage.acquireConnection`: prefer `pool.connect()`\n * when available (real `pg.Pool`); fall back to routing through `pool.query`\n * directly for single-connection wrappers (PGlite / PGLitePool). Failing\n * fast on anything else keeps BEGIN/COMMIT bracket sanity.\n *\n * Returns a pre-bound `boundQuery` rather than the raw `query` reference so\n * call sites invoking it as a plain function (`exec(sql, params)`) don't\n * lose the `pg.PoolClient` `this` binding.\n */\n private async acquireConnection(): Promise<{\n boundQuery: Pool[\"query\"];\n release: () => void;\n }> {\n const supportsConnect =\n typeof (this.pool as unknown as { connect?: unknown }).connect === \"function\";\n if (supportsConnect) {\n const client = await (\n this.pool as unknown as {\n connect: () => Promise<{ query: Pool[\"query\"]; release: () => void }>;\n }\n ).connect();\n return {\n boundQuery: client.query.bind(client) as Pool[\"query\"],\n release: () => client.release(),\n };\n }\n const poolAny = this.pool as unknown as {\n waitReady?: unknown;\n exec?: unknown;\n constructor?: { name?: string };\n };\n const ctorName = poolAny.constructor?.name;\n const looksLikePGlite = typeof poolAny.exec === \"function\" && poolAny.waitReady !== undefined;\n const looksLikePGLitePool = ctorName === \"PGLitePool\";\n if (!looksLikePGlite && !looksLikePGLitePool) {\n throw new Error(\n `PostgresFtsTextIndex requires a pg.Pool with connect() or a known single-connection wrapper (PGLitePool, PGlite); got ${\n ctorName ?? typeof this.pool\n }.`\n );\n }\n return {\n boundQuery: this.pool.query.bind(this.pool) as Pool[\"query\"],\n release: () => {},\n };\n }\n}\n"
6
6
  ],
7
- "mappings": "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",
7
+ "mappings": "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",
8
8
  "debugId": "D88659714AEA934764756E2164756E21",
9
9
  "names": []
10
10
  }
@@ -2,9 +2,9 @@
2
2
  "version": 3,
3
3
  "sources": ["../../src/text/PostgresFtsTextIndex.ts"],
4
4
  "sourcesContent": [
5
- "/**\n * @license\n * Copyright 2026 Steven Roussey <sroussey@gmail.com>\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport type { Pool } from \"@workglow/postgres/storage\";\nimport type {\n ITextIndex,\n TextFields,\n TextSearchOptions,\n TextSearchResult,\n} from \"@workglow/storage\";\n\n/**\n * Per-field weights for {@link PostgresFtsTextIndex}. Postgres FTS supports\n * exactly four weight buckets — A (heaviest), B, C, D — and that maps\n * naturally onto the hierarchy of chunk fields:\n *\n * - `text` → A: direct chunk content\n * - `doc_title` → B: navigational\n * - `sectionTitles` → B: navigational\n * - `summary` → C: condensed body\n * - `parentSummaries` → D: inherited context\n *\n * Fields outside this map are silently ignored at index time (no FTS bucket\n * to assign). Callers can override the mapping via\n * {@link PostgresFtsTextIndexOptions.fieldWeights}.\n */\nexport const DEFAULT_POSTGRES_FTS_FIELD_WEIGHTS: Readonly<\n Record<string, \"A\" | \"B\" | \"C\" | \"D\">\n> = {\n text: \"A\",\n doc_title: \"B\",\n sectionTitles: \"B\",\n summary: \"C\",\n parentSummaries: \"D\",\n};\n\nexport interface PostgresFtsTextIndexOptions {\n /** Postgres text-search configuration name. Defaults to `\"english\"`. */\n readonly tsvectorConfig?: string;\n /**\n * Per-field FTS weight bucket. See {@link DEFAULT_POSTGRES_FTS_FIELD_WEIGHTS}\n * for the default mapping. Fields not listed are ignored at index time.\n */\n readonly fieldWeights?: Readonly<Record<string, \"A\" | \"B\" | \"C\" | \"D\">>;\n}\n\n/**\n * Postgres-native full-text index for {@link KnowledgeBase.hybridSearch} /\n * {@link KnowledgeBase.textSearch}. Stores postings on a single side table\n * keyed by `chunk_id`:\n *\n * ```sql\n * CREATE TABLE <table> (\n * chunk_id TEXT PRIMARY KEY,\n * doc_id TEXT NOT NULL,\n * tsv TSVECTOR NOT NULL\n * );\n * CREATE INDEX <table>_tsv_idx ON <table> USING GIN (tsv);\n * CREATE INDEX <table>_doc_idx ON <table> (doc_id);\n * ```\n *\n * Scoring is `ts_rank_cd(tsv, plainto_tsquery(...))`; unbounded above and\n * non-negative, suitable for RRF fusion without normalisation.\n *\n * Lifecycle: {@link setupDatabase} must be called before any other method.\n * {@link beginRebuild}/{@link commitRebuild}/{@link abortRebuild} let\n * `KnowledgeBase.reindexText` wrap a full rebuild in a database\n * transaction; on `abort` the table is rolled back to its pre-`begin`\n * contents.\n *\n * State is durable on the database side; {@link toJSON} / {@link fromJSON}\n * are intentional no-ops (the table itself is the snapshot).\n */\nexport class PostgresFtsTextIndex implements ITextIndex {\n private readonly pool: Pool;\n private readonly table: string;\n private readonly options: PostgresFtsTextIndexOptions;\n\n // Tx state for beginRebuild/commitRebuild/abortRebuild. Acquired on\n // beginRebuild; released on commit/abort. When set, all DML methods\n // (`clear`, `add`, `remove`, `removeByDocument`) route through the\n // dedicated client so the rebuild is one BEGIN…COMMIT block.\n //\n // We hold a pre-bound `query` reference alongside the client. `pg.PoolClient`'s\n // `query` method depends on `this` being the client instance; if we returned\n // the raw method reference from the `exec` getter and call sites invoked it\n // as a plain function (which they do), `this` would be undefined and real\n // pg drivers would throw. Binding once at acquisition keeps every `exec(...)`\n // call safe regardless of how it's invoked.\n private rebuildClient:\n | {\n boundQuery: Pool[\"query\"];\n release: () => void;\n }\n | undefined;\n\n constructor(pool: Pool, table: string, options: PostgresFtsTextIndexOptions = {}) {\n this.pool = pool;\n this.table = table;\n this.options = options;\n }\n\n private get tsvectorConfig(): string {\n return this.options.tsvectorConfig ?? \"english\";\n }\n\n private get fieldWeights(): Readonly<Record<string, \"A\" | \"B\" | \"C\" | \"D\">> {\n return this.options.fieldWeights ?? DEFAULT_POSTGRES_FTS_FIELD_WEIGHTS;\n }\n\n /**\n * Routes a query to the rebuild-bound client when a rebuild is open.\n * Returns a function that is safe to call without preserving `this` —\n * the underlying `pg.PoolClient.query` is bound at acquisition time.\n */\n private get exec(): Pool[\"query\"] {\n if (this.rebuildClient) return this.rebuildClient.boundQuery;\n return this.pool.query.bind(this.pool) as Pool[\"query\"];\n }\n\n /**\n * Identifier-quote a table name. The constructor accepts an arbitrary\n * string, so we whitelist it (alphanumerics + underscores) and reject\n * anything else before splicing into DDL.\n */\n private get quotedTable(): string {\n if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(this.table)) {\n throw new Error(\n `PostgresFtsTextIndex: refusing to use unsafe table name ${JSON.stringify(this.table)}; ` +\n `expected /^[A-Za-z_][A-Za-z0-9_]*$/.`\n );\n }\n return `\"${this.table}\"`;\n }\n\n /**\n * Create the postings table + GIN index. Idempotent.\n */\n async setupDatabase(): Promise<void> {\n const table = this.quotedTable;\n const exec = this.exec as (sql: string) => Promise<unknown>;\n await exec(\n `CREATE TABLE IF NOT EXISTS ${table} (\n chunk_id TEXT PRIMARY KEY,\n doc_id TEXT NOT NULL,\n tsv TSVECTOR NOT NULL\n )`\n );\n await exec(`CREATE INDEX IF NOT EXISTS \"${this.table}_tsv_idx\" ON ${table} USING GIN (tsv)`);\n await exec(`CREATE INDEX IF NOT EXISTS \"${this.table}_doc_idx\" ON ${table} (doc_id)`);\n }\n\n async add(chunkId: string, docId: string, fields: TextFields): Promise<void> {\n const weights = this.fieldWeights;\n const cfg = this.tsvectorConfig;\n\n const params: (string | null)[] = [];\n const tsvParts: string[] = [];\n params.push(cfg); // $1\n\n let nextParam = 2;\n for (const [field, weight] of Object.entries(weights)) {\n const raw = fields[field];\n if (raw === undefined) continue;\n const text = Array.isArray(raw) ? raw.join(\" \") : (raw as string);\n // Skip empty/whitespace-only — they'd contribute a zero-length tsvector\n // and waste a parameter slot.\n if (typeof text !== \"string\" || text.trim().length === 0) continue;\n params.push(text);\n const pIdx = nextParam++;\n tsvParts.push(\n `setweight(to_tsvector($1::regconfig, coalesce($${pIdx}, '')), '${weight}')`\n );\n }\n\n const table = this.quotedTable;\n\n if (tsvParts.length === 0) {\n // Nothing indexable on this chunk — drop any prior postings so an\n // update that clears text is reflected. Mirror BM25Index.add's behaviour.\n await this.remove(chunkId);\n return;\n }\n\n params.push(chunkId);\n const chunkIdParam = `$${nextParam++}`;\n params.push(docId);\n const docIdParam = `$${nextParam++}`;\n\n const tsvExpr = tsvParts.join(\" || \");\n const sql = `INSERT INTO ${table} (chunk_id, doc_id, tsv)\n VALUES (${chunkIdParam}, ${docIdParam}, ${tsvExpr})\n ON CONFLICT (chunk_id) DO UPDATE\n SET doc_id = EXCLUDED.doc_id,\n tsv = EXCLUDED.tsv`;\n await (this.exec as (sql: string, params: unknown[]) => Promise<unknown>)(sql, params);\n }\n\n async remove(chunkId: string): Promise<void> {\n const sql = `DELETE FROM ${this.quotedTable} WHERE chunk_id = $1`;\n await (this.exec as (sql: string, params: unknown[]) => Promise<unknown>)(sql, [chunkId]);\n }\n\n async removeByDocument(docId: string): Promise<void> {\n const sql = `DELETE FROM ${this.quotedTable} WHERE doc_id = $1`;\n await (this.exec as (sql: string, params: unknown[]) => Promise<unknown>)(sql, [docId]);\n }\n\n async clear(): Promise<void> {\n // TRUNCATE is faster than DELETE and friendly to the GIN index; we hold\n // the rebuild's transaction (when present) so it still rolls back on abort.\n await (this.exec as (sql: string) => Promise<unknown>)(`TRUNCATE TABLE ${this.quotedTable}`);\n }\n\n async size(): Promise<number> {\n const res = (await (this.exec as (sql: string) => Promise<{ rows: Array<{ n: number }> }>)(\n `SELECT count(*)::int AS n FROM ${this.quotedTable}`\n )) as { rows: Array<{ n: number }> };\n return res.rows[0]?.n ?? 0;\n }\n\n async search(query: string, options: TextSearchOptions = {}): Promise<TextSearchResult[]> {\n const topK = options.topK ?? 10;\n if (typeof query !== \"string\" || query.trim().length === 0) return [];\n const sql = `\n SELECT chunk_id, ts_rank_cd(tsv, plainto_tsquery($1::regconfig, $2)) AS score\n FROM ${this.quotedTable}\n WHERE tsv @@ plainto_tsquery($1::regconfig, $2)\n ORDER BY score DESC\n LIMIT $3\n `;\n const res = (await (\n this.exec as (sql: string, params: unknown[]) => Promise<{\n rows: Array<{ chunk_id: string; score: number }>;\n }>\n )(sql, [this.tsvectorConfig, query, topK])) as {\n rows: Array<{ chunk_id: string; score: number }>;\n };\n return res.rows.map((r) => ({ chunkId: r.chunk_id, score: Number(r.score) }));\n }\n\n async beginRebuild(): Promise<void> {\n if (this.rebuildClient) {\n throw new Error(\n `PostgresFtsTextIndex.beginRebuild on table \"${this.table}\" called while a rebuild was already open. Call commitRebuild() or abortRebuild() first.`\n );\n }\n this.rebuildClient = await this.acquireConnection();\n try {\n await this.rebuildClient.boundQuery(`BEGIN`);\n } catch (err) {\n this.rebuildClient.release();\n this.rebuildClient = undefined;\n throw err;\n }\n }\n\n async commitRebuild(): Promise<void> {\n if (!this.rebuildClient) {\n throw new Error(\n `PostgresFtsTextIndex.commitRebuild on table \"${this.table}\" called without an open rebuild.`\n );\n }\n try {\n await this.rebuildClient.boundQuery(`COMMIT`);\n } finally {\n this.rebuildClient.release();\n this.rebuildClient = undefined;\n }\n }\n\n async abortRebuild(): Promise<void> {\n if (!this.rebuildClient) return;\n try {\n await this.rebuildClient.boundQuery(`ROLLBACK`);\n } finally {\n this.rebuildClient.release();\n this.rebuildClient = undefined;\n }\n }\n\n /**\n * State lives server-side. `toJSON`/`fromJSON` are no-ops, mirrored on\n * {@link PostgresFtsTextIndex} so the {@link ITextIndex} contract still\n * round-trips for callers that snapshot/restore the index. Callers\n * relying on a real serialisation should use {@link beginRebuild} /\n * {@link commitRebuild} / {@link abortRebuild} for atomic mutations\n * instead.\n */\n toJSON(): { kind: \"postgres-fts\"; table: string } {\n return { kind: \"postgres-fts\", table: this.table };\n }\n\n fromJSON(state: unknown): void {\n if (\n !state ||\n typeof state !== \"object\" ||\n (state as { kind?: unknown }).kind !== \"postgres-fts\"\n ) {\n throw new Error(\n `PostgresFtsTextIndex.fromJSON: expected { kind: \"postgres-fts\" }, got ${JSON.stringify(\n state\n )}`\n );\n }\n // Symmetric with toJSON: if the snapshot carries a `table`, it must\n // match this instance's table. A snapshot from a different table\n // round-tripping through a mismatched instance would silently mask\n // bugs (the server-side data still lives in the original table).\n const snapshotTable = (state as { table?: unknown }).table;\n if (snapshotTable !== undefined && snapshotTable !== this.table) {\n throw new Error(\n `PostgresFtsTextIndex.fromJSON: snapshot table \"${String(snapshotTable)}\" does not match this index's table \"${this.table}\".`\n );\n }\n // No-op beyond validation: state is server-side. We accept the\n // snapshot for API parity.\n }\n\n /**\n * Mirror of `PostgresTabularStorage.acquireConnection`: prefer `pool.connect()`\n * when available (real `pg.Pool`); fall back to routing through `pool.query`\n * directly for single-connection wrappers (PGlite / PGLitePool). Failing\n * fast on anything else keeps BEGIN/COMMIT bracket sanity.\n *\n * Returns a pre-bound `boundQuery` rather than the raw `query` reference so\n * call sites invoking it as a plain function (`exec(sql, params)`) don't\n * lose the `pg.PoolClient` `this` binding.\n */\n private async acquireConnection(): Promise<{\n boundQuery: Pool[\"query\"];\n release: () => void;\n }> {\n const supportsConnect =\n typeof (this.pool as unknown as { connect?: unknown }).connect === \"function\";\n if (supportsConnect) {\n const client = await (\n this.pool as unknown as {\n connect: () => Promise<{ query: Pool[\"query\"]; release: () => void }>;\n }\n ).connect();\n return {\n boundQuery: client.query.bind(client) as Pool[\"query\"],\n release: () => client.release(),\n };\n }\n const poolAny = this.pool as unknown as {\n waitReady?: unknown;\n exec?: unknown;\n constructor?: { name?: string };\n };\n const ctorName = poolAny.constructor?.name;\n const looksLikePGlite = typeof poolAny.exec === \"function\" && poolAny.waitReady !== undefined;\n const looksLikePGLitePool = ctorName === \"PGLitePool\";\n if (!looksLikePGlite && !looksLikePGLitePool) {\n throw new Error(\n `PostgresFtsTextIndex requires a pg.Pool with connect() or a known single-connection wrapper (PGLitePool, PGlite); got ${\n ctorName ?? typeof this.pool\n }.`\n );\n }\n return {\n boundQuery: this.pool.query.bind(this.pool) as Pool[\"query\"],\n release: () => {},\n };\n }\n}\n"
5
+ "/**\n * @license\n * Copyright 2026 Steven Roussey <sroussey@gmail.com>\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport type { Pool } from \"@workglow/postgres/storage\";\nimport type {\n ITextIndex,\n TextFields,\n TextSearchOptions,\n TextSearchResult,\n} from \"@workglow/storage\";\n\n/**\n * Per-field weights for {@link PostgresFtsTextIndex}. Postgres FTS supports\n * exactly four weight buckets — A (heaviest), B, C, D — and that maps\n * naturally onto the hierarchy of chunk fields:\n *\n * - `text` → A: direct chunk content\n * - `doc_title` → B: navigational\n * - `sectionTitles` → B: navigational\n * - `summary` → C: condensed body\n * - `parentSummaries` → D: inherited context\n *\n * Fields outside this map are silently ignored at index time (no FTS bucket\n * to assign). Callers can override the mapping via\n * {@link PostgresFtsTextIndexOptions.fieldWeights}.\n */\nexport const DEFAULT_POSTGRES_FTS_FIELD_WEIGHTS: Readonly<Record<string, \"A\" | \"B\" | \"C\" | \"D\">> = {\n text: \"A\",\n doc_title: \"B\",\n sectionTitles: \"B\",\n summary: \"C\",\n parentSummaries: \"D\",\n};\n\nexport interface PostgresFtsTextIndexOptions {\n /** Postgres text-search configuration name. Defaults to `\"english\"`. */\n readonly tsvectorConfig?: string;\n /**\n * Per-field FTS weight bucket. See {@link DEFAULT_POSTGRES_FTS_FIELD_WEIGHTS}\n * for the default mapping. Fields not listed are ignored at index time.\n */\n readonly fieldWeights?: Readonly<Record<string, \"A\" | \"B\" | \"C\" | \"D\">>;\n}\n\n/**\n * Postgres-native full-text index for {@link KnowledgeBase.hybridSearch} /\n * {@link KnowledgeBase.textSearch}. Stores postings on a single side table\n * keyed by `chunk_id`:\n *\n * ```sql\n * CREATE TABLE <table> (\n * chunk_id TEXT PRIMARY KEY,\n * doc_id TEXT NOT NULL,\n * tsv TSVECTOR NOT NULL\n * );\n * CREATE INDEX <table>_tsv_idx ON <table> USING GIN (tsv);\n * CREATE INDEX <table>_doc_idx ON <table> (doc_id);\n * ```\n *\n * Scoring is `ts_rank_cd(tsv, plainto_tsquery(...))`; unbounded above and\n * non-negative, suitable for RRF fusion without normalisation.\n *\n * Lifecycle: {@link setupDatabase} must be called before any other method.\n * {@link beginRebuild}/{@link commitRebuild}/{@link abortRebuild} let\n * `KnowledgeBase.reindexText` wrap a full rebuild in a database\n * transaction; on `abort` the table is rolled back to its pre-`begin`\n * contents.\n *\n * State is durable on the database side; {@link toJSON} / {@link fromJSON}\n * are intentional no-ops (the table itself is the snapshot).\n */\nexport class PostgresFtsTextIndex implements ITextIndex {\n private readonly pool: Pool;\n private readonly table: string;\n private readonly options: PostgresFtsTextIndexOptions;\n\n // Tx state for beginRebuild/commitRebuild/abortRebuild. Acquired on\n // beginRebuild; released on commit/abort. When set, all DML methods\n // (`clear`, `add`, `remove`, `removeByDocument`) route through the\n // dedicated client so the rebuild is one BEGIN…COMMIT block.\n //\n // We hold a pre-bound `query` reference alongside the client. `pg.PoolClient`'s\n // `query` method depends on `this` being the client instance; if we returned\n // the raw method reference from the `exec` getter and call sites invoked it\n // as a plain function (which they do), `this` would be undefined and real\n // pg drivers would throw. Binding once at acquisition keeps every `exec(...)`\n // call safe regardless of how it's invoked.\n private rebuildClient:\n | {\n boundQuery: Pool[\"query\"];\n release: () => void;\n }\n | undefined;\n\n constructor(pool: Pool, table: string, options: PostgresFtsTextIndexOptions = {}) {\n this.pool = pool;\n this.table = table;\n this.options = options;\n }\n\n private get tsvectorConfig(): string {\n return this.options.tsvectorConfig ?? \"english\";\n }\n\n private get fieldWeights(): Readonly<Record<string, \"A\" | \"B\" | \"C\" | \"D\">> {\n return this.options.fieldWeights ?? DEFAULT_POSTGRES_FTS_FIELD_WEIGHTS;\n }\n\n /**\n * Routes a query to the rebuild-bound client when a rebuild is open.\n * Returns a function that is safe to call without preserving `this` —\n * the underlying `pg.PoolClient.query` is bound at acquisition time.\n */\n private get exec(): Pool[\"query\"] {\n if (this.rebuildClient) return this.rebuildClient.boundQuery;\n return this.pool.query.bind(this.pool) as Pool[\"query\"];\n }\n\n /**\n * Identifier-quote a table name. The constructor accepts an arbitrary\n * string, so we whitelist it (alphanumerics + underscores) and reject\n * anything else before splicing into DDL.\n */\n private get quotedTable(): string {\n if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(this.table)) {\n throw new Error(\n `PostgresFtsTextIndex: refusing to use unsafe table name ${JSON.stringify(this.table)}; ` +\n `expected /^[A-Za-z_][A-Za-z0-9_]*$/.`\n );\n }\n return `\"${this.table}\"`;\n }\n\n /**\n * Create the postings table + GIN index. Idempotent.\n */\n async setupDatabase(): Promise<void> {\n const table = this.quotedTable;\n const exec = this.exec as (sql: string) => Promise<unknown>;\n await exec(\n `CREATE TABLE IF NOT EXISTS ${table} (\n chunk_id TEXT PRIMARY KEY,\n doc_id TEXT NOT NULL,\n tsv TSVECTOR NOT NULL\n )`\n );\n await exec(`CREATE INDEX IF NOT EXISTS \"${this.table}_tsv_idx\" ON ${table} USING GIN (tsv)`);\n await exec(`CREATE INDEX IF NOT EXISTS \"${this.table}_doc_idx\" ON ${table} (doc_id)`);\n }\n\n async add(chunkId: string, docId: string, fields: TextFields): Promise<void> {\n const weights = this.fieldWeights;\n const cfg = this.tsvectorConfig;\n\n const params: (string | null)[] = [];\n const tsvParts: string[] = [];\n params.push(cfg); // $1\n\n let nextParam = 2;\n for (const [field, weight] of Object.entries(weights)) {\n const raw = fields[field];\n if (raw === undefined) continue;\n const text = Array.isArray(raw) ? raw.join(\" \") : (raw as string);\n // Skip empty/whitespace-only — they'd contribute a zero-length tsvector\n // and waste a parameter slot.\n if (typeof text !== \"string\" || text.trim().length === 0) continue;\n params.push(text);\n const pIdx = nextParam++;\n tsvParts.push(`setweight(to_tsvector($1::regconfig, coalesce($${pIdx}, '')), '${weight}')`);\n }\n\n const table = this.quotedTable;\n\n if (tsvParts.length === 0) {\n // Nothing indexable on this chunk — drop any prior postings so an\n // update that clears text is reflected. Mirror BM25Index.add's behaviour.\n await this.remove(chunkId);\n return;\n }\n\n params.push(chunkId);\n const chunkIdParam = `$${nextParam++}`;\n params.push(docId);\n const docIdParam = `$${nextParam++}`;\n\n const tsvExpr = tsvParts.join(\" || \");\n const sql = `INSERT INTO ${table} (chunk_id, doc_id, tsv)\n VALUES (${chunkIdParam}, ${docIdParam}, ${tsvExpr})\n ON CONFLICT (chunk_id) DO UPDATE\n SET doc_id = EXCLUDED.doc_id,\n tsv = EXCLUDED.tsv`;\n await (this.exec as (sql: string, params: unknown[]) => Promise<unknown>)(sql, params);\n }\n\n async remove(chunkId: string): Promise<void> {\n const sql = `DELETE FROM ${this.quotedTable} WHERE chunk_id = $1`;\n await (this.exec as (sql: string, params: unknown[]) => Promise<unknown>)(sql, [chunkId]);\n }\n\n async removeByDocument(docId: string): Promise<void> {\n const sql = `DELETE FROM ${this.quotedTable} WHERE doc_id = $1`;\n await (this.exec as (sql: string, params: unknown[]) => Promise<unknown>)(sql, [docId]);\n }\n\n async clear(): Promise<void> {\n // TRUNCATE is faster than DELETE and friendly to the GIN index; we hold\n // the rebuild's transaction (when present) so it still rolls back on abort.\n await (this.exec as (sql: string) => Promise<unknown>)(`TRUNCATE TABLE ${this.quotedTable}`);\n }\n\n async size(): Promise<number> {\n const res = (await (this.exec as (sql: string) => Promise<{ rows: Array<{ n: number }> }>)(\n `SELECT count(*)::int AS n FROM ${this.quotedTable}`\n )) as { rows: Array<{ n: number }> };\n return res.rows[0]?.n ?? 0;\n }\n\n async search(query: string, options: TextSearchOptions = {}): Promise<TextSearchResult[]> {\n const topK = options.topK ?? 10;\n if (typeof query !== \"string\" || query.trim().length === 0) return [];\n const sql = `\n SELECT chunk_id, ts_rank_cd(tsv, plainto_tsquery($1::regconfig, $2)) AS score\n FROM ${this.quotedTable}\n WHERE tsv @@ plainto_tsquery($1::regconfig, $2)\n ORDER BY score DESC\n LIMIT $3\n `;\n const res = (await (\n this.exec as (\n sql: string,\n params: unknown[]\n ) => Promise<{\n rows: Array<{ chunk_id: string; score: number }>;\n }>\n )(sql, [this.tsvectorConfig, query, topK])) as {\n rows: Array<{ chunk_id: string; score: number }>;\n };\n return res.rows.map((r) => ({ chunkId: r.chunk_id, score: Number(r.score) }));\n }\n\n async beginRebuild(): Promise<void> {\n if (this.rebuildClient) {\n throw new Error(\n `PostgresFtsTextIndex.beginRebuild on table \"${this.table}\" called while a rebuild was already open. Call commitRebuild() or abortRebuild() first.`\n );\n }\n this.rebuildClient = await this.acquireConnection();\n try {\n await this.rebuildClient.boundQuery(`BEGIN`);\n } catch (err) {\n this.rebuildClient.release();\n this.rebuildClient = undefined;\n throw err;\n }\n }\n\n async commitRebuild(): Promise<void> {\n if (!this.rebuildClient) {\n throw new Error(\n `PostgresFtsTextIndex.commitRebuild on table \"${this.table}\" called without an open rebuild.`\n );\n }\n try {\n await this.rebuildClient.boundQuery(`COMMIT`);\n } finally {\n this.rebuildClient.release();\n this.rebuildClient = undefined;\n }\n }\n\n async abortRebuild(): Promise<void> {\n if (!this.rebuildClient) return;\n try {\n await this.rebuildClient.boundQuery(`ROLLBACK`);\n } finally {\n this.rebuildClient.release();\n this.rebuildClient = undefined;\n }\n }\n\n /**\n * State lives server-side. `toJSON`/`fromJSON` are no-ops, mirrored on\n * {@link PostgresFtsTextIndex} so the {@link ITextIndex} contract still\n * round-trips for callers that snapshot/restore the index. Callers\n * relying on a real serialisation should use {@link beginRebuild} /\n * {@link commitRebuild} / {@link abortRebuild} for atomic mutations\n * instead.\n */\n toJSON(): { kind: \"postgres-fts\"; table: string } {\n return { kind: \"postgres-fts\", table: this.table };\n }\n\n fromJSON(state: unknown): void {\n if (\n !state ||\n typeof state !== \"object\" ||\n (state as { kind?: unknown }).kind !== \"postgres-fts\"\n ) {\n throw new Error(\n `PostgresFtsTextIndex.fromJSON: expected { kind: \"postgres-fts\" }, got ${JSON.stringify(\n state\n )}`\n );\n }\n // Symmetric with toJSON: if the snapshot carries a `table`, it must\n // match this instance's table. A snapshot from a different table\n // round-tripping through a mismatched instance would silently mask\n // bugs (the server-side data still lives in the original table).\n const snapshotTable = (state as { table?: unknown }).table;\n if (snapshotTable !== undefined && snapshotTable !== this.table) {\n throw new Error(\n `PostgresFtsTextIndex.fromJSON: snapshot table \"${String(snapshotTable)}\" does not match this index's table \"${this.table}\".`\n );\n }\n // No-op beyond validation: state is server-side. We accept the\n // snapshot for API parity.\n }\n\n /**\n * Mirror of `PostgresTabularStorage.acquireConnection`: prefer `pool.connect()`\n * when available (real `pg.Pool`); fall back to routing through `pool.query`\n * directly for single-connection wrappers (PGlite / PGLitePool). Failing\n * fast on anything else keeps BEGIN/COMMIT bracket sanity.\n *\n * Returns a pre-bound `boundQuery` rather than the raw `query` reference so\n * call sites invoking it as a plain function (`exec(sql, params)`) don't\n * lose the `pg.PoolClient` `this` binding.\n */\n private async acquireConnection(): Promise<{\n boundQuery: Pool[\"query\"];\n release: () => void;\n }> {\n const supportsConnect =\n typeof (this.pool as unknown as { connect?: unknown }).connect === \"function\";\n if (supportsConnect) {\n const client = await (\n this.pool as unknown as {\n connect: () => Promise<{ query: Pool[\"query\"]; release: () => void }>;\n }\n ).connect();\n return {\n boundQuery: client.query.bind(client) as Pool[\"query\"],\n release: () => client.release(),\n };\n }\n const poolAny = this.pool as unknown as {\n waitReady?: unknown;\n exec?: unknown;\n constructor?: { name?: string };\n };\n const ctorName = poolAny.constructor?.name;\n const looksLikePGlite = typeof poolAny.exec === \"function\" && poolAny.waitReady !== undefined;\n const looksLikePGLitePool = ctorName === \"PGLitePool\";\n if (!looksLikePGlite && !looksLikePGLitePool) {\n throw new Error(\n `PostgresFtsTextIndex requires a pg.Pool with connect() or a known single-connection wrapper (PGLitePool, PGlite); got ${\n ctorName ?? typeof this.pool\n }.`\n );\n }\n return {\n boundQuery: this.pool.query.bind(this.pool) as Pool[\"query\"],\n release: () => {},\n };\n }\n}\n"
6
6
  ],
7
- "mappings": "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",
7
+ "mappings": "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",
8
8
  "debugId": "2500C3CA9F7BBBBF64756E2164756E21",
9
9
  "names": []
10
10
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@workglow/postgres",
3
3
  "type": "module",
4
- "version": "0.2.36",
4
+ "version": "0.2.37",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "https://github.com/workglow-dev/libs.git",
@@ -72,9 +72,9 @@
72
72
  "peerDependencies": {
73
73
  "@electric-sql/pglite": "^0.4.5",
74
74
  "pg": "^8.20.0",
75
- "@workglow/job-queue": "0.2.36",
76
- "@workglow/storage": "0.2.36",
77
- "@workglow/util": "0.2.36"
75
+ "@workglow/job-queue": "0.2.37",
76
+ "@workglow/storage": "0.2.37",
77
+ "@workglow/util": "0.2.37"
78
78
  },
79
79
  "peerDependenciesMeta": {
80
80
  "@electric-sql/pglite": {
@@ -95,9 +95,9 @@
95
95
  },
96
96
  "devDependencies": {
97
97
  "@types/pg": "^8.18.0",
98
- "@workglow/job-queue": "0.2.36",
99
- "@workglow/storage": "0.2.36",
100
- "@workglow/util": "0.2.36"
98
+ "@workglow/job-queue": "0.2.37",
99
+ "@workglow/storage": "0.2.37",
100
+ "@workglow/util": "0.2.37"
101
101
  },
102
102
  "files": [
103
103
  "dist",