@murumets-ee/db 0.110.0 → 0.112.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/extensions-CEyIHQA7.mjs +0 -1
- package/dist/index.d.mts +0 -1
- package/dist/index.mjs +0 -1
- package/dist/test-utils.d.mts +0 -1
- package/dist/test-utils.mjs +0 -1
- package/package.json +1 -1
- package/dist/extensions-CEyIHQA7.mjs.map +0 -1
- package/dist/index.d.mts.map +0 -1
- package/dist/index.mjs.map +0 -1
- package/dist/test-utils.d.mts.map +0 -1
- package/dist/test-utils.mjs.map +0 -1
|
@@ -1,2 +1 @@
|
|
|
1
1
|
const e=Symbol.for(`@murumets-ee/db:transaction-hooks`),t=Symbol.for(`@murumets-ee/db:transaction-hooks:tracked`);function n(){let t=globalThis,n=t[e];return n||(n={handles:new WeakMap},t[e]=n),n}function r(e){if(e[t])return e;let n=e.transaction.bind(e);return e.transaction=(e,t)=>o(e=>n(e,t),e),Object.defineProperty(e,t,{value:!0}),e}function i(e,t){if(e==null)return!1;let r=n().handles.get(e),i=r?.stack[r.stack.length-1];return i?(i.callbacks.push(t),!0):!1}function a(e){return e!=null&&n().handles.has(e)}async function o(e,t){let r={stack:[{callbacks:[]}]},i=await e(async e=>{s(e,r);try{return await t(e)}finally{n().handles.delete(e)}}),a=[...r.stack[0]?.callbacks??[]];for(let e of a)await c(e);return i}function s(e,t){n().handles.set(e,t);let r=e.transaction.bind(e);e.transaction=async e=>{let i=t.stack[t.stack.length-1];if(!i)throw Error(`transaction-hooks: a savepoint opened on a transaction with no open frame`);let a={callbacks:[]},o=await r(async r=>{t.stack.push(a),s(r,t);try{return await e(r)}finally{n().handles.delete(r);let e=t.stack.lastIndexOf(a);e!==-1&&t.stack.splice(e,1)}});return i.callbacks.push(...a.callbacks),o}}async function c(e){try{await e()}catch(e){let t=e instanceof Error?e.stack:void 0;process.emitWarning(`An on-commit callback threw after its transaction committed. The write stands, and the remaining callbacks still ran: `+(e instanceof Error?e.message:String(e)),{code:`TOOLKIT_ON_COMMIT_CALLBACK_FAILED`,...t!==void 0&&{detail:t}})}}const l=/^[a-z_][a-z0-9_]*$/;function u(e){if(!l.test(e))throw Error(`Invalid Postgres extension name: ${JSON.stringify(e)}. Must match ${l} (lowercase letters, digits, underscores; no quotes, whitespace, or punctuation).`)}function d(e){return u(e),`CREATE EXTENSION IF NOT EXISTS "${e}"`}export{i as a,a as i,u as n,r as o,d as r,l as t};
|
|
2
|
-
//# sourceMappingURL=extensions-CEyIHQA7.mjs.map
|
package/dist/index.d.mts
CHANGED
|
@@ -1646,4 +1646,3 @@ declare function onCommit(executor: object | null | undefined, callback: CommitC
|
|
|
1646
1646
|
declare function isTrackedTransaction(executor: object | null | undefined): boolean;
|
|
1647
1647
|
//#endregion
|
|
1648
1648
|
export { type AdvisoryLockKey, type AdvisoryLockResult, type AnyPgColumn, type ClaimOptions, type ColumnFactory, type ColumnKind, type ColumnOperators, type ColumnValue, type CommitCallback, DEFAULT_LIMIT, DEFAULT_READ_ONLY_STATEMENT_TIMEOUT_MS, DEPLOY_STEPS_LOCK, DEPLOY_STEPS_TABLE_NAME, DEPLOY_STEP_STATES, type DbConfig, type DefinedTable, type DependentCleanup, type DeployStepState, EXTENSION_NAME_PATTERN, type FindManyOptions, type IndexDefinition, type InsertRow, MAX_BATCH, MAX_LIMIT, MAX_STATEMENT_TIMEOUT_MS, type MigrateArgs, type MigrationLoader, type MigrationModule, type MigrationSource, type MigrationStatus, type OrderBySpec, type QueryStats, type Row, type SchemaRegistry, TOOLKIT_ADVISORY_LOCK_SPACE, type TableCleanupDescriptor, TableClient, TableClientError, type TableDefinition, type TableDependency, type UniqueDefinition, WhereBuilderError, type WhereClause, __clearDependentCleanups, assertValidExtensionName, column, createDbClient, createExtensionStatement, createListenClient, createReadOnlyClient, defineTable, deployStepsTable, getDependentCleanups, getMigrationStatus, getQueryStats, getTableCleanupDescriptors, getTablePartitionColumn, isNonEmptyWhere, isQueryCounterEnabled, isTrackedTransaction, onCommit, parseStatementTimeoutEnv, queryCountingLogger, registerDependentCleanup, rollbackMigrations, runMigrations, runWithQueryStats, schemaRegistry, tableRegistry, toolkitDeploySteps, trackTransactions, withAdvisoryLock };
|
|
1649
|
-
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.mjs
CHANGED
|
@@ -17,4 +17,3 @@ import{a as e,i as t,n,o as r,r as i,t as a}from"./extensions-CEyIHQA7.mjs";impo
|
|
|
17
17
|
DELETE FROM _toolkit_migrations
|
|
18
18
|
WHERE namespace = ${t.namespace} AND name = ${t.name}
|
|
19
19
|
`)}),console.log(` ✓ Rolled back ${t.namespace}/${t.name}`)}catch(e){throw console.error(` ✗ Failed to roll back ${t.namespace}/${t.name}:`,e),e}}console.log(`Successfully rolled back ${o.length} migration${o.length===1?``:`s`}`)}const Ze=new class{schemas=new Map;register(e,t){this.schemas.set(e,t)}get(e){return this.schemas.get(e)}all(){let e={};for(let[t,n]of this.schemas.entries())e[t]=n;return e}has(e){return this.schemas.has(e)}};export{Me as DEFAULT_LIMIT,D as DEFAULT_READ_ONLY_STATEMENT_TIMEOUT_MS,Te as DEPLOY_STEPS_LOCK,Z as DEPLOY_STEPS_TABLE_NAME,Ue as DEPLOY_STEP_STATES,a as EXTENSION_NAME_PATTERN,z as MAX_BATCH,R as MAX_LIMIT,O as MAX_STATEMENT_TIMEOUT_MS,A as TOOLKIT_ADVISORY_LOCK_SPACE,V as TableClient,B as TableClientError,M as WhereBuilderError,Fe as __clearDependentCleanups,n as assertValidExtensionName,U as column,Ce as createDbClient,i as createExtensionStatement,Ke as createListenClient,we as createReadOnlyClient,X as defineTable,We as deployStepsTable,Pe as getDependentCleanups,$ as getMigrationStatus,be as getQueryStats,Le as getTableCleanupDescriptors,ze as getTablePartitionColumn,F as isNonEmptyWhere,E as isQueryCounterEnabled,t as isTrackedTransaction,e as onCommit,Se as parseStatementTimeoutEnv,T as queryCountingLogger,Ne as registerDependentCleanup,Xe as rollbackMigrations,Ye as runMigrations,xe as runWithQueryStats,Ze as schemaRegistry,J as tableRegistry,Ge as toolkitDeploySteps,r as trackTransactions,Ee as withAdvisoryLock};
|
|
20
|
-
//# sourceMappingURL=index.mjs.map
|
package/dist/test-utils.d.mts
CHANGED
|
@@ -102,4 +102,3 @@ declare function createInMemoryTableClient<Row extends Record<string, unknown>>(
|
|
|
102
102
|
}): InMemoryTableClient<Row>;
|
|
103
103
|
//#endregion
|
|
104
104
|
export { InMemoryOrderBy, InMemoryTableClient, PushOptions, TestDb, createInMemoryTableClient, createTestDb };
|
|
105
|
-
//# sourceMappingURL=test-utils.d.mts.map
|
package/dist/test-utils.mjs
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
1
|
import{o as e,r as t}from"./extensions-CEyIHQA7.mjs";import{drizzle as n}from"drizzle-orm/postgres-js";import r from"postgres";import{sql as i}from"drizzle-orm";async function a(){if(process.env.NODE_ENV===`production`)throw Error(`createTestDb() is not allowed when NODE_ENV=production — it issues CREATE/DROP SCHEMA and is intended for tests only`);let a=process.env.DATABASE_TEST_URL||process.env.DATABASE_URL;if(!a)throw Error(`DATABASE_TEST_URL (or DATABASE_URL) env var is required for integration tests`);let o=`test_${crypto.randomUUID().replaceAll(`-`,``).slice(0,12)}`,s=r(a,{max:1});try{await s.unsafe(`CREATE SCHEMA "${o}"`)}finally{await s.end()}let c=r(a,{max:3,idle_timeout:10,connection:{search_path:`"${o}",public`}}),u=e(n(c));return{db:u,schema:o,async push(e,n){if(n?.extensions&&n.extensions.length>0){let e=r(a,{max:1,connection:{search_path:`public`}});try{for(let r of n.extensions)await e.unsafe(t(r))}finally{await e.end()}}let{createRequire:i}=await import(`node:module`),{pushSchema:o}=i(import.meta.url)(`drizzle-kit/api`);await(await o(e,l(u))).apply()},async cleanup(){await u.execute(i.raw(`DROP SCHEMA IF EXISTS "${o}" CASCADE`)),await c.end()}}}function o(e,t){return`eq`in t?e===t.eq:`ne`in t?e!==t.ne:`in`in t?t.in.includes(e):`notIn`in t?!t.notIn.includes(e):`lt`in t?e<t.lt:`lte`in t?e<=t.lte:`gt`in t?e>t.gt:`gte`in t?e>=t.gte:`isNull`in t?e==null:`isNotNull`in t?e!=null:!1}function s(e,t){for(let[n,r]of Object.entries(t)){if(n===`$or`){if(!r.some(t=>s(e,t)))return!1;continue}if(n===`$and`){if(!r.every(t=>s(e,t)))return!1;continue}let t=e[n];if(r===null){if(t!=null)return!1;continue}if(typeof r==`object`&&r&&!(r instanceof Date)){if(!o(t,r))return!1;continue}if(t!==r)return!1}return!0}function c(e={}){let t=e.idField??`id`,n=1,r=e.idGenerator??(()=>`row-${n++}`),i={rows:e.initial?[...e.initial]:[]};return{get rows(){return i.rows},set rows(e){i.rows=e},reset(e){i.rows=e?[...e]:[],n=1},async findOne(e){return i.rows.find(t=>s(t,e))??null},async findMany(e){let t=e?.where?i.rows.filter(t=>s(t,e.where)):[...i.rows];e?.orderBy&&e.orderBy.length>0&&(t=[...t].sort((t,n)=>{for(let r of e.orderBy??[]){let e=t[r.column],i=n[r.column],a=r.dir===`desc`?-1:1;if(e instanceof Date&&i instanceof Date){let t=(e.getTime()-i.getTime())*a;if(t!==0)return t}else if(typeof e==`number`&&typeof i==`number`){let t=(e-i)*a;if(t!==0)return t}else if(typeof e==`string`&&typeof i==`string`){let t=e.localeCompare(i)*a;if(t!==0)return t}}return 0}));let n=e?.offset??0,r=e?.limit;return n>0&&(t=t.slice(n)),r!==void 0&&r>=0&&(t=t.slice(0,r)),t},async aggregate(e){let t=e?.where?i.rows.filter(t=>s(t,e.where)):i.rows,n={};for(let[r,i]of Object.entries(e.select))if(i.fn===`count`)n[r]=t.length;else if(i.fn===`max`&&i.column){let e=null;for(let n of t){let t=n[i.column];typeof t==`number`&&(e===null||t>e)&&(e=t)}n[r]=e}else if(i.fn===`min`&&i.column){let e=null;for(let n of t){let t=n[i.column];typeof t==`number`&&(e===null||t<e)&&(e=t)}n[r]=e}else if(i.fn===`sum`&&i.column){let e=0;for(let n of t){let t=n[i.column];typeof t==`number`&&(e+=t)}n[r]=e}return[n]},async exists(e){return i.rows.some(t=>s(t,e))},async count(e){return e?i.rows.filter(t=>s(t,e)).length:i.rows.length},async insert(e){let n={[t]:r(),...e};return i.rows.push(n),n},async update(e,t){let n=i.rows.filter(t=>s(t,e));if(n.length===0)return null;let r=n[0];return Object.assign(r,t),r},async upsert(e,n){let a=Array.isArray(n.target)?n.target:[n.target],o={};for(let t of a)o[t]=e[t];let c={};for(let t of n.mergeJsonb??[]){let n=e[t];if(typeof n!=`object`||!n||Array.isArray(n))throw Error(`upsert mergeJsonb: '${t}' must be a JSON object to merge`);c[t]=n}let l=i.rows.find(e=>s(e,o));if(l){let t={...n.set??e};for(let[e,n]of Object.entries(c)){let r=l[e];t[e]={...typeof r==`object`&&r&&!Array.isArray(r)?r:{},...n}}return Object.assign(l,t),l}let u={[t]:r(),...e};return i.rows.push(u),u},async delete(e){let t=i.rows.findIndex(t=>s(t,e));if(t===-1)return null;let[n]=i.rows.splice(t,1);return n??null},async deleteMany(e){let t=i.rows.filter(t=>s(t,e));return i.rows=i.rows.filter(e=>!t.includes(e)),t},async tryClaim(e,n){let a=Array.isArray(n.target)?n.target:[n.target],o={};for(let t of a)o[t]=e[t];let c=i.rows.find(e=>s(e,o));if(!c){let n={[t]:r(),...e};return i.rows.push(n),{acquired:!0,row:n}}return s(c,n.replaceWhen)?(Object.assign(c,n.set??e),{acquired:!0,row:c}):{acquired:!1,row:c}}}}function l(e){return new Proxy(e,{get(e,t,n){return t===`execute`?async(...t)=>{let n=await e.execute(...t);return Array.isArray(n)&&!(`rows`in n)&&Object.defineProperty(n,"rows",{value:[...n],enumerable:!1}),n}:Reflect.get(e,t,n)}})}export{c as createInMemoryTableClient,a as createTestDb};
|
|
2
|
-
//# sourceMappingURL=test-utils.mjs.map
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"extensions-CEyIHQA7.mjs","names":["marked"],"sources":["../src/transaction-hooks.ts","../src/extensions.ts"],"sourcesContent":["/**\n * Per-transaction on-commit callbacks — Django's `transaction.on_commit`, for\n * drizzle's postgres-js driver (rent-go-live PR13, F023).\n *\n * ## Why this exists\n *\n * Some work is only correct once a write is TRUE: telling another process its\n * cache is stale, maintaining an index that other connections read. Inside a\n * transaction the CALLER owns, the code making the write cannot know whether it\n * will commit — and drizzle exposes no after-commit hook (postgres-js offers\n * `begin` and `savepoint`, nothing that runs after COMMIT). Firing immediately\n * announces writes that a later statement in the same transaction may still\n * roll back, which is exactly what F023 found `AdminClient` doing.\n *\n * ## Semantics — Django's, deliberately\n *\n * - A callback registered on a tracked transaction runs after that transaction\n * COMMITS and before `db.transaction()` resolves, in registration order, each\n * awaited — so a caller's next statement cannot race its own side effects.\n * - A rollback discards every callback the transaction registered, and \"a\n * rollback\" is whatever postgres-js decides, not only a throw out of the body:\n * `tx.rollback()`, a COMMIT that fails (a deferred constraint is checked\n * there), and a body that CAUGHT a failed statement and returned normally —\n * postgres-js records every statement's error on its transaction and rolls it\n * back once the body returns.\n * - A savepoint (`tx.transaction(...)`) is a frame of its own. Its callbacks join\n * the enclosing frame only once its savepoint scope has settled without a\n * ROLLBACK TO — never merely because its body returned, since the same\n * caught-statement rule can still roll it back after that. (postgres-js never\n * sends RELEASE SAVEPOINT; a completed savepoint simply stays part of its\n * parent until COMMIT.) Rolled back, its callbacks are discarded and the\n * enclosing frame's are kept.\n * - Registration binds to the innermost OPEN frame, whichever handle it arrives\n * through. One connection runs every statement, so a write made through the\n * outer `tx` while a savepoint is open is inside that savepoint and rolls back\n * with it — its callback must too.\n * - A callback that throws cannot fail the transaction: it has already\n * committed, and reporting a committed write as failed invites a retry of work\n * that is done. It is reported through `process.emitWarning` (this package is\n * logger-free, as `table/registry.ts` notes) and the remaining callbacks still\n * run. Callers that own a logger should catch and log themselves; this is the\n * last resort, not the reporting channel.\n * - `onCommit` on anything that is not an OPEN tracked transaction returns\n * `false` and does nothing. Django runs the callback immediately there;\n * returning instead leaves \"no transaction\" to the caller, whose immediate\n * path may rightly want errors to propagate.\n *\n * ## What running later means for a caller\n *\n * - A callback runs in the async context of the `db.transaction()` CALL, not of\n * the statement that registered it. A body that switches `AsyncLocalStorage`\n * context inside the transaction (elevating with `runWithContextAsync`, say)\n * must not expect its callbacks to inherit the switch.\n * - A connection lost DURING COMMIT rejects `db.transaction()` and discards the\n * callbacks, although the COMMIT may have reached the server. Django has the\n * same gap; it is the one case where \"discarded\" can mean \"not run for a write\n * that happened\".\n * - A transaction holds its callbacks, and whatever they close over, until it\n * commits: one closure per registration. That is the work the caller would\n * have done inline, moved later — it adds no fan-out of its own.\n *\n * ## Where tracking is installed\n *\n * `createDbClient` — so every `app.db.readWrite` built by `createApp`, and the\n * CLI's — and `createTestDb`, so integration suites run the production shape. A\n * client built with a bare `drizzle(...)` is untracked and `onCommit` refuses\n * its transactions.\n *\n * ## Why the registry lives on `globalThis`\n *\n * Tracking is installed through one entry (`createDbClient` in `.`,\n * `createTestDb` in `./test-utils`) and read through another (`onCommit`,\n * imported by `@murumets-ee/entity`). Our own build emits this module once, into\n * a chunk both entries share — but a consumer's bundler is free to duplicate it,\n * and Turbopack's dev module duplication is a failure mode this repo has already\n * shipped. A module-local registry would then exist twice, and a transaction\n * tracked by one copy would be invisible to `onCommit` from the other: silently,\n * with every callback refused and every seam falling back to firing in-tx. Same\n * `Symbol.for` slot shape as `@murumets-ee/queue`'s enqueue-on-commit slot and\n * `@murumets-ee/entity`'s write-observer slot.\n *\n * ## Not supported\n *\n * Concurrent savepoints on one transaction (`Promise.all` over\n * `tx.transaction(...)`). They share a connection, their SAVEPOINT and ROLLBACK TO\n * statements interleave, and a frame stack cannot know which callback belongs to\n * which; drizzle gives that shape no defined meaning either. Nor a savepoint the\n * body does not await: its callbacks merge whenever its promise settles, and if\n * that is after the transaction committed, they never run.\n */\n\nimport type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'\n\n/** Work to run once the transaction it was registered on has committed. */\nexport type CommitCallback = () => void | Promise<void>\n\ntype DatabaseTransaction = PostgresJsDatabase['transaction']\n/** The handle drizzle passes to a transaction body — outer or savepoint. */\ntype TransactionHandle = Parameters<Parameters<DatabaseTransaction>[0]>[0]\ntype TransactionConfig = Parameters<DatabaseTransaction>[1]\n\n/** Callbacks registered while one transaction or savepoint was the innermost open one. */\ninterface Frame {\n callbacks: CommitCallback[]\n}\n\n/**\n * One outermost transaction. `stack[0]` is the transaction's own frame; each\n * open savepoint pushes one above it.\n */\ninterface Root {\n stack: Frame[]\n}\n\ninterface HookState {\n /** Every OPEN handle (outer or savepoint) → the root transaction it belongs to. */\n handles: WeakMap<object, Root>\n}\n\nconst STATE_KEY = Symbol.for('@murumets-ee/db:transaction-hooks')\nconst TRACKED_KEY = Symbol.for('@murumets-ee/db:transaction-hooks:tracked')\n\nfunction hookState(): HookState {\n const slot = globalThis as { [STATE_KEY]?: HookState }\n let state = slot[STATE_KEY]\n if (!state) {\n state = { handles: new WeakMap() }\n slot[STATE_KEY] = state\n }\n return state\n}\n\n/**\n * Install on-commit tracking on a read-write client, in place, and return it.\n *\n * Idempotent: a client that is already tracked is returned untouched, so a\n * callback never runs twice because two layers each installed tracking.\n */\nexport function trackTransactions<TDb extends PostgresJsDatabase>(db: TDb): TDb {\n const marked = db as TDb & { [TRACKED_KEY]?: true }\n if (marked[TRACKED_KEY]) return db\n\n const begin = db.transaction.bind(db)\n db.transaction = <T>(\n run: (tx: TransactionHandle) => Promise<T>,\n config?: TransactionConfig,\n ): Promise<T> => runOutermost((body) => begin(body, config), run)\n\n Object.defineProperty(db, TRACKED_KEY, { value: true })\n return db\n}\n\n/**\n * Register `callback` to run after the transaction `executor` belongs to\n * commits. Returns `true` when it was registered, and `false` — having done\n * nothing — when `executor` is not an open tracked transaction, in which case\n * the caller decides whether to run the work now.\n */\nexport function onCommit(executor: object | null | undefined, callback: CommitCallback): boolean {\n if (executor === null || executor === undefined) return false\n const root = hookState().handles.get(executor)\n const frame = root?.stack[root.stack.length - 1]\n if (!frame) return false\n frame.callbacks.push(callback)\n return true\n}\n\n/** Whether `executor` is a transaction handle that is open and tracked right now. */\nexport function isTrackedTransaction(executor: object | null | undefined): boolean {\n return executor !== null && executor !== undefined && hookState().handles.has(executor)\n}\n\nasync function runOutermost<T>(\n begin: (body: (tx: TransactionHandle) => Promise<T>) => Promise<T>,\n run: (tx: TransactionHandle) => Promise<T>,\n): Promise<T> {\n const root: Root = { stack: [{ callbacks: [] }] }\n\n // Any rollback — a throw, `tx.rollback()`, a caught statement error, a failed\n // COMMIT — rejects here, before a callback is read, and the root is dropped\n // with everything registered on it.\n const result = await begin(async (tx) => {\n adopt(tx, root)\n try {\n return await run(tx)\n } finally {\n // No registrations once the body is done: it can no longer write.\n hookState().handles.delete(tx)\n }\n })\n\n // COMMITTED. A snapshot, so a straggler merging in while these run — an\n // unawaited savepoint settling late — is not picked up mid-iteration.\n const callbacks = [...(root.stack[0]?.callbacks ?? [])]\n for (const callback of callbacks) {\n await runCallback(callback)\n }\n return result\n}\n\n/** Track an open handle, and make its savepoints keep frames of their own. */\nfunction adopt(tx: TransactionHandle, root: Root): void {\n hookState().handles.set(tx, root)\n\n const savepoint = tx.transaction.bind(tx)\n tx.transaction = async <T>(run: (sp: TransactionHandle) => Promise<T>): Promise<T> => {\n const enclosing = root.stack[root.stack.length - 1]\n if (!enclosing) {\n throw new Error('transaction-hooks: a savepoint opened on a transaction with no open frame')\n }\n const frame: Frame = { callbacks: [] }\n\n // Rejects when postgres-js rolls the savepoint back — including for a failed\n // statement the body caught, which it only acts on AFTER the body returns —\n // and then `frame` is simply dropped.\n const value = await savepoint(async (sp) => {\n root.stack.push(frame)\n adopt(sp, root)\n try {\n return await run(sp)\n } finally {\n hookState().handles.delete(sp)\n const index = root.stack.lastIndexOf(frame)\n if (index !== -1) root.stack.splice(index, 1)\n }\n })\n\n // Settled without ROLLBACK TO — only now do these callbacks belong to the\n // enclosing frame.\n enclosing.callbacks.push(...frame.callbacks)\n return value\n }\n}\n\nasync function runCallback(callback: CommitCallback): Promise<void> {\n try {\n await callback()\n } catch (error) {\n const stack = error instanceof Error ? error.stack : undefined\n process.emitWarning(\n 'An on-commit callback threw after its transaction committed. The write stands, ' +\n 'and the remaining callbacks still ran: ' +\n (error instanceof Error ? error.message : String(error)),\n {\n code: 'TOOLKIT_ON_COMMIT_CALLBACK_FAILED',\n ...(stack !== undefined && { detail: stack }),\n },\n )\n }\n}\n","/**\n * Postgres extension helpers — the single source of truth for how the toolkit\n * enables a Postgres extension.\n *\n * Declarative source: plugins declare `server.extensions: ['btree_gist']`\n * (see `@murumets-ee/core`'s `PluginServer`). Consumers — `lumi migrate`\n * (emits into the generated migration) and `@murumets-ee/db`'s `createTestDb`\n * (enables before `pushSchema`) — both route through the helpers here so the\n * validation rule and the emitted SQL are identical everywhere (the migrate\n * dedup-scan relies on the canonical statement being byte-for-byte stable).\n *\n * Why validate the name: `CREATE EXTENSION` cannot parameterize an identifier,\n * so the extension name is interpolated as a literal. Names come from plugin\n * code (not request input), but per the \"validate at the boundary\" rule we\n * still whitelist the identifier shape so a typo or a hostile plugin can't\n * inject SQL through the one place a raw identifier reaches a DDL string.\n */\n\n/**\n * Valid Postgres extension identifier: a lowercase unquoted identifier —\n * leading letter/underscore, then letters/digits/underscores. Covers every\n * real extension name (`btree_gist`, `pg_trgm`, `uuid-ossp` is the lone\n * hyphenated outlier and is intentionally rejected here; declare it quoted via\n * a custom migration if ever needed). Anchored, so no embedded whitespace,\n * quotes, semicolons, or comment markers can slip through.\n */\nexport const EXTENSION_NAME_PATTERN = /^[a-z_][a-z0-9_]*$/\n\n/**\n * Throw if `name` is not a safe, unquoted Postgres extension identifier.\n * Kept separate from {@link createExtensionStatement} so callers that only\n * want to validate (e.g. a config linter) don't have to build SQL.\n */\nexport function assertValidExtensionName(name: string): void {\n if (!EXTENSION_NAME_PATTERN.test(name)) {\n throw new Error(\n `Invalid Postgres extension name: ${JSON.stringify(name)}. ` +\n `Must match ${EXTENSION_NAME_PATTERN} (lowercase letters, digits, underscores; ` +\n 'no quotes, whitespace, or punctuation).',\n )\n }\n}\n\n/**\n * The canonical, idempotent statement that enables one extension.\n * Validates first, then double-quotes the (already-safe) identifier.\n *\n * This exact string is what `lumi migrate` writes into migration files AND\n * what it scans prior migrations for to avoid re-emitting — do not change the\n * shape without updating the dedup scan in `packages/cli/src/commands/migrate.ts`.\n */\nexport function createExtensionStatement(name: string): string {\n assertValidExtensionName(name)\n return `CREATE EXTENSION IF NOT EXISTS \"${name}\"`\n}\n"],"mappings":"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"}
|
package/dist/index.d.mts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/client.ts","../src/deploy/advisory-lock.ts","../src/deploy/deploy-steps-table.ts","../src/extensions.ts","../src/listen.ts","../src/migrate.ts","../src/query-counter.ts","../src/schema-registry.ts","../src/table/types.ts","../src/table/client.ts","../src/table/columns.ts","../src/table/define.ts","../src/table/dependency-registry.ts","../src/table/registry.ts","../src/table/where-builder.ts","../src/transaction-hooks.ts"],"mappings":"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"}
|
package/dist/index.mjs.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":["andFn","countFn","drizzlePrimaryKey","drizzleUniqueIndex","drizzleUnique","drizzleIndex"],"sources":["../src/query-counter.ts","../src/client.ts","../src/deploy/advisory-lock.ts","../src/table/where-builder.ts","../src/table/client.ts","../src/table/columns.ts","../src/table/dependency-registry.ts","../src/table/registry.ts","../src/table/define.ts","../src/deploy/deploy-steps-table.ts","../src/listen.ts","../src/migrate.ts","../src/schema-registry.ts"],"sourcesContent":["import { AsyncLocalStorage } from 'node:async_hooks'\nimport type { Logger } from 'drizzle-orm/logger'\n\n/**\n * Per-request DB query counter — opt-in, off by default.\n *\n * Wired into the Drizzle client when `LUMI_DB_QUERY_COUNTER=1`. The harness\n * (apps/perf-harness) wraps each HTTP request in `runWithQueryStats` and emits\n * the count as an `x-db-query-count` response header, so k6 scenarios can\n * assert per-endpoint query budgets like:\n *\n * checks: { 'findById ≤ 2 queries': (r) => r.headers['X-Db-Query-Count'] <= '2' }\n *\n * No-op for any code path that does not enter `runWithQueryStats` — the ALS\n * store is undefined and `logQuery` returns immediately.\n */\n\nexport interface QueryStats {\n count: number\n}\n\nconst queryStatsAls = new AsyncLocalStorage<QueryStats>()\n\nexport function getQueryStats(): QueryStats | undefined {\n return queryStatsAls.getStore()\n}\n\nexport function runWithQueryStats<T>(fn: (stats: QueryStats) => Promise<T>): Promise<T> {\n const stats: QueryStats = { count: 0 }\n return queryStatsAls.run(stats, () => fn(stats))\n}\n\nclass QueryCountingLogger implements Logger {\n logQuery(_query: string, _params: unknown[]): void {\n const stats = queryStatsAls.getStore()\n if (stats) stats.count++\n }\n}\n\nexport const queryCountingLogger: Logger = new QueryCountingLogger()\n\nexport function isQueryCounterEnabled(): boolean {\n return process.env.LUMI_DB_QUERY_COUNTER === '1'\n}\n","import type { Logger } from 'drizzle-orm/logger'\nimport { drizzle, type PostgresJsDatabase } from 'drizzle-orm/postgres-js'\nimport postgres from 'postgres'\nimport { isQueryCounterEnabled, queryCountingLogger } from './query-counter.js'\nimport { trackTransactions } from './transaction-hooks.js'\n\nexport interface DbConfig {\n url: string\n readOnlyUrl?: string\n poolMin?: number\n poolMax?: number\n /**\n * Per-statement ceiling in milliseconds for the READ-ONLY pool only\n * (default {@link DEFAULT_READ_ONLY_STATEMENT_TIMEOUT_MS}). Must be a\n * positive integer; `0` and negatives are rejected rather than treated as\n * \"disabled\" — see {@link createReadOnlyClient}.\n *\n * The read-write pool is deliberately not covered: it serves migrations,\n * bulk imports and admin writes, where a statement running for minutes is\n * legitimate.\n */\n readOnlyStatementTimeoutMs?: number\n}\n\n/**\n * Default read-only `statement_timeout`, in milliseconds.\n *\n * Calibrated against the one perf-harness scenario that actually exercises\n * this pool: `locale-fallback` drives real content-api reads (which read\n * through `app.db.readOnly`) at ~540 rps with p95 ≈ 55 ms, flat from 1k to\n * 1M rows. 5 s sits ~100× above that.\n *\n * Deliberately NOT cited as justification: the harness's pathological\n * `tickets-per-agent` scenario (11.6 s p95 at 1M rows). It reads through\n * `AdminClient.aggregate` on the READ-WRITE pool, so it cannot reach the pool\n * being capped here — and its p50 at 1M is 5016 ms, i.e. this ceiling would\n * sit at its median rather than safely above it. The harness contains no\n * measurement of a >5 s read-ONLY-pool query, so this default rests on a\n * single scenario plus the ~100× margin, not on a measured bad case.\n */\nexport const DEFAULT_READ_ONLY_STATEMENT_TIMEOUT_MS = 5_000\n\n/**\n * Postgres' own upper bound for `statement_timeout` (a signed 32-bit\n * millisecond count). Anything larger is rejected with SQLSTATE 22023 — but\n * postgres-js connects LAZILY, so an out-of-range value would let the process\n * boot green and then fail EVERY read at connection time, with an error\n * pointing at the connection rather than at config. Rejecting it up front\n * keeps the failure attached to its cause.\n */\nexport const MAX_STATEMENT_TIMEOUT_MS = 2_147_483_647\n\n/**\n * Parse a `readOnlyStatementTimeoutMs` override out of an environment string,\n * returning `undefined` (i.e. \"use the default\") for anything unusable.\n *\n * This exists so an operator can raise the ceiling on a deployment whose read\n * path is legitimately slower WITHOUT a rebuild and redeploy. That makes it an\n * incident-time tool, which forces two properties:\n *\n * - **It must never throw.** `lumi.config.ts` is evaluated at BUILD time, and a\n * throw there fails the build rather than the request. Passing an unvalidated\n * value straight through would be worse still: `createReadOnlyClient` rejects\n * out-of-range values, so a typo'd env var would abort app construction — an\n * escape hatch that can itself take the process down is not an escape hatch.\n * - **It must not fail silently either.** Someone raising the ceiling mid-incident\n * and getting the 5 s default back with no signal would conclude the knob is\n * broken. Hence the warning: `console` rather than `@murumets-ee/logging`\n * because config is evaluated before any logger exists.\n */\nexport function parseStatementTimeoutEnv(raw: string | undefined): number | undefined {\n if (raw === undefined || raw.trim() === '') return undefined\n\n // Digits only, BEFORE `Number()`. `Number()` accepts `0x`/`0o`/`0b` literals\n // and exponent notation, so `DB_RO_STATEMENT_TIMEOUT_MS=0x10` would parse to\n // 16 — a 16-millisecond ceiling that cancels essentially every read, applied\n // silently because 16 is a perfectly valid integer in range. That is the one\n // input that fails in the dangerous direction without a warning, so it is\n // screened out here rather than by the range check below.\n const trimmed = raw.trim()\n const parsed = /^\\d+$/.test(trimmed) ? Number(trimmed) : Number.NaN\n if (!Number.isInteger(parsed) || parsed <= 0 || parsed > MAX_STATEMENT_TIMEOUT_MS) {\n console.warn(\n `[db] Ignoring invalid read-only statement timeout override ${JSON.stringify(raw)}: ` +\n `expected an integer between 1 and ${MAX_STATEMENT_TIMEOUT_MS} milliseconds. ` +\n `Falling back to the ${DEFAULT_READ_ONLY_STATEMENT_TIMEOUT_MS} ms default.`,\n )\n return undefined\n }\n return parsed\n}\n\nfunction drizzleConfig(): { logger: Logger } | undefined {\n return isQueryCounterEnabled() ? { logger: queryCountingLogger } : undefined\n}\n\n/**\n * Create a full read-write database client.\n *\n * Its transactions are tracked (`trackTransactions`), so `onCommit` works on\n * every `tx` it hands out. This is where `app.db.readWrite` and the CLI's\n * client come from, which is what lets `AdminClient` hold its post-commit\n * seams until a caller's transaction actually commits — with no change at any\n * call site that opens one.\n */\nexport function createDbClient(config: DbConfig): PostgresJsDatabase {\n const sql = postgres(config.url, {\n max: config.poolMax || 10,\n idle_timeout: 20,\n max_lifetime: 60 * 30,\n })\n\n const cfg = drizzleConfig()\n return trackTransactions(cfg ? drizzle(sql, cfg) : drizzle(sql))\n}\n\n/**\n * Create a read-only database client.\n *\n * Sets `default_transaction_read_only = on` as a startup parameter on every\n * connection in the pool, so any implicit transaction issued through this\n * client is read-only at the PostgreSQL level. This is the defense-in-depth\n * layer beneath the application's own routing of writes through `AdminClient`\n * — even if a write somehow reaches a read-only client, Postgres rejects it.\n *\n * Also sets `statement_timeout` so a single slow read cannot hold a pool\n * connection indefinitely. The pool defaults to 10 connections, so ten\n * concurrently-slow queries exhaust it and every later read blocks. Rate\n * limiting cannot close this gap: a limiter bounds how many requests ARRIVE,\n * this bounds how long each one HOLDS a connection.\n *\n * Implementation note: postgres-js's `connection` config object is sent in\n * the PostgreSQL startup packet for every new pool connection, so both GUCs\n * are applied uniformly across the whole pool — not just whichever connection\n * happened to run a one-off `SET` statement.\n *\n * @throws if `readOnlyStatementTimeoutMs` is not a positive integer. `0` is\n * rejected rather than accepted as Postgres' \"disabled\" sentinel, for two\n * reasons: this is an availability control, so a typo'd `0` must not silently\n * remove it; and postgres-js's startup-packet builder filters falsy values\n * out, so a `0` would never reach the server anyway — the connection would\n * quietly inherit whatever server- or role-level default happened to be set.\n * Deployments that need a longer ceiling raise the number.\n */\nexport function createReadOnlyClient(config: DbConfig): PostgresJsDatabase {\n const url = config.readOnlyUrl || config.url\n\n const statementTimeoutMs =\n config.readOnlyStatementTimeoutMs ?? DEFAULT_READ_ONLY_STATEMENT_TIMEOUT_MS\n if (\n !Number.isInteger(statementTimeoutMs) ||\n statementTimeoutMs <= 0 ||\n statementTimeoutMs > MAX_STATEMENT_TIMEOUT_MS\n ) {\n throw new Error(\n `Invalid db.readOnlyStatementTimeoutMs: expected an integer between 1 and ${MAX_STATEMENT_TIMEOUT_MS} milliseconds, got ${String(\n config.readOnlyStatementTimeoutMs,\n )}. To relax the ceiling, raise the value; it cannot be disabled.`,\n )\n }\n\n const sql = postgres(url, {\n max: config.poolMax || 10,\n idle_timeout: 20,\n max_lifetime: 60 * 30,\n connection: {\n application_name: 'toolkit_readonly',\n default_transaction_read_only: true,\n statement_timeout: statementTimeoutMs,\n },\n onnotice: () => {}, // Suppress NOTICE messages\n })\n\n const cfg = drizzleConfig()\n return cfg ? drizzle(sql, cfg) : drizzle(sql)\n}\n","/**\n * \"Only one of these at a time, per database\" — a Postgres advisory lock.\n *\n * Used by the deploy-step runner (plan `deploy-drift` D002): two deploys\n * against one database must not both apply the same step, and the second one\n * has to be TOLD, not queued behind the first.\n *\n * ## The shape, and why\n *\n * `pg_try_advisory_xact_lock` inside one transaction that stays open for the\n * duration of `fn`:\n *\n * - **try**, not wait — a second caller gets `acquired: false` at once and can\n * say \"a deploy is running\" instead of hanging a terminal.\n * - **xact**, not session — the lock belongs to the transaction, so it is\n * released when the transaction ends however it ends: commit, throw, or the\n * process being killed mid-run (the server ends the transaction when the\n * connection drops). A session lock taken on a POOLED connection would\n * outlive the work and need an unlock on that same connection.\n *\n * ## What `fn` must know\n *\n * `fn` is NOT handed the transaction, on purpose. The transaction exists only\n * to hold the lock; the guarded work runs on the pool as it always does and\n * commits as it goes. So a throw from `fn` releases the lock and rolls back\n * NOTHING — which is what a deploy step needs, since each attempt's ledger row\n * must survive the failure of the next one.\n *\n * ## The two costs, stated\n *\n * One pooled connection sits idle-in-transaction while `fn` runs. So:\n *\n * - **A pool of one cannot use this.** `fn`'s own queries would wait forever\n * for the connection the lock is holding.\n * - **A server that ends idle transactions** (`idle_in_transaction_session_\n * timeout` shorter than the work) drops the lock's connection while `fn` is\n * still running. The lock is gone from that moment: a second caller would be\n * admitted. `fn` is not interrupted — this function waits for it and returns\n * its result — but nothing here can restore the exclusion. Work guarded by\n * this lock has to be safe to run twice, which deploy steps promise to be.\n */\n\nimport { sql } from 'drizzle-orm'\nimport type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'\n\n/**\n * An advisory lock's two-part key. Both halves are signed 32-bit integers —\n * Postgres' `(int4, int4)` form, so toolkit locks sit in their own namespace\n * (`space`) and cannot collide with an application's single-`bigint` keys.\n */\nexport interface AdvisoryLockKey {\n readonly space: number\n readonly id: number\n}\n\n/** The first half of every toolkit-owned advisory lock key: ASCII `LUMI`. */\nexport const TOOLKIT_ADVISORY_LOCK_SPACE = 0x4c554d49\n\n/** One deploy-step runner per database. */\nexport const DEPLOY_STEPS_LOCK: AdvisoryLockKey = { space: TOOLKIT_ADVISORY_LOCK_SPACE, id: 1 }\n\nexport type AdvisoryLockResult<T> =\n | { readonly acquired: true; readonly value: T }\n | { readonly acquired: false }\n\nfunction assertInt32(value: number, label: string): void {\n if (!Number.isInteger(value) || value < -2_147_483_648 || value > 2_147_483_647) {\n throw new Error(`Advisory lock ${label} must be a signed 32-bit integer (got ${value})`)\n }\n}\n\n/**\n * Run `fn` while holding the advisory lock `key`, or return\n * `{ acquired: false }` without running it when someone else holds it.\n */\nexport async function withAdvisoryLock<T>(\n db: PostgresJsDatabase,\n key: AdvisoryLockKey,\n fn: () => Promise<T>,\n): Promise<AdvisoryLockResult<T>> {\n assertInt32(key.space, 'space')\n assertInt32(key.id, 'id')\n // The work, once it has STARTED. The driver rejects the transaction the\n // moment its connection closes — which, when the server ends an\n // idle-in-transaction session, is while `fn` is still running on the pool.\n // `fn` cannot be interrupted from here and its work is real, so the answer\n // has to be about the work: wait for it, and report what it did. Returning\n // the driver's rejection instead would call a deploy \"failed\" while its\n // steps were still being applied behind the caller's back.\n let work: Promise<T> | undefined\n try {\n return await db.transaction((tx) => runLocked(tx))\n } catch (err) {\n if (work === undefined) throw err\n // If `fn` itself failed, this rethrows ITS error — the one that matters.\n return { acquired: true, value: await work }\n }\n\n async function runLocked(tx: PostgresJsDatabase): Promise<AdvisoryLockResult<T>> {\n // `execute`, in the one package where infrastructure SQL lives (beside\n // `migrate.ts`): the statement reads no table, so there is no typed builder\n // to put it in — this drizzle has no FROM-less `select`. Both values are\n // bound parameters and were range-checked above; nothing here is a string.\n const rows = await tx.execute<{ acquired: boolean }>(\n sql`select pg_try_advisory_xact_lock(${key.space}::int4, ${key.id}::int4) as acquired`,\n )\n if (rows[0]?.acquired !== true) return { acquired: false }\n work = fn()\n return { acquired: true, value: await work }\n }\n}\n","/**\n * WhereClause → Drizzle SQL translator.\n *\n * This module is the *only* place in the table layer that touches Drizzle's\n * query operators. All `findOne`/`findMany`/`update`/`delete`/`count` calls\n * funnel their where-objects through `buildWhere()` here. That makes this\n * file the single security-review surface for query construction.\n *\n * Safety properties:\n *\n * 1. **Values are always parameterized.** Every operator passes its value\n * through Drizzle's typed operators (`eq`, `gt`, etc.) which use\n * parameter binding. Raw `sql` template literals are NEVER constructed\n * from caller-supplied values.\n * 2. **Column identifiers are looked up by key.** The `where` object's keys\n * are matched against the table's known column set; unknown keys throw.\n * No dynamic identifier interpolation.\n * 3. **jsonb path segments are whitelist-validated.** Each segment must\n * match `/^[a-zA-Z_][a-zA-Z0-9_-]*$|^\\d+$/`. Anything else throws.\n * 4. **`ilike`/`startsWith` values are escaped.** `%` and `_` in caller-\n * supplied strings are backslash-escaped before wrapping with wildcards\n * so callers cannot accidentally inject pattern characters.\n *\n * @internal\n */\n\nimport {\n and,\n eq,\n getTableColumns,\n gt,\n gte,\n ilike,\n inArray,\n isNotNull,\n isNull,\n lt,\n lte,\n ne,\n not,\n notInArray,\n or,\n type SQL,\n type SQLWrapper,\n sql,\n} from 'drizzle-orm'\nimport type { AnyPgColumn, PgTable } from 'drizzle-orm/pg-core'\nimport type { ColumnFactory, ColumnKind, WhereClause } from './types.js'\n\n/** Identifier shape allowed for jsonb path segments. */\nconst JSONB_SEGMENT_RE = /^[a-zA-Z_][a-zA-Z0-9_-]*$|^\\d+$/\n\n/** Reserved meta-keys in a where-clause object. */\nconst META_KEYS = new Set(['$and', '$or', '$not'])\n\n/**\n * Maximum nesting depth for `$and` / `$or` / `$not` recursion.\n *\n * Real production where-clauses are 2-3 levels deep at most. The cap\n * exists to prevent stack-overflow DoS attacks if anyone ever passes\n * untrusted user input through the where-builder (which the design\n * docs forbid, but warnings are not code).\n *\n * Bumping this is fine if a real consumer needs it — but think hard\n * about why a query needs more than 16 levels of nesting first.\n */\nconst MAX_WHERE_DEPTH = 16\n\n/** Operator keys recognised in a column-operator object. */\nconst OP_KEYS = new Set([\n 'eq',\n 'ne',\n 'in',\n 'notIn',\n 'gt',\n 'gte',\n 'lt',\n 'lte',\n 'isNull',\n 'isNotNull',\n 'ilike',\n 'startsWith',\n])\n\n/**\n * A custom error thrown by the where-builder for any caller-side mistake.\n *\n * Catching `WhereBuilderError` lets the table client surface a 400-style\n * error to its caller without conflating it with database errors.\n */\nexport class WhereBuilderError extends Error {\n constructor(message: string) {\n super(`WhereBuilder: ${message}`)\n this.name = 'WhereBuilderError'\n }\n}\n\n/**\n * Escape `%` and `_` (and the escape char `\\`) in a caller-supplied LIKE\n * pattern value. The result is safe to wrap with our own wildcards without\n * leaking pattern syntax to the caller.\n */\nfunction escapeLikeValue(value: string): string {\n return value.replace(/[\\\\%_]/g, '\\\\$&')\n}\n\n/**\n * Resolve a where-clause key to a Drizzle column reference or a jsonb path\n * SQL expression.\n *\n * - Plain column key: `'userId'` → `table.userId` (typed Drizzle column)\n * - jsonb dotted path: `'metadata.author.name'` → `metadata->'author'->>'name'`\n *\n * The parent column for any dotted path MUST be of kind `jsonb`. The\n * column kind is read from the column factory metadata stored at table\n * definition time.\n */\nfunction resolveTarget(\n table: PgTable,\n columnKinds: Readonly<Record<string, ColumnKind>>,\n key: string,\n): SQLWrapper {\n const cols = getTableColumns(table)\n if (!key.includes('.')) {\n const col = cols[key]\n if (!col) {\n throw new WhereBuilderError(`unknown column \"${key}\"`)\n }\n return col\n }\n\n const segments = key.split('.')\n const [columnName, ...path] = segments\n if (!columnName || path.length === 0) {\n throw new WhereBuilderError(`malformed dotted key \"${key}\"`)\n }\n const kind = columnKinds[columnName]\n if (!kind) {\n throw new WhereBuilderError(`unknown column \"${columnName}\" in dotted key \"${key}\"`)\n }\n if (kind !== 'jsonb') {\n throw new WhereBuilderError(\n `dotted-path access is only supported on jsonb columns, but \"${columnName}\" is ${kind}`,\n )\n }\n for (const seg of path) {\n if (!JSONB_SEGMENT_RE.test(seg)) {\n throw new WhereBuilderError(\n `invalid jsonb path segment \"${seg}\" — must match [a-zA-Z_][a-zA-Z0-9_-]* or be a non-negative integer`,\n )\n }\n }\n\n const col = cols[columnName]\n if (!col) {\n throw new WhereBuilderError(`unknown column \"${columnName}\"`)\n }\n\n // Build: col->'a'->'b'->>'c' (intermediate -> for nested object access,\n // final ->> for text extraction so comparison/ilike operators work).\n //\n // String segments are parameterized normally — Postgres binds them as\n // text and `->'key'` does object access.\n //\n // Numeric segments are inlined as raw SQL because parameter binding\n // doesn't reliably dispatch to the array-index variant of `->`/`->>`\n // (Postgres often binds bigint, but the array-index operator overload\n // requires a literal int). The segment is already whitelist-validated\n // as `^\\d+$`, so inlining is safe by construction. Re-validate here as\n // belt-and-braces.\n let target: SQL = sql`${col}`\n for (let i = 0; i < path.length; i++) {\n const seg = path[i]\n if (seg === undefined) {\n throw new WhereBuilderError(`jsonb path segment at index ${i} is undefined`)\n }\n const isLast = i === path.length - 1\n if (/^\\d+$/.test(seg)) {\n // Defence-in-depth re-check: must be pure digits, no leading zeros\n // beyond a single 0, length capped to avoid runaway integers.\n if (seg.length > 10) {\n throw new WhereBuilderError(`jsonb path index \"${seg}\" is too large`)\n }\n const literal = sql.raw(seg)\n target = isLast ? sql`${target}->>${literal}` : sql`${target}->${literal}`\n } else {\n target = isLast ? sql`${target}->>${seg}` : sql`${target}->${seg}`\n }\n }\n return target\n}\n\n/**\n * Apply a single column-operator object (or shorthand value) to a target\n * (column or jsonb-path expression). Returns the corresponding Drizzle SQL\n * predicate.\n */\nfunction applyOperator(target: SQLWrapper, value: unknown): SQL | undefined {\n // Shorthand: bare value → eq, bare null → isNull\n if (value === null) {\n return isNull(target as AnyPgColumn)\n }\n if (\n typeof value === 'string' ||\n typeof value === 'number' ||\n typeof value === 'boolean' ||\n typeof value === 'bigint' ||\n value instanceof Date\n ) {\n return eq(target as AnyPgColumn, value as never)\n }\n if (Array.isArray(value)) {\n throw new WhereBuilderError(\n 'arrays must be wrapped in an operator (e.g. { in: [...] }) — bare arrays are not allowed',\n )\n }\n if (typeof value !== 'object') {\n throw new WhereBuilderError(`unsupported value type: ${typeof value}`)\n }\n\n const obj = value as Record<string, unknown>\n const opKeys = Object.keys(obj)\n if (opKeys.length === 0) {\n throw new WhereBuilderError('empty operator object')\n }\n\n // Multiple operators in one object are AND-ed together\n const parts: SQL[] = []\n for (const op of opKeys) {\n if (!OP_KEYS.has(op)) {\n throw new WhereBuilderError(`unknown operator \"${op}\"`)\n }\n const v = obj[op]\n switch (op) {\n case 'eq':\n parts.push(eq(target as AnyPgColumn, v as never))\n break\n case 'ne':\n parts.push(ne(target as AnyPgColumn, v as never))\n break\n case 'in':\n if (!Array.isArray(v)) {\n throw new WhereBuilderError('\"in\" operator requires an array')\n }\n if (v.length === 0) {\n // IN () is invalid SQL; an empty IN should match nothing\n parts.push(sql`false`)\n } else {\n parts.push(inArray(target as AnyPgColumn, v as never[]))\n }\n break\n case 'notIn':\n if (!Array.isArray(v)) {\n throw new WhereBuilderError('\"notIn\" operator requires an array')\n }\n if (v.length === 0) {\n // NOT IN () matches everything\n parts.push(sql`true`)\n } else {\n parts.push(notInArray(target as AnyPgColumn, v as never[]))\n }\n break\n case 'gt':\n parts.push(gt(target as AnyPgColumn, v as never))\n break\n case 'gte':\n parts.push(gte(target as AnyPgColumn, v as never))\n break\n case 'lt':\n parts.push(lt(target as AnyPgColumn, v as never))\n break\n case 'lte':\n parts.push(lte(target as AnyPgColumn, v as never))\n break\n case 'isNull':\n if (v !== true) {\n throw new WhereBuilderError('\"isNull\" operator must be `true`')\n }\n parts.push(isNull(target as AnyPgColumn))\n break\n case 'isNotNull':\n if (v !== true) {\n throw new WhereBuilderError('\"isNotNull\" operator must be `true`')\n }\n parts.push(isNotNull(target as AnyPgColumn))\n break\n case 'ilike': {\n if (typeof v !== 'string') {\n throw new WhereBuilderError('\"ilike\" operator requires a string')\n }\n // Caller's value is treated as a literal string — outer wildcards\n // come from the operator semantics, not from caller-supplied %.\n // We escape any wildcard chars in the value to prevent accidental\n // pattern injection.\n parts.push(ilike(target as AnyPgColumn, `%${escapeLikeValue(v)}%`))\n break\n }\n case 'startsWith': {\n if (typeof v !== 'string') {\n throw new WhereBuilderError('\"startsWith\" operator requires a string')\n }\n parts.push(ilike(target as AnyPgColumn, `${escapeLikeValue(v)}%`))\n break\n }\n }\n }\n\n if (parts.length === 0) return undefined\n if (parts.length === 1) return parts[0]\n return and(...parts)\n}\n\n/**\n * Translate a {@link WhereClause} object into a single Drizzle SQL\n * expression suitable for `db.select(...).where(here)`.\n *\n * Returns `undefined` for an empty clause (no filtering). Callers that\n * require a non-empty where (update, delete) should reject `undefined`\n * before invoking the SQL.\n *\n * @param depth - internal recursion depth, used to enforce\n * {@link MAX_WHERE_DEPTH}. External callers should not pass this.\n * @throws {@link WhereBuilderError} for any malformed clause, unknown\n * column, invalid operator, invalid jsonb path segment, or recursion\n * deeper than {@link MAX_WHERE_DEPTH}.\n */\nexport function buildWhere<TCols extends Record<string, ColumnFactory>>(\n table: PgTable,\n columnKinds: Readonly<Record<string, ColumnKind>>,\n clause: WhereClause<TCols> | undefined,\n depth = 0,\n): SQL | undefined {\n if (!clause) return undefined\n if (typeof clause !== 'object' || Array.isArray(clause)) {\n throw new WhereBuilderError('where clause must be a plain object')\n }\n if (depth > MAX_WHERE_DEPTH) {\n throw new WhereBuilderError(\n `where clause exceeds maximum nesting depth of ${MAX_WHERE_DEPTH} — ` +\n 'real queries should not need this much nesting; if yours does, ' +\n 'reconsider whether it should be expressed differently',\n )\n }\n\n const parts: SQL[] = []\n\n for (const key of Object.keys(clause)) {\n const value = (clause as Record<string, unknown>)[key]\n\n if (key === '$and') {\n if (!Array.isArray(value)) {\n throw new WhereBuilderError('$and must be an array')\n }\n if (value.length === 0) {\n // $and: [] is almost certainly a bug — surface it loudly,\n // matching the $or: [] behaviour for consistency.\n throw new WhereBuilderError('$and must contain at least one clause')\n }\n const subParts = (value as WhereClause<TCols>[])\n .map((c) => buildWhere(table, columnKinds, c, depth + 1))\n .filter((p): p is SQL => p !== undefined)\n if (subParts.length > 0) parts.push(and(...subParts) as SQL)\n continue\n }\n\n if (key === '$or') {\n if (!Array.isArray(value)) {\n throw new WhereBuilderError('$or must be an array')\n }\n if (value.length === 0) {\n // $or: [] would match nothing; reject explicitly to surface the bug\n throw new WhereBuilderError('$or must contain at least one clause')\n }\n const subParts = (value as WhereClause<TCols>[])\n .map((c) => buildWhere(table, columnKinds, c, depth + 1))\n .filter((p): p is SQL => p !== undefined)\n if (subParts.length > 0) parts.push(or(...subParts) as SQL)\n continue\n }\n\n if (key === '$not') {\n if (!isNonEmptyWhere(value)) {\n // $not: {} or $not: undefined is almost certainly a bug — the\n // negation of \"match everything\" or \"no filter\" produces\n // surprising semantics.\n throw new WhereBuilderError('$not must contain a non-empty clause')\n }\n const sub = buildWhere(table, columnKinds, value as WhereClause<TCols>, depth + 1)\n if (sub) parts.push(not(sub))\n continue\n }\n\n if (META_KEYS.has(key)) {\n // unreachable — guarded above\n continue\n }\n\n const target = resolveTarget(table, columnKinds, key)\n const predicate = applyOperator(target, value)\n if (predicate) parts.push(predicate)\n }\n\n if (parts.length === 0) return undefined\n if (parts.length === 1) return parts[0]\n return and(...parts) as SQL\n}\n\n/**\n * Returns true if a {@link WhereClause} produces a non-trivial filter (at\n * least one column predicate or sub-clause). Useful for rejecting empty\n * `where` arguments to `update`/`delete`/`updateMany`/`deleteMany`.\n *\n * Note: this does not check whether `buildWhere()` would actually emit SQL\n * for the clause — it just checks that the user supplied something.\n */\nexport function isNonEmptyWhere(clause: unknown): boolean {\n if (clause === undefined || clause === null) return false\n if (typeof clause !== 'object') return false\n if (Array.isArray(clause)) return false\n return Object.keys(clause as object).length > 0\n}\n","/**\n * Generic typed CRUD client for `defineTable` tables.\n *\n * Single-table operations only — no joins, no aggregates beyond `count`,\n * no hooks. For typed multi-table queries, callers reach for the\n * underlying `table` object exposed by `defineTable` and write plain\n * Drizzle. That's the one sanctioned escape valve.\n *\n * Hard rules baked into the client (not just at the SQL level):\n *\n * - `update`, `updateMany`, `delete`, `deleteMany` REQUIRE a non-empty\n * `where` clause. Empty wheres throw `TableClientError` at the client,\n * not just \"0 rows updated\".\n * - `findMany.limit` defaults to {@link DEFAULT_LIMIT} (100). Values above\n * {@link MAX_LIMIT} (1000) THROW `TableClientError` (not clamped) —\n * paginate or use `claim()` for batch processing.\n * - `insertMany` is capped at {@link MAX_BATCH} (1000) per call to keep a\n * single round trip from monopolising the connection.\n * - All write methods return the affected row(s). No silent updates.\n *\n * For more advanced operations (`claim()`, `aggregate()`,\n * `countEstimate()`) see follow-up PRs in the `feat/define-table` series.\n */\n\nimport {\n and as andFn,\n count as countFn,\n eq,\n getTableColumns,\n getTableName,\n type InferInsertModel,\n type InferSelectModel,\n inArray,\n isNotNull,\n type SQL,\n sql,\n} from 'drizzle-orm'\nimport {\n type AnyPgColumn,\n type PgTable,\n type PgUpdateSetSource,\n pgTable,\n real,\n varchar,\n} from 'drizzle-orm/pg-core'\nimport type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'\nimport type { ColumnFactory, ColumnKind, ColumnValue, Row, WhereClause } from './types.js'\nimport { buildWhere, isNonEmptyWhere, WhereBuilderError } from './where-builder.js'\n\n/**\n * Allowed shape for a SQL identifier — used to validate caller-supplied\n * `aggregate()` output aliases before they are emitted as `... AS \"alias\"`.\n * Drizzle quotes identifiers, but accepting only the standard shape keeps\n * the surface predictable and removes implicit reliance on the quoting.\n */\nconst SQL_IDENTIFIER_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/\n\n/**\n * Minimal Drizzle table definition for Postgres `pg_class` system catalog.\n * Used by `countEstimate()` to query row count statistics without raw SQL.\n */\nconst pgClassTable = pgTable('pg_class', {\n relname: varchar('relname', { length: 63 }).notNull(),\n reltuples: real('reltuples').notNull(),\n})\n\n/**\n * Default page size for `findMany` when no `limit` is supplied.\n *\n * Chosen as a safe upper bound for \"show me a page of stuff\" — anything\n * above this should be paginated explicitly.\n */\nexport const DEFAULT_LIMIT = 100\n\n/**\n * Hard cap on `findMany.limit`. Caller-supplied values above this are\n * rejected with `TableClientError` (no clamping).\n */\nexport const MAX_LIMIT = 1000\n\n/**\n * Hard cap on `insertMany.values.length` per single call.\n */\nexport const MAX_BATCH = 1000\n\n/**\n * A custom error thrown by the table client for any caller-side mistake\n * (empty where on update/delete, batch too large, malformed where, …).\n *\n * Database errors are NOT wrapped — they propagate as-is from postgres-js\n * so callers can match on Postgres error codes.\n */\nexport class TableClientError extends Error {\n constructor(message: string) {\n super(`TableClient: ${message}`)\n this.name = 'TableClientError'\n }\n}\n\n/**\n * Order-by spec accepted by `findMany`.\n */\nexport interface OrderBySpec<TCols extends Record<string, ColumnFactory>> {\n column: keyof TCols & string\n dir?: 'asc' | 'desc'\n}\n\n/**\n * `findMany` options.\n */\nexport interface FindManyOptions<TCols extends Record<string, ColumnFactory>> {\n where?: WhereClause<TCols>\n orderBy?: OrderBySpec<TCols>[]\n limit?: number\n offset?: number\n}\n\n/**\n * Options for the `claim()` atomic locking operation.\n *\n * @see {@link TableClient.claim}\n */\nexport interface ClaimOptions<\n TCols extends Record<string, ColumnFactory>,\n TTable extends PgTable = PgTable,\n> {\n /** Filter for rows eligible to be claimed. Must be non-empty. */\n where: WhereClause<TCols>\n /** Literal values to SET on claimed rows. */\n set?: Partial<InferInsertModel<TTable>>\n /** Raw SQL expressions to SET (e.g. `{ attempts: sql\\`attempts + 1\\` }`). */\n setSql?: Record<string, SQL>\n /** ORDER BY for the subselect — controls claim priority. */\n orderBy?: OrderBySpec<TCols>[]\n /** Maximum number of rows to claim. Must be 1..MAX_LIMIT. */\n limit: number\n}\n\n/**\n * Options for the `tryClaim()` atomic insert-or-replace-if operation.\n *\n * @see {@link TableClient.tryClaim}\n */\nexport interface TryClaimOptions<\n TCols extends Record<string, ColumnFactory>,\n TTable extends PgTable = PgTable,\n> {\n /**\n * Unique-constraint column(s) that should trigger the conflict path.\n *\n * Single string for a single-column unique constraint; array for a\n * composite unique. Must reference a real column on this table.\n */\n target: (keyof TCols & string) | (keyof TCols & string)[]\n /**\n * Predicate that decides whether an existing conflicting row may be\n * **replaced**. Required and must be non-empty.\n *\n * - On INSERT (no conflict): always wins; predicate is irrelevant.\n * - On CONFLICT + predicate matches: row is replaced; result is\n * `{ acquired: true, row }`.\n * - On CONFLICT + predicate does NOT match: existing row is preserved;\n * result is `{ acquired: false, row }` (where `row` is the surviving\n * existing row).\n *\n * Use `$or` to express composite re-entrancy rules — e.g.\n * `{ $or: [{ expiresAt: { lt: now } }, { lockedBy: callerId }] }` for\n * \"replace if expired, or if I already hold it\".\n */\n replaceWhen: WhereClause<TCols>\n /**\n * Patch applied on the replace path. Defaults to the full `values`\n * payload (last-write-wins on the columns being inserted).\n */\n set?: Partial<InferInsertModel<TTable>>\n}\n\n/**\n * Specification for a single aggregate function.\n */\nexport type AggregateSpec<TCols extends Record<string, ColumnFactory>> =\n | { fn: 'count' }\n | { fn: 'count'; column: keyof TCols & string }\n | { fn: 'sum' | 'avg' | 'min' | 'max'; column: keyof TCols & string }\n\n/**\n * Options for the `aggregate()` method.\n */\nexport interface AggregateOptions<TCols extends Record<string, ColumnFactory>> {\n /** Named aggregate expressions. Each key becomes an output column. */\n select: Record<string, AggregateSpec<TCols>>\n /** Columns to GROUP BY. Included automatically in the result. */\n groupBy?: (keyof TCols & string)[]\n /** Filter rows before aggregation (WHERE). */\n where?: WhereClause<TCols>\n /** Order results. Can reference both grouped columns and aliases. */\n orderBy?: OrderBySpec<TCols>[]\n /** Limit the number of groups returned. */\n limit?: number\n}\n\n/**\n * Generic CRUD client. Constructed by `defineTable` — never instantiated\n * directly by callers.\n *\n * The same instance shape is exposed both at the top level (using the\n * package's main `db` connection) and inside a `transaction()` callback\n * (using the transactional `tx` handle), so user code is identical\n * regardless of transactional context.\n *\n * @template TCols The columns record passed to `defineTable`. Drives the\n * where-builder DSL and the `Row<TCols>` / `InsertRow<TCols>` shapes.\n * @template TTable The underlying Drizzle pgTable type, inferred from the\n * table value passed to `new TableClient(...)`. Carrying this through\n * lets `.insert().values(...)`, `.update().set(...)`, and `.returning()`\n * use Drizzle's own `InferInsertModel` / `InferSelectModel` instead of\n * falling back to `as never` casts.\n */\nexport class TableClient<\n TCols extends Record<string, ColumnFactory>,\n TTable extends PgTable = PgTable,\n> {\n /**\n * @internal — instances are created by `defineTable()`.\n */\n constructor(\n /** The Drizzle table object. Exposed via `defineTable().table` for typed JOINs. */\n public readonly table: TTable,\n /** Per-column kind metadata. Used by the where-builder for jsonb path validation. */\n private readonly columnKinds: Readonly<Record<string, ColumnKind>>,\n /**\n * The active database handle. May be a top-level `PostgresJsDatabase`\n * or a transaction handle of compatible shape — the client doesn't\n * distinguish.\n */\n private readonly db: PostgresJsDatabase,\n /** Primary key column names. Required by `claim()`. Empty for tables without a declared PK. */\n private readonly primaryKeyColumns: readonly string[] = [],\n /**\n * The partition this client is confined to — see\n * `TableDefinition.partitionColumn`. Absent for unpartitioned tables.\n */\n private readonly partition?: { readonly key: string; readonly value: string },\n ) {}\n\n /** `partition.key = partition.value`, or `undefined` for an unpartitioned client. */\n private partitionCondition(): SQL | undefined {\n if (!this.partition) return undefined\n return eq(this.getColumnOrThrow(this.partition.key, 'partition'), this.partition.value)\n }\n\n /** AND the partition into `condition` (which may be absent). */\n private confine(condition: SQL | undefined): SQL | undefined {\n const part = this.partitionCondition()\n if (!part) return condition\n if (!condition) return part\n return andFn(part, condition)\n }\n\n /** Stamp the partition onto a row about to be inserted (overwriting the caller's value). */\n private stamp<V>(values: V): V {\n if (!this.partition) return values\n return { ...(values as object), [this.partition.key]: this.partition.value } as V\n }\n\n /** Refuse a patch that would move a row out of its partition. */\n private refusePartitionPatch(keys: readonly string[], method: string): void {\n if (this.partition && keys.includes(this.partition.key)) {\n throw new TableClientError(\n `${method}: \"${this.partition.key}\" is this table's partition column and cannot be changed`,\n )\n }\n }\n\n /** Conflict-target keys with the partition column leading (the table's uniques lead with it). */\n private partitionTarget<K extends string>(keys: readonly K[]): K[] {\n if (!this.partition || keys.includes(this.partition.key as K)) return [...keys]\n return [this.partition.key as K, ...keys]\n }\n\n /**\n * The table typed as the non-generic `PgTable` base.\n *\n * Drizzle's `.from()` (select) uses a conditional type\n * `TableLikeHasEmptySelection<TTable>` that TypeScript cannot evaluate\n * against a bounded generic `TTable extends PgTable` — it can only\n * resolve when the argument is the concrete base `PgTable`. `.insert()`,\n * `.update()`, and `.delete()` have simpler signatures (`<T extends\n * PgTable>(t: T)`) that DO accept the generic directly, so those call\n * sites keep `this.table` and still get `InferInsertModel<TTable>` /\n * `InferSelectModel<TTable>` inference for `.values()`, `.set()`, and\n * `.returning()`.\n */\n private get _selectTable(): PgTable {\n return this.table\n }\n\n // -------------------------------------------------------------------------\n // Private helpers\n //\n // These collapse patterns that repeated 4-7 times across the public methods\n // (column lookup, ORDER BY emission, where-clause non-emptiness checks,\n // patch-key validation). Keep them at the top of the file so future\n // methods know where to wire in.\n // -------------------------------------------------------------------------\n\n /**\n * Look up a Drizzle column by its JS property name, throwing\n * `TableClientError` with the supplied `context` if no such column exists.\n * Wraps `getTableColumns(this.table)` — uses Drizzle's typed column map\n * instead of an indexed cast on the table object.\n */\n private getColumnOrThrow(name: string, context: string): AnyPgColumn {\n const col = getTableColumns(this.table)[name]\n if (!col) {\n throw new TableClientError(`${context}: unknown column \"${name}\"`)\n }\n return col\n }\n\n /**\n * Build an array of ORDER BY SQL expressions from an `OrderBySpec[]`.\n * Used by `findMany`, `distinct`, `claim`, and `aggregate`.\n */\n private buildOrderBy(specs: readonly OrderBySpec<TCols>[], context: string): SQL[] {\n return specs.map((spec) => {\n const col = this.getColumnOrThrow(spec.column, `${context}: orderBy`)\n return spec.dir === 'desc' ? sql`${col} DESC` : sql`${col} ASC`\n })\n }\n\n /**\n * Throw `TableClientError` if the where clause has no top-level keys.\n * Used by every method that mutates or reads-by-key (`update`, `delete`,\n * `findOne`, `exists`, `claim`, ...) — none of them have a sane\n * \"everything\" path, so empty wheres are always a caller mistake.\n */\n private requireNonEmptyWhere(where: WhereClause<TCols>, method: string): void {\n if (!isNonEmptyWhere(where)) {\n throw new TableClientError(`${method} requires a non-empty where clause`)\n }\n }\n\n /**\n * Verify every key in `patch` names a real column on this table. Used by\n * `update`, `updateMany`, `upsert`, and `claim` so a typo'd or hostile\n * patch key surfaces a clear error instead of being silently dropped by\n * Drizzle's set logic.\n */\n private validateColumnKeys(keys: readonly string[], method: string): void {\n if (keys.length === 0) return\n const cols = getTableColumns(this.table)\n for (const key of keys) {\n if (!Object.hasOwn(cols, key)) {\n throw new TableClientError(`${method}: unknown column \"${key}\"`)\n }\n }\n }\n\n // -------------------------------------------------------------------------\n // Reads\n // -------------------------------------------------------------------------\n\n /**\n * Find a single row matching the given where clause, or `null` if no row\n * matches. Throws if the where is empty.\n *\n * @example\n * ```ts\n * await readState.client.findOne({ ticketId: 't_1', userId: 'u_1' })\n * ```\n */\n async findOne(where: WhereClause<TCols>): Promise<Row<TCols> | null> {\n this.requireNonEmptyWhere(where, 'findOne')\n const condition = this.buildWhereOrThrow(where)\n const rows = await this.db.select().from(this._selectTable).where(condition).limit(1)\n return (rows[0] as Row<TCols> | undefined) ?? null\n }\n\n /**\n * Find multiple rows. Pagination, ordering, and filtering are all\n * optional but `limit` is hard-capped at {@link MAX_LIMIT}.\n *\n * @example\n * ```ts\n * await jobs.client.findMany({\n * where: { status: 'pending', runAt: { lte: new Date() } },\n * orderBy: [{ column: 'priority', dir: 'desc' }, { column: 'runAt', dir: 'asc' }],\n * limit: 25,\n * })\n * ```\n */\n async findMany(options: FindManyOptions<TCols> = {}): Promise<Row<TCols>[]> {\n // Validate all caller-supplied options up front, before touching the\n // database. This guarantees TableClientError is thrown for input\n // mistakes regardless of db state — important for unit testability\n // and for failing fast on misuse.\n const limit = this.validateLimit(options.limit)\n if (options.offset !== undefined) {\n if (!Number.isInteger(options.offset) || options.offset < 0) {\n throw new TableClientError(`offset must be a non-negative integer, got ${options.offset}`)\n }\n }\n const orderExprs =\n options.orderBy && options.orderBy.length > 0\n ? this.buildOrderBy(options.orderBy, 'findMany')\n : undefined\n const condition = options.where\n ? this.buildWhereOrThrow(options.where)\n : this.partitionCondition()\n\n let query = this.db.select().from(this._selectTable).$dynamic()\n if (condition) query = query.where(condition)\n if (orderExprs) query = query.orderBy(...orderExprs)\n query = query.limit(limit)\n if (options.offset !== undefined) query = query.offset(options.offset)\n\n const rows = await query\n return rows as Row<TCols>[]\n }\n\n /**\n * Count rows matching the where clause. Returns an exact count via\n * `SELECT COUNT(*)`. For estimated/cached counts on large tables, see\n * `countEstimate()` and `countCached()` (added in a follow-up PR).\n *\n * @example\n * ```ts\n * const open = await jobs.client.count({ status: 'pending' })\n * ```\n */\n async count(where?: WhereClause<TCols>): Promise<number> {\n const condition = where ? this.buildWhereOrThrow(where) : this.partitionCondition()\n let query = this.db.select({ value: countFn() }).from(this._selectTable).$dynamic()\n if (condition) query = query.where(condition)\n const rows = await query\n return Number(rows[0]?.value ?? 0)\n }\n\n /**\n * Returns `true` if at least one row matches the where clause. Always\n * uses `LIMIT 1` and short-circuits — cheaper than `count() > 0` on\n * large tables.\n */\n async exists(where: WhereClause<TCols>): Promise<boolean> {\n this.requireNonEmptyWhere(where, 'exists')\n const condition = this.buildWhereOrThrow(where)\n const rows = await this.db\n .select({ one: sql`1` })\n .from(this._selectTable)\n .where(condition)\n .limit(1)\n return rows.length > 0\n }\n\n /**\n * Return unique non-null values for a single column.\n *\n * Null values are excluded by default — pass `includeNull: true` to\n * include them. Results can be filtered with `where` and ordered with\n * `orderBy` (ascending or descending on the distinct column).\n *\n * @example Get distinct entity types from the audit log\n * ```ts\n * const types = await audit.client.distinct('entityType', { orderBy: 'asc' })\n * ```\n *\n * @example Distinct with a filter\n * ```ts\n * const actions = await audit.client.distinct('action', {\n * where: { userId: 'u_1' },\n * orderBy: 'asc',\n * })\n * ```\n */\n async distinct<K extends keyof TCols & string>(\n column: K,\n options?: {\n where?: WhereClause<TCols>\n orderBy?: 'asc' | 'desc'\n /** Include null values in the result. Defaults to `false`. */\n includeNull?: boolean\n },\n ): Promise<ColumnValue<TCols[K]>[]> {\n const col = this.getColumnOrThrow(column, 'distinct')\n\n const conditions: SQL[] = []\n if (!options?.includeNull) {\n conditions.push(isNotNull(col))\n }\n if (options?.where) {\n conditions.push(this.buildWhereOrThrow(options.where))\n } else {\n const part = this.partitionCondition()\n if (part) conditions.push(part)\n }\n\n let query = this.db.selectDistinct({ value: col }).from(this._selectTable).$dynamic()\n\n if (conditions.length === 1) {\n query = query.where(conditions[0])\n } else if (conditions.length > 1) {\n query = query.where(andFn(...conditions))\n }\n\n if (options?.orderBy) {\n const orderExpr = options.orderBy === 'desc' ? sql`${col} DESC` : sql`${col} ASC`\n query = query.orderBy(orderExpr)\n }\n\n const rows = await query\n return rows.map((r) => (r as { value: unknown }).value) as ColumnValue<TCols[K]>[]\n }\n\n // -------------------------------------------------------------------------\n // Writes\n // -------------------------------------------------------------------------\n\n /**\n * Insert a single row. Returns the inserted row (server-generated\n * defaults populated).\n */\n async insert(values: InferInsertModel<TTable>): Promise<Row<TCols>> {\n const rows = await this.db.insert(this.table).values(this.stamp(values)).returning()\n return rows[0] as Row<TCols>\n }\n\n /**\n * Upsert: insert a row, or update an existing row if a unique constraint\n * conflict occurs.\n *\n * `target` names the column(s) whose unique constraint should trigger\n * the conflict path. `set` is the patch applied on conflict — when\n * omitted, the conflict path applies the same `values` (i.e. \"last\n * write wins\").\n *\n * `setWhere` adds a conditional WHERE to the conflict update path. If\n * the condition does not match, the update is skipped and the existing\n * row is returned unchanged (via a fallback SELECT). This enables\n * patterns like \"only update if the new version is higher than the\n * existing one\".\n *\n * @example Idempotent \"mark as read\" tracking\n * ```ts\n * await readState.client.upsert(\n * { ticketId, userId, lastReadAt: new Date() },\n * { target: ['ticketId', 'userId'] },\n * )\n * ```\n *\n * @example Conditional update — only bump if newer\n * ```ts\n * await state.client.upsert(\n * { key: 'sync', version: 5, data: '...' },\n * { target: 'key', setWhere: { version: { lt: 5 } } },\n * )\n * ```\n *\n * `mergeJsonb` names jsonb columns whose conflict path MERGES instead of\n * replacing: the stored object becomes `stored || incoming` — a top-level,\n * right-wins merge, evaluated by Postgres inside the one `ON CONFLICT`\n * statement. Use it when two writers may each own part of one jsonb object and\n * a read-then-write would let the later writer erase the earlier one's keys.\n * The insert path is unaffected (there is nothing to merge with). A stored SQL\n * NULL or JSON `null` merges as `{}`. The incoming value must be a JSON object\n * — `||` concatenates arrays and scalars instead of merging — and a key named\n * in both `set` and `mergeJsonb` is merged (the merge wins).\n *\n * @example Merge a partial patch into a staged document\n * ```ts\n * await drafts.client.upsert(\n * { entityId, locale, data: { title: 'New' }, updatedAt: now },\n * { target: ['entityId', 'locale'], set: { updatedAt: now }, mergeJsonb: ['data'] },\n * )\n * ```\n */\n async upsert(\n values: InferInsertModel<TTable>,\n opts: {\n target: (keyof TCols & string) | (keyof TCols & string)[]\n set?: Partial<InferInsertModel<TTable>>\n /** Conditional WHERE on the conflict update path. Empty objects are ignored. */\n setWhere?: WhereClause<TCols>\n /** jsonb columns merged (`stored || incoming`) rather than replaced on conflict. */\n mergeJsonb?: readonly (keyof TCols & string)[]\n },\n ): Promise<Row<TCols>> {\n const requested = Array.isArray(opts.target) ? opts.target : [opts.target]\n if (requested.length === 0) {\n throw new TableClientError('upsert requires at least one target column')\n }\n // A partitioned client's uniques lead with the partition column, so the\n // conflict target must too — and the row is stamped with the partition,\n // whatever the caller passed.\n const targetKeys = this.partitionTarget(requested)\n const targetCols = targetKeys.map((k) => this.getColumnOrThrow(k, 'upsert target'))\n values = this.stamp(values)\n\n // When `set` is omitted, fall back to applying the same values on\n // conflict — this is the natural \"last write wins\" semantics that\n // covers the common upsert case.\n const setPatch: Partial<InferInsertModel<TTable>> =\n opts.set ?? (values as Partial<InferInsertModel<TTable>>)\n if (opts.set) {\n this.validateColumnKeys(Object.keys(opts.set), 'upsert set')\n this.refusePartitionPatch(Object.keys(opts.set), 'upsert set')\n }\n\n // Build the optional conditional WHERE for the conflict update path.\n // Empty `setWhere: {}` is treated as \"no condition\" (not an error).\n const setWhereCondition =\n opts.setWhere && isNonEmptyWhere(opts.setWhere)\n ? this.buildWhereOrThrow(opts.setWhere)\n : undefined\n\n // `PgUpdateSetSource` takes a value OR an SQL expression per column — the\n // same union `claim`'s `setSql` relies on. A merged column's value comes from\n // `values`, bound as a parameter, never interpolated.\n const conflictSet = { ...setPatch } as PgUpdateSetSource<TTable>\n for (const key of opts.mergeJsonb ?? []) {\n if (this.columnKinds[key] !== 'jsonb') {\n throw new TableClientError(\n `upsert mergeJsonb: '${key}' is not a jsonb column (kind: ${this.columnKinds[key] ?? 'unknown'})`,\n )\n }\n const col = this.getColumnOrThrow(key, 'upsert mergeJsonb')\n const value = (values as Record<string, unknown>)[key]\n // Postgres `||` CONCATENATES when either side is not an object — an array\n // or a scalar would turn the stored object into an array. A merge is only\n // defined for objects, so anything else is refused here.\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new TableClientError(\n `upsert mergeJsonb: '${key}' must be a JSON object to merge (got ${Array.isArray(value) ? 'array' : value === null ? 'null' : typeof value})`,\n )\n }\n const incoming = JSON.stringify(value)\n ;(conflictSet as Record<string, SQL>)[key] =\n sql`COALESCE(NULLIF(${col}, 'null'::jsonb), '{}'::jsonb) || ${incoming}::jsonb`\n }\n\n const rows = await this.db\n .insert(this.table)\n .values(values)\n .onConflictDoUpdate({\n target: targetCols,\n set: conflictSet,\n ...(setWhereCondition ? { setWhere: setWhereCondition } : {}),\n })\n .returning()\n\n if (rows.length > 0) {\n return rows[0] as Row<TCols>\n }\n\n // setWhere blocked the update — Postgres returns 0 rows from RETURNING.\n // Fall back to reading the existing row by the target columns.\n const where: Record<string, unknown> = {}\n for (const k of targetKeys) {\n where[k] = (values as Record<string, unknown>)[k]\n }\n const existing = await this.findOne(where as WhereClause<TCols>)\n if (!existing) {\n // Reachable when another connection deletes the conflicting row\n // between the failed conditional UPDATE and this SELECT. Rare in\n // practice, but the message must not lie — callers that care need\n // to distinguish \"race during upsert\" from invariant violations.\n throw new TableClientError(\n 'upsert with setWhere: conflicting row was deleted concurrently — retry the operation',\n )\n }\n return existing as Row<TCols>\n }\n\n /**\n * Atomically claim a row by either inserting it or replacing an existing\n * conflicting row that satisfies `replaceWhen`. The single SQL statement\n * makes this race-free — concurrent callers cannot both observe an empty\n * slot and both INSERT.\n *\n * Distinct from {@link upsert}:\n * - `upsert` always wins on conflict (or hits its `setWhere` block and\n * silently falls back to the existing row). It returns a single\n * `Row<TCols>` and the caller cannot tell which path fired.\n * - `tryClaim` returns a discriminated union — the caller knows whether\n * they took the slot or someone else still holds it.\n *\n * Use case: leasing/locking primitives where the caller must distinguish\n * \"I now hold this lock\" from \"someone else holds it, here is who\".\n *\n * @example Acquire a lock, replacing only if expired\n * ```ts\n * const result = await locks.tryClaim(\n * { entityId, lockedBy: me, expiresAt: in5Min },\n * {\n * target: 'entityId',\n * replaceWhen: { expiresAt: { lt: now } },\n * },\n * )\n * if (result.acquired) {\n * // I have the lock\n * } else {\n * // result.row.lockedBy holds it (and lock is fresh)\n * }\n * ```\n *\n * @example Re-entrant lock (claim if expired, or if I already hold it)\n * ```ts\n * await locks.tryClaim(\n * { entityId, lockedBy: me, expiresAt: in5Min },\n * {\n * target: 'entityId',\n * replaceWhen: { $or: [{ expiresAt: { lt: now } }, { lockedBy: me }] },\n * },\n * )\n * ```\n */\n async tryClaim(\n values: InferInsertModel<TTable>,\n opts: TryClaimOptions<TCols, TTable>,\n ): Promise<{ acquired: true; row: Row<TCols> } | { acquired: false; row: Row<TCols> }> {\n const requested = Array.isArray(opts.target) ? opts.target : [opts.target]\n if (requested.length === 0) {\n throw new TableClientError('tryClaim requires at least one target column')\n }\n const targetKeys = this.partitionTarget(requested)\n const targetCols = targetKeys.map((k) => this.getColumnOrThrow(k, 'tryClaim target'))\n values = this.stamp(values)\n\n const setPatch: Partial<InferInsertModel<TTable>> =\n opts.set ?? (values as Partial<InferInsertModel<TTable>>)\n if (opts.set) {\n this.validateColumnKeys(Object.keys(opts.set), 'tryClaim set')\n this.refusePartitionPatch(Object.keys(opts.set), 'tryClaim set')\n }\n\n if (!isNonEmptyWhere(opts.replaceWhen)) {\n throw new TableClientError('tryClaim requires a non-empty replaceWhen clause')\n }\n const setWhereCondition = this.buildWhereOrThrow(opts.replaceWhen)\n\n const rows = await this.db\n .insert(this.table)\n .values(values)\n .onConflictDoUpdate({\n target: targetCols,\n set: setPatch,\n setWhere: setWhereCondition,\n })\n .returning()\n\n if (rows.length > 0) {\n return { acquired: true, row: rows[0] as Row<TCols> }\n }\n\n // Conflict + replaceWhen blocked the update — the existing row is\n // preserved. Read it back so the caller learns who holds the slot.\n const where: Record<string, unknown> = {}\n for (const k of targetKeys) {\n where[k] = (values as Record<string, unknown>)[k]\n }\n const existing = await this.findOne(where as WhereClause<TCols>)\n if (!existing) {\n // Reachable when another caller deletes the conflicting row\n // between the failed conditional UPDATE and this SELECT (e.g.\n // for the lock primitive: a `releaseLock` lands in the gap).\n // Rare but real; surface it honestly so observability tracks\n // races vs invariant breaks.\n throw new TableClientError(\n 'tryClaim: conflicting row was deleted concurrently — retry the operation',\n )\n }\n return { acquired: false, row: existing }\n }\n\n /**\n * Insert many rows in a single statement. Capped at {@link MAX_BATCH}.\n * Returns all inserted rows in input order.\n */\n async insertMany(values: InferInsertModel<TTable>[]): Promise<Row<TCols>[]> {\n if (!Array.isArray(values)) {\n throw new TableClientError('insertMany requires an array of rows')\n }\n if (values.length === 0) return []\n if (values.length > MAX_BATCH) {\n throw new TableClientError(`insertMany batch too large: ${values.length} > ${MAX_BATCH}`)\n }\n const rows = await this.db\n .insert(this.table)\n .values(values.map((v) => this.stamp(v)))\n .returning()\n return rows as Row<TCols>[]\n }\n\n /**\n * Update **at most one** row matching the where clause. Returns the\n * updated row, or `null` if no row matched.\n *\n * Throws `TableClientError` if:\n * - the where is empty\n * - the where matches more than one row\n *\n * The multi-row guard exists to prevent silent bulk updates from a\n * mistakenly-broad where. If you genuinely want to update multiple\n * rows in one call, use {@link updateMany}, which is explicit about\n * its plurality.\n *\n * Implementation note: this issues a single `UPDATE … RETURNING` and\n * inspects the returned row count *after* the fact. The extra rows\n * over the wire are the cost of the safety check; the alternative\n * (a SELECT round-trip first) would be more rows AND a race window.\n * If the affected count is > 1 the update has *already* happened —\n * the throw is to surface the bug, not to prevent it. Wrap in a\n * transaction if you need atomic rollback.\n */\n async update(\n where: WhereClause<TCols>,\n patch: Partial<InferInsertModel<TTable>>,\n ): Promise<Row<TCols> | null> {\n this.requireNonEmptyWhere(where, 'update')\n this.validateColumnKeys(Object.keys(patch as object), 'update')\n this.refusePartitionPatch(Object.keys(patch as object), 'update')\n const condition = this.buildWhereOrThrow(where)\n const rows = await this.db.update(this.table).set(patch).where(condition).returning()\n if (rows.length > 1) {\n throw new TableClientError(\n `update matched ${rows.length} rows but is expected to match at most one — use updateMany for bulk updates. ` +\n `WARNING: the update has already been applied; wrap in a transaction if you need rollback.`,\n )\n }\n return (rows[0] as Row<TCols> | undefined) ?? null\n }\n\n /**\n * Update all rows matching the where clause. Returns every updated row.\n * Throws if the where is empty — there is no \"update everything\" path.\n */\n async updateMany(\n where: WhereClause<TCols>,\n patch: Partial<InferInsertModel<TTable>>,\n ): Promise<Row<TCols>[]> {\n this.requireNonEmptyWhere(where, 'updateMany')\n this.validateColumnKeys(Object.keys(patch as object), 'updateMany')\n this.refusePartitionPatch(Object.keys(patch as object), 'updateMany')\n const condition = this.buildWhereOrThrow(where)\n const rows = await this.db.update(this.table).set(patch).where(condition).returning()\n return rows as Row<TCols>[]\n }\n\n /**\n * Delete **at most one** row matching the where clause. Returns the\n * deleted row, or `null` if no row matched.\n *\n * Throws `TableClientError` if:\n * - the where is empty\n * - the where matches more than one row\n *\n * Same multi-row safety guard as {@link update}. Use {@link deleteMany}\n * for explicit bulk deletes.\n *\n * Implementation note: same as `update` — the delete has already\n * happened by the time the throw fires. Wrap in a transaction if you\n * need atomic rollback.\n */\n async delete(where: WhereClause<TCols>): Promise<Row<TCols> | null> {\n this.requireNonEmptyWhere(where, 'delete')\n const condition = this.buildWhereOrThrow(where)\n const rows = await this.db.delete(this.table).where(condition).returning()\n if (rows.length > 1) {\n throw new TableClientError(\n `delete matched ${rows.length} rows but is expected to match at most one — use deleteMany for bulk deletes. ` +\n `WARNING: the delete has already been applied; wrap in a transaction if you need rollback.`,\n )\n }\n return (rows[0] as Row<TCols> | undefined) ?? null\n }\n\n /**\n * Delete all rows matching the where clause. Returns every deleted row.\n * Throws if the where is empty — there is no \"delete everything\" path.\n */\n async deleteMany(where: WhereClause<TCols>): Promise<Row<TCols>[]> {\n this.requireNonEmptyWhere(where, 'deleteMany')\n const condition = this.buildWhereOrThrow(where)\n const rows = await this.db.delete(this.table).where(condition).returning()\n return rows as Row<TCols>[]\n }\n\n // -------------------------------------------------------------------------\n // Transactions\n // -------------------------------------------------------------------------\n\n /**\n * Run a callback inside a database transaction. The callback receives a\n * transactional `TableClient` instance with the same shape as the\n * top-level client — write code identically inside or outside.\n *\n * Throwing from the callback rolls back; returning a value commits.\n *\n * @example\n * ```ts\n * await files.client.transaction(async (tx) => {\n * const row = await tx.insert(values)\n * await adapter.upload(row.key, buffer) // if this throws, the row is rolled back\n * return row\n * })\n * ```\n */\n async transaction<T>(fn: (tx: TableClient<TCols, TTable>) => Promise<T>): Promise<T> {\n return await this.db.transaction(async (innerDb) => {\n const txClient = new TableClient<TCols, TTable>(\n this.table,\n this.columnKinds,\n innerDb as unknown as PostgresJsDatabase,\n this.primaryKeyColumns,\n this.partition,\n )\n return await fn(txClient)\n })\n }\n\n // -------------------------------------------------------------------------\n // Advanced operations\n // -------------------------------------------------------------------------\n\n /**\n * Atomically claim rows using `FOR UPDATE SKIP LOCKED`.\n *\n * Selects rows matching `where`, locks them (skipping any already locked\n * by another worker), updates them with `set`/`setSql`, and returns the\n * claimed rows in a single atomic operation. This is the standard pattern\n * for job queues, task distribution, and any producer-consumer table.\n *\n * Requires the table to have a declared primary key (either via\n * `column.uuid({ primaryKey: true })` or `defineTable({ primaryKey })`.\n *\n * @example Job queue claim\n * ```ts\n * const jobs = await client.claim({\n * where: { status: 'pending', runAt: { lte: new Date() } },\n * set: { status: 'processing', lockedBy: workerId, lockedAt: new Date() },\n * setSql: { attempts: sql`${jobsTable.table.attempts} + 1` },\n * orderBy: [{ column: 'priority', dir: 'desc' }, { column: 'createdAt', dir: 'asc' }],\n * limit: 5,\n * })\n * ```\n */\n async claim(options: ClaimOptions<TCols, TTable>): Promise<Row<TCols>[]> {\n // --- validation ---\n if (this.primaryKeyColumns.length === 0) {\n throw new TableClientError(\n 'claim() requires the table to have a primary key — ' +\n 'declare primaryKey in defineTable() or use column.uuid({ primaryKey: true })',\n )\n }\n if (this.primaryKeyColumns.length > 1) {\n throw new TableClientError(\n 'claim() does not support composite primary keys — ' +\n 'use a single-column primary key or call transaction() with FOR UPDATE directly',\n )\n }\n this.requireNonEmptyWhere(options.where, 'claim')\n if (!Number.isInteger(options.limit) || options.limit < 1) {\n throw new TableClientError(`claim limit must be a positive integer, got ${options.limit}`)\n }\n if (options.limit > MAX_LIMIT) {\n throw new TableClientError(`claim limit ${options.limit} exceeds MAX_LIMIT ${MAX_LIMIT}`)\n }\n\n // Validate set/setSql have at least one field, and that every named\n // column actually exists. Using one helper keeps the literal-set path\n // and the SQL-expression path consistent (same error message shape,\n // same column resolution).\n const setKeys = Object.keys(options.set ?? {})\n const setSqlKeys = Object.keys(options.setSql ?? {})\n if (setKeys.length === 0 && setSqlKeys.length === 0) {\n throw new TableClientError('claim requires at least one field in set or setSql')\n }\n this.validateColumnKeys(setKeys, 'claim set')\n this.validateColumnKeys(setSqlKeys, 'claim setSql')\n this.refusePartitionPatch([...setKeys, ...setSqlKeys], 'claim')\n\n const whereCondition = this.buildWhereOrThrow(options.where)\n const orderExprs =\n options.orderBy && options.orderBy.length > 0\n ? this.buildOrderBy(options.orderBy, 'claim')\n : undefined\n\n // Resolve PK column for the SELECT. `primaryKeyColumns` is validated\n // non-empty + single-column above, so [0] is always defined.\n const pkName = this.primaryKeyColumns[0] as string\n const pkCol = this.getColumnOrThrow(pkName, 'claim primary key')\n\n // Two-step atomic claim inside a transaction:\n // 1. SELECT ... FOR UPDATE SKIP LOCKED (Drizzle's .for() method)\n // 2. UPDATE ... WHERE id IN (locked ids) RETURNING *\n // Both use Drizzle's typed builder — no raw SQL, no Date serialization issues.\n return await this.db.transaction(async (tx) => {\n // Step 1: Lock rows\n let selectQuery = tx\n .select({ _pk: pkCol })\n .from(this._selectTable)\n .where(whereCondition)\n .limit(options.limit)\n .for('update', { skipLocked: true })\n .$dynamic()\n\n if (orderExprs) {\n selectQuery = selectQuery.orderBy(...orderExprs)\n }\n\n const locked = await selectQuery\n if (locked.length === 0) return []\n\n const lockedIds = locked.map((r) => (r as Record<string, unknown>)._pk)\n\n // Step 2: Update locked rows\n // Merge set (literal values) and setSql (SQL expressions) into one object.\n // `PgUpdateSetSource<TTable>` natively accepts either column values OR\n // `SQL` expressions for each column, which is exactly what we want.\n const setObj: PgUpdateSetSource<TTable> = {\n ...(options.set ?? {}),\n } as PgUpdateSetSource<TTable>\n if (options.setSql) {\n for (const [key, expr] of Object.entries(options.setSql)) {\n ;(setObj as Record<string, SQL>)[key] = expr\n }\n }\n\n const rows = await tx\n .update(this.table)\n .set(setObj)\n .where(\n inArray(pkCol, lockedIds as InferSelectModel<TTable>[keyof InferSelectModel<TTable>][]),\n )\n .returning()\n\n return rows as Row<TCols>[]\n })\n }\n\n /**\n * Run an aggregate query with GROUP BY.\n *\n * Each key in `select` is an output alias mapped to an aggregate\n * specification. The `groupBy` columns are included automatically in\n * the result. Returns typed rows with grouped column values plus the\n * computed aggregates.\n *\n * @example Count jobs by status\n * ```ts\n * const rows = await client.aggregate({\n * select: { count: { fn: 'count' } },\n * groupBy: ['status'],\n * })\n * // → [{ status: 'pending', count: 12 }, { status: 'completed', count: 45 }]\n * ```\n *\n * @example Average priority of open jobs\n * ```ts\n * const [row] = await client.aggregate({\n * select: { avgPriority: { fn: 'avg', column: 'priority' } },\n * where: { status: 'open' },\n * })\n * ```\n */\n async aggregate<TResult extends Record<string, unknown> = Record<string, unknown>>(\n options: AggregateOptions<TCols>,\n ): Promise<TResult[]> {\n const selectEntries = Object.entries(options.select)\n if (selectEntries.length === 0) {\n throw new TableClientError('aggregate requires at least one select field')\n }\n\n // Validate output aliases up front. Drizzle quotes identifier aliases\n // when emitting `... AS \"alias\"`, but accepting only the standard SQL\n // identifier shape removes that implicit reliance and keeps result\n // keys safe to interpolate elsewhere downstream.\n for (const [alias] of selectEntries) {\n if (!SQL_IDENTIFIER_RE.test(alias)) {\n throw new TableClientError(\n `aggregate: invalid output alias \"${alias}\" — must match ${SQL_IDENTIFIER_RE.source}`,\n )\n }\n }\n\n // Build the select object: group-by columns first, then aggregate\n // expressions. `getColumnOrThrow` is the single column-resolution path.\n const selectObj: Record<string, AnyPgColumn | SQL> = {}\n if (options.groupBy) {\n for (const key of options.groupBy) {\n selectObj[key] = this.getColumnOrThrow(key, 'aggregate groupBy')\n }\n }\n for (const [alias, spec] of selectEntries) {\n selectObj[alias] = this.buildAggregateExpr(spec)\n }\n\n let query = this.db.select(selectObj).from(this._selectTable).$dynamic()\n\n const aggregateWhere = options.where\n ? this.buildWhereOrThrow(options.where)\n : this.partitionCondition()\n if (aggregateWhere) {\n query = query.where(aggregateWhere)\n }\n\n if (options.groupBy && options.groupBy.length > 0) {\n const groupCols = options.groupBy.map((key) =>\n this.getColumnOrThrow(key, 'aggregate groupBy'),\n )\n query = query.groupBy(...groupCols)\n }\n\n if (options.orderBy && options.orderBy.length > 0) {\n query = query.orderBy(...this.buildOrderBy(options.orderBy, 'aggregate'))\n }\n\n if (options.limit !== undefined) {\n query = query.limit(this.validateLimit(options.limit))\n }\n\n return (await query) as TResult[]\n }\n\n /**\n * Fast approximate row count using Postgres `reltuples` statistics.\n *\n * Returns the estimated row count from `pg_class` — updated by\n * `ANALYZE` and autovacuum. Much faster than `COUNT(*)` on large\n * tables (no sequential scan), but may be stale by up to a few\n * percent. Returns 0 for tables that have never been analyzed.\n *\n * Use `count()` when you need an exact number. Use `countEstimate()`\n * for UI display, pagination hints, or \"is this table large?\" checks\n * where ±5% accuracy is fine.\n *\n * @example\n * ```ts\n * const approx = await client.countEstimate()\n * if (approx > 100_000) {\n * // Switch to keyset pagination instead of offset\n * }\n * ```\n */\n async countEstimate(): Promise<number> {\n if (this.partition) {\n throw new TableClientError(\n 'countEstimate reads table-wide statistics and cannot be confined to a partition — use count()',\n )\n }\n const tableName = getTableName(this.table)\n\n const [row] = await this.db\n .select({ estimate: sql<number>`greatest(${pgClassTable.reltuples}::bigint, 0)` })\n .from(pgClassTable)\n .where(eq(pgClassTable.relname, tableName))\n .limit(1)\n\n return row ? Number(row.estimate) : 0\n }\n\n // -------------------------------------------------------------------------\n // Internals\n // -------------------------------------------------------------------------\n\n /** Build a single aggregate SQL expression from a spec. */\n private buildAggregateExpr(spec: AggregateSpec<TCols>): SQL {\n if ('column' in spec && spec.column) {\n const col = this.getColumnOrThrow(spec.column, 'aggregate')\n switch (spec.fn) {\n case 'count':\n return sql<number>`count(${col})::int`\n case 'sum':\n return sql<number>`sum(${col})`\n case 'avg':\n return sql<number>`avg(${col})`\n case 'min':\n return sql`min(${col})`\n case 'max':\n return sql`max(${col})`\n default:\n throw new TableClientError(`aggregate: unknown function \"${(spec as { fn: string }).fn}\"`)\n }\n }\n // count without column = COUNT(*)\n if (spec.fn === 'count') {\n return sql<number>`count(*)::int`\n }\n throw new TableClientError(`aggregate: ${spec.fn} requires a column`)\n }\n\n private buildWhereOrThrow(where: WhereClause<TCols>): SQL {\n try {\n const condition = buildWhere(this.table, this.columnKinds, where)\n if (!condition) {\n throw new TableClientError('where clause produced no SQL predicate')\n }\n // A partitioned client ANDs its partition into EVERY caller predicate —\n // here, the one place every WHERE is built.\n return this.confine(condition) ?? condition\n } catch (err) {\n if (err instanceof WhereBuilderError) {\n throw new TableClientError(err.message.replace(/^WhereBuilder: /, ''))\n }\n throw err\n }\n }\n\n private validateLimit(limit: number | undefined): number {\n if (limit === undefined) return DEFAULT_LIMIT\n if (!Number.isInteger(limit) || limit < 1) {\n throw new TableClientError(`limit must be a positive integer, got ${limit}`)\n }\n // Strict throw — *not* silent clamping. The previous behaviour\n // (clamp + console.warn) was the worst of both worlds: callers got\n // a quietly different result than they asked for, AND the warn\n // didn't go through the project logger. If a caller passes a limit\n // above MAX_LIMIT it's almost certainly a bug. Make them be\n // explicit: either pass a value within the cap, or refactor to\n // paginate / use claim() (PR 4) for batch processing.\n if (limit > MAX_LIMIT) {\n throw new TableClientError(\n `limit ${limit} exceeds MAX_LIMIT ${MAX_LIMIT} — paginate or use claim() for batch processing`,\n )\n }\n return limit\n }\n}\n","/**\n * Column builders for `defineTable`.\n *\n * Each builder returns a {@link ColumnFactory} — a callable that, given a\n * column name, produces the corresponding Drizzle column builder. Phantom\n * type parameters carry the JS type, \"kind\" tag, nullability, and\n * has-default flag through to the row/insert/where types.\n *\n * Conventions vs. `@murumets-ee/entity` field builders:\n * - `notNull` here is the equivalent of `required` in field builders.\n * Renamed because tables don't have a UI distinction between\n * \"required at the schema level\" and \"required to display\".\n * - No `translatable`, `indexed`, `unique`, `access` — those are entity-only\n * concepts. `unique` and indexes are declared at the table level instead.\n * - `column.jsonb<T>()` accepts a phantom type parameter for typed reads.\n *\n * @example\n * ```ts\n * column.uuid({ primaryKey: true, defaultRandom: true })\n * column.varchar({ length: 255, notNull: true })\n * column.timestamp({ notNull: true, defaultNow: true, withTimezone: true })\n * column.jsonb<{ description: string; tags: string[] }>({ notNull: true })\n * ```\n */\n\nimport {\n type AnyPgColumn,\n bigint,\n boolean,\n doublePrecision,\n integer,\n jsonb,\n type PgColumnBuilderBase,\n text,\n timestamp,\n uuid,\n varchar,\n} from 'drizzle-orm/pg-core'\nimport type { ColumnFactory, ColumnKind } from './types.js'\n\n// ---------------------------------------------------------------------------\n// Internal: factory constructor\n// ---------------------------------------------------------------------------\n\n/**\n * Wrap a Drizzle column-builder factory function with the phantom metadata\n * needed for type inference. The phantom fields are non-enumerable so they\n * don't pollute logging or JSON serialization.\n */\nfunction makeFactory<\n TType,\n TKind extends ColumnKind,\n TNotNull extends boolean,\n THasDefault extends boolean,\n>(\n build: (name: string) => PgColumnBuilderBase,\n meta: {\n kind: TKind\n notNull: TNotNull\n hasDefault: THasDefault\n pgName?: string | undefined\n primaryKey?: boolean | undefined\n },\n): ColumnFactory<TType, TKind, TNotNull, THasDefault> {\n const factory = build as ColumnFactory<TType, TKind, TNotNull, THasDefault>\n Object.defineProperty(factory, '__kind', { value: meta.kind, enumerable: false })\n Object.defineProperty(factory, '__notNull', { value: meta.notNull, enumerable: false })\n Object.defineProperty(factory, '__hasDefault', { value: meta.hasDefault, enumerable: false })\n // __type is purely a phantom — never read at runtime\n Object.defineProperty(factory, '__type', { value: undefined, enumerable: false })\n if (meta.pgName) {\n Object.defineProperty(factory, '__pgName', { value: meta.pgName, enumerable: false })\n }\n if (meta.primaryKey) {\n Object.defineProperty(factory, '__primaryKey', { value: true, enumerable: false })\n }\n return factory\n}\n\n// ---------------------------------------------------------------------------\n// Overloaded type interface\n// ---------------------------------------------------------------------------\n\n/**\n * Type interface for the `column` builder namespace.\n *\n * Overloads discriminate on whether `default` is present, so\n * `THasDefault` is inferred correctly:\n *\n * - `column.varchar({ length: 20, default: 'x' })` → `hasDefault = true`\n * - `column.varchar({ length: 20 })` → `hasDefault = false`\n *\n * Object-literal methods don't support overloads in TypeScript, so we\n * declare the overloaded signatures on an interface and cast the\n * implementation object.\n */\ninterface ColumnBuilders {\n uuid<\n TPrimaryKey extends boolean = false,\n TNotNull extends boolean = false,\n THasDefault extends boolean = false,\n >(opts?: {\n primaryKey?: TPrimaryKey\n notNull?: TNotNull\n defaultRandom?: THasDefault\n pgName?: string\n }): ColumnFactory<\n string,\n 'uuid',\n TPrimaryKey extends true ? true : TNotNull extends true ? true : false,\n TPrimaryKey extends true ? true : THasDefault extends true ? true : false\n >\n\n // varchar: with `default` → hasDefault = true\n varchar<TNotNull extends boolean = false>(opts: {\n length: number\n notNull?: TNotNull\n default: string\n pgName?: string\n }): ColumnFactory<string, 'varchar', TNotNull extends true ? true : false, true>\n // varchar: without `default` → hasDefault = false\n varchar<TNotNull extends boolean = false>(opts: {\n length: number\n notNull?: TNotNull\n pgName?: string\n }): ColumnFactory<string, 'varchar', TNotNull extends true ? true : false, false>\n\n // text: with `default` → hasDefault = true\n text<TNotNull extends boolean = false>(opts: {\n notNull?: TNotNull\n default: string\n pgName?: string\n }): ColumnFactory<string, 'text', TNotNull extends true ? true : false, true>\n // text: without `default` → hasDefault = false\n text<TNotNull extends boolean = false>(opts?: {\n notNull?: TNotNull\n pgName?: string\n }): ColumnFactory<string, 'text', TNotNull extends true ? true : false, false>\n\n // integer: with `default` → hasDefault = true\n integer<TNotNull extends boolean = false>(opts: {\n notNull?: TNotNull\n default: number\n pgName?: string\n }): ColumnFactory<number, 'integer', TNotNull extends true ? true : false, true>\n // integer: without `default` → hasDefault = false\n integer<TNotNull extends boolean = false>(opts?: {\n notNull?: TNotNull\n pgName?: string\n }): ColumnFactory<number, 'integer', TNotNull extends true ? true : false, false>\n\n // bigint: with `default` → hasDefault = true\n bigint<TMode extends 'number' | 'bigint' = 'number', TNotNull extends boolean = false>(opts: {\n mode?: TMode\n notNull?: TNotNull\n default: TMode extends 'bigint' ? bigint : number\n pgName?: string\n }): ColumnFactory<\n TMode extends 'bigint' ? bigint : number,\n 'bigint',\n TNotNull extends true ? true : false,\n true\n >\n // bigint: without `default` → hasDefault = false\n bigint<TMode extends 'number' | 'bigint' = 'number', TNotNull extends boolean = false>(opts?: {\n mode?: TMode\n notNull?: TNotNull\n pgName?: string\n }): ColumnFactory<\n TMode extends 'bigint' ? bigint : number,\n 'bigint',\n TNotNull extends true ? true : false,\n false\n >\n\n // double: with `default` → hasDefault = true\n double<TNotNull extends boolean = false>(opts: {\n notNull?: TNotNull\n default: number\n pgName?: string\n }): ColumnFactory<number, 'double', TNotNull extends true ? true : false, true>\n // double: without `default` → hasDefault = false\n double<TNotNull extends boolean = false>(opts?: {\n notNull?: TNotNull\n pgName?: string\n }): ColumnFactory<number, 'double', TNotNull extends true ? true : false, false>\n\n // boolean: with `default` → hasDefault = true\n boolean<TNotNull extends boolean = false>(opts: {\n notNull?: TNotNull\n default: boolean\n pgName?: string\n }): ColumnFactory<boolean, 'boolean', TNotNull extends true ? true : false, true>\n // boolean: without `default` → hasDefault = false\n boolean<TNotNull extends boolean = false>(opts?: {\n notNull?: TNotNull\n pgName?: string\n }): ColumnFactory<boolean, 'boolean', TNotNull extends true ? true : false, false>\n\n timestamp<TNotNull extends boolean = false, THasDefault extends boolean = false>(opts?: {\n notNull?: TNotNull\n defaultNow?: THasDefault\n withTimezone?: boolean\n pgName?: string\n }): ColumnFactory<\n Date,\n 'timestamp',\n TNotNull extends true ? true : false,\n THasDefault extends true ? true : false\n >\n\n // jsonb: with `default` → hasDefault = true\n jsonb<T = unknown, TNotNull extends boolean = false>(opts: {\n notNull?: TNotNull\n default: T\n pgName?: string\n }): ColumnFactory<T, 'jsonb', TNotNull extends true ? true : false, true>\n // jsonb: without `default` → hasDefault = false\n jsonb<T = unknown, TNotNull extends boolean = false>(opts?: {\n notNull?: TNotNull\n pgName?: string\n }): ColumnFactory<T, 'jsonb', TNotNull extends true ? true : false, false>\n\n uuidArray<TNotNull extends boolean = false>(opts?: {\n notNull?: TNotNull\n pgName?: string\n }): ColumnFactory<string[], 'uuidArray', TNotNull extends true ? true : false, false>\n}\n\n// ---------------------------------------------------------------------------\n// Public column builders\n// ---------------------------------------------------------------------------\n\n/**\n * Column builder namespace.\n *\n * Each builder produces a {@link ColumnFactory}. Pass the result to a\n * `defineTable` `columns` field. The builders are typed so that the\n * resulting `Row` and `InsertRow` types track each column's value type\n * and nullability.\n *\n * @example\n * ```ts\n * import { column, defineTable } from '@murumets-ee/db'\n *\n * const jobs = defineTable({\n * name: 'toolkit_jobs',\n * columns: {\n * id: column.uuid({ primaryKey: true, defaultRandom: true }),\n * type: column.varchar({ length: 100, notNull: true }),\n * payload: column.jsonb<{ subject: string; body: string }>({ notNull: true }),\n * status: column.varchar({ length: 20, notNull: true, default: 'pending' }),\n * priority: column.integer({ notNull: true, default: 0 }),\n * runAt: column.timestamp({ notNull: true, defaultNow: true, withTimezone: true }),\n * },\n * })\n * ```\n */\n// Implementation object — no type annotation here so each method can\n// return `makeFactory(...)` without per-method casts. The overloaded\n// `ColumnBuilders` interface is applied once at the export below.\nconst _column = {\n /**\n * UUID column.\n *\n * For primary keys, use `{ primaryKey: true, defaultRandom: true }` to\n * get a server-generated UUIDv4.\n */\n uuid(opts?: {\n primaryKey?: boolean\n notNull?: boolean\n defaultRandom?: boolean\n pgName?: string\n }) {\n const notNull = (opts?.notNull ?? false) as boolean\n const hasDefault = (opts?.defaultRandom ?? false) as boolean\n const isPk = (opts?.primaryKey ?? false) as boolean\n return makeFactory(\n (name) => {\n let col = uuid(name)\n if (isPk) col = col.primaryKey()\n if (opts?.defaultRandom) col = col.defaultRandom()\n // Primary keys are implicitly NOT NULL in Postgres; only call notNull for non-PK\n if (notNull && !isPk) col = col.notNull()\n return col\n },\n {\n kind: 'uuid',\n notNull: (notNull || isPk) as boolean,\n hasDefault: (hasDefault || isPk) as boolean,\n pgName: opts?.pgName,\n primaryKey: isPk || undefined,\n },\n )\n },\n\n /**\n * Variable-length string column. `length` is required and must be a\n * positive integer up to Postgres' actual `varchar` ceiling\n * (1073741823 — i.e. ~1GB / 4 bytes per char). For unbounded text use\n * `column.text()` instead — it stores the same way and skips the\n * length check at write time.\n *\n * Conventional sizes for reference:\n * - 50-100: short identifiers, slugs, status codes\n * - 255: legacy \"common max length\" — fits a tweet, an email, etc.\n * - 1024: file paths, URLs\n * - 10000+: prefer `text` instead, varchar with very large lengths\n * buys you nothing\n */\n varchar(opts: { length: number; notNull?: boolean; default?: string; pgName?: string }) {\n // Postgres' real varchar limit is 1073741823 (varchar without length\n // mod uses TEXT internally). We enforce the same upper bound so the\n // error fires at definition time instead of at INSERT time.\n if (!Number.isInteger(opts.length) || opts.length < 1 || opts.length > 1073741823) {\n throw new Error(\n `column.varchar: length must be an integer between 1 and 1073741823, got ${opts.length}`,\n )\n }\n const notNull = (opts.notNull ?? false) as boolean\n const hasDefault = opts.default !== undefined\n return makeFactory(\n (name) => {\n let col = varchar(name, { length: opts.length })\n if (notNull) col = col.notNull()\n if (opts.default !== undefined) col = col.default(opts.default)\n return col\n },\n {\n kind: 'varchar',\n notNull: notNull as boolean,\n hasDefault: hasDefault as boolean,\n pgName: opts.pgName,\n },\n )\n },\n\n /**\n * Unbounded text column.\n */\n text(opts?: { notNull?: boolean; default?: string; pgName?: string }) {\n const notNull = (opts?.notNull ?? false) as boolean\n const hasDefault = opts?.default !== undefined\n return makeFactory(\n (name) => {\n let col = text(name)\n if (notNull) col = col.notNull()\n if (opts?.default !== undefined) col = col.default(opts.default)\n return col\n },\n {\n kind: 'text',\n notNull: notNull as boolean,\n hasDefault: hasDefault as boolean,\n pgName: opts?.pgName,\n },\n )\n },\n\n /**\n * 32-bit integer column.\n */\n integer(opts?: { notNull?: boolean; default?: number; pgName?: string }) {\n const notNull = (opts?.notNull ?? false) as boolean\n const hasDefault = opts?.default !== undefined\n return makeFactory(\n (name) => {\n let col = integer(name)\n if (notNull) col = col.notNull()\n if (opts?.default !== undefined) col = col.default(opts.default)\n return col\n },\n {\n kind: 'integer',\n notNull: notNull as boolean,\n hasDefault: hasDefault as boolean,\n pgName: opts?.pgName,\n },\n )\n },\n\n /**\n * 64-bit integer column. `mode: 'number'` returns a JS number (safe for\n * values up to 2^53), `mode: 'bigint'` returns a JS BigInt.\n */\n bigint(opts?: {\n mode?: 'number' | 'bigint'\n notNull?: boolean\n default?: number | bigint\n pgName?: string\n }) {\n const mode = (opts?.mode ?? 'number') as 'number' | 'bigint'\n const notNull = (opts?.notNull ?? false) as boolean\n const hasDefault = opts?.default !== undefined\n return makeFactory(\n (name) => {\n let col = bigint(name, { mode })\n if (notNull) col = col.notNull()\n if (opts?.default !== undefined) col = col.default(opts.default as never)\n return col\n },\n {\n kind: 'bigint',\n notNull: notNull as boolean,\n hasDefault: hasDefault as boolean,\n pgName: opts?.pgName,\n },\n )\n },\n\n /**\n * Double-precision floating-point column.\n */\n double(opts?: { notNull?: boolean; default?: number; pgName?: string }) {\n const notNull = (opts?.notNull ?? false) as boolean\n const hasDefault = opts?.default !== undefined\n return makeFactory(\n (name) => {\n let col = doublePrecision(name)\n if (notNull) col = col.notNull()\n if (opts?.default !== undefined) col = col.default(opts.default)\n return col\n },\n {\n kind: 'double',\n notNull: notNull as boolean,\n hasDefault: hasDefault as boolean,\n pgName: opts?.pgName,\n },\n )\n },\n\n /**\n * Boolean column. Defaults to `false` if no explicit default is supplied\n * AND `notNull` is true — matches the entity package convention.\n */\n boolean(opts?: { notNull?: boolean; default?: boolean; pgName?: string }) {\n const notNull = (opts?.notNull ?? false) as boolean\n const hasDefault = opts?.default !== undefined\n return makeFactory(\n (name) => {\n let col = boolean(name)\n if (notNull) col = col.notNull()\n if (opts?.default !== undefined) col = col.default(opts.default)\n return col\n },\n {\n kind: 'boolean',\n notNull: notNull as boolean,\n hasDefault: hasDefault as boolean,\n pgName: opts?.pgName,\n },\n )\n },\n\n /**\n * Timestamp column. `withTimezone: true` is strongly recommended for all\n * timestamps in this codebase — entity audit fields use it consistently.\n */\n timestamp(opts?: {\n notNull?: boolean\n defaultNow?: boolean\n withTimezone?: boolean\n pgName?: string\n }) {\n const notNull = (opts?.notNull ?? false) as boolean\n const hasDefault = (opts?.defaultNow ?? false) as boolean\n const withTimezone = opts?.withTimezone ?? true\n return makeFactory(\n (name) => {\n let col = timestamp(name, { withTimezone })\n if (notNull) col = col.notNull()\n if (opts?.defaultNow) col = col.defaultNow()\n return col\n },\n {\n kind: 'timestamp',\n notNull: notNull as boolean,\n hasDefault: hasDefault as boolean,\n pgName: opts?.pgName,\n },\n )\n },\n\n /**\n * JSONB column. The phantom type parameter `T` propagates to row reads\n * and where-clause shorthand for the whole-column case. For dotted-path\n * access via the where-builder, the value type at the path is `unknown`\n * (TypeScript can't follow runtime path navigation).\n */\n jsonb(opts?: { notNull?: boolean; default?: unknown; pgName?: string }) {\n const notNull = (opts?.notNull ?? false) as boolean\n const hasDefault = opts?.default !== undefined\n return makeFactory(\n (name) => {\n let col = jsonb(name)\n if (notNull) col = col.notNull()\n if (opts?.default !== undefined) col = col.default(opts.default)\n return col\n },\n {\n kind: 'jsonb',\n notNull: notNull as boolean,\n hasDefault: hasDefault as boolean,\n pgName: opts?.pgName,\n },\n )\n },\n\n /**\n * UUID array column (`uuid[]`).\n */\n uuidArray(opts?: { notNull?: boolean; pgName?: string }) {\n const notNull = (opts?.notNull ?? false) as boolean\n return makeFactory(\n (name) => {\n // `PgArrayBuilder` (returned by `.array()`) extends `PgColumnBuilder`,\n // which is what `pgTable(...)` ultimately consumes — no cast needed.\n const col = uuid(name).array()\n return notNull ? col.notNull() : col\n },\n {\n kind: 'uuidArray',\n notNull: notNull as boolean,\n hasDefault: false as false,\n pgName: opts?.pgName,\n },\n )\n },\n}\n\n/**\n * Column builder namespace — overloaded interface applied via single\n * assertion. Each builder's implementation returns `makeFactory(...)` with\n * widened boolean params; the `ColumnBuilders` overloads narrow them for\n * callers based on whether `default`/`primaryKey` etc. are present.\n */\nexport const column: ColumnBuilders = _column as ColumnBuilders\n\n// Re-export the Drizzle column type for advanced consumers that need to\n// reach into the underlying pgTable for typed JOINs.\nexport type { AnyPgColumn }\n","/**\n * Dependency-cleanup wiring for `defineTable` infra tables.\n *\n * Two stores, two readers:\n *\n * 1. **Table-keyed descriptors** (WeakMap) — populated by `defineTable` at\n * construction time. Keyed by the raw Drizzle table object because that\n * is the only handle the framework merge step has (plugins contribute\n * `definedTable.table`, not the `DefinedTable` wrapper). The merge step\n * reads these and copies them into the entity-keyed registry below.\n *\n * 2. **Entity-keyed registry** (Map) — populated by the merge step at boot,\n * read by `AdminClient.delete*` at delete time. Keyed by the entity name\n * whose deletion triggers the cleanup.\n *\n * Splitting the two keeps merge-time scoping: only tables actually included\n * in the app (via a plugin's `tables` slot) get their cleanups registered —\n * exactly like `schemaRegistry`. A table defined but not included never\n * registers a cleanup, so a delete never targets a relation that isn't in\n * the database.\n */\n\nimport type { Table } from 'drizzle-orm'\nimport type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'\n\n/** Runs the typed cleanup for one dependency, inside the caller's tx. */\nexport type DependentCleanup = (tx: PostgresJsDatabase, entityIds: string[]) => Promise<void>\n\n/** A single table→entity cleanup descriptor produced by `defineTable`. */\nexport interface TableCleanupDescriptor {\n /** Entity whose deletion triggers this cleanup. */\n entity: string\n /** The typed, tx-bound cleanup executor. */\n cleanup: DependentCleanup\n}\n\n// ── Entity-keyed registry (read by AdminClient.delete*) ──────────────────\n\nconst cleanupsByEntity = new Map<string, DependentCleanup[]>()\n\n/** Register a cleanup executor for `entityName`. Called by the merge step. */\nexport function registerDependentCleanup(entityName: string, cleanup: DependentCleanup): void {\n const existing = cleanupsByEntity.get(entityName)\n if (existing) existing.push(cleanup)\n else cleanupsByEntity.set(entityName, [cleanup])\n}\n\n/** All cleanup executors declared as depending on `entityName` (may be empty). */\nexport function getDependentCleanups(entityName: string): readonly DependentCleanup[] {\n return cleanupsByEntity.get(entityName) ?? []\n}\n\n/** Test seam — clears all entity-keyed registrations. */\nexport function __clearDependentCleanups(): void {\n cleanupsByEntity.clear()\n}\n\n// ── Table-keyed descriptors (populated by defineTable, read by merge step) ─\n\nconst descriptorsByTable = new WeakMap<Table, TableCleanupDescriptor[]>()\n\n/** Record the cleanup descriptors a table contributes. Called by `defineTable`. */\nexport function setTableCleanupDescriptors(\n table: Table,\n descriptors: TableCleanupDescriptor[],\n): void {\n descriptorsByTable.set(table, descriptors)\n}\n\n/** Read the cleanup descriptors for a raw Drizzle table (may be empty). */\nexport function getTableCleanupDescriptors(table: Table): readonly TableCleanupDescriptor[] {\n return descriptorsByTable.get(table) ?? []\n}\n\n// ── Table-keyed partition columns (populated by defineTable) ─────────────\n\nconst partitionByTable = new WeakMap<Table, string>()\n\n/** Record a table's `partitionColumn`. Called by `defineTable`. */\nexport function setTablePartitionColumn(table: Table, column: string): void {\n partitionByTable.set(table, column)\n}\n\n/**\n * The column a raw Drizzle table is partitioned by (`TableDefinition.\n * partitionColumn`), or `undefined`. Lets whole-install maintenance — the one\n * sanctioned site rename (`lumi site:rename-default`) — find every table that\n * keys rows by a site handle without an FK to cascade through.\n */\nexport function getTablePartitionColumn(table: Table): string | undefined {\n return partitionByTable.get(table)\n}\n","/**\n * Process-wide registry of all `defineTable()` calls.\n *\n * Used by:\n * - **Runtime table lookup** — code paths that hold a table name string\n * can resolve it to a `PgTable` without importing the source file.\n * - **Test setup** — `createTestDb().push()` accepts a record of Drizzle\n * tables; pass `tableRegistry.allTables()` to push every defined table\n * into a fresh test schema in one call.\n * - **Audits / introspection** — list all known infra tables for tooling.\n *\n * Note: NOT used for migration discovery. `lumi migrate` discovers tables\n * via `Plugin.tables` fields + `buildEntitySchemaMap()`.\n *\n * The registry is intentionally separate from `schemaRegistry` (which is\n * for entity-generated schemas) so the two layers stay decoupled. They\n * may be merged in a follow-up if it simplifies migration discovery.\n */\n\nimport { getTableColumns, getTableName } from 'drizzle-orm'\nimport type { PgColumn, PgTable } from 'drizzle-orm/pg-core'\n\ninterface RegistryEntry {\n name: string\n table: PgTable\n /** Source file (best-effort, captured at definition time for debugging). */\n source?: string | undefined\n}\n\n/**\n * Structural signature of a table: its own SQL name plus a stable, sorted\n * `propKey:dbName:sqlType:notNull` per column. Two `defineTable` calls that a bundler\n * DUPLICATED across separate output chunks (the same source code emitted into two files)\n * produce byte-identical tables → identical signatures. Two GENUINELY different tables\n * differ in their SQL name and/or at least one column → different signature. The SQL name\n * is included so two distinct tables that happen to share a column layout (e.g. both are\n * just `{ id: uuid pk }`) are NOT mistaken for a duplicate.\n */\nfunction tableSignature(table: PgTable): string {\n const cols = getTableColumns(table)\n const colSig = Object.entries(cols)\n .map(([key, col]: [string, PgColumn]) => `${key}:${col.name}:${col.getSQLType()}:${col.notNull ? '1' : '0'}`)\n .sort()\n .join('|')\n return `${getTableName(table)}#${colSig}`\n}\n\n/** True iff two tables have an identical column signature (see {@link tableSignature}). */\nfunction sameTableShape(a: PgTable, b: PgTable): boolean {\n try {\n return tableSignature(a) === tableSignature(b)\n } catch {\n // A malformed table can't be proven identical — fall through to the collision throw.\n return false\n }\n}\n\nclass TableRegistryImpl {\n private byName: Map<string, RegistryEntry> = new Map()\n /** Table names we've already surfaced a bundler-duplicate warning for (once per name). */\n private warnedDuplicates: Set<string> = new Set()\n\n /**\n * Register a defined table. Throws if a table with the same name is\n * already registered (catches accidental double-registration from a\n * file being imported under two paths).\n */\n register(name: string, table: PgTable, source?: string): void {\n const existing = this.byName.get(name)\n if (existing) {\n // Same table object → idempotent re-import; allow silently.\n if (existing.table === table) return\n // Different object ref but same source location → bundler dual-evaluated\n // the same module (Turbopack chunking, Next.js dev HMR, dual CJS/ESM\n // resolution). The underlying defineTable call is the same code. Treat\n // as idempotent: keep the first registration, silently drop the second.\n if (existing.source && source && existing.source === source) return\n // Both sources KNOWN and DIFFERENT, and the tables are structurally identical → one\n // logical table a bundler inlined into two SEPARATE output chunks that both load in\n // this process. E.g. `@murumets-ee/blocks` emits its shared `editor_op` table into\n // its `plugin`, `server`, AND `admin` build entries (separate tsdown builds can't\n // share a chunk), so a Node process importing two of them registers the same table\n // from two files (both source paths known + distinct). The definition is the same\n // source code; treat as idempotent (keep the first, drop the duplicate).\n //\n // Requiring both sources present + distinct scopes the relaxation to exactly that\n // case: a MISSING source can't widen it into silently masking an INDEPENDENT\n // same-shape `defineTable()` under the same registry key. A genuine collision — a\n // DIFFERENT shape, or a same-shape table with unknown provenance — still throws.\n if (\n existing.source &&\n source &&\n existing.source !== source &&\n sameTableShape(existing.table, table)\n ) {\n // Tolerated — but NOT silent. Surface it once per table via a Node process\n // warning so the duplication is visible (a build shipping the same table in\n // multiple chunks is a smell worth fixing at the source, even though it's safe\n // here). `process.emitWarning` keeps the `db` leaf logger-free.\n if (!this.warnedDuplicates.has(name)) {\n this.warnedDuplicates.add(name)\n process.emitWarning(\n `table \"${name}\" was registered from two different bundles ` +\n `(first \"${existing.source}\", now \"${source}\"). Tolerated because the ` +\n `definitions are structurally identical — a bundler inlined one defineTable() ` +\n `into separate output chunks. Emit the shared table from a single chunk to ` +\n `remove the duplicate.`,\n { code: 'TOOLKIT_TABLE_REGISTRY_DUPLICATE' },\n )\n }\n return\n }\n throw new Error(\n `tableRegistry: a different table is already registered under name \"${name}\". ` +\n `Did you call defineTable twice? First registered from ${existing.source ?? '<unknown>'}, ` +\n `now from ${source ?? '<unknown>'}.`,\n )\n }\n this.byName.set(name, { name, table, source })\n }\n\n /**\n * Get a defined table by name.\n */\n get(name: string): PgTable | undefined {\n return this.byName.get(name)?.table\n }\n\n /**\n * Returns true if a table with the given name has been registered.\n */\n has(name: string): boolean {\n return this.byName.has(name)\n }\n\n /**\n * Return every registered table as a `{ [name]: PgTable }` map.\n *\n * Useful for `createTestDb().push(tableRegistry.allTables())` and\n * runtime introspection.\n */\n allTables(): Record<string, PgTable> {\n const out: Record<string, PgTable> = {}\n for (const entry of this.byName.values()) {\n out[entry.name] = entry.table\n }\n return out\n }\n\n /**\n * Clear the registry. **Test-only.**\n *\n * Production code should never call this — clearing the registry\n * silently breaks migration discovery and any code path that looks\n * up a table by name. The runtime guard below refuses to run when\n * `NODE_ENV === 'production'` to catch accidental misuse.\n *\n * Test runners typically set `NODE_ENV=test` (Vitest does), but the\n * guard is permissive about other values — it only blocks the one\n * environment where calling it is definitely wrong.\n */\n clear(): void {\n if (process.env.NODE_ENV === 'production') {\n throw new Error(\n 'tableRegistry.clear() is not allowed when NODE_ENV=production — ' +\n 'this is a test-only utility. If you reached this from production ' +\n 'code, you have a bug.',\n )\n }\n this.byName.clear()\n this.warnedDuplicates.clear()\n }\n}\n\n/**\n * Process-wide table registry singleton.\n *\n * Tables register themselves on import via `defineTable()`. The registry\n * is module-level state, which means a single Node process sees one\n * consistent view of all defined tables across packages.\n */\nexport const tableRegistry = new TableRegistryImpl()\n\n/**\n * Create an isolated registry — useful for unit tests that want to\n * verify registration behaviour without polluting the global registry.\n */\nexport function createTableRegistry() {\n return new TableRegistryImpl()\n}\n","/**\n * `defineTable()` — the entry point for the table layer.\n *\n * Given a {@link TableDefinition}, builds:\n *\n * - The underlying Drizzle `pgTable` (exposed for typed JOINs)\n * - A typed {@link TableClient} factory (callers wire in their `db` handle)\n * - The original schema metadata (for introspection / migrations)\n *\n * The table is also registered in the process-wide {@link tableRegistry}\n * so migration tooling and test setup can discover it.\n *\n * @example\n * ```ts\n * import { column, defineTable } from '@murumets-ee/db'\n *\n * export const ticketReadState = defineTable({\n * name: 'ticketing_read_state',\n * columns: {\n * ticketId: column.uuid({ notNull: true }),\n * userId: column.varchar({ length: 255, notNull: true }),\n * lastReadAt: column.timestamp({ notNull: true, defaultNow: true, withTimezone: true }),\n * },\n * primaryKey: ['ticketId', 'userId'],\n * indexes: [{ on: ['userId'] }],\n * })\n *\n * // Use the typed Drizzle table for migrations / typed JOINs:\n * export const ticketReadStateTable = ticketReadState.table\n *\n * // Build a client by wiring in a db handle:\n * const client = ticketReadState.makeClient(db)\n * await client.upsert(...) // (upsert lands in PR 3)\n * ```\n */\n\nimport type { InferInsertModel, SQL } from 'drizzle-orm'\nimport {\n type AnyPgColumn,\n index as drizzleIndex,\n primaryKey as drizzlePrimaryKey,\n unique as drizzleUnique,\n uniqueIndex as drizzleUniqueIndex,\n type PgColumnBuilderBase,\n type PgTable,\n type PgTableExtraConfigValue,\n type PgTableWithColumns,\n pgTable,\n type TableConfig,\n} from 'drizzle-orm/pg-core'\nimport type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'\nimport { TableClient, TableClientError } from './client.js'\nimport {\n type DependentCleanup,\n setTableCleanupDescriptors,\n setTablePartitionColumn,\n type TableCleanupDescriptor,\n} from './dependency-registry.js'\nimport { tableRegistry } from './registry.js'\nimport type {\n ColumnFactory,\n ColumnKind,\n TableDefinition,\n TableDependency,\n TablePredicate,\n WhereClause,\n} from './types.js'\n\n/** Allowed shape for table names. Snake_case, leading lowercase letter. */\nconst TABLE_NAME_RE = /^[a-z][a-z0-9_]*$/\n\n/**\n * Postgres' default identifier length limit (NAMEDATALEN - 1).\n *\n * Postgres silently truncates identifiers longer than this — which can\n * cause auto-generated constraint/index names to collide. We enforce\n * the same limit at definition time so the failure is loud and\n * caller-controllable.\n *\n * Bumping this requires a custom Postgres build (NAMEDATALEN), which\n * no production setup we ship has.\n */\nconst PG_IDENTIFIER_MAX = 63\n\n/**\n * Check that a Postgres identifier (table/column/constraint/index name)\n * fits in the 63-byte limit, throwing with a useful error otherwise.\n *\n * BYTES, not characters: NAMEDATALEN is a byte count, so a name of 40\n * characters that are two bytes each in UTF-8 is 80 bytes, and Postgres cuts\n * it (CodeRabbit on PR #1394).\n *\n * The `kind` argument is for error messages only — e.g. \"table name\",\n * \"auto-generated index name\". The `suggestion` is the auto-generated\n * name's caller-facing alternative (\"supply an explicit `name`\").\n */\nfunction assertIdentifierLength(identifier: string, kind: string, suggestion?: string): void {\n const bytes = Buffer.byteLength(identifier, 'utf8')\n if (bytes > PG_IDENTIFIER_MAX) {\n throw new Error(\n `defineTable: ${kind} \"${identifier}\" is ${bytes} bytes — ` +\n `Postgres truncates identifiers at ${PG_IDENTIFIER_MAX} bytes (${PG_IDENTIFIER_MAX} chars of plain ASCII) silently, ` +\n `which can cause name collisions${suggestion ? `. ${suggestion}` : ''}`,\n )\n }\n}\n\n/**\n * The result of `defineTable()`.\n *\n * @template TCols The columns record passed to `defineTable`.\n * @template TTable The concrete Drizzle `pgTable` type produced by the\n * definition. Defaulted to the non-generic `PgTable` for callers that\n * pass `DefinedTable` around without propagating inference — inside\n * `defineTable` itself, the actual `typeof table` is captured so\n * `makeClient` returns a fully-typed `TableClient<TCols, typeof table>`.\n */\nexport interface DefinedTable<\n TCols extends Record<string, ColumnFactory>,\n TTable extends PgTable = PgTableWithColumns<TableConfig>,\n> {\n /**\n * The underlying Drizzle `pgTable`. Exposed so callers can use it in\n * typed JOINs (the one sanctioned escape valve), and so it can be\n * included in `Plugin.tables` for migration discovery by `lumi migrate`.\n */\n table: TTable\n /**\n * The original schema metadata. Useful for introspection / tooling\n * that needs to know about indexes, unique constraints, etc.\n */\n schema: TableDefinition<TCols>\n /**\n * Per-column kind metadata (column name → kind tag). Used internally\n * by the where-builder for jsonb path validation.\n */\n columnKinds: Readonly<Record<string, ColumnKind>>\n /**\n * Primary key column names (JS property names). Detected from either\n * `schema.primaryKey` or column-level `{ primaryKey: true }`. Empty\n * array if no PK is declared. Required by `claim()`.\n */\n primaryKeyColumns: readonly string[]\n /**\n * Build a typed CRUD client wired to the given database handle.\n *\n * Callers typically construct one client per logical \"module\" and\n * cache it. The client is stateless aside from the db handle, so it\n * is safe to share.\n */\n makeClient(db: PostgresJsDatabase, partitionValue?: string): TableClient<TCols, TTable>\n /** The confining column, when the table declares one (`partitionColumn`). */\n partitionColumn?: string | undefined\n}\n\n/**\n * Define a typed table.\n *\n * The function signature uses two const-generic parameters to preserve\n * column literal types end-to-end. This is what enables `Row`,\n * `InsertRow`, and `WhereClause` to know exact column shapes at the call\n * site.\n *\n * @throws if the table name doesn't match the snake_case regex, or if\n * the same name is registered twice with different table objects.\n */\nexport function defineTable<const TCols extends Record<string, ColumnFactory>>(\n def: TableDefinition<TCols>,\n) {\n // Validate the table name shape — fail loudly at definition time rather\n // than letting Postgres catch it at migration time.\n if (!TABLE_NAME_RE.test(def.name)) {\n throw new Error(\n `defineTable: invalid table name \"${def.name}\" — must match /^[a-z][a-z0-9_]*$/`,\n )\n }\n assertIdentifierLength(def.name, 'table name')\n\n // Build the per-column drizzle column builders by invoking each factory\n // with its property name. The factory contract requires the produced\n // column to be a `PgColumnBuilderBase` ready for `pgTable(...)`.\n const drizzleCols: Record<string, PgColumnBuilderBase> = {}\n const columnKinds: Record<string, ColumnKind> = {}\n for (const [name, factory] of Object.entries(def.columns)) {\n if (typeof factory !== 'function') {\n throw new Error(\n `defineTable: column \"${name}\" is not a column factory — did you call e.g. column.uuid() with parens?`,\n )\n }\n assertIdentifierLength(name, `column name in table \"${def.name}\"`)\n // Use explicit pgName when porting existing tables with snake_case columns\n const pgName = factory.__pgName\n if (pgName) {\n assertIdentifierLength(pgName, `pgName for column \"${name}\" in table \"${def.name}\"`)\n }\n drizzleCols[name] = factory(pgName ?? name)\n columnKinds[name] = factory.__kind\n }\n\n // Validate that all constraint references point at known columns BEFORE\n // building the pgTable. The pgTable constraint callback may not surface\n // errors at construction time, so we check eagerly here for fast failure\n // and consistent error messages.\n const knownColumns = new Set(Object.keys(drizzleCols))\n if (def.primaryKey !== undefined) {\n const pkCols = Array.isArray(def.primaryKey) ? def.primaryKey : [def.primaryKey]\n for (const k of pkCols) {\n if (!knownColumns.has(String(k))) {\n throw new Error(`defineTable: primaryKey references unknown column \"${String(k)}\"`)\n }\n }\n }\n if (def.unique) {\n for (const uq of def.unique) {\n for (const k of uq.on) {\n if (!knownColumns.has(String(k))) {\n throw new Error(`defineTable: unique constraint references unknown column \"${String(k)}\"`)\n }\n }\n // Measure the identifier Postgres will actually receive, which differs by\n // path: a PARTIAL unique is a `uniqueIndex` named `uq_<table>_<props>`\n // (see the emission below); a PLAIN unique without a name gets drizzle's\n // own default, `<table>_<db col names>_unique` (drizzle-orm's\n // `uniqueKeyName` in pg-core/unique-constraint), built from the columns'\n // DATABASE names — a `pgName` changes it.\n const name =\n uq.name ??\n (uq.where !== undefined\n ? `uq_${def.name}_${uq.on.map(String).join('_')}`\n : `${def.name}_${uq.on.map((k) => def.columns[String(k)]?.__pgName ?? String(k)).join('_')}_unique`)\n assertIdentifierLength(\n name,\n uq.name ? 'unique constraint name' : 'auto-generated unique constraint name',\n uq.name ? undefined : 'Supply a shorter explicit `name` to the unique definition.',\n )\n }\n }\n if (def.indexes) {\n for (const ix of def.indexes) {\n for (const k of ix.on) {\n if (!knownColumns.has(String(k))) {\n throw new Error(`defineTable: index references unknown column \"${String(k)}\"`)\n }\n }\n const name = ix.name ?? `idx_${def.name}_${ix.on.map(String).join('_')}`\n assertIdentifierLength(\n name,\n ix.name ? 'index name' : 'auto-generated index name',\n ix.name ? undefined : 'Supply a shorter explicit `name` to the index definition.',\n )\n }\n }\n\n // Build the constraint callback for indexes / unique / primary key.\n // pgTable accepts an optional callback that returns a constraint object.\n const hasConstraints =\n def.primaryKey !== undefined ||\n (def.unique && def.unique.length > 0) ||\n (def.indexes && def.indexes.length > 0)\n\n // Helper: look up a column by key from the pgTable callback parameter.\n // `t` has a string index (`{ [x: string]: PgColumn }`) so access is typed.\n function resolveCol(t: Record<string, AnyPgColumn>, k: string, context: string): AnyPgColumn {\n const col = t[k]\n if (!col) throw new Error(`defineTable: ${context} references unknown column \"${k}\"`)\n return col\n }\n\n // Resolve a partial-index predicate (index / unique `where`) against the\n // table's RESOLVED columns. The callback form receives the live column map so\n // `(c) => sql`${c.foo} = true`` emits the correctly-quoted identifier; the\n // plain-SQL form is passed through unchanged. Runs inside the pgTable callback\n // where `tCols` exists — the resolved columns don't exist at defineTable-call\n // time, which is why a def-time predicate can't reference them directly.\n function resolvePredicate(pred: TablePredicate, tCols: Record<string, AnyPgColumn>): SQL {\n // The callback param is `Record<string, AnyPgColumn>` (see TablePredicate's\n // @remarks — a `TCols`-shaped param would collapse const-inference), so the\n // resolved column map is passed through with no cast.\n return typeof pred === 'function' ? pred(tCols) : pred\n }\n\n const table = hasConstraints\n ? pgTable(def.name, drizzleCols, (t) => {\n const tCols = t as unknown as Record<string, AnyPgColumn>\n const constraints: PgTableExtraConfigValue[] = []\n\n // Composite primary key\n if (def.primaryKey !== undefined) {\n const pkCols = Array.isArray(def.primaryKey) ? def.primaryKey : [def.primaryKey]\n const first = resolveCol(tCols, String(pkCols[0]), 'primaryKey')\n const rest = pkCols.slice(1).map((k) => resolveCol(tCols, String(k), 'primaryKey'))\n constraints.push(drizzlePrimaryKey({ columns: [first, ...rest] }))\n }\n\n // Unique constraints\n if (def.unique) {\n for (const uq of def.unique) {\n const first = resolveCol(tCols, String(uq.on[0]), 'unique constraint')\n const rest = uq.on\n .slice(1)\n .map((k) => resolveCol(tCols, String(k), 'unique constraint'))\n if (uq.where !== undefined) {\n // Postgres has no predicate on a table-level UNIQUE constraint —\n // a PARTIAL unique must be a unique INDEX. `uniqueIndex` requires\n // an explicit name (a bare `unique()` auto-generates one).\n const name = uq.name ?? `uq_${def.name}_${uq.on.map(String).join('_')}`\n constraints.push(\n drizzleUniqueIndex(name)\n .on(first, ...rest)\n .where(resolvePredicate(uq.where, tCols)),\n )\n } else {\n // Plain UNIQUE constraint. An explicit `name` is honoured; without\n // one drizzle names it `<table>_<db cols>_unique`, which is the\n // string the length check above measures.\n constraints.push(drizzleUnique(uq.name).on(first, ...rest))\n }\n }\n }\n\n // Indexes\n if (def.indexes) {\n for (const ix of def.indexes) {\n const first = resolveCol(tCols, String(ix.on[0]), 'index')\n const rest = ix.on.slice(1).map((k) => resolveCol(tCols, String(k), 'index'))\n const builder = drizzleIndex(\n ix.name ?? `idx_${def.name}_${ix.on.map(String).join('_')}`,\n ).on(first, ...rest)\n constraints.push(\n ix.where !== undefined ? builder.where(resolvePredicate(ix.where, tCols)) : builder,\n )\n }\n }\n\n return constraints\n })\n : pgTable(def.name, drizzleCols)\n\n // Capture caller location for nicer double-registration errors. Best-\n // effort — falls back to undefined if stack parsing fails.\n const source = captureCallerLocation()\n tableRegistry.register(def.name, table, source)\n\n const frozenKinds: Readonly<Record<string, ColumnKind>> = Object.freeze({ ...columnKinds })\n\n // Determine primary key column names from table-level or column-level declarations.\n let pkCols: string[]\n if (def.primaryKey !== undefined) {\n pkCols = Array.isArray(def.primaryKey) ? def.primaryKey.map(String) : [String(def.primaryKey)]\n } else {\n pkCols = []\n for (const [name, factory] of Object.entries(def.columns)) {\n if (factory.__primaryKey) pkCols.push(name)\n }\n }\n const frozenPk: readonly string[] = Object.freeze([...pkCols])\n\n const partitionColumn = def.partitionColumn\n if (partitionColumn !== undefined && !knownColumns.has(partitionColumn)) {\n throw new Error(\n `defineTable: partitionColumn references unknown column \"${partitionColumn}\" on \"${def.name}\"`,\n )\n }\n if (partitionColumn !== undefined) {\n // Two shapes that would define fine and fail later, refused here:\n // - a unique that does not lead with the partition column — the client\n // prepends it to every upsert / tryClaim conflict target, which then\n // matches no constraint (\"no unique or exclusion constraint matching\");\n // - `dependsOn` cleanup — it runs through an unconfined client by entity\n // id, which a partitioned table refuses to build (and which must reach\n // every partition anyway).\n for (const uq of def.unique ?? []) {\n if (String(uq.on[0]) !== partitionColumn) {\n throw new Error(\n `defineTable: on partitioned \"${def.name}\", every unique must lead with \"${partitionColumn}\" ` +\n `(got [${uq.on.map(String).join(', ')}])`,\n )\n }\n }\n if ((def.dependsOn ?? []).length > 0) {\n throw new Error(\n `defineTable: partitioned \"${def.name}\" cannot declare dependsOn — its cleanup runs unconfined`,\n )\n }\n }\n const makeClient = (\n database: PostgresJsDatabase,\n partitionValue?: string,\n ): TableClient<TCols, typeof table> => {\n if (partitionColumn === undefined) {\n if (partitionValue !== undefined) {\n throw new TableClientError(\n `table \"${def.name}\" declares no partitionColumn — a partition value makes no sense here`,\n )\n }\n return new TableClient<TCols, typeof table>(table, frozenKinds, database, frozenPk)\n }\n if (typeof partitionValue !== 'string' || partitionValue === '') {\n throw new TableClientError(\n `table \"${def.name}\" is partitioned by \"${partitionColumn}\" — makeClient(db, value) needs ` +\n 'the partition value (e.g. the site). Refusing to build an unconfined client.',\n )\n }\n return new TableClient<TCols, typeof table>(table, frozenKinds, database, frozenPk, {\n key: partitionColumn,\n value: partitionValue,\n })\n }\n\n // ── Declarative dependent-table cleanup (dependsOn) ────────────────────\n // Validate each declared dependency at definition time (fail loud, not at\n // delete time), then build a typed, tx-bound cleanup executor per entry\n // and stash them keyed by the raw table object so the framework merge step\n // can register them into the entity-keyed dependency registry.\n const dependsOn = def.dependsOn ?? []\n for (const dep of dependsOn) {\n // Validate the entity key — an empty/whitespace value would register a\n // cleanup keyed to '' that delete-time `getDependentCleanups(name)` never\n // matches, silently skipping cleanup and orphaning infra rows. Fail loud\n // at definition time instead. (`typeof` guard covers untyped JS callers.)\n if (typeof dep.entity !== 'string' || dep.entity.trim().length === 0) {\n throw new Error(\n `defineTable: dependsOn.entity must be a non-empty string on table \"${def.name}\"`,\n )\n }\n // Look the factory up by key — a missing entry IS the unknown-column case\n // (and narrows `factory` to non-undefined for the nullability guard below,\n // which `knownColumns.has(...)` alone can't do under noUncheckedIndexedAccess).\n const factory = def.columns[dep.column]\n if (!knownColumns.has(String(dep.column)) || factory === undefined) {\n throw new Error(\n `defineTable: dependsOn references unknown column \"${String(dep.column)}\" on table \"${def.name}\"`,\n )\n }\n if (dep.onDelete !== 'cascade' && dep.onDelete !== 'set-null') {\n throw new Error(\n `defineTable: dependsOn.onDelete must be 'cascade' or 'set-null' on table \"${def.name}\", got \"${String(dep.onDelete)}\"`,\n )\n }\n if (dep.onDelete === 'set-null' && factory.__notNull === true) {\n throw new Error(\n `defineTable: dependsOn onDelete:'set-null' targets non-nullable column \"${String(dep.column)}\" on table \"${def.name}\" — make the column nullable or use 'cascade'`,\n )\n }\n }\n\n function makeCleanup(dep: TableDependency<TCols>): DependentCleanup {\n const column = dep.column as keyof TCols & string\n return async (txDb: PostgresJsDatabase, entityIds: string[]): Promise<void> => {\n if (entityIds.length === 0) return\n const client = makeClient(txDb)\n // Computed-key object literal — TS can't infer the precise WhereClause /\n // insert-model shape from a dynamic key, so cast at this boundary only.\n const where = { [column]: { in: entityIds } } as WhereClause<TCols>\n if (dep.onDelete === 'cascade') {\n await client.deleteMany(where)\n } else {\n const patch = { [column]: null } as Partial<InferInsertModel<typeof table>>\n await client.updateMany(where, patch)\n }\n }\n }\n\n const cleanupDescriptors: TableCleanupDescriptor[] = dependsOn.map((dep) => ({\n entity: dep.entity,\n cleanup: makeCleanup(dep),\n }))\n if (cleanupDescriptors.length > 0) {\n setTableCleanupDescriptors(table, cleanupDescriptors)\n }\n if (partitionColumn !== undefined) setTablePartitionColumn(table, partitionColumn)\n\n return {\n table,\n schema: def,\n columnKinds: frozenKinds,\n primaryKeyColumns: frozenPk,\n makeClient,\n partitionColumn,\n }\n}\n\n/**\n * Best-effort: walk the stack to find the first non-internal frame so\n * double-registration errors point at the user's call site instead of\n * deep into `defineTable`.\n */\nfunction captureCallerLocation(): string | undefined {\n const stack = new Error().stack\n if (!stack) return undefined\n const lines = stack.split('\\n').slice(1)\n for (const line of lines) {\n if (line.includes('/table/define.') || line.includes('/table/registry.')) continue\n const match = line.match(/\\(([^)]+)\\)/) ?? line.match(/at (.+)$/)\n if (match?.[1]) return match[1].trim()\n }\n return undefined\n}\n","/**\n * The deploy-step ledger (plan `deploy-drift` D002): one row per ATTEMPT of a\n * deploy step on a site. Nothing here is ever updated or deleted — the current\n * state of a step on a site is its NEWEST row, and the older ones are its\n * history. Resetting a step is a new attempt, never a removed row.\n *\n * A framework table, not a plugin's: every lumi-cms project has deploy steps\n * available and therefore has this table, whichever plugins it loads. It is\n * wired where `entity_refs` is — `createApp`, `buildCliApp` and `lumi migrate`'s\n * schema map — so it reaches the schema snapshot and gets a generated\n * migration like any other table (unlike `_toolkit_migrations`, which is raw\n * SQL outside the snapshots and is not the shape to copy).\n *\n * It lives in `@murumets-ee/db` because the runner that writes it lives in\n * `@murumets-ee/core`, and core owns no tables of its own.\n */\n\nimport { column, defineTable } from '../table/index.js'\n\n/** The table's name — also its key in the migration schema map. */\nexport const DEPLOY_STEPS_TABLE_NAME = 'toolkit_deploy_steps'\n\n/**\n * What an attempt ended as. STORED states only: \"waiting\" and \"waiting for\n * another step\" are computed on every read from what is declared and what is\n * recorded, and are never written (a cached pending list is the bug Drupal had).\n */\nexport const DEPLOY_STEP_STATES = ['applied', 'needs-a-person', 'marked-done'] as const\nexport type DeployStepState = (typeof DEPLOY_STEP_STATES)[number]\n\nexport const deployStepsTable = defineTable({\n name: DEPLOY_STEPS_TABLE_NAME,\n columns: {\n id: column.uuid({ primaryKey: true, defaultRandom: true }),\n /** The seeder's `name`. */\n step: column.varchar({ length: 255, notNull: true }),\n /** The site handle the attempt was for. */\n site: column.varchar({ length: 64, notNull: true }),\n /** One of {@link DEPLOY_STEP_STATES}. */\n state: column.varchar({ length: 32, notNull: true }),\n /** Why it needs a person — the step's own words, or the error's message. */\n reason: column.text(),\n /** What a person should do about it — the step's own words. */\n whatToDo: column.text({ pgName: 'what_to_do' }),\n /** Rows the step said it created. Null for a mark and for a throw. */\n created: column.integer(),\n at: column.timestamp({ notNull: true, defaultNow: true, withTimezone: true }),\n /**\n * Who ran it: `deploy`, `cli`, or a user id. A user id is varchar, never\n * uuid — better-auth ids are random strings.\n */\n by: column.varchar({ length: 255, notNull: true }),\n /** That person's name or e-mail as it was then; null for `deploy` / `cli`. */\n byName: column.varchar({ length: 255, pgName: 'by_name' }),\n /** The commit of the build that ran it; null when the build was not told. */\n buildCommit: column.varchar({ length: 64, pgName: 'build_commit' }),\n /** The toolkit version that ran it. */\n toolkitVersion: column.varchar({ length: 64, pgName: 'toolkit_version' }),\n },\n indexes: [{ on: ['step', 'site', 'at'], name: 'idx_deploy_steps_step_site_at' }],\n})\n\nexport const toolkitDeploySteps = deployStepsTable.table\n","import postgres, { type Sql } from 'postgres'\nimport type { DbConfig } from './client.js'\n\n/**\n * Create a dedicated postgres-js connection sized for `LISTEN/NOTIFY`.\n *\n * The underlying postgres-js Sql instance is intentionally NOT a Drizzle\n * client — `LISTEN`/`NOTIFY` is a low-level Postgres feature that the\n * Drizzle query builder doesn't model. The realtime cross-process bridge\n * (in `@murumets-ee/core/realtime`) holds one of these per process:\n *\n * - `sql.listen(channel, onnotify, onlisten)` opens a dedicated long-lived\n * connection and delivers every `NOTIFY` on the channel to `onnotify`.\n * - `sql.notify(channel, payload)` (or `pg_notify`) issues a notification.\n *\n * Pool size is intentionally small (max 2): one connection is parked on\n * `LISTEN`, leaving headroom for occasional `NOTIFY` issues from the same\n * Sql instance without contending with the rest of the app's pool.\n */\nexport function createListenClient(config: Pick<DbConfig, 'url'>): Sql {\n return postgres(config.url, {\n max: 2,\n idle_timeout: 0,\n max_lifetime: 60 * 60,\n connection: {\n application_name: 'toolkit_realtime',\n },\n onnotice: () => {},\n })\n}\n","import { readdir } from 'node:fs/promises'\nimport { join } from 'node:path'\nimport { sql } from 'drizzle-orm'\nimport type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'\n\n/**\n * Read a directory's `.ts` filenames, returning `[]` if the directory does\n * not exist. Single round-trip — avoids the `existsSync` + `readdir` race.\n */\nasync function readTsFilesOrEmpty(dir: string): Promise<string[]> {\n try {\n const entries = await readdir(dir)\n return entries.filter((name) => name.endsWith('.ts'))\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') return []\n throw err\n }\n}\n\n/**\n * Arguments passed to migration `up` / `down` functions.\n *\n * Intentionally tiny: we hand over a read-write Drizzle handle bound to\n * the current transaction. If a migration needs anything beyond raw SQL\n * (e.g. calling an AdminClient for a data backfill), it can import the\n * relevant toolkit helpers inside the migration file — the CLI loader\n * runs the migration in the same process as the config, so all package\n * imports resolve normally.\n */\nexport interface MigrateArgs {\n db: PostgresJsDatabase\n}\n\n/**\n * Shape of a migration module loaded from a `.ts` file.\n *\n * At minimum a migration must export `up`. `down` is optional — when\n * absent, `lumi migrate:rollback` refuses to roll that file back.\n */\nexport interface MigrationModule {\n up: (args: MigrateArgs) => Promise<void>\n down?: (args: MigrateArgs) => Promise<void>\n}\n\nexport interface MigrationSource {\n /** Absolute path to the migration file on disk. */\n path: string\n /** `project` for user migrations, `toolkit` for shipped-with-toolkit ones. */\n namespace: 'toolkit' | 'project'\n /** Filename only, used for ordering + tracking. */\n name: string\n}\n\nexport interface MigrationStatus {\n applied: MigrationSource[]\n pending: MigrationSource[]\n}\n\n/**\n * Loader callback — imports a migration `.ts` file and returns its exports.\n *\n * The CLI owns `jiti` (already used to load `lumi.config.ts`), so\n * `@murumets-ee/db` stays free of TS-runtime loader dependencies.\n * Callers who never apply migrations (read-only servers, tests) never\n * pay the loader cost.\n */\nexport type MigrationLoader = (absolutePath: string) => Promise<MigrationModule>\n\n/** Ensure the `_toolkit_migrations` tracking table exists. */\nasync function ensureMigrationsTable(db: PostgresJsDatabase): Promise<void> {\n await db.execute(sql`\n CREATE TABLE IF NOT EXISTS _toolkit_migrations (\n id SERIAL PRIMARY KEY,\n namespace VARCHAR(50) NOT NULL,\n name VARCHAR(255) NOT NULL,\n applied_at TIMESTAMP NOT NULL DEFAULT NOW(),\n UNIQUE(namespace, name)\n )\n `)\n}\n\n/**\n * Discover all migration files under `projectRoot/migrations`.\n *\n * Layout:\n * migrations/\n * 20260411_180000_add_articles.ts ← user migrations (sort chronologically)\n * 20260411_180000_add_articles.snapshot.json\n * .toolkit/\n * 0001_initial_schema.ts ← toolkit-owned migrations (sort numerically)\n *\n * Module-private — callers reach migrations via {@link getMigrationStatus}\n * (returns `applied` + `pending`) or {@link runMigrations} / {@link rollbackMigrations}.\n */\nasync function discoverMigrations(projectRoot: string): Promise<MigrationSource[]> {\n const migrationsDir = join(projectRoot, 'migrations')\n const toolkitDir = join(migrationsDir, '.toolkit')\n\n const [toolkitFiles, projectEntries] = await Promise.all([\n readTsFilesOrEmpty(toolkitDir),\n readTsFilesOrEmpty(migrationsDir),\n ])\n\n const toolkit: MigrationSource[] = toolkitFiles\n .map((name) => ({ path: join(toolkitDir, name), namespace: 'toolkit' as const, name }))\n .sort((a, b) => a.name.localeCompare(b.name))\n\n const project: MigrationSource[] = projectEntries\n // The `.toolkit` directory itself does not end in `.ts`, so it is filtered\n // out by `readTsFilesOrEmpty`. Filtering here is just defence-in-depth.\n .filter((name) => name !== '.toolkit')\n .map((name) => ({ path: join(migrationsDir, name), namespace: 'project' as const, name }))\n .sort((a, b) => a.name.localeCompare(b.name))\n\n // Stable ordering: toolkit first (numeric prefix), then project (timestamp prefix).\n return [...toolkit, ...project]\n}\n\n/** Split discovered migrations into applied vs pending by consulting the tracking table. */\nexport async function getMigrationStatus(\n db: PostgresJsDatabase,\n projectRoot: string,\n): Promise<MigrationStatus> {\n await ensureMigrationsTable(db)\n\n const all = await discoverMigrations(projectRoot)\n\n const appliedRows = await db.execute<{ namespace: string; name: string }>(sql`\n SELECT namespace, name FROM _toolkit_migrations\n ORDER BY applied_at ASC\n `)\n const appliedSet = new Set(appliedRows.map((row) => `${row.namespace}:${row.name}`))\n\n const applied: MigrationSource[] = []\n const pending: MigrationSource[] = []\n for (const m of all) {\n if (appliedSet.has(`${m.namespace}:${m.name}`)) applied.push(m)\n else pending.push(m)\n }\n return { applied, pending }\n}\n\n/**\n * Apply every pending migration in order.\n *\n * Each file runs inside its own transaction: the migration's `up()`\n * executes first, then the `_toolkit_migrations` insert commits. If\n * `up()` throws, the transaction rolls back and the whole command\n * aborts — nothing gets half-applied.\n *\n * The `loader` callback is injected so `@murumets-ee/db` itself doesn't\n * depend on a TS-runtime loader. Callers pass a jiti-backed loader from\n * the CLI or their own equivalent.\n */\nexport async function runMigrations(\n db: PostgresJsDatabase,\n projectRoot: string,\n loader: MigrationLoader,\n): Promise<void> {\n await ensureMigrationsTable(db)\n\n const { pending } = await getMigrationStatus(db, projectRoot)\n\n if (pending.length === 0) {\n console.log('No pending migrations')\n return\n }\n\n console.log(`Running ${pending.length} pending migration${pending.length === 1 ? '' : 's'}...`)\n\n for (const migration of pending) {\n console.log(` Applying ${migration.namespace}/${migration.name}...`)\n\n const mod = await loader(migration.path)\n if (typeof mod.up !== 'function') {\n throw new Error(\n `Migration ${migration.namespace}/${migration.name} has no \\`up\\` export. ` +\n 'Every migration file must export an async `up({ db })` function.',\n )\n }\n\n try {\n await db.transaction(async (tx) => {\n await mod.up({ db: tx as PostgresJsDatabase })\n await tx.execute(sql`\n INSERT INTO _toolkit_migrations (namespace, name)\n VALUES (${migration.namespace}, ${migration.name})\n `)\n })\n console.log(` ✓ Applied ${migration.namespace}/${migration.name}`)\n } catch (error) {\n console.error(` ✗ Failed to apply ${migration.namespace}/${migration.name}:`, error)\n throw error\n }\n }\n\n console.log(`Successfully applied ${pending.length} migration${pending.length === 1 ? '' : 's'}`)\n}\n\n/**\n * Roll back the most recent applied migration (or N most recent).\n *\n * Each file's `down()` runs in its own transaction. Files without a\n * `down` export cause the rollback to abort before running anything —\n * we never roll back some but not all of a requested batch.\n */\nexport async function rollbackMigrations(\n db: PostgresJsDatabase,\n projectRoot: string,\n loader: MigrationLoader,\n count = 1,\n): Promise<void> {\n await ensureMigrationsTable(db)\n const { applied } = await getMigrationStatus(db, projectRoot)\n\n if (applied.length === 0) {\n console.log('No applied migrations to roll back')\n return\n }\n\n // Take the last `count` applied, in reverse application order.\n const toRollback = applied.slice(-count).reverse()\n\n // Load all requested modules first and verify every one has `down`.\n const modules = await Promise.all(\n toRollback.map(async (m) => ({ meta: m, mod: await loader(m.path) })),\n )\n for (const { meta, mod } of modules) {\n if (typeof mod.down !== 'function') {\n throw new Error(\n `Migration ${meta.namespace}/${meta.name} has no \\`down\\` export — cannot roll back. ` +\n 'Add an async `down({ db })` function to the migration file.',\n )\n }\n }\n\n for (const { meta, mod } of modules) {\n console.log(` Rolling back ${meta.namespace}/${meta.name}...`)\n try {\n await db.transaction(async (tx) => {\n // biome-ignore lint/style/noNonNullAssertion: guarded above\n await mod.down!({ db: tx as PostgresJsDatabase })\n await tx.execute(sql`\n DELETE FROM _toolkit_migrations\n WHERE namespace = ${meta.namespace} AND name = ${meta.name}\n `)\n })\n console.log(` ✓ Rolled back ${meta.namespace}/${meta.name}`)\n } catch (error) {\n console.error(` ✗ Failed to roll back ${meta.namespace}/${meta.name}:`, error)\n throw error\n }\n }\n\n console.log(\n `Successfully rolled back ${modules.length} migration${modules.length === 1 ? '' : 's'}`,\n )\n}\n","import type { PgTableWithColumns } from 'drizzle-orm/pg-core'\n\n/**\n * Schema registry for storing Drizzle schemas\n * This will be populated by @org/entity when schemas are generated from entity definitions\n *\n * We use PgTableWithColumns<any> because entity table schemas are dynamic (columns\n * not known at compile time) and we need runtime property access (.id, .status, etc.).\n * AnyPgTable won't work here — it doesn't expose column accessors.\n */\n\n// biome-ignore lint/suspicious/noExplicitAny: dynamic table columns require PgTableWithColumns<any> for property access\ntype AnyTable = PgTableWithColumns<any>\n\nexport interface SchemaRegistry {\n register(name: string, schema: AnyTable): void\n get(name: string): AnyTable | undefined\n all(): Record<string, AnyTable>\n has(name: string): boolean\n}\n\nclass SchemaRegistryImpl implements SchemaRegistry {\n private schemas: Map<string, AnyTable> = new Map()\n\n register(name: string, schema: AnyTable): void {\n this.schemas.set(name, schema)\n }\n\n get(name: string): AnyTable | undefined {\n return this.schemas.get(name)\n }\n\n all(): Record<string, AnyTable> {\n const result: Record<string, AnyTable> = {}\n for (const [name, schema] of this.schemas.entries()) {\n result[name] = schema\n }\n return result\n }\n\n has(name: string): boolean {\n return this.schemas.has(name)\n }\n}\n\n/**\n * Global schema registry instance\n * Entities will register their schemas here when they are defined\n */\nexport const schemaRegistry = new SchemaRegistryImpl()\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"test-utils.d.mts","names":[],"sources":["../src/test-utils.ts"],"mappings":";;;UAMiB,WAAA;;AAAjB;;;;AAUY;AAGZ;;;EAHE,UAAU;AAAA;AAAA,UAGK,MAAA;EACf,EAAA,EAAI,kBAAA;EACJ,MAAA;EAGsB;EADtB,IAAA,GAAO,OAAA,EAAS,MAAA,mBAAyB,IAAA,GAAO,WAAA,KAAgB,OAAA;EAChE,OAAA,QAAe,OAAA;AAAA;;;;;;;;;;;AAAO;AAkBxB;;;;iBAAsB,YAAA,IAAgB,OAAO,CAAC,MAAA;AA+J9C;AAAA,UAAiB,eAAA;EACf,MAAA;EACA,GAAG;AAAA;AAIL;AAAA,UAAiB,mBAAA,aAAgC,MAAA;EAC/C,IAAA,EAAM,GAAA;EACN,KAAA,CAAM,OAAA,GAAU,GAAA;EAChB,OAAA,CAAQ,KAAA,EAAO,MAAA,oBAA0B,OAAA,CAAQ,GAAA;EACjD,QAAA,CAAS,IAAA;IACP,KAAA,GAAQ,MAAA;IACR,OAAA,GAAU,eAAA;IACV,KAAA;IACA,MAAA;EAAA,IACE,OAAA,CAAQ,GAAA;EACZ,MAAA,CAAO,KAAA,EAAO,MAAA,oBAA0B,OAAA;EACxC,KAAA,CAAM,KAAA,GAAQ,MAAA,oBAA0B,OAAA;EACxC,MAAA,CAAO,MAAA,EAAQ,OAAA,CAAQ,GAAA,IAAO,OAAA,CAAQ,GAAA;EACtC,MAAA,CAAO,KAAA,EAAO,MAAA,mBAAyB,KAAA,EAAO,OAAA,CAAQ,GAAA,IAAO,OAAA,CAAQ,GAAA;EACrE,MAAA,CACE,MAAA,EAAQ,OAAA,CAAQ,GAAA,GAChB,IAAA;IAAQ,MAAA;IAA2B,GAAA,GAAM,OAAA,CAAQ,GAAA;IAAM,UAAA;EAAA,IACtD,OAAA,CAAQ,GAAA;EACX,MAAA,CAAO,KAAA,EAAO,MAAA,oBAA0B,OAAA,CAAQ,GAAA;EAChD,UAAA,CAAW,KAAA,EAAO,MAAA,oBAA0B,OAAA,CAAQ,GAAA;EACpD,QAAA,CACE,MAAA,EAAQ,OAAA,CAAQ,GAAA,GAChB,IAAA;IACE,MAAA;IACA,WAAA,EAAa,MAAA;IACb,GAAA,GAAM,OAAA,CAAQ,GAAA;EAAA,IAEf,OAAA;IAAU,QAAA;IAAgB,GAAA,EAAK,GAAA;EAAA;IAAU,QAAA;IAAiB,GAAA,EAAK,GAAA;EAAA;EAClE,SAAA,IAAa,IAAA;IACX,MAAA,EAAQ,MAAA;MAAiB,EAAA;MAA6C,MAAA;IAAA;IACtE,KAAA,GAAQ,MAAA;EAAA,MACJ,OAAA,CAAQ,KAAA,CAAM,MAAA;AAAA;;;;;;;;;;;;;;iBAgBN,yBAAA,aAAsC,MAAA,mBACpD,OAAA;EAAW,OAAA,SAAgB,GAAA;EAAc,WAAA;EAA4B,OAAA,GAAU,GAAA;AAAA,IAC9E,mBAAA,CAAoB,GAAA"}
|
package/dist/test-utils.mjs.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"test-utils.mjs","names":["branches"],"sources":["../src/test-utils.ts"],"sourcesContent":["import { sql } from 'drizzle-orm'\nimport { drizzle, type PostgresJsDatabase } from 'drizzle-orm/postgres-js'\nimport postgres from 'postgres'\nimport { createExtensionStatement } from './extensions.js'\nimport { trackTransactions } from './transaction-hooks.js'\n\nexport interface PushOptions {\n /**\n * Postgres extensions to enable before pushing the schema — the test-side\n * mirror of a plugin's `server.extensions` (e.g. `['btree_gist']` for the\n * reservation GIST exclusion constraint). Installed into `public` (NOT the\n * per-suite schema) because an extension is per-database and installs into a\n * single schema: a per-suite install would be invisible to other parallel\n * suites' `search_path`. `public` is on every suite's search_path, so the\n * operator classes resolve everywhere. Idempotent (`IF NOT EXISTS`).\n */\n extensions?: readonly string[]\n}\n\nexport interface TestDb {\n db: PostgresJsDatabase\n schema: string\n /** Push Drizzle table definitions to the test schema (uses drizzle-kit/api) */\n push: (schemas: Record<string, unknown>, opts?: PushOptions) => Promise<void>\n cleanup: () => Promise<void>\n}\n\n/**\n * Create an isolated test database using a unique PostgreSQL schema.\n * Each test suite gets its own schema so tests can run in parallel safely.\n *\n * Requires `DATABASE_TEST_URL` env var pointing to a running PostgreSQL instance.\n * Falls back to `DATABASE_URL` if test URL is not set.\n *\n * Usage:\n * ```ts\n * const testDb = await createTestDb()\n * await testDb.push({ articles: articlesTable, entity_refs: entityRefsTable })\n * // ... run tests with testDb.db ...\n * await testDb.cleanup()\n * ```\n */\nexport async function createTestDb(): Promise<TestDb> {\n // Refuse to ever provision a scratch schema in production. `createTestDb`\n // issues `CREATE SCHEMA` and (via `cleanup()`) `DROP SCHEMA … CASCADE`,\n // which would be catastrophic if pointed at a real database. The whole\n // function is test-only, but the npm subpath export means an app could\n // import it by accident — fail loud instead of running.\n if (process.env.NODE_ENV === 'production') {\n throw new Error(\n 'createTestDb() is not allowed when NODE_ENV=production — it issues CREATE/DROP SCHEMA and is intended for tests only',\n )\n }\n\n const url = process.env.DATABASE_TEST_URL || process.env.DATABASE_URL\n if (!url) {\n throw new Error('DATABASE_TEST_URL (or DATABASE_URL) env var is required for integration tests')\n }\n\n const schemaName = `test_${crypto.randomUUID().replaceAll('-', '').slice(0, 12)}`\n\n // First connection: create the schema. We can't pass `search_path` as a\n // connection startup parameter for this client because the schema doesn't\n // exist yet — postgres rejects connections asking to enter a missing\n // schema with `invalid value for parameter \"search_path\"`.\n const setupConnection = postgres(url, { max: 1 })\n try {\n await setupConnection.unsafe(`CREATE SCHEMA \"${schemaName}\"`)\n } finally {\n // Always close the setup connection — even on CREATE SCHEMA failure\n // (rare with a UUID-derived name, but possible if the role lacks\n // permission). Without this, a thrown error would leak a postgres\n // connection until the process exits.\n await setupConnection.end()\n }\n\n // Working connection pool: every connection in the pool starts with\n // `search_path` pointing at the test schema. Without this, a second\n // pooled connection would fall back to the default search_path and miss\n // the test tables — which is exactly the bug that surfaced when concurrent\n // queries (e.g. tryClaim's race test) bypassed connection 1.\n const connection = postgres(url, {\n max: 3,\n idle_timeout: 10,\n connection: {\n search_path: `\"${schemaName}\",public`,\n },\n })\n\n // Tracked, like `createDbClient`'s — a suite that opens a transaction must\n // get the on-commit behaviour production runs, not a bare drizzle client's.\n const db = trackTransactions(drizzle(connection))\n\n return {\n db,\n schema: schemaName,\n\n async push(schemas: Record<string, unknown>, opts?: PushOptions) {\n // Enable declared extensions into `public` FIRST (see PushOptions docs for\n // the per-database / parallel-suite reasoning). A dedicated single-use\n // connection pinned to `search_path=public` makes the install schema\n // deterministic regardless of the working pool's per-suite search_path.\n if (opts?.extensions && opts.extensions.length > 0) {\n const extConnection = postgres(url, { max: 1, connection: { search_path: 'public' } })\n try {\n for (const name of opts.extensions) {\n // createExtensionStatement validates the name + builds the canonical SQL.\n await extConnection.unsafe(createExtensionStatement(name))\n }\n } finally {\n await extConnection.end()\n }\n }\n\n // Use createRequire to load drizzle-kit/api via Node's CJS resolver.\n // drizzle-kit/api.mjs bundles CJS code with require('fs') — Vite's ESM\n // transform replaces require() with a polyfill that throws. Native CJS bypasses this.\n const { createRequire } = await import('node:module')\n const require = createRequire(import.meta.url)\n const { pushSchema } = require('drizzle-kit/api') as typeof import('drizzle-kit/api')\n\n // Use default schemaFilters ([\"public\"]). Our tables are unqualified (= public),\n // and SET search_path ensures DDL actually creates them in the test schema.\n const result = await pushSchema(schemas, withRowsShim(db))\n await result.apply()\n },\n\n async cleanup() {\n await db.execute(sql.raw(`DROP SCHEMA IF EXISTS \"${schemaName}\" CASCADE`))\n await connection.end()\n },\n }\n}\n\n// ---------------------------------------------------------------------------\n// In-memory TableClient shim — for unit tests that exercise consumer code\n// (LockService, ContentClient draft methods, etc.) without spinning up\n// Postgres. Implements the subset of TableClient that consumers actually\n// use; the matcher understands the same WhereClause shape (eq shorthand,\n// lt/gte/in/notIn operators, $or branches, jsonb-path keys are NOT\n// supported — use the real DB for those).\n//\n// Each consumer test previously hand-rolled its own ~100-LoC version of\n// this; collapsed into one place per audit-quality-content L#12.\n// ---------------------------------------------------------------------------\n\n/** Operators recognized by the in-memory matcher. Mirrors WhereClause. */\ntype MemOp =\n | { eq: unknown }\n | { ne: unknown }\n | { in: unknown[] }\n | { notIn: unknown[] }\n | { lt: unknown }\n | { lte: unknown }\n | { gt: unknown }\n | { gte: unknown }\n | { isNull: true }\n | { isNotNull: true }\n\nfunction matchesOp(rowVal: unknown, op: MemOp): boolean {\n if ('eq' in op) return rowVal === op.eq\n if ('ne' in op) return rowVal !== op.ne\n if ('in' in op) return op.in.includes(rowVal)\n if ('notIn' in op) return !op.notIn.includes(rowVal)\n if ('lt' in op) return (rowVal as Date | number) < (op.lt as Date | number)\n if ('lte' in op) return (rowVal as Date | number) <= (op.lte as Date | number)\n if ('gt' in op) return (rowVal as Date | number) > (op.gt as Date | number)\n if ('gte' in op) return (rowVal as Date | number) >= (op.gte as Date | number)\n if ('isNull' in op) return rowVal === null || rowVal === undefined\n if ('isNotNull' in op) return rowVal !== null && rowVal !== undefined\n return false\n}\n\n/** Recursive matcher: handles top-level keys, $or, value shorthand, and operator objects. */\nfunction matchesRow(row: Record<string, unknown>, where: Record<string, unknown>): boolean {\n for (const [key, val] of Object.entries(where)) {\n if (key === '$or') {\n const branches = val as Record<string, unknown>[]\n if (!branches.some((b) => matchesRow(row, b))) return false\n continue\n }\n if (key === '$and') {\n const branches = val as Record<string, unknown>[]\n if (!branches.every((b) => matchesRow(row, b))) return false\n continue\n }\n const rowVal = row[key]\n if (val === null) {\n if (rowVal !== null && rowVal !== undefined) return false\n continue\n }\n if (val !== null && typeof val === 'object' && !(val instanceof Date)) {\n if (!matchesOp(rowVal, val as MemOp)) return false\n continue\n }\n if (rowVal !== val) return false\n }\n return true\n}\n\n/** OrderBy spec mirrors TableClient's `OrderBySpec`. */\nexport interface InMemoryOrderBy {\n column: string\n dir?: 'asc' | 'desc'\n}\n\n/** Subset of TableClient methods used by tests. */\nexport interface InMemoryTableClient<Row extends Record<string, unknown>> {\n rows: Row[] // direct mutation for test seeding / inspection\n reset(initial?: Row[]): void\n findOne(where: Record<string, unknown>): Promise<Row | null>\n findMany(opts?: {\n where?: Record<string, unknown>\n orderBy?: InMemoryOrderBy[]\n limit?: number\n offset?: number\n }): Promise<Row[]>\n exists(where: Record<string, unknown>): Promise<boolean>\n count(where?: Record<string, unknown>): Promise<number>\n insert(values: Partial<Row>): Promise<Row>\n update(where: Record<string, unknown>, patch: Partial<Row>): Promise<Row | null>\n upsert(\n values: Partial<Row>,\n opts: { target: string[] | string; set?: Partial<Row>; mergeJsonb?: readonly string[] },\n ): Promise<Row>\n delete(where: Record<string, unknown>): Promise<Row | null>\n deleteMany(where: Record<string, unknown>): Promise<Row[]>\n tryClaim(\n values: Partial<Row>,\n opts: {\n target: string | string[]\n replaceWhen: Record<string, unknown>\n set?: Partial<Row>\n },\n ): Promise<{ acquired: true; row: Row } | { acquired: false; row: Row }>\n aggregate?: (opts: {\n select: Record<string, { fn: 'max' | 'min' | 'count' | 'sum' | 'avg'; column?: string }>\n where?: Record<string, unknown>\n }) => Promise<Array<Record<string, unknown>>>\n}\n\n/**\n * Build an in-memory TableClient stand-in for unit tests. Pass\n * `idGenerator` if your test wants deterministic synthetic IDs\n * (defaults to a monotonic `row-1`, `row-2`, ... counter).\n *\n * Example:\n * ```ts\n * const drafts = createInMemoryTableClient<DraftRow>({ idField: 'id' })\n * vi.mock('@murumets-ee/content/schema', () => ({\n * getContentTables: () => ({ drafts: { makeClient: () => drafts } })\n * }))\n * ```\n */\nexport function createInMemoryTableClient<Row extends Record<string, unknown>>(\n options: { idField?: keyof Row & string; idGenerator?: () => string; initial?: Row[] } = {},\n): InMemoryTableClient<Row> {\n const idField = (options.idField ?? 'id') as keyof Row & string\n let nextId = 1\n const idGen = options.idGenerator ?? (() => `row-${nextId++}`)\n\n const state: { rows: Row[] } = { rows: options.initial ? [...options.initial] : [] }\n\n const client: InMemoryTableClient<Row> = {\n get rows() {\n return state.rows\n },\n set rows(next: Row[]) {\n state.rows = next\n },\n\n reset(initial?: Row[]) {\n state.rows = initial ? [...initial] : []\n nextId = 1\n },\n\n async findOne(where) {\n return state.rows.find((r) => matchesRow(r, where)) ?? null\n },\n\n async findMany(opts) {\n let out = opts?.where\n ? state.rows.filter((r) => matchesRow(r, opts.where as Record<string, unknown>))\n : [...state.rows]\n if (opts?.orderBy && opts.orderBy.length > 0) {\n out = [...out].sort((a, b) => {\n for (const spec of opts.orderBy ?? []) {\n const av = a[spec.column]\n const bv = b[spec.column]\n const dir = spec.dir === 'desc' ? -1 : 1\n // Date and number comparisons via subtraction; string via localeCompare.\n if (av instanceof Date && bv instanceof Date) {\n const d = (av.getTime() - bv.getTime()) * dir\n if (d !== 0) return d\n } else if (typeof av === 'number' && typeof bv === 'number') {\n const d = (av - bv) * dir\n if (d !== 0) return d\n } else if (typeof av === 'string' && typeof bv === 'string') {\n const d = av.localeCompare(bv) * dir\n if (d !== 0) return d\n } else {\n // Mixed / nullable: fall through to next spec.\n }\n }\n return 0\n })\n }\n const offset = opts?.offset ?? 0\n const limit = opts?.limit\n if (offset > 0) out = out.slice(offset)\n if (limit !== undefined && limit >= 0) out = out.slice(0, limit)\n return out\n },\n\n async aggregate(opts) {\n const subset = opts?.where\n ? state.rows.filter((r) => matchesRow(r, opts.where as Record<string, unknown>))\n : state.rows\n const result: Record<string, unknown> = {}\n for (const [alias, spec] of Object.entries(opts.select)) {\n if (spec.fn === 'count') {\n result[alias] = subset.length\n } else if (spec.fn === 'max' && spec.column) {\n let max: number | null = null\n for (const r of subset) {\n const v = r[spec.column]\n if (typeof v === 'number') {\n if (max === null || v > max) max = v\n }\n }\n result[alias] = max\n } else if (spec.fn === 'min' && spec.column) {\n let min: number | null = null\n for (const r of subset) {\n const v = r[spec.column]\n if (typeof v === 'number') {\n if (min === null || v < min) min = v\n }\n }\n result[alias] = min\n } else if (spec.fn === 'sum' && spec.column) {\n let sum = 0\n for (const r of subset) {\n const v = r[spec.column]\n if (typeof v === 'number') sum += v\n }\n result[alias] = sum\n }\n }\n return [result]\n },\n\n async exists(where) {\n return state.rows.some((r) => matchesRow(r, where))\n },\n\n async count(where) {\n if (!where) return state.rows.length\n return state.rows.filter((r) => matchesRow(r, where)).length\n },\n\n async insert(values) {\n const row = { [idField]: idGen(), ...values } as Row\n state.rows.push(row)\n return row\n },\n\n async update(where, patch) {\n const matched = state.rows.filter((r) => matchesRow(r, where))\n if (matched.length === 0) return null\n // TableClient.update expects at-most-one match\n const target = matched[0] as Row\n Object.assign(target, patch)\n return target\n },\n\n async upsert(values, opts) {\n const targetKeys = Array.isArray(opts.target) ? opts.target : [opts.target]\n const targetWhere: Record<string, unknown> = {}\n for (const k of targetKeys) targetWhere[k] = (values as Record<string, unknown>)[k]\n // Same refusal as TableClient, before either path: `||` concatenates a\n // non-object, and TableClient refuses one whether or not a row exists.\n const merged: Record<string, Record<string, unknown>> = {}\n for (const key of opts.mergeJsonb ?? []) {\n const incoming = (values as Record<string, unknown>)[key]\n if (typeof incoming !== 'object' || incoming === null || Array.isArray(incoming)) {\n throw new Error(`upsert mergeJsonb: '${key}' must be a JSON object to merge`)\n }\n merged[key] = incoming as Record<string, unknown>\n }\n const existing = state.rows.find((r) => matchesRow(r, targetWhere))\n if (existing) {\n const patch: Record<string, unknown> = { ...(opts.set ?? values) }\n // Mirrors TableClient's `stored || incoming` — a top-level, right-wins\n // merge, with a stored non-object treated as `{}`.\n for (const [key, incoming] of Object.entries(merged)) {\n const stored = (existing as Record<string, unknown>)[key]\n const base =\n typeof stored === 'object' && stored !== null && !Array.isArray(stored) ? stored : {}\n patch[key] = { ...base, ...incoming }\n }\n Object.assign(existing, patch)\n return existing\n }\n const row = { [idField]: idGen(), ...values } as Row\n state.rows.push(row)\n return row\n },\n\n async delete(where) {\n const idx = state.rows.findIndex((r) => matchesRow(r, where))\n if (idx === -1) return null\n const [removed] = state.rows.splice(idx, 1)\n return (removed ?? null) as Row | null\n },\n\n async deleteMany(where) {\n const matched = state.rows.filter((r) => matchesRow(r, where))\n state.rows = state.rows.filter((r) => !matched.includes(r))\n return matched\n },\n\n async tryClaim(values, opts) {\n const targetKeys = Array.isArray(opts.target) ? opts.target : [opts.target]\n const targetWhere: Record<string, unknown> = {}\n for (const k of targetKeys) targetWhere[k] = (values as Record<string, unknown>)[k]\n const existing = state.rows.find((r) => matchesRow(r, targetWhere))\n\n if (!existing) {\n const row = { [idField]: idGen(), ...values } as Row\n state.rows.push(row)\n return { acquired: true, row }\n }\n\n if (matchesRow(existing, opts.replaceWhen)) {\n Object.assign(existing, opts.set ?? values)\n return { acquired: true, row: existing }\n }\n\n return { acquired: false, row: existing }\n },\n }\n\n return client\n}\n\n/**\n * Wrap a Drizzle handle so `.execute()` returns an array that *also* exposes\n * a `.rows` property. `drizzle-kit/api`'s `pushSchema` reads `res.rows`, but\n * postgres-js (the driver Drizzle is built on here) returns plain arrays\n * with no `.rows`. The shim is local to test-utils — production code never\n * needs it.\n */\nfunction withRowsShim(db: PostgresJsDatabase): PostgresJsDatabase {\n return new Proxy(db, {\n get(target, prop, receiver) {\n if (prop === 'execute') {\n return async (...args: unknown[]) => {\n const result = await (target.execute as (...a: unknown[]) => Promise<unknown[]>)(...args)\n if (Array.isArray(result) && !('rows' in result)) {\n Object.defineProperty(result, 'rows', { value: [...result], enumerable: false })\n }\n return result\n }\n }\n return Reflect.get(target, prop, receiver)\n },\n })\n}\n"],"mappings":"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"}
|