@murumets-ee/db 0.0.0-preview-20261014110621

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.
@@ -0,0 +1,1649 @@
1
+ import { PostgresJsDatabase } from "drizzle-orm/postgres-js";
2
+ import { Sql } from "postgres";
3
+ import { InferInsertModel, SQL, Table } from "drizzle-orm";
4
+ import { AnyPgColumn, AnyPgColumn as AnyPgColumn$1, PgColumnBuilderBase, PgTable, PgTableWithColumns, TableConfig } from "drizzle-orm/pg-core";
5
+ import { Logger } from "drizzle-orm/logger";
6
+
7
+ //#region src/client.d.ts
8
+ interface DbConfig {
9
+ url: string;
10
+ readOnlyUrl?: string;
11
+ poolMin?: number;
12
+ poolMax?: number;
13
+ /**
14
+ * Per-statement ceiling in milliseconds for the READ-ONLY pool only
15
+ * (default {@link DEFAULT_READ_ONLY_STATEMENT_TIMEOUT_MS}). Must be a
16
+ * positive integer; `0` and negatives are rejected rather than treated as
17
+ * "disabled" — see {@link createReadOnlyClient}.
18
+ *
19
+ * The read-write pool is deliberately not covered: it serves migrations,
20
+ * bulk imports and admin writes, where a statement running for minutes is
21
+ * legitimate.
22
+ */
23
+ readOnlyStatementTimeoutMs?: number;
24
+ }
25
+ /**
26
+ * Default read-only `statement_timeout`, in milliseconds.
27
+ *
28
+ * Calibrated against the one perf-harness scenario that actually exercises
29
+ * this pool: `locale-fallback` drives real content-api reads (which read
30
+ * through `app.db.readOnly`) at ~540 rps with p95 ≈ 55 ms, flat from 1k to
31
+ * 1M rows. 5 s sits ~100× above that.
32
+ *
33
+ * Deliberately NOT cited as justification: the harness's pathological
34
+ * `tickets-per-agent` scenario (11.6 s p95 at 1M rows). It reads through
35
+ * `AdminClient.aggregate` on the READ-WRITE pool, so it cannot reach the pool
36
+ * being capped here — and its p50 at 1M is 5016 ms, i.e. this ceiling would
37
+ * sit at its median rather than safely above it. The harness contains no
38
+ * measurement of a >5 s read-ONLY-pool query, so this default rests on a
39
+ * single scenario plus the ~100× margin, not on a measured bad case.
40
+ */
41
+ declare const DEFAULT_READ_ONLY_STATEMENT_TIMEOUT_MS = 5000;
42
+ /**
43
+ * Postgres' own upper bound for `statement_timeout` (a signed 32-bit
44
+ * millisecond count). Anything larger is rejected with SQLSTATE 22023 — but
45
+ * postgres-js connects LAZILY, so an out-of-range value would let the process
46
+ * boot green and then fail EVERY read at connection time, with an error
47
+ * pointing at the connection rather than at config. Rejecting it up front
48
+ * keeps the failure attached to its cause.
49
+ */
50
+ declare const MAX_STATEMENT_TIMEOUT_MS = 2147483647;
51
+ /**
52
+ * Parse a `readOnlyStatementTimeoutMs` override out of an environment string,
53
+ * returning `undefined` (i.e. "use the default") for anything unusable.
54
+ *
55
+ * This exists so an operator can raise the ceiling on a deployment whose read
56
+ * path is legitimately slower WITHOUT a rebuild and redeploy. That makes it an
57
+ * incident-time tool, which forces two properties:
58
+ *
59
+ * - **It must never throw.** `lumi.config.ts` is evaluated at BUILD time, and a
60
+ * throw there fails the build rather than the request. Passing an unvalidated
61
+ * value straight through would be worse still: `createReadOnlyClient` rejects
62
+ * out-of-range values, so a typo'd env var would abort app construction — an
63
+ * escape hatch that can itself take the process down is not an escape hatch.
64
+ * - **It must not fail silently either.** Someone raising the ceiling mid-incident
65
+ * and getting the 5 s default back with no signal would conclude the knob is
66
+ * broken. Hence the warning: `console` rather than `@murumets-ee/logging`
67
+ * because config is evaluated before any logger exists.
68
+ */
69
+ declare function parseStatementTimeoutEnv(raw: string | undefined): number | undefined;
70
+ /**
71
+ * Create a full read-write database client.
72
+ *
73
+ * Its transactions are tracked (`trackTransactions`), so `onCommit` works on
74
+ * every `tx` it hands out. This is where `app.db.readWrite` and the CLI's
75
+ * client come from, which is what lets `AdminClient` hold its post-commit
76
+ * seams until a caller's transaction actually commits — with no change at any
77
+ * call site that opens one.
78
+ */
79
+ declare function createDbClient(config: DbConfig): PostgresJsDatabase;
80
+ /**
81
+ * Create a read-only database client.
82
+ *
83
+ * Sets `default_transaction_read_only = on` as a startup parameter on every
84
+ * connection in the pool, so any implicit transaction issued through this
85
+ * client is read-only at the PostgreSQL level. This is the defense-in-depth
86
+ * layer beneath the application's own routing of writes through `AdminClient`
87
+ * — even if a write somehow reaches a read-only client, Postgres rejects it.
88
+ *
89
+ * Also sets `statement_timeout` so a single slow read cannot hold a pool
90
+ * connection indefinitely. The pool defaults to 10 connections, so ten
91
+ * concurrently-slow queries exhaust it and every later read blocks. Rate
92
+ * limiting cannot close this gap: a limiter bounds how many requests ARRIVE,
93
+ * this bounds how long each one HOLDS a connection.
94
+ *
95
+ * Implementation note: postgres-js's `connection` config object is sent in
96
+ * the PostgreSQL startup packet for every new pool connection, so both GUCs
97
+ * are applied uniformly across the whole pool — not just whichever connection
98
+ * happened to run a one-off `SET` statement.
99
+ *
100
+ * @throws if `readOnlyStatementTimeoutMs` is not a positive integer. `0` is
101
+ * rejected rather than accepted as Postgres' "disabled" sentinel, for two
102
+ * reasons: this is an availability control, so a typo'd `0` must not silently
103
+ * remove it; and postgres-js's startup-packet builder filters falsy values
104
+ * out, so a `0` would never reach the server anyway — the connection would
105
+ * quietly inherit whatever server- or role-level default happened to be set.
106
+ * Deployments that need a longer ceiling raise the number.
107
+ */
108
+ declare function createReadOnlyClient(config: DbConfig): PostgresJsDatabase;
109
+ //#endregion
110
+ //#region src/deploy/advisory-lock.d.ts
111
+ /**
112
+ * An advisory lock's two-part key. Both halves are signed 32-bit integers —
113
+ * Postgres' `(int4, int4)` form, so toolkit locks sit in their own namespace
114
+ * (`space`) and cannot collide with an application's single-`bigint` keys.
115
+ */
116
+ interface AdvisoryLockKey {
117
+ readonly space: number;
118
+ readonly id: number;
119
+ }
120
+ /** The first half of every toolkit-owned advisory lock key: ASCII `LUMI`. */
121
+ declare const TOOLKIT_ADVISORY_LOCK_SPACE = 1280658761;
122
+ /** One deploy-step runner per database. */
123
+ declare const DEPLOY_STEPS_LOCK: AdvisoryLockKey;
124
+ type AdvisoryLockResult<T> = {
125
+ readonly acquired: true;
126
+ readonly value: T;
127
+ } | {
128
+ readonly acquired: false;
129
+ };
130
+ /**
131
+ * Run `fn` while holding the advisory lock `key`, or return
132
+ * `{ acquired: false }` without running it when someone else holds it.
133
+ */
134
+ declare function withAdvisoryLock<T>(db: PostgresJsDatabase, key: AdvisoryLockKey, fn: () => Promise<T>): Promise<AdvisoryLockResult<T>>;
135
+ //#endregion
136
+ //#region src/deploy/deploy-steps-table.d.ts
137
+ /**
138
+ * The deploy-step ledger (plan `deploy-drift` D002): one row per ATTEMPT of a
139
+ * deploy step on a site. Nothing here is ever updated or deleted — the current
140
+ * state of a step on a site is its NEWEST row, and the older ones are its
141
+ * history. Resetting a step is a new attempt, never a removed row.
142
+ *
143
+ * A framework table, not a plugin's: every lumi-cms project has deploy steps
144
+ * available and therefore has this table, whichever plugins it loads. It is
145
+ * wired where `entity_refs` is — `createApp`, `buildCliApp` and `lumi migrate`'s
146
+ * schema map — so it reaches the schema snapshot and gets a generated
147
+ * migration like any other table (unlike `_toolkit_migrations`, which is raw
148
+ * SQL outside the snapshots and is not the shape to copy).
149
+ *
150
+ * It lives in `@murumets-ee/db` because the runner that writes it lives in
151
+ * `@murumets-ee/core`, and core owns no tables of its own.
152
+ */
153
+ /** The table's name — also its key in the migration schema map. */
154
+ declare const DEPLOY_STEPS_TABLE_NAME = "toolkit_deploy_steps";
155
+ /**
156
+ * What an attempt ended as. STORED states only: "waiting" and "waiting for
157
+ * another step" are computed on every read from what is declared and what is
158
+ * recorded, and are never written (a cached pending list is the bug Drupal had).
159
+ */
160
+ declare const DEPLOY_STEP_STATES: readonly ["applied", "needs-a-person", "marked-done"];
161
+ type DeployStepState = (typeof DEPLOY_STEP_STATES)[number];
162
+ declare const deployStepsTable: {
163
+ table: import("drizzle-orm/pg-core").PgTableWithColumns<{
164
+ name: string;
165
+ schema: undefined;
166
+ columns: {
167
+ [x: string]: import("drizzle-orm/pg-core").PgColumn<{
168
+ name: string;
169
+ tableName: string;
170
+ dataType: import("drizzle-orm").ColumnDataType;
171
+ columnType: string;
172
+ data: unknown;
173
+ driverParam: unknown;
174
+ notNull: false;
175
+ hasDefault: false;
176
+ isPrimaryKey: false;
177
+ isAutoincrement: false;
178
+ hasRuntimeDefault: false;
179
+ enumValues: string[] | undefined;
180
+ baseColumn: never;
181
+ identity: undefined;
182
+ generated: undefined;
183
+ }, {}, {}>;
184
+ };
185
+ dialect: "pg";
186
+ }>;
187
+ schema: TableDefinition<{
188
+ readonly id: ColumnFactory<string, "uuid", true, true>; /** The seeder's `name`. */
189
+ readonly step: ColumnFactory<string, "varchar", true, false>; /** The site handle the attempt was for. */
190
+ readonly site: ColumnFactory<string, "varchar", true, false>; /** One of {@link DEPLOY_STEP_STATES}. */
191
+ readonly state: ColumnFactory<string, "varchar", true, false>; /** Why it needs a person — the step's own words, or the error's message. */
192
+ readonly reason: ColumnFactory<string, "text", false, false>; /** What a person should do about it — the step's own words. */
193
+ readonly whatToDo: ColumnFactory<string, "text", false, false>; /** Rows the step said it created. Null for a mark and for a throw. */
194
+ readonly created: ColumnFactory<number, "integer", false, false>;
195
+ readonly at: ColumnFactory<Date, "timestamp", true, true>;
196
+ /**
197
+ * Who ran it: `deploy`, `cli`, or a user id. A user id is varchar, never
198
+ * uuid — better-auth ids are random strings.
199
+ */
200
+ readonly by: ColumnFactory<string, "varchar", true, false>; /** That person's name or e-mail as it was then; null for `deploy` / `cli`. */
201
+ readonly byName: ColumnFactory<string, "varchar", false, false>; /** The commit of the build that ran it; null when the build was not told. */
202
+ readonly buildCommit: ColumnFactory<string, "varchar", false, false>; /** The toolkit version that ran it. */
203
+ readonly toolkitVersion: ColumnFactory<string, "varchar", false, false>;
204
+ }>;
205
+ columnKinds: Readonly<Record<string, ColumnKind>>;
206
+ primaryKeyColumns: readonly string[];
207
+ makeClient: (database: import("drizzle-orm/postgres-js").PostgresJsDatabase, partitionValue?: string) => TableClient<{
208
+ readonly id: ColumnFactory<string, "uuid", true, true>; /** The seeder's `name`. */
209
+ readonly step: ColumnFactory<string, "varchar", true, false>; /** The site handle the attempt was for. */
210
+ readonly site: ColumnFactory<string, "varchar", true, false>; /** One of {@link DEPLOY_STEP_STATES}. */
211
+ readonly state: ColumnFactory<string, "varchar", true, false>; /** Why it needs a person — the step's own words, or the error's message. */
212
+ readonly reason: ColumnFactory<string, "text", false, false>; /** What a person should do about it — the step's own words. */
213
+ readonly whatToDo: ColumnFactory<string, "text", false, false>; /** Rows the step said it created. Null for a mark and for a throw. */
214
+ readonly created: ColumnFactory<number, "integer", false, false>;
215
+ readonly at: ColumnFactory<Date, "timestamp", true, true>;
216
+ /**
217
+ * Who ran it: `deploy`, `cli`, or a user id. A user id is varchar, never
218
+ * uuid — better-auth ids are random strings.
219
+ */
220
+ readonly by: ColumnFactory<string, "varchar", true, false>; /** That person's name or e-mail as it was then; null for `deploy` / `cli`. */
221
+ readonly byName: ColumnFactory<string, "varchar", false, false>; /** The commit of the build that ran it; null when the build was not told. */
222
+ readonly buildCommit: ColumnFactory<string, "varchar", false, false>; /** The toolkit version that ran it. */
223
+ readonly toolkitVersion: ColumnFactory<string, "varchar", false, false>;
224
+ }, import("drizzle-orm/pg-core").PgTableWithColumns<{
225
+ name: string;
226
+ schema: undefined;
227
+ columns: {
228
+ [x: string]: import("drizzle-orm/pg-core").PgColumn<{
229
+ name: string;
230
+ tableName: string;
231
+ dataType: import("drizzle-orm").ColumnDataType;
232
+ columnType: string;
233
+ data: unknown;
234
+ driverParam: unknown;
235
+ notNull: false;
236
+ hasDefault: false;
237
+ isPrimaryKey: false;
238
+ isAutoincrement: false;
239
+ hasRuntimeDefault: false;
240
+ enumValues: string[] | undefined;
241
+ baseColumn: never;
242
+ identity: undefined;
243
+ generated: undefined;
244
+ }, {}, {}>;
245
+ };
246
+ dialect: "pg";
247
+ }>>;
248
+ partitionColumn: "step" | "site" | "at" | "id" | "state" | "reason" | "whatToDo" | "created" | "by" | "byName" | "buildCommit" | "toolkitVersion" | undefined;
249
+ };
250
+ declare const toolkitDeploySteps: import("drizzle-orm/pg-core").PgTableWithColumns<{
251
+ name: string;
252
+ schema: undefined;
253
+ columns: {
254
+ [x: string]: import("drizzle-orm/pg-core").PgColumn<{
255
+ name: string;
256
+ tableName: string;
257
+ dataType: import("drizzle-orm").ColumnDataType;
258
+ columnType: string;
259
+ data: unknown;
260
+ driverParam: unknown;
261
+ notNull: false;
262
+ hasDefault: false;
263
+ isPrimaryKey: false;
264
+ isAutoincrement: false;
265
+ hasRuntimeDefault: false;
266
+ enumValues: string[] | undefined;
267
+ baseColumn: never;
268
+ identity: undefined;
269
+ generated: undefined;
270
+ }, {}, {}>;
271
+ };
272
+ dialect: "pg";
273
+ }>;
274
+ //#endregion
275
+ //#region src/extensions.d.ts
276
+ /**
277
+ * Postgres extension helpers — the single source of truth for how the toolkit
278
+ * enables a Postgres extension.
279
+ *
280
+ * Declarative source: plugins declare `server.extensions: ['btree_gist']`
281
+ * (see `@murumets-ee/core`'s `PluginServer`). Consumers — `lumi migrate`
282
+ * (emits into the generated migration) and `@murumets-ee/db`'s `createTestDb`
283
+ * (enables before `pushSchema`) — both route through the helpers here so the
284
+ * validation rule and the emitted SQL are identical everywhere (the migrate
285
+ * dedup-scan relies on the canonical statement being byte-for-byte stable).
286
+ *
287
+ * Why validate the name: `CREATE EXTENSION` cannot parameterize an identifier,
288
+ * so the extension name is interpolated as a literal. Names come from plugin
289
+ * code (not request input), but per the "validate at the boundary" rule we
290
+ * still whitelist the identifier shape so a typo or a hostile plugin can't
291
+ * inject SQL through the one place a raw identifier reaches a DDL string.
292
+ */
293
+ /**
294
+ * Valid Postgres extension identifier: a lowercase unquoted identifier —
295
+ * leading letter/underscore, then letters/digits/underscores. Covers every
296
+ * real extension name (`btree_gist`, `pg_trgm`, `uuid-ossp` is the lone
297
+ * hyphenated outlier and is intentionally rejected here; declare it quoted via
298
+ * a custom migration if ever needed). Anchored, so no embedded whitespace,
299
+ * quotes, semicolons, or comment markers can slip through.
300
+ */
301
+ declare const EXTENSION_NAME_PATTERN: RegExp;
302
+ /**
303
+ * Throw if `name` is not a safe, unquoted Postgres extension identifier.
304
+ * Kept separate from {@link createExtensionStatement} so callers that only
305
+ * want to validate (e.g. a config linter) don't have to build SQL.
306
+ */
307
+ declare function assertValidExtensionName(name: string): void;
308
+ /**
309
+ * The canonical, idempotent statement that enables one extension.
310
+ * Validates first, then double-quotes the (already-safe) identifier.
311
+ *
312
+ * This exact string is what `lumi migrate` writes into migration files AND
313
+ * what it scans prior migrations for to avoid re-emitting — do not change the
314
+ * shape without updating the dedup scan in `packages/cli/src/commands/migrate.ts`.
315
+ */
316
+ declare function createExtensionStatement(name: string): string;
317
+ //#endregion
318
+ //#region src/listen.d.ts
319
+ /**
320
+ * Create a dedicated postgres-js connection sized for `LISTEN/NOTIFY`.
321
+ *
322
+ * The underlying postgres-js Sql instance is intentionally NOT a Drizzle
323
+ * client — `LISTEN`/`NOTIFY` is a low-level Postgres feature that the
324
+ * Drizzle query builder doesn't model. The realtime cross-process bridge
325
+ * (in `@murumets-ee/core/realtime`) holds one of these per process:
326
+ *
327
+ * - `sql.listen(channel, onnotify, onlisten)` opens a dedicated long-lived
328
+ * connection and delivers every `NOTIFY` on the channel to `onnotify`.
329
+ * - `sql.notify(channel, payload)` (or `pg_notify`) issues a notification.
330
+ *
331
+ * Pool size is intentionally small (max 2): one connection is parked on
332
+ * `LISTEN`, leaving headroom for occasional `NOTIFY` issues from the same
333
+ * Sql instance without contending with the rest of the app's pool.
334
+ */
335
+ declare function createListenClient(config: Pick<DbConfig, 'url'>): Sql;
336
+ //#endregion
337
+ //#region src/migrate.d.ts
338
+ /**
339
+ * Arguments passed to migration `up` / `down` functions.
340
+ *
341
+ * Intentionally tiny: we hand over a read-write Drizzle handle bound to
342
+ * the current transaction. If a migration needs anything beyond raw SQL
343
+ * (e.g. calling an AdminClient for a data backfill), it can import the
344
+ * relevant toolkit helpers inside the migration file — the CLI loader
345
+ * runs the migration in the same process as the config, so all package
346
+ * imports resolve normally.
347
+ */
348
+ interface MigrateArgs {
349
+ db: PostgresJsDatabase;
350
+ }
351
+ /**
352
+ * Shape of a migration module loaded from a `.ts` file.
353
+ *
354
+ * At minimum a migration must export `up`. `down` is optional — when
355
+ * absent, `lumi migrate:rollback` refuses to roll that file back.
356
+ */
357
+ interface MigrationModule {
358
+ up: (args: MigrateArgs) => Promise<void>;
359
+ down?: (args: MigrateArgs) => Promise<void>;
360
+ }
361
+ interface MigrationSource {
362
+ /** Absolute path to the migration file on disk. */
363
+ path: string;
364
+ /** `project` for user migrations, `toolkit` for shipped-with-toolkit ones. */
365
+ namespace: 'toolkit' | 'project';
366
+ /** Filename only, used for ordering + tracking. */
367
+ name: string;
368
+ }
369
+ interface MigrationStatus {
370
+ applied: MigrationSource[];
371
+ pending: MigrationSource[];
372
+ }
373
+ /**
374
+ * Loader callback — imports a migration `.ts` file and returns its exports.
375
+ *
376
+ * The CLI owns `jiti` (already used to load `lumi.config.ts`), so
377
+ * `@murumets-ee/db` stays free of TS-runtime loader dependencies.
378
+ * Callers who never apply migrations (read-only servers, tests) never
379
+ * pay the loader cost.
380
+ */
381
+ type MigrationLoader = (absolutePath: string) => Promise<MigrationModule>;
382
+ /** Split discovered migrations into applied vs pending by consulting the tracking table. */
383
+ declare function getMigrationStatus(db: PostgresJsDatabase, projectRoot: string): Promise<MigrationStatus>;
384
+ /**
385
+ * Apply every pending migration in order.
386
+ *
387
+ * Each file runs inside its own transaction: the migration's `up()`
388
+ * executes first, then the `_toolkit_migrations` insert commits. If
389
+ * `up()` throws, the transaction rolls back and the whole command
390
+ * aborts — nothing gets half-applied.
391
+ *
392
+ * The `loader` callback is injected so `@murumets-ee/db` itself doesn't
393
+ * depend on a TS-runtime loader. Callers pass a jiti-backed loader from
394
+ * the CLI or their own equivalent.
395
+ */
396
+ declare function runMigrations(db: PostgresJsDatabase, projectRoot: string, loader: MigrationLoader): Promise<void>;
397
+ /**
398
+ * Roll back the most recent applied migration (or N most recent).
399
+ *
400
+ * Each file's `down()` runs in its own transaction. Files without a
401
+ * `down` export cause the rollback to abort before running anything —
402
+ * we never roll back some but not all of a requested batch.
403
+ */
404
+ declare function rollbackMigrations(db: PostgresJsDatabase, projectRoot: string, loader: MigrationLoader, count?: number): Promise<void>;
405
+ //#endregion
406
+ //#region src/query-counter.d.ts
407
+ /**
408
+ * Per-request DB query counter — opt-in, off by default.
409
+ *
410
+ * Wired into the Drizzle client when `LUMI_DB_QUERY_COUNTER=1`. The harness
411
+ * (apps/perf-harness) wraps each HTTP request in `runWithQueryStats` and emits
412
+ * the count as an `x-db-query-count` response header, so k6 scenarios can
413
+ * assert per-endpoint query budgets like:
414
+ *
415
+ * checks: { 'findById ≤ 2 queries': (r) => r.headers['X-Db-Query-Count'] <= '2' }
416
+ *
417
+ * No-op for any code path that does not enter `runWithQueryStats` — the ALS
418
+ * store is undefined and `logQuery` returns immediately.
419
+ */
420
+ interface QueryStats {
421
+ count: number;
422
+ }
423
+ declare function getQueryStats(): QueryStats | undefined;
424
+ declare function runWithQueryStats<T>(fn: (stats: QueryStats) => Promise<T>): Promise<T>;
425
+ declare const queryCountingLogger: Logger;
426
+ declare function isQueryCounterEnabled(): boolean;
427
+ //#endregion
428
+ //#region src/schema-registry.d.ts
429
+ /**
430
+ * Schema registry for storing Drizzle schemas
431
+ * This will be populated by @org/entity when schemas are generated from entity definitions
432
+ *
433
+ * We use PgTableWithColumns<any> because entity table schemas are dynamic (columns
434
+ * not known at compile time) and we need runtime property access (.id, .status, etc.).
435
+ * AnyPgTable won't work here — it doesn't expose column accessors.
436
+ */
437
+ type AnyTable = PgTableWithColumns<any>;
438
+ interface SchemaRegistry {
439
+ register(name: string, schema: AnyTable): void;
440
+ get(name: string): AnyTable | undefined;
441
+ all(): Record<string, AnyTable>;
442
+ has(name: string): boolean;
443
+ }
444
+ declare class SchemaRegistryImpl implements SchemaRegistry {
445
+ private schemas;
446
+ register(name: string, schema: AnyTable): void;
447
+ get(name: string): AnyTable | undefined;
448
+ all(): Record<string, AnyTable>;
449
+ has(name: string): boolean;
450
+ }
451
+ /**
452
+ * Global schema registry instance
453
+ * Entities will register their schemas here when they are defined
454
+ */
455
+ declare const schemaRegistry: SchemaRegistryImpl;
456
+ //#endregion
457
+ //#region src/table/types.d.ts
458
+ /**
459
+ * The discriminating "kind" tag for a column factory.
460
+ *
461
+ * Used at the type level to gate features that only make sense for certain
462
+ * kinds — e.g. only `'jsonb'` columns accept dotted-path keys in the
463
+ * where-builder.
464
+ */
465
+ type ColumnKind = 'uuid' | 'varchar' | 'text' | 'integer' | 'bigint' | 'double' | 'boolean' | 'timestamp' | 'jsonb' | 'uuidArray';
466
+ /**
467
+ * Internal column factory.
468
+ *
469
+ * A `ColumnFactory` is a callable that, given a column name, produces the
470
+ * corresponding Drizzle column builder. The phantom type parameters
471
+ * (`__type`, `__kind`, `__notNull`) are used purely for compile-time
472
+ * inference of {@link Row} and {@link InsertRow} types — they have no runtime
473
+ * representation beyond a small metadata object.
474
+ *
475
+ * Users do not construct `ColumnFactory` values directly; they use the
476
+ * `column.*` builders from `./columns.ts`.
477
+ */
478
+ interface ColumnFactory<TType = unknown, TKind extends ColumnKind = ColumnKind, TNotNull extends boolean = boolean, THasDefault extends boolean = boolean> {
479
+ /** Build the underlying Drizzle column for the given column name. */
480
+ (name: string): PgColumnBuilderBase;
481
+ readonly __type: TType;
482
+ readonly __kind: TKind;
483
+ readonly __notNull: TNotNull;
484
+ readonly __hasDefault: THasDefault;
485
+ /**
486
+ * Optional explicit Postgres column name. When set, the column uses this
487
+ * name in the database instead of the JS property name. Needed when porting
488
+ * existing tables that use snake_case column names to `defineTable`.
489
+ */
490
+ readonly __pgName?: string;
491
+ /** True when the column is declared as a primary key at the column level. */
492
+ readonly __primaryKey?: boolean;
493
+ }
494
+ /**
495
+ * Convenience helper: extract the JS value type for a single column.
496
+ *
497
+ * - `notNull: true` → `T`
498
+ * - `notNull: false` → `T | null`
499
+ */
500
+ type ColumnValue<C> = C extends ColumnFactory<infer T, ColumnKind, infer N> ? (N extends true ? T : T | null) : never;
501
+ /**
502
+ * Map a columns record to its row shape (the result of a SELECT).
503
+ */
504
+ type Row<TCols extends Record<string, ColumnFactory>> = { [K in keyof TCols]: ColumnValue<TCols[K]> };
505
+ /**
506
+ * Map a columns record to its insert shape.
507
+ *
508
+ * - Required columns: must be supplied unless they have a default
509
+ * - Nullable columns: optional, accept `null`
510
+ * - Columns with defaults (`defaultRandom`, `defaultNow`, explicit `default`): optional
511
+ */
512
+ type InsertRow<TCols extends Record<string, ColumnFactory>> = { [K in keyof TCols as TCols[K] extends ColumnFactory<unknown, ColumnKind, true, false> ? K : never]: ColumnValue<TCols[K]> } & { [K in keyof TCols as TCols[K] extends ColumnFactory<unknown, ColumnKind, true, false> ? never : K]?: ColumnValue<TCols[K]> };
513
+ /**
514
+ * Operator object for a single column.
515
+ *
516
+ * Each property is a comparison operator. Combine with `$and` / `$or` /
517
+ * `$not` at the parent level to build composite predicates.
518
+ *
519
+ * `ilike` and `startsWith` accept user-supplied strings — the table client
520
+ * automatically escapes `%` and `_` in the value before wrapping with
521
+ * pattern wildcards, so callers cannot inject pattern characters by accident.
522
+ *
523
+ * @example
524
+ * ```ts
525
+ * { status: 'open' } // shorthand for { eq: 'open' }
526
+ * { status: { in: ['open', 'pending'] } }
527
+ * { createdAt: { gte: new Date('2026-01-01') } }
528
+ * { name: { ilike: 'foo%' } } // % escaped from value, only outer wildcards work
529
+ * { description: { isNull: true } }
530
+ * ```
531
+ */
532
+ type ColumnOperators<T> = T | null | {
533
+ eq: T;
534
+ } | {
535
+ ne: T;
536
+ } | {
537
+ in: T[];
538
+ } | {
539
+ notIn: T[];
540
+ } | {
541
+ gt: T;
542
+ } | {
543
+ gte: T;
544
+ } | {
545
+ lt: T;
546
+ } | {
547
+ lte: T;
548
+ } | {
549
+ isNull: true;
550
+ } | {
551
+ isNotNull: true;
552
+ } | {
553
+ ilike: string;
554
+ } | {
555
+ startsWith: string;
556
+ };
557
+ /**
558
+ * The full WhereClause type.
559
+ *
560
+ * Keys are either column names or dotted-path keys for jsonb columns
561
+ * (e.g. `'metadata.author.name'`). Values are operator objects per
562
+ * {@link ColumnOperators}.
563
+ *
564
+ * Combine with `$and` / `$or` / `$not` for composite predicates. All
565
+ * top-level keys are implicitly AND-ed together.
566
+ *
567
+ * The type intentionally accepts `string` for jsonb-path keys rather than
568
+ * a precisely-narrowed template literal, because the path's nested type is
569
+ * unknown to the column definition. Path segments are validated at runtime
570
+ * against `/^[a-zA-Z_][a-zA-Z0-9_-]*$|^\d+$/` and the parent column must
571
+ * be a `jsonb` kind, otherwise the where-builder throws.
572
+ *
573
+ * @example
574
+ * ```ts
575
+ * // Simple equality
576
+ * { userId: 'u_123', status: 'active' }
577
+ *
578
+ * // Composite with $or
579
+ * { $or: [
580
+ * { status: 'open' },
581
+ * { status: 'pending', assignee: { isNull: true } },
582
+ * ] }
583
+ *
584
+ * // jsonb path access (column 'metadata' must be jsonb)
585
+ * { 'metadata.author.name': { ilike: 'alice%' } }
586
+ * ```
587
+ */
588
+ type WhereClause<TCols extends Record<string, ColumnFactory>> = { [K in keyof TCols]?: ColumnOperators<ColumnValue<TCols[K]>> } & {
589
+ [key: `${string}.${string}`]: ColumnOperators<unknown>;
590
+ } & {
591
+ $and?: WhereClause<TCols>[];
592
+ $or?: WhereClause<TCols>[];
593
+ $not?: WhereClause<TCols>;
594
+ };
595
+ /**
596
+ * A partial-index predicate for a table-level index / unique definition.
597
+ *
598
+ * Two forms are accepted:
599
+ *
600
+ * - A plain {@link SQL} fragment (e.g. `sql`deleted_at IS NULL``). Use only
601
+ * when the predicate references columns by their raw Postgres identifier —
602
+ * fragile if the column is stored case-sensitively (Drizzle quotes column
603
+ * identifiers), so prefer the callback form below.
604
+ * - A callback `(columns) => SQL` that receives the table's **resolved**
605
+ * Drizzle column objects (keyed by their JS property name). Interpolating a
606
+ * resolved column into the fragment (`` (c) => sql`${c.isCanonical} = true` ``)
607
+ * always emits the correct quoted identifier regardless of column casing.
608
+ * This is the only place the resolved columns exist — they are produced
609
+ * inside Drizzle's `pgTable(...)` constructor callback, AFTER `defineTable`
610
+ * has been called — so a def-time `SQL` cannot reference them directly.
611
+ *
612
+ * Mirrors the `SchemaFragment.uniqueIndex.where` channel on the
613
+ * `@murumets-ee/entity` behavior `constraints()` API — same drizzle
614
+ * `uniqueIndex().on(...).where(sql)` mechanism, different (infra-table) surface.
615
+ *
616
+ * @remarks Use an INLINE boolean literal (`` sql`${col} = true` ``), NOT
617
+ * `eq(col, true)` — the latter binds a `$1` parameter, which is invalid in
618
+ * DDL ("there is no parameter $1").
619
+ *
620
+ * @remarks The callback param is typed as a loose `Record<string, AnyPgColumn>`
621
+ * rather than the exact `{ [K in keyof TCols]: AnyPgColumn }`. That mapped
622
+ * type would sit in a contravariant (function-argument) position of the
623
+ * object passed to `defineTable<const TCols>`, which collapses `TCols` to the
624
+ * base `Record<string, ColumnFactory>` and silently degrades `Row<TCols>` /
625
+ * `InsertRow<TCols>` / `WhereClause<TCols>` to `unknown`-typed for EVERY table
626
+ * that declares a partial `where` (neither `NoInfer<...>` nor `NoInfer` inside
627
+ * `keyof` prevents the `const`-inference fallback here). The loose param keeps
628
+ * `c.<col>` usable (each is `AnyPgColumn`, exactly what `sql`${col}`` needs)
629
+ * while letting `TCols` be inferred solely from `columns`.
630
+ */
631
+ type TablePredicate = SQL | ((columns: Record<string, AnyPgColumn$1>) => SQL);
632
+ /**
633
+ * Index definition for a table.
634
+ *
635
+ * Indexes are declared at the table level (not per-column) so composite
636
+ * indexes are first-class. Each index gets an auto-generated name unless
637
+ * `name` is supplied: `idx_<table>_<col1>_<col2>`.
638
+ */
639
+ interface IndexDefinition<TCols extends Record<string, ColumnFactory>> {
640
+ on: (keyof TCols)[];
641
+ name?: string;
642
+ /**
643
+ * Optional partial-index predicate — emits `CREATE INDEX ... WHERE <pred>`.
644
+ * See {@link TablePredicate}. Omit for a full index (unchanged behavior).
645
+ */
646
+ where?: TablePredicate;
647
+ }
648
+ /**
649
+ * Unique constraint definition.
650
+ *
651
+ * Composite uniques (multi-column) are declared here. Single-column
652
+ * uniqueness is also supported here rather than as a column-level option,
653
+ * to keep the column builder API uniform.
654
+ */
655
+ interface UniqueDefinition<TCols extends Record<string, ColumnFactory>> {
656
+ on: (keyof TCols)[];
657
+ /**
658
+ * The constraint (or, with `where`, unique index) name in Postgres.
659
+ *
660
+ * Without it, a plain unique is named by drizzle — `<table>_<db col
661
+ * names>_unique`, using each column's `pgName` when set — and a partial
662
+ * unique is named `uq_<table>_<props>`. Either way `defineTable` throws when
663
+ * the resulting identifier exceeds Postgres's 63-character limit (which
664
+ * would otherwise truncate it silently); supply a shorter `name` then.
665
+ */
666
+ name?: string;
667
+ /**
668
+ * Optional partial-uniqueness predicate — emits a partial UNIQUE **INDEX**
669
+ * (`CREATE UNIQUE INDEX ... WHERE <pred>`) rather than a `UNIQUE` constraint,
670
+ * because Postgres only supports a predicate on a unique INDEX, never on a
671
+ * table-level `UNIQUE` constraint. When omitted, a plain `UNIQUE` constraint
672
+ * is emitted exactly as before (fully backward compatible). See
673
+ * {@link TablePredicate}.
674
+ */
675
+ where?: TablePredicate;
676
+ }
677
+ /**
678
+ * Declares that rows in this infrastructure table belong to an entity and
679
+ * should be cleaned up when that entity is deleted — the declarative
680
+ * replacement for hand-coded `db.delete(infraTable).where(...)` in routes.
681
+ *
682
+ * - `cascade` → delete the dependent rows
683
+ * - `set-null` → null the FK column on the dependent rows (column must be nullable)
684
+ *
685
+ * The cleanup runs inside the entity-delete transaction via the table's own
686
+ * TableClient (no raw DB access escapes the typed-client layer).
687
+ */
688
+ interface TableDependency<TCols extends Record<string, ColumnFactory>> {
689
+ /** Entity name whose deletion triggers this cleanup (e.g. 'ticket'). */
690
+ entity: string;
691
+ /** Column on THIS table holding the entity id. */
692
+ column: keyof TCols;
693
+ /** `restrict` is intentionally unsupported for infra dependencies. */
694
+ onDelete: 'cascade' | 'set-null';
695
+ }
696
+ /**
697
+ * The shape passed to `defineTable`.
698
+ *
699
+ * @example
700
+ * ```ts
701
+ * defineTable({
702
+ * name: 'ticket_read_state',
703
+ * columns: {
704
+ * ticketId: column.uuid({ notNull: true }),
705
+ * userId: column.varchar({ length: 255, notNull: true }),
706
+ * lastReadAt: column.timestamp({ notNull: true, defaultNow: true, withTimezone: true }),
707
+ * },
708
+ * primaryKey: ['ticketId', 'userId'],
709
+ * indexes: [{ on: ['userId'] }],
710
+ * })
711
+ * ```
712
+ */
713
+ interface TableDefinition<TCols extends Record<string, ColumnFactory>> {
714
+ /**
715
+ * Postgres table name. Convention: snake_case, prefix with package name
716
+ * (e.g. `toolkit_jobs`, `ticketing_read_state`).
717
+ *
718
+ * Validated at definition time against `/^[a-z][a-z0-9_]*$/` to prevent
719
+ * accidental quoting issues and to enforce a consistent naming scheme.
720
+ */
721
+ name: string;
722
+ columns: TCols;
723
+ /**
724
+ * Primary key column(s). May be a single column key or an array for
725
+ * composite keys.
726
+ *
727
+ * If omitted, the table has no primary key — only valid for tables that
728
+ * declare a column with `primaryKey: true` at the column level (e.g. via
729
+ * `column.uuid({ primaryKey: true, defaultRandom: true })`).
730
+ */
731
+ primaryKey?: keyof TCols | (keyof TCols)[];
732
+ unique?: UniqueDefinition<TCols>[];
733
+ indexes?: IndexDefinition<TCols>[];
734
+ /**
735
+ * Declarative cleanup when a referenced entity is deleted. The framework
736
+ * registers each entry and executes it (via TableClient) inside the
737
+ * entity-delete transaction. See {@link TableDependency}.
738
+ */
739
+ dependsOn?: TableDependency<TCols>[];
740
+ /**
741
+ * A column that CONFINES every client of this table to one value — the
742
+ * site of a site-bound entity's companion table (plan `multisite` D003).
743
+ *
744
+ * When set, `makeClient(db)` refuses to build a client without a partition
745
+ * value, and the client it builds with one:
746
+ * - ANDs `column = value` into every WHERE (reads, updates, deletes, claims,
747
+ * counts, aggregates — including the calls that pass no WHERE at all);
748
+ * - stamps `column = value` on every inserted / upserted row, overwriting
749
+ * whatever the caller passed;
750
+ * - prepends `column` to every upsert / tryClaim conflict target (the
751
+ * table's uniques lead with it);
752
+ * - refuses a patch that tries to change `column`.
753
+ *
754
+ * A partitioned table therefore cannot be read or written unconfined by
755
+ * accident: forgetting the value is an error, not a wider result.
756
+ */
757
+ partitionColumn?: keyof TCols & string;
758
+ }
759
+ //#endregion
760
+ //#region src/table/client.d.ts
761
+ /**
762
+ * Default page size for `findMany` when no `limit` is supplied.
763
+ *
764
+ * Chosen as a safe upper bound for "show me a page of stuff" — anything
765
+ * above this should be paginated explicitly.
766
+ */
767
+ declare const DEFAULT_LIMIT = 100;
768
+ /**
769
+ * Hard cap on `findMany.limit`. Caller-supplied values above this are
770
+ * rejected with `TableClientError` (no clamping).
771
+ */
772
+ declare const MAX_LIMIT = 1000;
773
+ /**
774
+ * Hard cap on `insertMany.values.length` per single call.
775
+ */
776
+ declare const MAX_BATCH = 1000;
777
+ /**
778
+ * A custom error thrown by the table client for any caller-side mistake
779
+ * (empty where on update/delete, batch too large, malformed where, …).
780
+ *
781
+ * Database errors are NOT wrapped — they propagate as-is from postgres-js
782
+ * so callers can match on Postgres error codes.
783
+ */
784
+ declare class TableClientError extends Error {
785
+ constructor(message: string);
786
+ }
787
+ /**
788
+ * Order-by spec accepted by `findMany`.
789
+ */
790
+ interface OrderBySpec<TCols extends Record<string, ColumnFactory>> {
791
+ column: keyof TCols & string;
792
+ dir?: 'asc' | 'desc';
793
+ }
794
+ /**
795
+ * `findMany` options.
796
+ */
797
+ interface FindManyOptions<TCols extends Record<string, ColumnFactory>> {
798
+ where?: WhereClause<TCols>;
799
+ orderBy?: OrderBySpec<TCols>[];
800
+ limit?: number;
801
+ offset?: number;
802
+ }
803
+ /**
804
+ * Options for the `claim()` atomic locking operation.
805
+ *
806
+ * @see {@link TableClient.claim}
807
+ */
808
+ interface ClaimOptions<TCols extends Record<string, ColumnFactory>, TTable extends PgTable = PgTable> {
809
+ /** Filter for rows eligible to be claimed. Must be non-empty. */
810
+ where: WhereClause<TCols>;
811
+ /** Literal values to SET on claimed rows. */
812
+ set?: Partial<InferInsertModel<TTable>>;
813
+ /** Raw SQL expressions to SET (e.g. `{ attempts: sql\`attempts + 1\` }`). */
814
+ setSql?: Record<string, SQL>;
815
+ /** ORDER BY for the subselect — controls claim priority. */
816
+ orderBy?: OrderBySpec<TCols>[];
817
+ /** Maximum number of rows to claim. Must be 1..MAX_LIMIT. */
818
+ limit: number;
819
+ }
820
+ /**
821
+ * Options for the `tryClaim()` atomic insert-or-replace-if operation.
822
+ *
823
+ * @see {@link TableClient.tryClaim}
824
+ */
825
+ interface TryClaimOptions<TCols extends Record<string, ColumnFactory>, TTable extends PgTable = PgTable> {
826
+ /**
827
+ * Unique-constraint column(s) that should trigger the conflict path.
828
+ *
829
+ * Single string for a single-column unique constraint; array for a
830
+ * composite unique. Must reference a real column on this table.
831
+ */
832
+ target: (keyof TCols & string) | (keyof TCols & string)[];
833
+ /**
834
+ * Predicate that decides whether an existing conflicting row may be
835
+ * **replaced**. Required and must be non-empty.
836
+ *
837
+ * - On INSERT (no conflict): always wins; predicate is irrelevant.
838
+ * - On CONFLICT + predicate matches: row is replaced; result is
839
+ * `{ acquired: true, row }`.
840
+ * - On CONFLICT + predicate does NOT match: existing row is preserved;
841
+ * result is `{ acquired: false, row }` (where `row` is the surviving
842
+ * existing row).
843
+ *
844
+ * Use `$or` to express composite re-entrancy rules — e.g.
845
+ * `{ $or: [{ expiresAt: { lt: now } }, { lockedBy: callerId }] }` for
846
+ * "replace if expired, or if I already hold it".
847
+ */
848
+ replaceWhen: WhereClause<TCols>;
849
+ /**
850
+ * Patch applied on the replace path. Defaults to the full `values`
851
+ * payload (last-write-wins on the columns being inserted).
852
+ */
853
+ set?: Partial<InferInsertModel<TTable>>;
854
+ }
855
+ /**
856
+ * Specification for a single aggregate function.
857
+ */
858
+ type AggregateSpec<TCols extends Record<string, ColumnFactory>> = {
859
+ fn: 'count';
860
+ } | {
861
+ fn: 'count';
862
+ column: keyof TCols & string;
863
+ } | {
864
+ fn: 'sum' | 'avg' | 'min' | 'max';
865
+ column: keyof TCols & string;
866
+ };
867
+ /**
868
+ * Options for the `aggregate()` method.
869
+ */
870
+ interface AggregateOptions<TCols extends Record<string, ColumnFactory>> {
871
+ /** Named aggregate expressions. Each key becomes an output column. */
872
+ select: Record<string, AggregateSpec<TCols>>;
873
+ /** Columns to GROUP BY. Included automatically in the result. */
874
+ groupBy?: (keyof TCols & string)[];
875
+ /** Filter rows before aggregation (WHERE). */
876
+ where?: WhereClause<TCols>;
877
+ /** Order results. Can reference both grouped columns and aliases. */
878
+ orderBy?: OrderBySpec<TCols>[];
879
+ /** Limit the number of groups returned. */
880
+ limit?: number;
881
+ }
882
+ /**
883
+ * Generic CRUD client. Constructed by `defineTable` — never instantiated
884
+ * directly by callers.
885
+ *
886
+ * The same instance shape is exposed both at the top level (using the
887
+ * package's main `db` connection) and inside a `transaction()` callback
888
+ * (using the transactional `tx` handle), so user code is identical
889
+ * regardless of transactional context.
890
+ *
891
+ * @template TCols The columns record passed to `defineTable`. Drives the
892
+ * where-builder DSL and the `Row<TCols>` / `InsertRow<TCols>` shapes.
893
+ * @template TTable The underlying Drizzle pgTable type, inferred from the
894
+ * table value passed to `new TableClient(...)`. Carrying this through
895
+ * lets `.insert().values(...)`, `.update().set(...)`, and `.returning()`
896
+ * use Drizzle's own `InferInsertModel` / `InferSelectModel` instead of
897
+ * falling back to `as never` casts.
898
+ */
899
+ declare class TableClient<TCols extends Record<string, ColumnFactory>, TTable extends PgTable = PgTable> {
900
+ /** The Drizzle table object. Exposed via `defineTable().table` for typed JOINs. */
901
+ readonly table: TTable;
902
+ /** Per-column kind metadata. Used by the where-builder for jsonb path validation. */
903
+ private readonly columnKinds;
904
+ /**
905
+ * The active database handle. May be a top-level `PostgresJsDatabase`
906
+ * or a transaction handle of compatible shape — the client doesn't
907
+ * distinguish.
908
+ */
909
+ private readonly db;
910
+ /** Primary key column names. Required by `claim()`. Empty for tables without a declared PK. */
911
+ private readonly primaryKeyColumns;
912
+ /**
913
+ * The partition this client is confined to — see
914
+ * `TableDefinition.partitionColumn`. Absent for unpartitioned tables.
915
+ */
916
+ private readonly partition?;
917
+ /**
918
+ * @internal — instances are created by `defineTable()`.
919
+ */
920
+ constructor(/** The Drizzle table object. Exposed via `defineTable().table` for typed JOINs. */
921
+
922
+ table: TTable, /** Per-column kind metadata. Used by the where-builder for jsonb path validation. */
923
+
924
+ columnKinds: Readonly<Record<string, ColumnKind>>,
925
+ /**
926
+ * The active database handle. May be a top-level `PostgresJsDatabase`
927
+ * or a transaction handle of compatible shape — the client doesn't
928
+ * distinguish.
929
+ */
930
+
931
+ db: PostgresJsDatabase, /** Primary key column names. Required by `claim()`. Empty for tables without a declared PK. */
932
+
933
+ primaryKeyColumns?: readonly string[],
934
+ /**
935
+ * The partition this client is confined to — see
936
+ * `TableDefinition.partitionColumn`. Absent for unpartitioned tables.
937
+ */
938
+
939
+ partition?: {
940
+ readonly key: string;
941
+ readonly value: string;
942
+ } | undefined);
943
+ /** `partition.key = partition.value`, or `undefined` for an unpartitioned client. */
944
+ private partitionCondition;
945
+ /** AND the partition into `condition` (which may be absent). */
946
+ private confine;
947
+ /** Stamp the partition onto a row about to be inserted (overwriting the caller's value). */
948
+ private stamp;
949
+ /** Refuse a patch that would move a row out of its partition. */
950
+ private refusePartitionPatch;
951
+ /** Conflict-target keys with the partition column leading (the table's uniques lead with it). */
952
+ private partitionTarget;
953
+ /**
954
+ * The table typed as the non-generic `PgTable` base.
955
+ *
956
+ * Drizzle's `.from()` (select) uses a conditional type
957
+ * `TableLikeHasEmptySelection<TTable>` that TypeScript cannot evaluate
958
+ * against a bounded generic `TTable extends PgTable` — it can only
959
+ * resolve when the argument is the concrete base `PgTable`. `.insert()`,
960
+ * `.update()`, and `.delete()` have simpler signatures (`<T extends
961
+ * PgTable>(t: T)`) that DO accept the generic directly, so those call
962
+ * sites keep `this.table` and still get `InferInsertModel<TTable>` /
963
+ * `InferSelectModel<TTable>` inference for `.values()`, `.set()`, and
964
+ * `.returning()`.
965
+ */
966
+ private get _selectTable();
967
+ /**
968
+ * Look up a Drizzle column by its JS property name, throwing
969
+ * `TableClientError` with the supplied `context` if no such column exists.
970
+ * Wraps `getTableColumns(this.table)` — uses Drizzle's typed column map
971
+ * instead of an indexed cast on the table object.
972
+ */
973
+ private getColumnOrThrow;
974
+ /**
975
+ * Build an array of ORDER BY SQL expressions from an `OrderBySpec[]`.
976
+ * Used by `findMany`, `distinct`, `claim`, and `aggregate`.
977
+ */
978
+ private buildOrderBy;
979
+ /**
980
+ * Throw `TableClientError` if the where clause has no top-level keys.
981
+ * Used by every method that mutates or reads-by-key (`update`, `delete`,
982
+ * `findOne`, `exists`, `claim`, ...) — none of them have a sane
983
+ * "everything" path, so empty wheres are always a caller mistake.
984
+ */
985
+ private requireNonEmptyWhere;
986
+ /**
987
+ * Verify every key in `patch` names a real column on this table. Used by
988
+ * `update`, `updateMany`, `upsert`, and `claim` so a typo'd or hostile
989
+ * patch key surfaces a clear error instead of being silently dropped by
990
+ * Drizzle's set logic.
991
+ */
992
+ private validateColumnKeys;
993
+ /**
994
+ * Find a single row matching the given where clause, or `null` if no row
995
+ * matches. Throws if the where is empty.
996
+ *
997
+ * @example
998
+ * ```ts
999
+ * await readState.client.findOne({ ticketId: 't_1', userId: 'u_1' })
1000
+ * ```
1001
+ */
1002
+ findOne(where: WhereClause<TCols>): Promise<Row<TCols> | null>;
1003
+ /**
1004
+ * Find multiple rows. Pagination, ordering, and filtering are all
1005
+ * optional but `limit` is hard-capped at {@link MAX_LIMIT}.
1006
+ *
1007
+ * @example
1008
+ * ```ts
1009
+ * await jobs.client.findMany({
1010
+ * where: { status: 'pending', runAt: { lte: new Date() } },
1011
+ * orderBy: [{ column: 'priority', dir: 'desc' }, { column: 'runAt', dir: 'asc' }],
1012
+ * limit: 25,
1013
+ * })
1014
+ * ```
1015
+ */
1016
+ findMany(options?: FindManyOptions<TCols>): Promise<Row<TCols>[]>;
1017
+ /**
1018
+ * Count rows matching the where clause. Returns an exact count via
1019
+ * `SELECT COUNT(*)`. For estimated/cached counts on large tables, see
1020
+ * `countEstimate()` and `countCached()` (added in a follow-up PR).
1021
+ *
1022
+ * @example
1023
+ * ```ts
1024
+ * const open = await jobs.client.count({ status: 'pending' })
1025
+ * ```
1026
+ */
1027
+ count(where?: WhereClause<TCols>): Promise<number>;
1028
+ /**
1029
+ * Returns `true` if at least one row matches the where clause. Always
1030
+ * uses `LIMIT 1` and short-circuits — cheaper than `count() > 0` on
1031
+ * large tables.
1032
+ */
1033
+ exists(where: WhereClause<TCols>): Promise<boolean>;
1034
+ /**
1035
+ * Return unique non-null values for a single column.
1036
+ *
1037
+ * Null values are excluded by default — pass `includeNull: true` to
1038
+ * include them. Results can be filtered with `where` and ordered with
1039
+ * `orderBy` (ascending or descending on the distinct column).
1040
+ *
1041
+ * @example Get distinct entity types from the audit log
1042
+ * ```ts
1043
+ * const types = await audit.client.distinct('entityType', { orderBy: 'asc' })
1044
+ * ```
1045
+ *
1046
+ * @example Distinct with a filter
1047
+ * ```ts
1048
+ * const actions = await audit.client.distinct('action', {
1049
+ * where: { userId: 'u_1' },
1050
+ * orderBy: 'asc',
1051
+ * })
1052
+ * ```
1053
+ */
1054
+ distinct<K extends keyof TCols & string>(column: K, options?: {
1055
+ where?: WhereClause<TCols>;
1056
+ orderBy?: 'asc' | 'desc'; /** Include null values in the result. Defaults to `false`. */
1057
+ includeNull?: boolean;
1058
+ }): Promise<ColumnValue<TCols[K]>[]>;
1059
+ /**
1060
+ * Insert a single row. Returns the inserted row (server-generated
1061
+ * defaults populated).
1062
+ */
1063
+ insert(values: InferInsertModel<TTable>): Promise<Row<TCols>>;
1064
+ /**
1065
+ * Upsert: insert a row, or update an existing row if a unique constraint
1066
+ * conflict occurs.
1067
+ *
1068
+ * `target` names the column(s) whose unique constraint should trigger
1069
+ * the conflict path. `set` is the patch applied on conflict — when
1070
+ * omitted, the conflict path applies the same `values` (i.e. "last
1071
+ * write wins").
1072
+ *
1073
+ * `setWhere` adds a conditional WHERE to the conflict update path. If
1074
+ * the condition does not match, the update is skipped and the existing
1075
+ * row is returned unchanged (via a fallback SELECT). This enables
1076
+ * patterns like "only update if the new version is higher than the
1077
+ * existing one".
1078
+ *
1079
+ * @example Idempotent "mark as read" tracking
1080
+ * ```ts
1081
+ * await readState.client.upsert(
1082
+ * { ticketId, userId, lastReadAt: new Date() },
1083
+ * { target: ['ticketId', 'userId'] },
1084
+ * )
1085
+ * ```
1086
+ *
1087
+ * @example Conditional update — only bump if newer
1088
+ * ```ts
1089
+ * await state.client.upsert(
1090
+ * { key: 'sync', version: 5, data: '...' },
1091
+ * { target: 'key', setWhere: { version: { lt: 5 } } },
1092
+ * )
1093
+ * ```
1094
+ *
1095
+ * `mergeJsonb` names jsonb columns whose conflict path MERGES instead of
1096
+ * replacing: the stored object becomes `stored || incoming` — a top-level,
1097
+ * right-wins merge, evaluated by Postgres inside the one `ON CONFLICT`
1098
+ * statement. Use it when two writers may each own part of one jsonb object and
1099
+ * a read-then-write would let the later writer erase the earlier one's keys.
1100
+ * The insert path is unaffected (there is nothing to merge with). A stored SQL
1101
+ * NULL or JSON `null` merges as `{}`. The incoming value must be a JSON object
1102
+ * — `||` concatenates arrays and scalars instead of merging — and a key named
1103
+ * in both `set` and `mergeJsonb` is merged (the merge wins).
1104
+ *
1105
+ * @example Merge a partial patch into a staged document
1106
+ * ```ts
1107
+ * await drafts.client.upsert(
1108
+ * { entityId, locale, data: { title: 'New' }, updatedAt: now },
1109
+ * { target: ['entityId', 'locale'], set: { updatedAt: now }, mergeJsonb: ['data'] },
1110
+ * )
1111
+ * ```
1112
+ */
1113
+ upsert(values: InferInsertModel<TTable>, opts: {
1114
+ target: (keyof TCols & string) | (keyof TCols & string)[];
1115
+ set?: Partial<InferInsertModel<TTable>>; /** Conditional WHERE on the conflict update path. Empty objects are ignored. */
1116
+ setWhere?: WhereClause<TCols>; /** jsonb columns merged (`stored || incoming`) rather than replaced on conflict. */
1117
+ mergeJsonb?: readonly (keyof TCols & string)[];
1118
+ }): Promise<Row<TCols>>;
1119
+ /**
1120
+ * Atomically claim a row by either inserting it or replacing an existing
1121
+ * conflicting row that satisfies `replaceWhen`. The single SQL statement
1122
+ * makes this race-free — concurrent callers cannot both observe an empty
1123
+ * slot and both INSERT.
1124
+ *
1125
+ * Distinct from {@link upsert}:
1126
+ * - `upsert` always wins on conflict (or hits its `setWhere` block and
1127
+ * silently falls back to the existing row). It returns a single
1128
+ * `Row<TCols>` and the caller cannot tell which path fired.
1129
+ * - `tryClaim` returns a discriminated union — the caller knows whether
1130
+ * they took the slot or someone else still holds it.
1131
+ *
1132
+ * Use case: leasing/locking primitives where the caller must distinguish
1133
+ * "I now hold this lock" from "someone else holds it, here is who".
1134
+ *
1135
+ * @example Acquire a lock, replacing only if expired
1136
+ * ```ts
1137
+ * const result = await locks.tryClaim(
1138
+ * { entityId, lockedBy: me, expiresAt: in5Min },
1139
+ * {
1140
+ * target: 'entityId',
1141
+ * replaceWhen: { expiresAt: { lt: now } },
1142
+ * },
1143
+ * )
1144
+ * if (result.acquired) {
1145
+ * // I have the lock
1146
+ * } else {
1147
+ * // result.row.lockedBy holds it (and lock is fresh)
1148
+ * }
1149
+ * ```
1150
+ *
1151
+ * @example Re-entrant lock (claim if expired, or if I already hold it)
1152
+ * ```ts
1153
+ * await locks.tryClaim(
1154
+ * { entityId, lockedBy: me, expiresAt: in5Min },
1155
+ * {
1156
+ * target: 'entityId',
1157
+ * replaceWhen: { $or: [{ expiresAt: { lt: now } }, { lockedBy: me }] },
1158
+ * },
1159
+ * )
1160
+ * ```
1161
+ */
1162
+ tryClaim(values: InferInsertModel<TTable>, opts: TryClaimOptions<TCols, TTable>): Promise<{
1163
+ acquired: true;
1164
+ row: Row<TCols>;
1165
+ } | {
1166
+ acquired: false;
1167
+ row: Row<TCols>;
1168
+ }>;
1169
+ /**
1170
+ * Insert many rows in a single statement. Capped at {@link MAX_BATCH}.
1171
+ * Returns all inserted rows in input order.
1172
+ */
1173
+ insertMany(values: InferInsertModel<TTable>[]): Promise<Row<TCols>[]>;
1174
+ /**
1175
+ * Update **at most one** row matching the where clause. Returns the
1176
+ * updated row, or `null` if no row matched.
1177
+ *
1178
+ * Throws `TableClientError` if:
1179
+ * - the where is empty
1180
+ * - the where matches more than one row
1181
+ *
1182
+ * The multi-row guard exists to prevent silent bulk updates from a
1183
+ * mistakenly-broad where. If you genuinely want to update multiple
1184
+ * rows in one call, use {@link updateMany}, which is explicit about
1185
+ * its plurality.
1186
+ *
1187
+ * Implementation note: this issues a single `UPDATE … RETURNING` and
1188
+ * inspects the returned row count *after* the fact. The extra rows
1189
+ * over the wire are the cost of the safety check; the alternative
1190
+ * (a SELECT round-trip first) would be more rows AND a race window.
1191
+ * If the affected count is > 1 the update has *already* happened —
1192
+ * the throw is to surface the bug, not to prevent it. Wrap in a
1193
+ * transaction if you need atomic rollback.
1194
+ */
1195
+ update(where: WhereClause<TCols>, patch: Partial<InferInsertModel<TTable>>): Promise<Row<TCols> | null>;
1196
+ /**
1197
+ * Update all rows matching the where clause. Returns every updated row.
1198
+ * Throws if the where is empty — there is no "update everything" path.
1199
+ */
1200
+ updateMany(where: WhereClause<TCols>, patch: Partial<InferInsertModel<TTable>>): Promise<Row<TCols>[]>;
1201
+ /**
1202
+ * Delete **at most one** row matching the where clause. Returns the
1203
+ * deleted row, or `null` if no row matched.
1204
+ *
1205
+ * Throws `TableClientError` if:
1206
+ * - the where is empty
1207
+ * - the where matches more than one row
1208
+ *
1209
+ * Same multi-row safety guard as {@link update}. Use {@link deleteMany}
1210
+ * for explicit bulk deletes.
1211
+ *
1212
+ * Implementation note: same as `update` — the delete has already
1213
+ * happened by the time the throw fires. Wrap in a transaction if you
1214
+ * need atomic rollback.
1215
+ */
1216
+ delete(where: WhereClause<TCols>): Promise<Row<TCols> | null>;
1217
+ /**
1218
+ * Delete all rows matching the where clause. Returns every deleted row.
1219
+ * Throws if the where is empty — there is no "delete everything" path.
1220
+ */
1221
+ deleteMany(where: WhereClause<TCols>): Promise<Row<TCols>[]>;
1222
+ /**
1223
+ * Run a callback inside a database transaction. The callback receives a
1224
+ * transactional `TableClient` instance with the same shape as the
1225
+ * top-level client — write code identically inside or outside.
1226
+ *
1227
+ * Throwing from the callback rolls back; returning a value commits.
1228
+ *
1229
+ * @example
1230
+ * ```ts
1231
+ * await files.client.transaction(async (tx) => {
1232
+ * const row = await tx.insert(values)
1233
+ * await adapter.upload(row.key, buffer) // if this throws, the row is rolled back
1234
+ * return row
1235
+ * })
1236
+ * ```
1237
+ */
1238
+ transaction<T>(fn: (tx: TableClient<TCols, TTable>) => Promise<T>): Promise<T>;
1239
+ /**
1240
+ * Atomically claim rows using `FOR UPDATE SKIP LOCKED`.
1241
+ *
1242
+ * Selects rows matching `where`, locks them (skipping any already locked
1243
+ * by another worker), updates them with `set`/`setSql`, and returns the
1244
+ * claimed rows in a single atomic operation. This is the standard pattern
1245
+ * for job queues, task distribution, and any producer-consumer table.
1246
+ *
1247
+ * Requires the table to have a declared primary key (either via
1248
+ * `column.uuid({ primaryKey: true })` or `defineTable({ primaryKey })`.
1249
+ *
1250
+ * @example Job queue claim
1251
+ * ```ts
1252
+ * const jobs = await client.claim({
1253
+ * where: { status: 'pending', runAt: { lte: new Date() } },
1254
+ * set: { status: 'processing', lockedBy: workerId, lockedAt: new Date() },
1255
+ * setSql: { attempts: sql`${jobsTable.table.attempts} + 1` },
1256
+ * orderBy: [{ column: 'priority', dir: 'desc' }, { column: 'createdAt', dir: 'asc' }],
1257
+ * limit: 5,
1258
+ * })
1259
+ * ```
1260
+ */
1261
+ claim(options: ClaimOptions<TCols, TTable>): Promise<Row<TCols>[]>;
1262
+ /**
1263
+ * Run an aggregate query with GROUP BY.
1264
+ *
1265
+ * Each key in `select` is an output alias mapped to an aggregate
1266
+ * specification. The `groupBy` columns are included automatically in
1267
+ * the result. Returns typed rows with grouped column values plus the
1268
+ * computed aggregates.
1269
+ *
1270
+ * @example Count jobs by status
1271
+ * ```ts
1272
+ * const rows = await client.aggregate({
1273
+ * select: { count: { fn: 'count' } },
1274
+ * groupBy: ['status'],
1275
+ * })
1276
+ * // → [{ status: 'pending', count: 12 }, { status: 'completed', count: 45 }]
1277
+ * ```
1278
+ *
1279
+ * @example Average priority of open jobs
1280
+ * ```ts
1281
+ * const [row] = await client.aggregate({
1282
+ * select: { avgPriority: { fn: 'avg', column: 'priority' } },
1283
+ * where: { status: 'open' },
1284
+ * })
1285
+ * ```
1286
+ */
1287
+ aggregate<TResult extends Record<string, unknown> = Record<string, unknown>>(options: AggregateOptions<TCols>): Promise<TResult[]>;
1288
+ /**
1289
+ * Fast approximate row count using Postgres `reltuples` statistics.
1290
+ *
1291
+ * Returns the estimated row count from `pg_class` — updated by
1292
+ * `ANALYZE` and autovacuum. Much faster than `COUNT(*)` on large
1293
+ * tables (no sequential scan), but may be stale by up to a few
1294
+ * percent. Returns 0 for tables that have never been analyzed.
1295
+ *
1296
+ * Use `count()` when you need an exact number. Use `countEstimate()`
1297
+ * for UI display, pagination hints, or "is this table large?" checks
1298
+ * where ±5% accuracy is fine.
1299
+ *
1300
+ * @example
1301
+ * ```ts
1302
+ * const approx = await client.countEstimate()
1303
+ * if (approx > 100_000) {
1304
+ * // Switch to keyset pagination instead of offset
1305
+ * }
1306
+ * ```
1307
+ */
1308
+ countEstimate(): Promise<number>;
1309
+ /** Build a single aggregate SQL expression from a spec. */
1310
+ private buildAggregateExpr;
1311
+ private buildWhereOrThrow;
1312
+ private validateLimit;
1313
+ }
1314
+ //#endregion
1315
+ //#region src/table/columns.d.ts
1316
+ /**
1317
+ * Type interface for the `column` builder namespace.
1318
+ *
1319
+ * Overloads discriminate on whether `default` is present, so
1320
+ * `THasDefault` is inferred correctly:
1321
+ *
1322
+ * - `column.varchar({ length: 20, default: 'x' })` → `hasDefault = true`
1323
+ * - `column.varchar({ length: 20 })` → `hasDefault = false`
1324
+ *
1325
+ * Object-literal methods don't support overloads in TypeScript, so we
1326
+ * declare the overloaded signatures on an interface and cast the
1327
+ * implementation object.
1328
+ */
1329
+ interface ColumnBuilders {
1330
+ uuid<TPrimaryKey extends boolean = false, TNotNull extends boolean = false, THasDefault extends boolean = false>(opts?: {
1331
+ primaryKey?: TPrimaryKey;
1332
+ notNull?: TNotNull;
1333
+ defaultRandom?: THasDefault;
1334
+ pgName?: string;
1335
+ }): ColumnFactory<string, 'uuid', TPrimaryKey extends true ? true : TNotNull extends true ? true : false, TPrimaryKey extends true ? true : THasDefault extends true ? true : false>;
1336
+ varchar<TNotNull extends boolean = false>(opts: {
1337
+ length: number;
1338
+ notNull?: TNotNull;
1339
+ default: string;
1340
+ pgName?: string;
1341
+ }): ColumnFactory<string, 'varchar', TNotNull extends true ? true : false, true>;
1342
+ varchar<TNotNull extends boolean = false>(opts: {
1343
+ length: number;
1344
+ notNull?: TNotNull;
1345
+ pgName?: string;
1346
+ }): ColumnFactory<string, 'varchar', TNotNull extends true ? true : false, false>;
1347
+ text<TNotNull extends boolean = false>(opts: {
1348
+ notNull?: TNotNull;
1349
+ default: string;
1350
+ pgName?: string;
1351
+ }): ColumnFactory<string, 'text', TNotNull extends true ? true : false, true>;
1352
+ text<TNotNull extends boolean = false>(opts?: {
1353
+ notNull?: TNotNull;
1354
+ pgName?: string;
1355
+ }): ColumnFactory<string, 'text', TNotNull extends true ? true : false, false>;
1356
+ integer<TNotNull extends boolean = false>(opts: {
1357
+ notNull?: TNotNull;
1358
+ default: number;
1359
+ pgName?: string;
1360
+ }): ColumnFactory<number, 'integer', TNotNull extends true ? true : false, true>;
1361
+ integer<TNotNull extends boolean = false>(opts?: {
1362
+ notNull?: TNotNull;
1363
+ pgName?: string;
1364
+ }): ColumnFactory<number, 'integer', TNotNull extends true ? true : false, false>;
1365
+ bigint<TMode extends 'number' | 'bigint' = 'number', TNotNull extends boolean = false>(opts: {
1366
+ mode?: TMode;
1367
+ notNull?: TNotNull;
1368
+ default: TMode extends 'bigint' ? bigint : number;
1369
+ pgName?: string;
1370
+ }): ColumnFactory<TMode extends 'bigint' ? bigint : number, 'bigint', TNotNull extends true ? true : false, true>;
1371
+ bigint<TMode extends 'number' | 'bigint' = 'number', TNotNull extends boolean = false>(opts?: {
1372
+ mode?: TMode;
1373
+ notNull?: TNotNull;
1374
+ pgName?: string;
1375
+ }): ColumnFactory<TMode extends 'bigint' ? bigint : number, 'bigint', TNotNull extends true ? true : false, false>;
1376
+ double<TNotNull extends boolean = false>(opts: {
1377
+ notNull?: TNotNull;
1378
+ default: number;
1379
+ pgName?: string;
1380
+ }): ColumnFactory<number, 'double', TNotNull extends true ? true : false, true>;
1381
+ double<TNotNull extends boolean = false>(opts?: {
1382
+ notNull?: TNotNull;
1383
+ pgName?: string;
1384
+ }): ColumnFactory<number, 'double', TNotNull extends true ? true : false, false>;
1385
+ boolean<TNotNull extends boolean = false>(opts: {
1386
+ notNull?: TNotNull;
1387
+ default: boolean;
1388
+ pgName?: string;
1389
+ }): ColumnFactory<boolean, 'boolean', TNotNull extends true ? true : false, true>;
1390
+ boolean<TNotNull extends boolean = false>(opts?: {
1391
+ notNull?: TNotNull;
1392
+ pgName?: string;
1393
+ }): ColumnFactory<boolean, 'boolean', TNotNull extends true ? true : false, false>;
1394
+ timestamp<TNotNull extends boolean = false, THasDefault extends boolean = false>(opts?: {
1395
+ notNull?: TNotNull;
1396
+ defaultNow?: THasDefault;
1397
+ withTimezone?: boolean;
1398
+ pgName?: string;
1399
+ }): ColumnFactory<Date, 'timestamp', TNotNull extends true ? true : false, THasDefault extends true ? true : false>;
1400
+ jsonb<T = unknown, TNotNull extends boolean = false>(opts: {
1401
+ notNull?: TNotNull;
1402
+ default: T;
1403
+ pgName?: string;
1404
+ }): ColumnFactory<T, 'jsonb', TNotNull extends true ? true : false, true>;
1405
+ jsonb<T = unknown, TNotNull extends boolean = false>(opts?: {
1406
+ notNull?: TNotNull;
1407
+ pgName?: string;
1408
+ }): ColumnFactory<T, 'jsonb', TNotNull extends true ? true : false, false>;
1409
+ uuidArray<TNotNull extends boolean = false>(opts?: {
1410
+ notNull?: TNotNull;
1411
+ pgName?: string;
1412
+ }): ColumnFactory<string[], 'uuidArray', TNotNull extends true ? true : false, false>;
1413
+ }
1414
+ /**
1415
+ * Column builder namespace — overloaded interface applied via single
1416
+ * assertion. Each builder's implementation returns `makeFactory(...)` with
1417
+ * widened boolean params; the `ColumnBuilders` overloads narrow them for
1418
+ * callers based on whether `default`/`primaryKey` etc. are present.
1419
+ */
1420
+ declare const column: ColumnBuilders;
1421
+ //#endregion
1422
+ //#region src/table/define.d.ts
1423
+ /**
1424
+ * The result of `defineTable()`.
1425
+ *
1426
+ * @template TCols The columns record passed to `defineTable`.
1427
+ * @template TTable The concrete Drizzle `pgTable` type produced by the
1428
+ * definition. Defaulted to the non-generic `PgTable` for callers that
1429
+ * pass `DefinedTable` around without propagating inference — inside
1430
+ * `defineTable` itself, the actual `typeof table` is captured so
1431
+ * `makeClient` returns a fully-typed `TableClient<TCols, typeof table>`.
1432
+ */
1433
+ interface DefinedTable<TCols extends Record<string, ColumnFactory>, TTable extends PgTable = PgTableWithColumns<TableConfig>> {
1434
+ /**
1435
+ * The underlying Drizzle `pgTable`. Exposed so callers can use it in
1436
+ * typed JOINs (the one sanctioned escape valve), and so it can be
1437
+ * included in `Plugin.tables` for migration discovery by `lumi migrate`.
1438
+ */
1439
+ table: TTable;
1440
+ /**
1441
+ * The original schema metadata. Useful for introspection / tooling
1442
+ * that needs to know about indexes, unique constraints, etc.
1443
+ */
1444
+ schema: TableDefinition<TCols>;
1445
+ /**
1446
+ * Per-column kind metadata (column name → kind tag). Used internally
1447
+ * by the where-builder for jsonb path validation.
1448
+ */
1449
+ columnKinds: Readonly<Record<string, ColumnKind>>;
1450
+ /**
1451
+ * Primary key column names (JS property names). Detected from either
1452
+ * `schema.primaryKey` or column-level `{ primaryKey: true }`. Empty
1453
+ * array if no PK is declared. Required by `claim()`.
1454
+ */
1455
+ primaryKeyColumns: readonly string[];
1456
+ /**
1457
+ * Build a typed CRUD client wired to the given database handle.
1458
+ *
1459
+ * Callers typically construct one client per logical "module" and
1460
+ * cache it. The client is stateless aside from the db handle, so it
1461
+ * is safe to share.
1462
+ */
1463
+ makeClient(db: PostgresJsDatabase, partitionValue?: string): TableClient<TCols, TTable>;
1464
+ /** The confining column, when the table declares one (`partitionColumn`). */
1465
+ partitionColumn?: string | undefined;
1466
+ }
1467
+ /**
1468
+ * Define a typed table.
1469
+ *
1470
+ * The function signature uses two const-generic parameters to preserve
1471
+ * column literal types end-to-end. This is what enables `Row`,
1472
+ * `InsertRow`, and `WhereClause` to know exact column shapes at the call
1473
+ * site.
1474
+ *
1475
+ * @throws if the table name doesn't match the snake_case regex, or if
1476
+ * the same name is registered twice with different table objects.
1477
+ */
1478
+ declare function defineTable<const TCols extends Record<string, ColumnFactory>>(def: TableDefinition<TCols>): {
1479
+ table: PgTableWithColumns<{
1480
+ name: string;
1481
+ schema: undefined;
1482
+ columns: {
1483
+ [x: string]: import("drizzle-orm/pg-core").PgColumn<{
1484
+ name: string;
1485
+ tableName: string;
1486
+ dataType: import("drizzle-orm").ColumnDataType;
1487
+ columnType: string;
1488
+ data: unknown;
1489
+ driverParam: unknown;
1490
+ notNull: false;
1491
+ hasDefault: false;
1492
+ isPrimaryKey: false;
1493
+ isAutoincrement: false;
1494
+ hasRuntimeDefault: false;
1495
+ enumValues: string[] | undefined;
1496
+ baseColumn: never;
1497
+ identity: undefined;
1498
+ generated: undefined;
1499
+ }, {}, {}>;
1500
+ };
1501
+ dialect: "pg";
1502
+ }>;
1503
+ schema: TableDefinition<TCols>;
1504
+ columnKinds: Readonly<Record<string, ColumnKind>>;
1505
+ primaryKeyColumns: readonly string[];
1506
+ makeClient: (database: PostgresJsDatabase, partitionValue?: string) => TableClient<TCols, PgTableWithColumns<{
1507
+ name: string;
1508
+ schema: undefined;
1509
+ columns: {
1510
+ [x: string]: import("drizzle-orm/pg-core").PgColumn<{
1511
+ name: string;
1512
+ tableName: string;
1513
+ dataType: import("drizzle-orm").ColumnDataType;
1514
+ columnType: string;
1515
+ data: unknown;
1516
+ driverParam: unknown;
1517
+ notNull: false;
1518
+ hasDefault: false;
1519
+ isPrimaryKey: false;
1520
+ isAutoincrement: false;
1521
+ hasRuntimeDefault: false;
1522
+ enumValues: string[] | undefined;
1523
+ baseColumn: never;
1524
+ identity: undefined;
1525
+ generated: undefined;
1526
+ }, {}, {}>;
1527
+ };
1528
+ dialect: "pg";
1529
+ }>>;
1530
+ partitionColumn: (keyof TCols & string) | undefined;
1531
+ };
1532
+ //#endregion
1533
+ //#region src/table/dependency-registry.d.ts
1534
+ /** Runs the typed cleanup for one dependency, inside the caller's tx. */
1535
+ type DependentCleanup = (tx: PostgresJsDatabase, entityIds: string[]) => Promise<void>;
1536
+ /** A single table→entity cleanup descriptor produced by `defineTable`. */
1537
+ interface TableCleanupDescriptor {
1538
+ /** Entity whose deletion triggers this cleanup. */
1539
+ entity: string;
1540
+ /** The typed, tx-bound cleanup executor. */
1541
+ cleanup: DependentCleanup;
1542
+ }
1543
+ /** Register a cleanup executor for `entityName`. Called by the merge step. */
1544
+ declare function registerDependentCleanup(entityName: string, cleanup: DependentCleanup): void;
1545
+ /** All cleanup executors declared as depending on `entityName` (may be empty). */
1546
+ declare function getDependentCleanups(entityName: string): readonly DependentCleanup[];
1547
+ /** Test seam — clears all entity-keyed registrations. */
1548
+ declare function __clearDependentCleanups(): void;
1549
+ /** Read the cleanup descriptors for a raw Drizzle table (may be empty). */
1550
+ declare function getTableCleanupDescriptors(table: Table): readonly TableCleanupDescriptor[];
1551
+ /**
1552
+ * The column a raw Drizzle table is partitioned by (`TableDefinition.
1553
+ * partitionColumn`), or `undefined`. Lets whole-install maintenance — the one
1554
+ * sanctioned site rename (`lumi site:rename-default`) — find every table that
1555
+ * keys rows by a site handle without an FK to cascade through.
1556
+ */
1557
+ declare function getTablePartitionColumn(table: Table): string | undefined;
1558
+ //#endregion
1559
+ //#region src/table/registry.d.ts
1560
+ declare class TableRegistryImpl {
1561
+ private byName;
1562
+ /** Table names we've already surfaced a bundler-duplicate warning for (once per name). */
1563
+ private warnedDuplicates;
1564
+ /**
1565
+ * Register a defined table. Throws if a table with the same name is
1566
+ * already registered (catches accidental double-registration from a
1567
+ * file being imported under two paths).
1568
+ */
1569
+ register(name: string, table: PgTable, source?: string): void;
1570
+ /**
1571
+ * Get a defined table by name.
1572
+ */
1573
+ get(name: string): PgTable | undefined;
1574
+ /**
1575
+ * Returns true if a table with the given name has been registered.
1576
+ */
1577
+ has(name: string): boolean;
1578
+ /**
1579
+ * Return every registered table as a `{ [name]: PgTable }` map.
1580
+ *
1581
+ * Useful for `createTestDb().push(tableRegistry.allTables())` and
1582
+ * runtime introspection.
1583
+ */
1584
+ allTables(): Record<string, PgTable>;
1585
+ /**
1586
+ * Clear the registry. **Test-only.**
1587
+ *
1588
+ * Production code should never call this — clearing the registry
1589
+ * silently breaks migration discovery and any code path that looks
1590
+ * up a table by name. The runtime guard below refuses to run when
1591
+ * `NODE_ENV === 'production'` to catch accidental misuse.
1592
+ *
1593
+ * Test runners typically set `NODE_ENV=test` (Vitest does), but the
1594
+ * guard is permissive about other values — it only blocks the one
1595
+ * environment where calling it is definitely wrong.
1596
+ */
1597
+ clear(): void;
1598
+ }
1599
+ /**
1600
+ * Process-wide table registry singleton.
1601
+ *
1602
+ * Tables register themselves on import via `defineTable()`. The registry
1603
+ * is module-level state, which means a single Node process sees one
1604
+ * consistent view of all defined tables across packages.
1605
+ */
1606
+ declare const tableRegistry: TableRegistryImpl;
1607
+ //#endregion
1608
+ //#region src/table/where-builder.d.ts
1609
+ /**
1610
+ * A custom error thrown by the where-builder for any caller-side mistake.
1611
+ *
1612
+ * Catching `WhereBuilderError` lets the table client surface a 400-style
1613
+ * error to its caller without conflating it with database errors.
1614
+ */
1615
+ declare class WhereBuilderError extends Error {
1616
+ constructor(message: string);
1617
+ }
1618
+ /**
1619
+ * Returns true if a {@link WhereClause} produces a non-trivial filter (at
1620
+ * least one column predicate or sub-clause). Useful for rejecting empty
1621
+ * `where` arguments to `update`/`delete`/`updateMany`/`deleteMany`.
1622
+ *
1623
+ * Note: this does not check whether `buildWhere()` would actually emit SQL
1624
+ * for the clause — it just checks that the user supplied something.
1625
+ */
1626
+ declare function isNonEmptyWhere(clause: unknown): boolean;
1627
+ //#endregion
1628
+ //#region src/transaction-hooks.d.ts
1629
+ /** Work to run once the transaction it was registered on has committed. */
1630
+ type CommitCallback = () => void | Promise<void>;
1631
+ /**
1632
+ * Install on-commit tracking on a read-write client, in place, and return it.
1633
+ *
1634
+ * Idempotent: a client that is already tracked is returned untouched, so a
1635
+ * callback never runs twice because two layers each installed tracking.
1636
+ */
1637
+ declare function trackTransactions<TDb extends PostgresJsDatabase>(db: TDb): TDb;
1638
+ /**
1639
+ * Register `callback` to run after the transaction `executor` belongs to
1640
+ * commits. Returns `true` when it was registered, and `false` — having done
1641
+ * nothing — when `executor` is not an open tracked transaction, in which case
1642
+ * the caller decides whether to run the work now.
1643
+ */
1644
+ declare function onCommit(executor: object | null | undefined, callback: CommitCallback): boolean;
1645
+ /** Whether `executor` is a transaction handle that is open and tracked right now. */
1646
+ declare function isTrackedTransaction(executor: object | null | undefined): boolean;
1647
+ //#endregion
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