@murumets-ee/db 0.1.5 → 0.3.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/index.d.mts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { PostgresJsDatabase } from "drizzle-orm/postgres-js";
2
- import { PgTableWithColumns } from "drizzle-orm/pg-core";
2
+ import { SQL } from "drizzle-orm";
3
+ import { AnyPgColumn, PgColumnBuilderBase, PgTable, PgTableWithColumns, TableConfig } from "drizzle-orm/pg-core";
3
4
 
4
5
  //#region src/client.d.ts
5
6
  interface DbConfig {
@@ -19,9 +20,35 @@ declare function createDbClient(config: DbConfig): PostgresJsDatabase;
19
20
  declare function createReadOnlyClient(config: DbConfig): PostgresJsDatabase;
20
21
  //#endregion
21
22
  //#region src/migrate.d.ts
23
+ /**
24
+ * Arguments passed to migration `up` / `down` functions.
25
+ *
26
+ * Intentionally tiny: we hand over a read-write Drizzle handle bound to
27
+ * the current transaction. If a migration needs anything beyond raw SQL
28
+ * (e.g. calling an AdminClient for a data backfill), it can import the
29
+ * relevant toolkit helpers inside the migration file — the CLI loader
30
+ * runs the migration in the same process as the config, so all package
31
+ * imports resolve normally.
32
+ */
33
+ interface MigrateArgs {
34
+ db: PostgresJsDatabase;
35
+ }
36
+ /**
37
+ * Shape of a migration module loaded from a `.ts` file.
38
+ *
39
+ * At minimum a migration must export `up`. `down` is optional — when
40
+ * absent, `lumi migrate:rollback` refuses to roll that file back.
41
+ */
42
+ interface MigrationModule {
43
+ up: (args: MigrateArgs) => Promise<void>;
44
+ down?: (args: MigrateArgs) => Promise<void>;
45
+ }
22
46
  interface MigrationSource {
47
+ /** Absolute path to the migration file on disk. */
23
48
  path: string;
49
+ /** `project` for user migrations, `toolkit` for shipped-with-toolkit ones. */
24
50
  namespace: 'toolkit' | 'project';
51
+ /** Filename only, used for ordering + tracking. */
25
52
  name: string;
26
53
  }
27
54
  interface MigrationStatus {
@@ -29,17 +56,48 @@ interface MigrationStatus {
29
56
  pending: MigrationSource[];
30
57
  }
31
58
  /**
32
- * Discover migrations from both toolkit (.toolkit/) and project (migrations/) directories
59
+ * Loader callback imports a migration `.ts` file and returns its exports.
60
+ *
61
+ * The CLI owns `jiti` (already used to load `toolkit.config.ts`), so
62
+ * `@murumets-ee/db` stays free of TS-runtime loader dependencies.
63
+ * Callers who never apply migrations (read-only servers, tests) never
64
+ * pay the loader cost.
33
65
  */
34
- declare function discoverMigrations(projectRoot: string): Promise<MigrationSource[]>;
66
+ type MigrationLoader = (absolutePath: string) => Promise<MigrationModule>;
35
67
  /**
36
- * Get the status of migrations (applied vs pending)
68
+ * Discover all migration files under `projectRoot/migrations`.
69
+ *
70
+ * Layout:
71
+ * migrations/
72
+ * 20260411_180000_add_articles.ts ← user migrations (sort chronologically)
73
+ * 20260411_180000_add_articles.snapshot.json
74
+ * .toolkit/
75
+ * 0001_initial_schema.ts ← toolkit-owned migrations (sort numerically)
37
76
  */
77
+ declare function discoverMigrations(projectRoot: string): Promise<MigrationSource[]>;
78
+ /** Split discovered migrations into applied vs pending by consulting the tracking table. */
38
79
  declare function getMigrationStatus(db: PostgresJsDatabase, projectRoot: string): Promise<MigrationStatus>;
39
80
  /**
40
- * Run pending migrations
81
+ * Apply every pending migration in order.
82
+ *
83
+ * Each file runs inside its own transaction: the migration's `up()`
84
+ * executes first, then the `_toolkit_migrations` insert commits. If
85
+ * `up()` throws, the transaction rolls back and the whole command
86
+ * aborts — nothing gets half-applied.
87
+ *
88
+ * The `loader` callback is injected so `@murumets-ee/db` itself doesn't
89
+ * depend on a TS-runtime loader. Callers pass a jiti-backed loader from
90
+ * the CLI or their own equivalent.
91
+ */
92
+ declare function runMigrations(db: PostgresJsDatabase, projectRoot: string, loader: MigrationLoader): Promise<void>;
93
+ /**
94
+ * Roll back the most recent applied migration (or N most recent).
95
+ *
96
+ * Each file's `down()` runs in its own transaction. Files without a
97
+ * `down` export cause the rollback to abort before running anything —
98
+ * we never roll back some but not all of a requested batch.
41
99
  */
42
- declare function runMigrations(db: PostgresJsDatabase, projectRoot: string): Promise<void>;
100
+ declare function rollbackMigrations(db: PostgresJsDatabase, projectRoot: string, loader: MigrationLoader, count?: number): Promise<void>;
43
101
  //#endregion
44
102
  //#region src/schema-registry.d.ts
45
103
  /**
@@ -74,5 +132,816 @@ declare const schemaRegistry: SchemaRegistryImpl;
74
132
  */
75
133
  declare function createSchemaRegistry(): SchemaRegistry;
76
134
  //#endregion
77
- export { type DbConfig, type MigrationSource, type MigrationStatus, type SchemaRegistry, createDbClient, createReadOnlyClient, createSchemaRegistry, discoverMigrations, getMigrationStatus, runMigrations, schemaRegistry };
135
+ //#region src/table/types.d.ts
136
+ /**
137
+ * The discriminating "kind" tag for a column factory.
138
+ *
139
+ * Used at the type level to gate features that only make sense for certain
140
+ * kinds — e.g. only `'jsonb'` columns accept dotted-path keys in the
141
+ * where-builder.
142
+ */
143
+ type ColumnKind = 'uuid' | 'varchar' | 'text' | 'integer' | 'bigint' | 'double' | 'boolean' | 'timestamp' | 'jsonb' | 'uuidArray';
144
+ /**
145
+ * Internal column factory.
146
+ *
147
+ * A `ColumnFactory` is a callable that, given a column name, produces the
148
+ * corresponding Drizzle column builder. The phantom type parameters
149
+ * (`__type`, `__kind`, `__notNull`) are used purely for compile-time
150
+ * inference of {@link Row} and {@link InsertRow} types — they have no runtime
151
+ * representation beyond a small metadata object.
152
+ *
153
+ * Users do not construct `ColumnFactory` values directly; they use the
154
+ * `column.*` builders from `./columns.ts`.
155
+ */
156
+ interface ColumnFactory<TType = unknown, TKind extends ColumnKind = ColumnKind, TNotNull extends boolean = boolean, THasDefault extends boolean = boolean> {
157
+ /** Build the underlying Drizzle column for the given column name. */
158
+ (name: string): PgColumnBuilderBase;
159
+ readonly __type: TType;
160
+ readonly __kind: TKind;
161
+ readonly __notNull: TNotNull;
162
+ readonly __hasDefault: THasDefault;
163
+ /**
164
+ * Optional explicit Postgres column name. When set, the column uses this
165
+ * name in the database instead of the JS property name. Needed when porting
166
+ * existing tables that use snake_case column names to `defineTable`.
167
+ */
168
+ readonly __pgName?: string;
169
+ /** True when the column is declared as a primary key at the column level. */
170
+ readonly __primaryKey?: boolean;
171
+ }
172
+ /**
173
+ * Convenience helper: extract the JS value type for a single column.
174
+ *
175
+ * - `notNull: true` → `T`
176
+ * - `notNull: false` → `T | null`
177
+ */
178
+ type ColumnValue<C> = C extends ColumnFactory<infer T, ColumnKind, infer N> ? N extends true ? T : T | null : never;
179
+ /**
180
+ * Map a columns record to its row shape (the result of a SELECT).
181
+ */
182
+ type Row<TCols extends Record<string, ColumnFactory>> = { [K in keyof TCols]: ColumnValue<TCols[K]> };
183
+ /**
184
+ * Map a columns record to its insert shape.
185
+ *
186
+ * - Required columns: must be supplied unless they have a default
187
+ * - Nullable columns: optional, accept `null`
188
+ * - Columns with defaults (`defaultRandom`, `defaultNow`, explicit `default`): optional
189
+ */
190
+ 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]> };
191
+ /**
192
+ * Operator object for a single column.
193
+ *
194
+ * Each property is a comparison operator. Combine with `$and` / `$or` /
195
+ * `$not` at the parent level to build composite predicates.
196
+ *
197
+ * `ilike` and `startsWith` accept user-supplied strings — the table client
198
+ * automatically escapes `%` and `_` in the value before wrapping with
199
+ * pattern wildcards, so callers cannot inject pattern characters by accident.
200
+ *
201
+ * @example
202
+ * ```ts
203
+ * { status: 'open' } // shorthand for { eq: 'open' }
204
+ * { status: { in: ['open', 'pending'] } }
205
+ * { createdAt: { gte: new Date('2026-01-01') } }
206
+ * { name: { ilike: 'foo%' } } // % escaped from value, only outer wildcards work
207
+ * { description: { isNull: true } }
208
+ * ```
209
+ */
210
+ type ColumnOperators<T> = T | null | {
211
+ eq: T;
212
+ } | {
213
+ ne: T;
214
+ } | {
215
+ in: T[];
216
+ } | {
217
+ notIn: T[];
218
+ } | {
219
+ gt: T;
220
+ } | {
221
+ gte: T;
222
+ } | {
223
+ lt: T;
224
+ } | {
225
+ lte: T;
226
+ } | {
227
+ isNull: true;
228
+ } | {
229
+ isNotNull: true;
230
+ } | {
231
+ ilike: string;
232
+ } | {
233
+ startsWith: string;
234
+ };
235
+ /**
236
+ * The full WhereClause type.
237
+ *
238
+ * Keys are either column names or dotted-path keys for jsonb columns
239
+ * (e.g. `'metadata.author.name'`). Values are operator objects per
240
+ * {@link ColumnOperators}.
241
+ *
242
+ * Combine with `$and` / `$or` / `$not` for composite predicates. All
243
+ * top-level keys are implicitly AND-ed together.
244
+ *
245
+ * The type intentionally accepts `string` for jsonb-path keys rather than
246
+ * a precisely-narrowed template literal, because the path's nested type is
247
+ * unknown to the column definition. Path segments are validated at runtime
248
+ * against `/^[a-zA-Z_][a-zA-Z0-9_-]*$|^\d+$/` and the parent column must
249
+ * be a `jsonb` kind, otherwise the where-builder throws.
250
+ *
251
+ * @example
252
+ * ```ts
253
+ * // Simple equality
254
+ * { userId: 'u_123', status: 'active' }
255
+ *
256
+ * // Composite with $or
257
+ * { $or: [
258
+ * { status: 'open' },
259
+ * { status: 'pending', assignee: { isNull: true } },
260
+ * ] }
261
+ *
262
+ * // jsonb path access (column 'metadata' must be jsonb)
263
+ * { 'metadata.author.name': { ilike: 'alice%' } }
264
+ * ```
265
+ */
266
+ type WhereClause<TCols extends Record<string, ColumnFactory>> = { [K in keyof TCols]?: ColumnOperators<ColumnValue<TCols[K]>> } & {
267
+ [key: `${string}.${string}`]: ColumnOperators<unknown>;
268
+ } & {
269
+ $and?: WhereClause<TCols>[];
270
+ $or?: WhereClause<TCols>[];
271
+ $not?: WhereClause<TCols>;
272
+ };
273
+ /**
274
+ * Index definition for a table.
275
+ *
276
+ * Indexes are declared at the table level (not per-column) so composite
277
+ * indexes are first-class. Each index gets an auto-generated name unless
278
+ * `name` is supplied: `idx_<table>_<col1>_<col2>`.
279
+ */
280
+ interface IndexDefinition<TCols extends Record<string, ColumnFactory>> {
281
+ on: (keyof TCols)[];
282
+ name?: string;
283
+ }
284
+ /**
285
+ * Unique constraint definition.
286
+ *
287
+ * Composite uniques (multi-column) are declared here. Single-column
288
+ * uniqueness is also supported here rather than as a column-level option,
289
+ * to keep the column builder API uniform.
290
+ */
291
+ interface UniqueDefinition<TCols extends Record<string, ColumnFactory>> {
292
+ on: (keyof TCols)[];
293
+ name?: string;
294
+ }
295
+ /**
296
+ * The shape passed to `defineTable`.
297
+ *
298
+ * @example
299
+ * ```ts
300
+ * defineTable({
301
+ * name: 'ticket_read_state',
302
+ * columns: {
303
+ * ticketId: column.uuid({ notNull: true }),
304
+ * userId: column.varchar({ length: 255, notNull: true }),
305
+ * lastReadAt: column.timestamp({ notNull: true, defaultNow: true, withTimezone: true }),
306
+ * },
307
+ * primaryKey: ['ticketId', 'userId'],
308
+ * indexes: [{ on: ['userId'] }],
309
+ * })
310
+ * ```
311
+ */
312
+ interface TableDefinition<TCols extends Record<string, ColumnFactory>> {
313
+ /**
314
+ * Postgres table name. Convention: snake_case, prefix with package name
315
+ * (e.g. `toolkit_jobs`, `ticketing_read_state`).
316
+ *
317
+ * Validated at definition time against `/^[a-z][a-z0-9_]*$/` to prevent
318
+ * accidental quoting issues and to enforce a consistent naming scheme.
319
+ */
320
+ name: string;
321
+ columns: TCols;
322
+ /**
323
+ * Primary key column(s). May be a single column key or an array for
324
+ * composite keys.
325
+ *
326
+ * If omitted, the table has no primary key — only valid for tables that
327
+ * declare a column with `primaryKey: true` at the column level (e.g. via
328
+ * `column.uuid({ primaryKey: true, defaultRandom: true })`).
329
+ */
330
+ primaryKey?: keyof TCols | (keyof TCols)[];
331
+ unique?: UniqueDefinition<TCols>[];
332
+ indexes?: IndexDefinition<TCols>[];
333
+ }
334
+ //#endregion
335
+ //#region src/table/columns.d.ts
336
+ /**
337
+ * Type interface for the `column` builder namespace.
338
+ *
339
+ * Overloads discriminate on whether `default` is present, so
340
+ * `THasDefault` is inferred correctly:
341
+ *
342
+ * - `column.varchar({ length: 20, default: 'x' })` → `hasDefault = true`
343
+ * - `column.varchar({ length: 20 })` → `hasDefault = false`
344
+ *
345
+ * Object-literal methods don't support overloads in TypeScript, so we
346
+ * declare the overloaded signatures on an interface and cast the
347
+ * implementation object.
348
+ */
349
+ interface ColumnBuilders {
350
+ uuid<TPrimaryKey extends boolean = false, TNotNull extends boolean = false, THasDefault extends boolean = false>(opts?: {
351
+ primaryKey?: TPrimaryKey;
352
+ notNull?: TNotNull;
353
+ defaultRandom?: THasDefault;
354
+ pgName?: string;
355
+ }): ColumnFactory<string, 'uuid', TPrimaryKey extends true ? true : TNotNull extends true ? true : false, TPrimaryKey extends true ? true : THasDefault extends true ? true : false>;
356
+ varchar<TNotNull extends boolean = false>(opts: {
357
+ length: number;
358
+ notNull?: TNotNull;
359
+ default: string;
360
+ pgName?: string;
361
+ }): ColumnFactory<string, 'varchar', TNotNull extends true ? true : false, true>;
362
+ varchar<TNotNull extends boolean = false>(opts: {
363
+ length: number;
364
+ notNull?: TNotNull;
365
+ pgName?: string;
366
+ }): ColumnFactory<string, 'varchar', TNotNull extends true ? true : false, false>;
367
+ text<TNotNull extends boolean = false>(opts: {
368
+ notNull?: TNotNull;
369
+ default: string;
370
+ pgName?: string;
371
+ }): ColumnFactory<string, 'text', TNotNull extends true ? true : false, true>;
372
+ text<TNotNull extends boolean = false>(opts?: {
373
+ notNull?: TNotNull;
374
+ pgName?: string;
375
+ }): ColumnFactory<string, 'text', TNotNull extends true ? true : false, false>;
376
+ integer<TNotNull extends boolean = false>(opts: {
377
+ notNull?: TNotNull;
378
+ default: number;
379
+ pgName?: string;
380
+ }): ColumnFactory<number, 'integer', TNotNull extends true ? true : false, true>;
381
+ integer<TNotNull extends boolean = false>(opts?: {
382
+ notNull?: TNotNull;
383
+ pgName?: string;
384
+ }): ColumnFactory<number, 'integer', TNotNull extends true ? true : false, false>;
385
+ bigint<TMode extends 'number' | 'bigint' = 'number', TNotNull extends boolean = false>(opts: {
386
+ mode?: TMode;
387
+ notNull?: TNotNull;
388
+ default: TMode extends 'bigint' ? bigint : number;
389
+ pgName?: string;
390
+ }): ColumnFactory<TMode extends 'bigint' ? bigint : number, 'bigint', TNotNull extends true ? true : false, true>;
391
+ bigint<TMode extends 'number' | 'bigint' = 'number', TNotNull extends boolean = false>(opts?: {
392
+ mode?: TMode;
393
+ notNull?: TNotNull;
394
+ pgName?: string;
395
+ }): ColumnFactory<TMode extends 'bigint' ? bigint : number, 'bigint', TNotNull extends true ? true : false, false>;
396
+ double<TNotNull extends boolean = false>(opts: {
397
+ notNull?: TNotNull;
398
+ default: number;
399
+ pgName?: string;
400
+ }): ColumnFactory<number, 'double', TNotNull extends true ? true : false, true>;
401
+ double<TNotNull extends boolean = false>(opts?: {
402
+ notNull?: TNotNull;
403
+ pgName?: string;
404
+ }): ColumnFactory<number, 'double', TNotNull extends true ? true : false, false>;
405
+ boolean<TNotNull extends boolean = false>(opts: {
406
+ notNull?: TNotNull;
407
+ default: boolean;
408
+ pgName?: string;
409
+ }): ColumnFactory<boolean, 'boolean', TNotNull extends true ? true : false, true>;
410
+ boolean<TNotNull extends boolean = false>(opts?: {
411
+ notNull?: TNotNull;
412
+ pgName?: string;
413
+ }): ColumnFactory<boolean, 'boolean', TNotNull extends true ? true : false, false>;
414
+ timestamp<TNotNull extends boolean = false, THasDefault extends boolean = false>(opts?: {
415
+ notNull?: TNotNull;
416
+ defaultNow?: THasDefault;
417
+ withTimezone?: boolean;
418
+ pgName?: string;
419
+ }): ColumnFactory<Date, 'timestamp', TNotNull extends true ? true : false, THasDefault extends true ? true : false>;
420
+ jsonb<T = unknown, TNotNull extends boolean = false>(opts: {
421
+ notNull?: TNotNull;
422
+ default: T;
423
+ pgName?: string;
424
+ }): ColumnFactory<T, 'jsonb', TNotNull extends true ? true : false, true>;
425
+ jsonb<T = unknown, TNotNull extends boolean = false>(opts?: {
426
+ notNull?: TNotNull;
427
+ pgName?: string;
428
+ }): ColumnFactory<T, 'jsonb', TNotNull extends true ? true : false, false>;
429
+ uuidArray<TNotNull extends boolean = false>(opts?: {
430
+ notNull?: TNotNull;
431
+ pgName?: string;
432
+ }): ColumnFactory<string[], 'uuidArray', TNotNull extends true ? true : false, false>;
433
+ }
434
+ /**
435
+ * Column builder namespace — overloaded interface applied via single
436
+ * assertion. Each builder's implementation returns `makeFactory(...)` with
437
+ * widened boolean params; the `ColumnBuilders` overloads narrow them for
438
+ * callers based on whether `default`/`primaryKey` etc. are present.
439
+ */
440
+ declare const column: ColumnBuilders;
441
+ //#endregion
442
+ //#region src/table/client.d.ts
443
+ /**
444
+ * Default page size for `findMany` when no `limit` is supplied.
445
+ *
446
+ * Chosen as a safe upper bound for "show me a page of stuff" — anything
447
+ * above this should be paginated explicitly.
448
+ */
449
+ declare const DEFAULT_LIMIT = 100;
450
+ /**
451
+ * Hard cap on `findMany.limit`. Caller-supplied values above this are
452
+ * clamped and a warning is emitted via `console.warn`.
453
+ */
454
+ declare const MAX_LIMIT = 1000;
455
+ /**
456
+ * Hard cap on `insertMany.values.length` per single call.
457
+ */
458
+ declare const MAX_BATCH = 1000;
459
+ /**
460
+ * A custom error thrown by the table client for any caller-side mistake
461
+ * (empty where on update/delete, batch too large, malformed where, …).
462
+ *
463
+ * Database errors are NOT wrapped — they propagate as-is from postgres-js
464
+ * so callers can match on Postgres error codes.
465
+ */
466
+ declare class TableClientError extends Error {
467
+ constructor(message: string);
468
+ }
469
+ /**
470
+ * Order-by spec accepted by `findMany`.
471
+ */
472
+ interface OrderBySpec<TCols extends Record<string, ColumnFactory>> {
473
+ column: keyof TCols & string;
474
+ dir?: 'asc' | 'desc';
475
+ }
476
+ /**
477
+ * `findMany` options.
478
+ */
479
+ interface FindManyOptions<TCols extends Record<string, ColumnFactory>> {
480
+ where?: WhereClause<TCols>;
481
+ orderBy?: OrderBySpec<TCols>[];
482
+ limit?: number;
483
+ offset?: number;
484
+ }
485
+ /**
486
+ * Options for the `claim()` atomic locking operation.
487
+ *
488
+ * @see {@link TableClient.claim}
489
+ */
490
+ interface ClaimOptions<TCols extends Record<string, ColumnFactory>> {
491
+ /** Filter for rows eligible to be claimed. Must be non-empty. */
492
+ where: WhereClause<TCols>;
493
+ /** Literal values to SET on claimed rows. */
494
+ set?: Partial<InsertRow<TCols>>;
495
+ /** Raw SQL expressions to SET (e.g. `{ attempts: sql\`attempts + 1\` }`). */
496
+ setSql?: Record<string, SQL>;
497
+ /** ORDER BY for the subselect — controls claim priority. */
498
+ orderBy?: OrderBySpec<TCols>[];
499
+ /** Maximum number of rows to claim. Must be 1..MAX_LIMIT. */
500
+ limit: number;
501
+ }
502
+ /**
503
+ * Specification for a single aggregate function.
504
+ */
505
+ type AggregateSpec<TCols extends Record<string, ColumnFactory>> = {
506
+ fn: 'count';
507
+ } | {
508
+ fn: 'count';
509
+ column: keyof TCols & string;
510
+ } | {
511
+ fn: 'sum' | 'avg' | 'min' | 'max';
512
+ column: keyof TCols & string;
513
+ };
514
+ /**
515
+ * Options for the `aggregate()` method.
516
+ */
517
+ interface AggregateOptions<TCols extends Record<string, ColumnFactory>> {
518
+ /** Named aggregate expressions. Each key becomes an output column. */
519
+ select: Record<string, AggregateSpec<TCols>>;
520
+ /** Columns to GROUP BY. Included automatically in the result. */
521
+ groupBy?: (keyof TCols & string)[];
522
+ /** Filter rows before aggregation (WHERE). */
523
+ where?: WhereClause<TCols>;
524
+ /** Order results. Can reference both grouped columns and aliases. */
525
+ orderBy?: OrderBySpec<TCols>[];
526
+ /** Limit the number of groups returned. */
527
+ limit?: number;
528
+ }
529
+ /**
530
+ * Generic CRUD client. Constructed by `defineTable` — never instantiated
531
+ * directly by callers.
532
+ *
533
+ * The same instance shape is exposed both at the top level (using the
534
+ * package's main `db` connection) and inside a `transaction()` callback
535
+ * (using the transactional `tx` handle), so user code is identical
536
+ * regardless of transactional context.
537
+ *
538
+ * @template TCols The columns record passed to `defineTable`.
539
+ */
540
+ declare class TableClient<TCols extends Record<string, ColumnFactory>> {
541
+ /** The Drizzle table object. Exposed via `defineTable().table` for typed JOINs. */
542
+ readonly table: PgTable;
543
+ /** Per-column kind metadata. Used by the where-builder for jsonb path validation. */
544
+ private readonly columnKinds;
545
+ /**
546
+ * The active database handle. May be a top-level `PostgresJsDatabase`
547
+ * or a transaction handle of compatible shape — the client doesn't
548
+ * distinguish.
549
+ */
550
+ private readonly db;
551
+ /** Primary key column names. Required by `claim()`. Empty for tables without a declared PK. */
552
+ private readonly primaryKeyColumns;
553
+ /**
554
+ * @internal — instances are created by `defineTable()`.
555
+ */
556
+ constructor(/** The Drizzle table object. Exposed via `defineTable().table` for typed JOINs. */
557
+
558
+ table: PgTable, /** Per-column kind metadata. Used by the where-builder for jsonb path validation. */
559
+
560
+ columnKinds: Readonly<Record<string, ColumnKind>>,
561
+ /**
562
+ * The active database handle. May be a top-level `PostgresJsDatabase`
563
+ * or a transaction handle of compatible shape — the client doesn't
564
+ * distinguish.
565
+ */
566
+
567
+ db: PostgresJsDatabase, /** Primary key column names. Required by `claim()`. Empty for tables without a declared PK. */
568
+
569
+ primaryKeyColumns?: readonly string[]);
570
+ /**
571
+ * Find a single row matching the given where clause, or `null` if no row
572
+ * matches. Throws if the where is empty.
573
+ *
574
+ * @example
575
+ * ```ts
576
+ * await readState.client.findOne({ ticketId: 't_1', userId: 'u_1' })
577
+ * ```
578
+ */
579
+ findOne(where: WhereClause<TCols>): Promise<Row<TCols> | null>;
580
+ /**
581
+ * Find multiple rows. Pagination, ordering, and filtering are all
582
+ * optional but `limit` is hard-capped at {@link MAX_LIMIT}.
583
+ *
584
+ * @example
585
+ * ```ts
586
+ * await jobs.client.findMany({
587
+ * where: { status: 'pending', runAt: { lte: new Date() } },
588
+ * orderBy: [{ column: 'priority', dir: 'desc' }, { column: 'runAt', dir: 'asc' }],
589
+ * limit: 25,
590
+ * })
591
+ * ```
592
+ */
593
+ findMany(options?: FindManyOptions<TCols>): Promise<Row<TCols>[]>;
594
+ /**
595
+ * Count rows matching the where clause. Returns an exact count via
596
+ * `SELECT COUNT(*)`. For estimated/cached counts on large tables, see
597
+ * `countEstimate()` and `countCached()` (added in a follow-up PR).
598
+ *
599
+ * @example
600
+ * ```ts
601
+ * const open = await jobs.client.count({ status: 'pending' })
602
+ * ```
603
+ */
604
+ count(where?: WhereClause<TCols>): Promise<number>;
605
+ /**
606
+ * Returns `true` if at least one row matches the where clause. Always
607
+ * uses `LIMIT 1` and short-circuits — cheaper than `count() > 0` on
608
+ * large tables.
609
+ */
610
+ exists(where: WhereClause<TCols>): Promise<boolean>;
611
+ /**
612
+ * Return unique non-null values for a single column.
613
+ *
614
+ * Null values are excluded by default — pass `includeNull: true` to
615
+ * include them. Results can be filtered with `where` and ordered with
616
+ * `orderBy` (ascending or descending on the distinct column).
617
+ *
618
+ * @example Get distinct entity types from the audit log
619
+ * ```ts
620
+ * const types = await audit.client.distinct('entityType', { orderBy: 'asc' })
621
+ * ```
622
+ *
623
+ * @example Distinct with a filter
624
+ * ```ts
625
+ * const actions = await audit.client.distinct('action', {
626
+ * where: { userId: 'u_1' },
627
+ * orderBy: 'asc',
628
+ * })
629
+ * ```
630
+ */
631
+ distinct<K extends keyof TCols & string>(column: K, options?: {
632
+ where?: WhereClause<TCols>;
633
+ orderBy?: 'asc' | 'desc'; /** Include null values in the result. Defaults to `false`. */
634
+ includeNull?: boolean;
635
+ }): Promise<ColumnValue<TCols[K]>[]>;
636
+ /**
637
+ * Insert a single row. Returns the inserted row (server-generated
638
+ * defaults populated).
639
+ */
640
+ insert(values: InsertRow<TCols>): Promise<Row<TCols>>;
641
+ /**
642
+ * Upsert: insert a row, or update an existing row if a unique constraint
643
+ * conflict occurs.
644
+ *
645
+ * `target` names the column(s) whose unique constraint should trigger
646
+ * the conflict path. `set` is the patch applied on conflict — when
647
+ * omitted, the conflict path applies the same `values` (i.e. "last
648
+ * write wins").
649
+ *
650
+ * `setWhere` adds a conditional WHERE to the conflict update path. If
651
+ * the condition does not match, the update is skipped and the existing
652
+ * row is returned unchanged (via a fallback SELECT). This enables
653
+ * patterns like "only update if the new version is higher than the
654
+ * existing one".
655
+ *
656
+ * @example Idempotent "mark as read" tracking
657
+ * ```ts
658
+ * await readState.client.upsert(
659
+ * { ticketId, userId, lastReadAt: new Date() },
660
+ * { target: ['ticketId', 'userId'] },
661
+ * )
662
+ * ```
663
+ *
664
+ * @example Conditional update — only bump if newer
665
+ * ```ts
666
+ * await state.client.upsert(
667
+ * { key: 'sync', version: 5, data: '...' },
668
+ * { target: 'key', setWhere: { version: { lt: 5 } } },
669
+ * )
670
+ * ```
671
+ */
672
+ upsert(values: InsertRow<TCols>, opts: {
673
+ target: (keyof TCols & string) | (keyof TCols & string)[];
674
+ set?: Partial<InsertRow<TCols>>; /** Conditional WHERE on the conflict update path. Empty objects are ignored. */
675
+ setWhere?: WhereClause<TCols>;
676
+ }): Promise<Row<TCols>>;
677
+ /**
678
+ * Insert many rows in a single statement. Capped at {@link MAX_BATCH}.
679
+ * Returns all inserted rows in input order.
680
+ */
681
+ insertMany(values: InsertRow<TCols>[]): Promise<Row<TCols>[]>;
682
+ /**
683
+ * Update **at most one** row matching the where clause. Returns the
684
+ * updated row, or `null` if no row matched.
685
+ *
686
+ * Throws `TableClientError` if:
687
+ * - the where is empty
688
+ * - the where matches more than one row
689
+ *
690
+ * The multi-row guard exists to prevent silent bulk updates from a
691
+ * mistakenly-broad where. If you genuinely want to update multiple
692
+ * rows in one call, use {@link updateMany}, which is explicit about
693
+ * its plurality.
694
+ *
695
+ * Implementation note: this issues a single `UPDATE … RETURNING` and
696
+ * inspects the returned row count *after* the fact. The extra rows
697
+ * over the wire are the cost of the safety check; the alternative
698
+ * (a SELECT round-trip first) would be more rows AND a race window.
699
+ * If the affected count is > 1 the update has *already* happened —
700
+ * the throw is to surface the bug, not to prevent it. Wrap in a
701
+ * transaction if you need atomic rollback.
702
+ */
703
+ update(where: WhereClause<TCols>, patch: Partial<InsertRow<TCols>>): Promise<Row<TCols> | null>;
704
+ /**
705
+ * Update all rows matching the where clause. Returns every updated row.
706
+ * Throws if the where is empty — there is no "update everything" path.
707
+ */
708
+ updateMany(where: WhereClause<TCols>, patch: Partial<InsertRow<TCols>>): Promise<Row<TCols>[]>;
709
+ /**
710
+ * Delete **at most one** row matching the where clause. Returns the
711
+ * deleted row, or `null` if no row matched.
712
+ *
713
+ * Throws `TableClientError` if:
714
+ * - the where is empty
715
+ * - the where matches more than one row
716
+ *
717
+ * Same multi-row safety guard as {@link update}. Use {@link deleteMany}
718
+ * for explicit bulk deletes.
719
+ *
720
+ * Implementation note: same as `update` — the delete has already
721
+ * happened by the time the throw fires. Wrap in a transaction if you
722
+ * need atomic rollback.
723
+ */
724
+ delete(where: WhereClause<TCols>): Promise<Row<TCols> | null>;
725
+ /**
726
+ * Delete all rows matching the where clause. Returns every deleted row.
727
+ * Throws if the where is empty — there is no "delete everything" path.
728
+ */
729
+ deleteMany(where: WhereClause<TCols>): Promise<Row<TCols>[]>;
730
+ /**
731
+ * Run a callback inside a database transaction. The callback receives a
732
+ * transactional `TableClient` instance with the same shape as the
733
+ * top-level client — write code identically inside or outside.
734
+ *
735
+ * Throwing from the callback rolls back; returning a value commits.
736
+ *
737
+ * @example
738
+ * ```ts
739
+ * await files.client.transaction(async (tx) => {
740
+ * const row = await tx.insert(values)
741
+ * await adapter.upload(row.key, buffer) // if this throws, the row is rolled back
742
+ * return row
743
+ * })
744
+ * ```
745
+ */
746
+ transaction<T>(fn: (tx: TableClient<TCols>) => Promise<T>): Promise<T>;
747
+ /**
748
+ * Atomically claim rows using `FOR UPDATE SKIP LOCKED`.
749
+ *
750
+ * Selects rows matching `where`, locks them (skipping any already locked
751
+ * by another worker), updates them with `set`/`setSql`, and returns the
752
+ * claimed rows in a single atomic operation. This is the standard pattern
753
+ * for job queues, task distribution, and any producer-consumer table.
754
+ *
755
+ * Requires the table to have a declared primary key (either via
756
+ * `column.uuid({ primaryKey: true })` or `defineTable({ primaryKey })`.
757
+ *
758
+ * @example Job queue claim
759
+ * ```ts
760
+ * const jobs = await client.claim({
761
+ * where: { status: 'pending', runAt: { lte: new Date() } },
762
+ * set: { status: 'processing', lockedBy: workerId, lockedAt: new Date() },
763
+ * setSql: { attempts: sql`${jobsTable.table.attempts} + 1` },
764
+ * orderBy: [{ column: 'priority', dir: 'desc' }, { column: 'createdAt', dir: 'asc' }],
765
+ * limit: 5,
766
+ * })
767
+ * ```
768
+ */
769
+ claim(options: ClaimOptions<TCols>): Promise<Row<TCols>[]>;
770
+ /**
771
+ * Run an aggregate query with GROUP BY.
772
+ *
773
+ * Each key in `select` is an output alias mapped to an aggregate
774
+ * specification. The `groupBy` columns are included automatically in
775
+ * the result. Returns typed rows with grouped column values plus the
776
+ * computed aggregates.
777
+ *
778
+ * @example Count jobs by status
779
+ * ```ts
780
+ * const rows = await client.aggregate({
781
+ * select: { count: { fn: 'count' } },
782
+ * groupBy: ['status'],
783
+ * })
784
+ * // → [{ status: 'pending', count: 12 }, { status: 'completed', count: 45 }]
785
+ * ```
786
+ *
787
+ * @example Average priority of open jobs
788
+ * ```ts
789
+ * const [row] = await client.aggregate({
790
+ * select: { avgPriority: { fn: 'avg', column: 'priority' } },
791
+ * where: { status: 'open' },
792
+ * })
793
+ * ```
794
+ */
795
+ aggregate<TResult extends Record<string, unknown> = Record<string, unknown>>(options: AggregateOptions<TCols>): Promise<TResult[]>;
796
+ /**
797
+ * Fast approximate row count using Postgres `reltuples` statistics.
798
+ *
799
+ * Returns the estimated row count from `pg_class` — updated by
800
+ * `ANALYZE` and autovacuum. Much faster than `COUNT(*)` on large
801
+ * tables (no sequential scan), but may be stale by up to a few
802
+ * percent. Returns 0 for tables that have never been analyzed.
803
+ *
804
+ * Use `count()` when you need an exact number. Use `countEstimate()`
805
+ * for UI display, pagination hints, or "is this table large?" checks
806
+ * where ±5% accuracy is fine.
807
+ *
808
+ * @example
809
+ * ```ts
810
+ * const approx = await client.countEstimate()
811
+ * if (approx > 100_000) {
812
+ * // Switch to keyset pagination instead of offset
813
+ * }
814
+ * ```
815
+ */
816
+ countEstimate(): Promise<number>;
817
+ /** Build a single aggregate SQL expression from a spec. */
818
+ private buildAggregateExpr;
819
+ private buildWhereOrThrow;
820
+ private validateLimit;
821
+ }
822
+ //#endregion
823
+ //#region src/table/define.d.ts
824
+ /**
825
+ * The result of `defineTable()`.
826
+ *
827
+ * @template TCols The columns record passed to `defineTable`.
828
+ */
829
+ interface DefinedTable<TCols extends Record<string, ColumnFactory>> {
830
+ /**
831
+ * The underlying Drizzle `pgTable`. Exposed so callers can use it in
832
+ * typed JOINs (the one sanctioned escape valve), and so it can be
833
+ * included in `Plugin.tables` for migration discovery by `lumi migrate`.
834
+ */
835
+ table: PgTableWithColumns<TableConfig>;
836
+ /**
837
+ * The original schema metadata. Useful for introspection / tooling
838
+ * that needs to know about indexes, unique constraints, etc.
839
+ */
840
+ schema: TableDefinition<TCols>;
841
+ /**
842
+ * Per-column kind metadata (column name → kind tag). Used internally
843
+ * by the where-builder for jsonb path validation.
844
+ */
845
+ columnKinds: Readonly<Record<string, ColumnKind>>;
846
+ /**
847
+ * Primary key column names (JS property names). Detected from either
848
+ * `schema.primaryKey` or column-level `{ primaryKey: true }`. Empty
849
+ * array if no PK is declared. Required by `claim()`.
850
+ */
851
+ primaryKeyColumns: readonly string[];
852
+ /**
853
+ * Build a typed CRUD client wired to the given database handle.
854
+ *
855
+ * Callers typically construct one client per logical "module" and
856
+ * cache it. The client is stateless aside from the db handle, so it
857
+ * is safe to share.
858
+ */
859
+ makeClient(db: PostgresJsDatabase): TableClient<TCols>;
860
+ }
861
+ /**
862
+ * Define a typed table.
863
+ *
864
+ * The function signature uses two const-generic parameters to preserve
865
+ * column literal types end-to-end. This is what enables `Row`,
866
+ * `InsertRow`, and `WhereClause` to know exact column shapes at the call
867
+ * site.
868
+ *
869
+ * @throws if the table name doesn't match the snake_case regex, or if
870
+ * the same name is registered twice with different table objects.
871
+ */
872
+ declare function defineTable<const TCols extends Record<string, ColumnFactory>>(def: TableDefinition<TCols>): DefinedTable<TCols>;
873
+ //#endregion
874
+ //#region src/table/registry.d.ts
875
+ declare class TableRegistryImpl {
876
+ private byName;
877
+ /**
878
+ * Register a defined table. Throws if a table with the same name is
879
+ * already registered (catches accidental double-registration from a
880
+ * file being imported under two paths).
881
+ */
882
+ register(name: string, table: PgTable, source?: string): void;
883
+ /**
884
+ * Get a defined table by name.
885
+ */
886
+ get(name: string): PgTable | undefined;
887
+ /**
888
+ * Returns true if a table with the given name has been registered.
889
+ */
890
+ has(name: string): boolean;
891
+ /**
892
+ * Return every registered table as a `{ [name]: PgTable }` map.
893
+ *
894
+ * Useful for `createTestDb().push(tableRegistry.allTables())` and
895
+ * runtime introspection.
896
+ */
897
+ allTables(): Record<string, PgTable>;
898
+ /**
899
+ * Clear the registry. **Test-only.**
900
+ *
901
+ * Production code should never call this — clearing the registry
902
+ * silently breaks migration discovery and any code path that looks
903
+ * up a table by name. The runtime guard below refuses to run when
904
+ * `NODE_ENV === 'production'` to catch accidental misuse.
905
+ *
906
+ * Test runners typically set `NODE_ENV=test` (Vitest does), but the
907
+ * guard is permissive about other values — it only blocks the one
908
+ * environment where calling it is definitely wrong.
909
+ */
910
+ clear(): void;
911
+ }
912
+ /**
913
+ * Process-wide table registry singleton.
914
+ *
915
+ * Tables register themselves on import via `defineTable()`. The registry
916
+ * is module-level state, which means a single Node process sees one
917
+ * consistent view of all defined tables across packages.
918
+ */
919
+ declare const tableRegistry: TableRegistryImpl;
920
+ /**
921
+ * Create an isolated registry — useful for unit tests that want to
922
+ * verify registration behaviour without polluting the global registry.
923
+ */
924
+ declare function createTableRegistry(): TableRegistryImpl;
925
+ //#endregion
926
+ //#region src/table/where-builder.d.ts
927
+ /**
928
+ * A custom error thrown by the where-builder for any caller-side mistake.
929
+ *
930
+ * Catching `WhereBuilderError` lets the table client surface a 400-style
931
+ * error to its caller without conflating it with database errors.
932
+ */
933
+ declare class WhereBuilderError extends Error {
934
+ constructor(message: string);
935
+ }
936
+ /**
937
+ * Returns true if a {@link WhereClause} produces a non-trivial filter (at
938
+ * least one column predicate or sub-clause). Useful for rejecting empty
939
+ * `where` arguments to `update`/`delete`/`updateMany`/`deleteMany`.
940
+ *
941
+ * Note: this does not check whether `buildWhere()` would actually emit SQL
942
+ * for the clause — it just checks that the user supplied something.
943
+ */
944
+ declare function isNonEmptyWhere(clause: unknown): boolean;
945
+ //#endregion
946
+ export { type AnyPgColumn, type ClaimOptions, type ColumnFactory, type ColumnKind, type ColumnOperators, type ColumnValue, DEFAULT_LIMIT, type DbConfig, type DefinedTable, type FindManyOptions, type IndexDefinition, type InsertRow, MAX_BATCH, MAX_LIMIT, type MigrateArgs, type MigrationLoader, type MigrationModule, type MigrationSource, type MigrationStatus, type OrderBySpec, type Row, type SchemaRegistry, TableClient, TableClientError, type TableDefinition, type UniqueDefinition, WhereBuilderError, type WhereClause, column, createDbClient, createReadOnlyClient, createSchemaRegistry, createTableRegistry, defineTable, discoverMigrations, getMigrationStatus, isNonEmptyWhere, rollbackMigrations, runMigrations, schemaRegistry, tableRegistry };
78
947
  //# sourceMappingURL=index.d.mts.map