@murumets-ee/db 0.2.0 → 0.4.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,8 @@
1
1
  import { PostgresJsDatabase } from "drizzle-orm/postgres-js";
2
- import { AnyPgColumn, PgColumnBuilderBase, PgTable, PgTableWithColumns } from "drizzle-orm/pg-core";
2
+ import * as _$drizzle_orm0 from "drizzle-orm";
3
+ import { InferInsertModel, SQL } from "drizzle-orm";
4
+ import * as _$drizzle_orm_pg_core0 from "drizzle-orm/pg-core";
5
+ import { AnyPgColumn, PgColumnBuilderBase, PgTable, PgTableWithColumns, TableConfig } from "drizzle-orm/pg-core";
3
6
 
4
7
  //#region src/client.d.ts
5
8
  interface DbConfig {
@@ -159,6 +162,14 @@ interface ColumnFactory<TType = unknown, TKind extends ColumnKind = ColumnKind,
159
162
  readonly __kind: TKind;
160
163
  readonly __notNull: TNotNull;
161
164
  readonly __hasDefault: THasDefault;
165
+ /**
166
+ * Optional explicit Postgres column name. When set, the column uses this
167
+ * name in the database instead of the JS property name. Needed when porting
168
+ * existing tables that use snake_case column names to `defineTable`.
169
+ */
170
+ readonly __pgName?: string;
171
+ /** True when the column is declared as a primary key at the column level. */
172
+ readonly __primaryKey?: boolean;
162
173
  }
163
174
  /**
164
175
  * Convenience helper: extract the JS value type for a single column.
@@ -325,124 +336,110 @@ interface TableDefinition<TCols extends Record<string, ColumnFactory>> {
325
336
  //#endregion
326
337
  //#region src/table/columns.d.ts
327
338
  /**
328
- * Column builder namespace.
339
+ * Type interface for the `column` builder namespace.
329
340
  *
330
- * Each builder produces a {@link ColumnFactory}. Pass the result to a
331
- * `defineTable` `columns` field. The builders are typed so that the
332
- * resulting `Row` and `InsertRow` types track each column's value type
333
- * and nullability.
341
+ * Overloads discriminate on whether `default` is present, so
342
+ * `THasDefault` is inferred correctly:
334
343
  *
335
- * @example
336
- * ```ts
337
- * import { column, defineTable } from '@murumets-ee/db'
344
+ * - `column.varchar({ length: 20, default: 'x' })` → `hasDefault = true`
345
+ * - `column.varchar({ length: 20 })` → `hasDefault = false`
338
346
  *
339
- * const jobs = defineTable({
340
- * name: 'toolkit_jobs',
341
- * columns: {
342
- * id: column.uuid({ primaryKey: true, defaultRandom: true }),
343
- * type: column.varchar({ length: 100, notNull: true }),
344
- * payload: column.jsonb<{ subject: string; body: string }>({ notNull: true }),
345
- * status: column.varchar({ length: 20, notNull: true, default: 'pending' }),
346
- * priority: column.integer({ notNull: true, default: 0 }),
347
- * runAt: column.timestamp({ notNull: true, defaultNow: true, withTimezone: true }),
348
- * },
349
- * })
350
- * ```
347
+ * Object-literal methods don't support overloads in TypeScript, so we
348
+ * declare the overloaded signatures on an interface and cast the
349
+ * implementation object.
351
350
  */
352
- declare const column: {
353
- /**
354
- * UUID column.
355
- *
356
- * For primary keys, use `{ primaryKey: true, defaultRandom: true }` to
357
- * get a server-generated UUIDv4.
358
- */
359
- uuid<TNotNull extends boolean = false, THasDefault extends boolean = false>(opts?: {
360
- primaryKey?: boolean;
361
- notNull?: TNotNull; /** Server-side `gen_random_uuid()` default. Counts as a default for type inference. */
351
+ interface ColumnBuilders {
352
+ uuid<TPrimaryKey extends boolean = false, TNotNull extends boolean = false, THasDefault extends boolean = false>(opts?: {
353
+ primaryKey?: TPrimaryKey;
354
+ notNull?: TNotNull;
362
355
  defaultRandom?: THasDefault;
363
- }): ColumnFactory<string, "uuid", TNotNull extends true ? true : false, THasDefault extends true ? true : boolean extends THasDefault ? false : false>;
364
- /**
365
- * Variable-length string column. `length` is required and must be a
366
- * positive integer up to Postgres' actual `varchar` ceiling
367
- * (1073741823 — i.e. ~1GB / 4 bytes per char). For unbounded text use
368
- * `column.text()` instead — it stores the same way and skips the
369
- * length check at write time.
370
- *
371
- * Conventional sizes for reference:
372
- * - 50–100: short identifiers, slugs, status codes
373
- * - 255: legacy "common max length" — fits a tweet, an email, etc.
374
- * - 1024: file paths, URLs
375
- * - 10000+: prefer `text` instead, varchar with very large lengths
376
- * buys you nothing
377
- */
378
- varchar<TNotNull extends boolean = false, THasDefault extends boolean = false>(opts: {
356
+ pgName?: string;
357
+ }): ColumnFactory<string, 'uuid', TPrimaryKey extends true ? true : TNotNull extends true ? true : false, TPrimaryKey extends true ? true : THasDefault extends true ? true : false>;
358
+ varchar<TNotNull extends boolean = false>(opts: {
379
359
  length: number;
380
360
  notNull?: TNotNull;
381
- default?: THasDefault extends true ? string : string | undefined;
382
- }): ColumnFactory<string, "varchar", TNotNull extends true ? true : false, THasDefault extends true ? true : false>;
383
- /**
384
- * Unbounded text column.
385
- */
386
- text<TNotNull extends boolean = false, THasDefault extends boolean = false>(opts?: {
361
+ default: string;
362
+ pgName?: string;
363
+ }): ColumnFactory<string, 'varchar', TNotNull extends true ? true : false, true>;
364
+ varchar<TNotNull extends boolean = false>(opts: {
365
+ length: number;
387
366
  notNull?: TNotNull;
388
- default?: THasDefault extends true ? string : string | undefined;
389
- }): ColumnFactory<string, "text", TNotNull extends true ? true : false, THasDefault extends true ? true : false>;
390
- /**
391
- * 32-bit integer column.
392
- */
393
- integer<TNotNull extends boolean = false, THasDefault extends boolean = false>(opts?: {
367
+ pgName?: string;
368
+ }): ColumnFactory<string, 'varchar', TNotNull extends true ? true : false, false>;
369
+ text<TNotNull extends boolean = false>(opts: {
394
370
  notNull?: TNotNull;
395
- default?: THasDefault extends true ? number : number | undefined;
396
- }): ColumnFactory<number, "integer", TNotNull extends true ? true : false, THasDefault extends true ? true : false>;
397
- /**
398
- * 64-bit integer column. `mode: 'number'` returns a JS number (safe for
399
- * values up to 2^53), `mode: 'bigint'` returns a JS BigInt.
400
- */
401
- bigint<TMode extends "number" | "bigint" = "number", TNotNull extends boolean = false, THasDefault extends boolean = false>(opts?: {
371
+ default: string;
372
+ pgName?: string;
373
+ }): ColumnFactory<string, 'text', TNotNull extends true ? true : false, true>;
374
+ text<TNotNull extends boolean = false>(opts?: {
375
+ notNull?: TNotNull;
376
+ pgName?: string;
377
+ }): ColumnFactory<string, 'text', TNotNull extends true ? true : false, false>;
378
+ integer<TNotNull extends boolean = false>(opts: {
379
+ notNull?: TNotNull;
380
+ default: number;
381
+ pgName?: string;
382
+ }): ColumnFactory<number, 'integer', TNotNull extends true ? true : false, true>;
383
+ integer<TNotNull extends boolean = false>(opts?: {
384
+ notNull?: TNotNull;
385
+ pgName?: string;
386
+ }): ColumnFactory<number, 'integer', TNotNull extends true ? true : false, false>;
387
+ bigint<TMode extends 'number' | 'bigint' = 'number', TNotNull extends boolean = false>(opts: {
402
388
  mode?: TMode;
403
389
  notNull?: TNotNull;
404
- default?: THasDefault extends true ? (TMode extends "bigint" ? bigint : number) : (TMode extends "bigint" ? bigint : number) | undefined;
405
- }): ColumnFactory<TMode extends "bigint" ? bigint : number, "bigint", TNotNull extends true ? true : false, THasDefault extends true ? true : false>;
406
- /**
407
- * Double-precision floating-point column.
408
- */
409
- double<TNotNull extends boolean = false, THasDefault extends boolean = false>(opts?: {
390
+ default: TMode extends 'bigint' ? bigint : number;
391
+ pgName?: string;
392
+ }): ColumnFactory<TMode extends 'bigint' ? bigint : number, 'bigint', TNotNull extends true ? true : false, true>;
393
+ bigint<TMode extends 'number' | 'bigint' = 'number', TNotNull extends boolean = false>(opts?: {
394
+ mode?: TMode;
410
395
  notNull?: TNotNull;
411
- default?: THasDefault extends true ? number : number | undefined;
412
- }): ColumnFactory<number, "double", TNotNull extends true ? true : false, THasDefault extends true ? true : false>;
413
- /**
414
- * Boolean column. Defaults to `false` if no explicit default is supplied
415
- * AND `notNull` is true — matches the entity package convention.
416
- */
417
- boolean<TNotNull extends boolean = false, THasDefault extends boolean = false>(opts?: {
396
+ pgName?: string;
397
+ }): ColumnFactory<TMode extends 'bigint' ? bigint : number, 'bigint', TNotNull extends true ? true : false, false>;
398
+ double<TNotNull extends boolean = false>(opts: {
418
399
  notNull?: TNotNull;
419
- default?: THasDefault extends true ? boolean : boolean | undefined;
420
- }): ColumnFactory<boolean, "boolean", TNotNull extends true ? true : false, THasDefault extends true ? true : false>;
421
- /**
422
- * Timestamp column. `withTimezone: true` is strongly recommended for all
423
- * timestamps in this codebase — entity audit fields use it consistently.
424
- */
400
+ default: number;
401
+ pgName?: string;
402
+ }): ColumnFactory<number, 'double', TNotNull extends true ? true : false, true>;
403
+ double<TNotNull extends boolean = false>(opts?: {
404
+ notNull?: TNotNull;
405
+ pgName?: string;
406
+ }): ColumnFactory<number, 'double', TNotNull extends true ? true : false, false>;
407
+ boolean<TNotNull extends boolean = false>(opts: {
408
+ notNull?: TNotNull;
409
+ default: boolean;
410
+ pgName?: string;
411
+ }): ColumnFactory<boolean, 'boolean', TNotNull extends true ? true : false, true>;
412
+ boolean<TNotNull extends boolean = false>(opts?: {
413
+ notNull?: TNotNull;
414
+ pgName?: string;
415
+ }): ColumnFactory<boolean, 'boolean', TNotNull extends true ? true : false, false>;
425
416
  timestamp<TNotNull extends boolean = false, THasDefault extends boolean = false>(opts?: {
426
- notNull?: TNotNull; /** Server-side `NOW()` default. Counts as a default for type inference. */
417
+ notNull?: TNotNull;
427
418
  defaultNow?: THasDefault;
428
419
  withTimezone?: boolean;
429
- }): ColumnFactory<Date, "timestamp", TNotNull extends true ? true : false, THasDefault extends true ? true : false>;
430
- /**
431
- * JSONB column. The phantom type parameter `T` propagates to row reads
432
- * and where-clause shorthand for the whole-column case. For dotted-path
433
- * access via the where-builder, the value type at the path is `unknown`
434
- * (TypeScript can't follow runtime path navigation).
435
- */
420
+ pgName?: string;
421
+ }): ColumnFactory<Date, 'timestamp', TNotNull extends true ? true : false, THasDefault extends true ? true : false>;
422
+ jsonb<T = unknown, TNotNull extends boolean = false>(opts: {
423
+ notNull?: TNotNull;
424
+ default: T;
425
+ pgName?: string;
426
+ }): ColumnFactory<T, 'jsonb', TNotNull extends true ? true : false, true>;
436
427
  jsonb<T = unknown, TNotNull extends boolean = false>(opts?: {
437
428
  notNull?: TNotNull;
438
- }): ColumnFactory<T, "jsonb", TNotNull extends true ? true : false, false>;
439
- /**
440
- * UUID array column (`uuid[]`).
441
- */
429
+ pgName?: string;
430
+ }): ColumnFactory<T, 'jsonb', TNotNull extends true ? true : false, false>;
442
431
  uuidArray<TNotNull extends boolean = false>(opts?: {
443
432
  notNull?: TNotNull;
444
- }): ColumnFactory<string[], "uuidArray", TNotNull extends true ? true : false, false>;
445
- };
433
+ pgName?: string;
434
+ }): ColumnFactory<string[], 'uuidArray', TNotNull extends true ? true : false, false>;
435
+ }
436
+ /**
437
+ * Column builder namespace — overloaded interface applied via single
438
+ * assertion. Each builder's implementation returns `makeFactory(...)` with
439
+ * widened boolean params; the `ColumnBuilders` overloads narrow them for
440
+ * callers based on whether `default`/`primaryKey` etc. are present.
441
+ */
442
+ declare const column: ColumnBuilders;
446
443
  //#endregion
447
444
  //#region src/table/client.d.ts
448
445
  /**
@@ -487,6 +484,50 @@ interface FindManyOptions<TCols extends Record<string, ColumnFactory>> {
487
484
  limit?: number;
488
485
  offset?: number;
489
486
  }
487
+ /**
488
+ * Options for the `claim()` atomic locking operation.
489
+ *
490
+ * @see {@link TableClient.claim}
491
+ */
492
+ interface ClaimOptions<TCols extends Record<string, ColumnFactory>, TTable extends PgTable = PgTable> {
493
+ /** Filter for rows eligible to be claimed. Must be non-empty. */
494
+ where: WhereClause<TCols>;
495
+ /** Literal values to SET on claimed rows. */
496
+ set?: Partial<InferInsertModel<TTable>>;
497
+ /** Raw SQL expressions to SET (e.g. `{ attempts: sql\`attempts + 1\` }`). */
498
+ setSql?: Record<string, SQL>;
499
+ /** ORDER BY for the subselect — controls claim priority. */
500
+ orderBy?: OrderBySpec<TCols>[];
501
+ /** Maximum number of rows to claim. Must be 1..MAX_LIMIT. */
502
+ limit: number;
503
+ }
504
+ /**
505
+ * Specification for a single aggregate function.
506
+ */
507
+ type AggregateSpec<TCols extends Record<string, ColumnFactory>> = {
508
+ fn: 'count';
509
+ } | {
510
+ fn: 'count';
511
+ column: keyof TCols & string;
512
+ } | {
513
+ fn: 'sum' | 'avg' | 'min' | 'max';
514
+ column: keyof TCols & string;
515
+ };
516
+ /**
517
+ * Options for the `aggregate()` method.
518
+ */
519
+ interface AggregateOptions<TCols extends Record<string, ColumnFactory>> {
520
+ /** Named aggregate expressions. Each key becomes an output column. */
521
+ select: Record<string, AggregateSpec<TCols>>;
522
+ /** Columns to GROUP BY. Included automatically in the result. */
523
+ groupBy?: (keyof TCols & string)[];
524
+ /** Filter rows before aggregation (WHERE). */
525
+ where?: WhereClause<TCols>;
526
+ /** Order results. Can reference both grouped columns and aliases. */
527
+ orderBy?: OrderBySpec<TCols>[];
528
+ /** Limit the number of groups returned. */
529
+ limit?: number;
530
+ }
490
531
  /**
491
532
  * Generic CRUD client. Constructed by `defineTable` — never instantiated
492
533
  * directly by callers.
@@ -496,11 +537,17 @@ interface FindManyOptions<TCols extends Record<string, ColumnFactory>> {
496
537
  * (using the transactional `tx` handle), so user code is identical
497
538
  * regardless of transactional context.
498
539
  *
499
- * @template TCols The columns record passed to `defineTable`.
540
+ * @template TCols The columns record passed to `defineTable`. Drives the
541
+ * where-builder DSL and the `Row<TCols>` / `InsertRow<TCols>` shapes.
542
+ * @template TTable The underlying Drizzle pgTable type, inferred from the
543
+ * table value passed to `new TableClient(...)`. Carrying this through
544
+ * lets `.insert().values(...)`, `.update().set(...)`, and `.returning()`
545
+ * use Drizzle's own `InferInsertModel` / `InferSelectModel` instead of
546
+ * falling back to `as never` casts.
500
547
  */
501
- declare class TableClient<TCols extends Record<string, ColumnFactory>> {
548
+ declare class TableClient<TCols extends Record<string, ColumnFactory>, TTable extends PgTable = PgTable> {
502
549
  /** The Drizzle table object. Exposed via `defineTable().table` for typed JOINs. */
503
- readonly table: PgTable;
550
+ readonly table: TTable;
504
551
  /** Per-column kind metadata. Used by the where-builder for jsonb path validation. */
505
552
  private readonly columnKinds;
506
553
  /**
@@ -509,12 +556,14 @@ declare class TableClient<TCols extends Record<string, ColumnFactory>> {
509
556
  * distinguish.
510
557
  */
511
558
  private readonly db;
559
+ /** Primary key column names. Required by `claim()`. Empty for tables without a declared PK. */
560
+ private readonly primaryKeyColumns;
512
561
  /**
513
562
  * @internal — instances are created by `defineTable()`.
514
563
  */
515
564
  constructor(/** The Drizzle table object. Exposed via `defineTable().table` for typed JOINs. */
516
565
 
517
- table: PgTable, /** Per-column kind metadata. Used by the where-builder for jsonb path validation. */
566
+ table: TTable, /** Per-column kind metadata. Used by the where-builder for jsonb path validation. */
518
567
 
519
568
  columnKinds: Readonly<Record<string, ColumnKind>>,
520
569
  /**
@@ -523,7 +572,23 @@ declare class TableClient<TCols extends Record<string, ColumnFactory>> {
523
572
  * distinguish.
524
573
  */
525
574
 
526
- db: PostgresJsDatabase);
575
+ db: PostgresJsDatabase, /** Primary key column names. Required by `claim()`. Empty for tables without a declared PK. */
576
+
577
+ primaryKeyColumns?: readonly string[]);
578
+ /**
579
+ * The table typed as the non-generic `PgTable` base.
580
+ *
581
+ * Drizzle's `.from()` (select) uses a conditional type
582
+ * `TableLikeHasEmptySelection<TTable>` that TypeScript cannot evaluate
583
+ * against a bounded generic `TTable extends PgTable` — it can only
584
+ * resolve when the argument is the concrete base `PgTable`. `.insert()`,
585
+ * `.update()`, and `.delete()` have simpler signatures (`<T extends
586
+ * PgTable>(t: T)`) that DO accept the generic directly, so those call
587
+ * sites keep `this.table` and still get `InferInsertModel<TTable>` /
588
+ * `InferSelectModel<TTable>` inference for `.values()`, `.set()`, and
589
+ * `.returning()`.
590
+ */
591
+ private get _selectTable();
527
592
  /**
528
593
  * Find a single row matching the given where clause, or `null` if no row
529
594
  * matches. Throws if the where is empty.
@@ -565,11 +630,36 @@ declare class TableClient<TCols extends Record<string, ColumnFactory>> {
565
630
  * large tables.
566
631
  */
567
632
  exists(where: WhereClause<TCols>): Promise<boolean>;
633
+ /**
634
+ * Return unique non-null values for a single column.
635
+ *
636
+ * Null values are excluded by default — pass `includeNull: true` to
637
+ * include them. Results can be filtered with `where` and ordered with
638
+ * `orderBy` (ascending or descending on the distinct column).
639
+ *
640
+ * @example Get distinct entity types from the audit log
641
+ * ```ts
642
+ * const types = await audit.client.distinct('entityType', { orderBy: 'asc' })
643
+ * ```
644
+ *
645
+ * @example Distinct with a filter
646
+ * ```ts
647
+ * const actions = await audit.client.distinct('action', {
648
+ * where: { userId: 'u_1' },
649
+ * orderBy: 'asc',
650
+ * })
651
+ * ```
652
+ */
653
+ distinct<K extends keyof TCols & string>(column: K, options?: {
654
+ where?: WhereClause<TCols>;
655
+ orderBy?: 'asc' | 'desc'; /** Include null values in the result. Defaults to `false`. */
656
+ includeNull?: boolean;
657
+ }): Promise<ColumnValue<TCols[K]>[]>;
568
658
  /**
569
659
  * Insert a single row. Returns the inserted row (server-generated
570
660
  * defaults populated).
571
661
  */
572
- insert(values: InsertRow<TCols>): Promise<Row<TCols>>;
662
+ insert(values: InferInsertModel<TTable>): Promise<Row<TCols>>;
573
663
  /**
574
664
  * Upsert: insert a row, or update an existing row if a unique constraint
575
665
  * conflict occurs.
@@ -577,9 +667,13 @@ declare class TableClient<TCols extends Record<string, ColumnFactory>> {
577
667
  * `target` names the column(s) whose unique constraint should trigger
578
668
  * the conflict path. `set` is the patch applied on conflict — when
579
669
  * omitted, the conflict path applies the same `values` (i.e. "last
580
- * write wins"). The PR 1 version does not support `setWhere`
581
- * (conditional updates on conflict); see PR 3 for the atomic-locking
582
- * pattern that uses it.
670
+ * write wins").
671
+ *
672
+ * `setWhere` adds a conditional WHERE to the conflict update path. If
673
+ * the condition does not match, the update is skipped and the existing
674
+ * row is returned unchanged (via a fallback SELECT). This enables
675
+ * patterns like "only update if the new version is higher than the
676
+ * existing one".
583
677
  *
584
678
  * @example Idempotent "mark as read" tracking
585
679
  * ```ts
@@ -589,23 +683,24 @@ declare class TableClient<TCols extends Record<string, ColumnFactory>> {
589
683
  * )
590
684
  * ```
591
685
  *
592
- * @example Counter increment
686
+ * @example Conditional update — only bump if newer
593
687
  * ```ts
594
- * await counters.client.upsert(
595
- * { key: 'page_views', value: 1 },
596
- * { target: 'key', set: { value: sql`${counters.table.value} + 1` } as never },
688
+ * await state.client.upsert(
689
+ * { key: 'sync', version: 5, data: '...' },
690
+ * { target: 'key', setWhere: { version: { lt: 5 } } },
597
691
  * )
598
692
  * ```
599
693
  */
600
- upsert(values: InsertRow<TCols>, opts: {
694
+ upsert(values: InferInsertModel<TTable>, opts: {
601
695
  target: (keyof TCols & string) | (keyof TCols & string)[];
602
- set?: Partial<InsertRow<TCols>>;
696
+ set?: Partial<InferInsertModel<TTable>>; /** Conditional WHERE on the conflict update path. Empty objects are ignored. */
697
+ setWhere?: WhereClause<TCols>;
603
698
  }): Promise<Row<TCols>>;
604
699
  /**
605
700
  * Insert many rows in a single statement. Capped at {@link MAX_BATCH}.
606
701
  * Returns all inserted rows in input order.
607
702
  */
608
- insertMany(values: InsertRow<TCols>[]): Promise<Row<TCols>[]>;
703
+ insertMany(values: InferInsertModel<TTable>[]): Promise<Row<TCols>[]>;
609
704
  /**
610
705
  * Update **at most one** row matching the where clause. Returns the
611
706
  * updated row, or `null` if no row matched.
@@ -627,12 +722,12 @@ declare class TableClient<TCols extends Record<string, ColumnFactory>> {
627
722
  * the throw is to surface the bug, not to prevent it. Wrap in a
628
723
  * transaction if you need atomic rollback.
629
724
  */
630
- update(where: WhereClause<TCols>, patch: Partial<InsertRow<TCols>>): Promise<Row<TCols> | null>;
725
+ update(where: WhereClause<TCols>, patch: Partial<InferInsertModel<TTable>>): Promise<Row<TCols> | null>;
631
726
  /**
632
727
  * Update all rows matching the where clause. Returns every updated row.
633
728
  * Throws if the where is empty — there is no "update everything" path.
634
729
  */
635
- updateMany(where: WhereClause<TCols>, patch: Partial<InsertRow<TCols>>): Promise<Row<TCols>[]>;
730
+ updateMany(where: WhereClause<TCols>, patch: Partial<InferInsertModel<TTable>>): Promise<Row<TCols>[]>;
636
731
  /**
637
732
  * Delete **at most one** row matching the where clause. Returns the
638
733
  * deleted row, or `null` if no row matched.
@@ -670,7 +765,79 @@ declare class TableClient<TCols extends Record<string, ColumnFactory>> {
670
765
  * })
671
766
  * ```
672
767
  */
673
- transaction<T>(fn: (tx: TableClient<TCols>) => Promise<T>): Promise<T>;
768
+ transaction<T>(fn: (tx: TableClient<TCols, TTable>) => Promise<T>): Promise<T>;
769
+ /**
770
+ * Atomically claim rows using `FOR UPDATE SKIP LOCKED`.
771
+ *
772
+ * Selects rows matching `where`, locks them (skipping any already locked
773
+ * by another worker), updates them with `set`/`setSql`, and returns the
774
+ * claimed rows in a single atomic operation. This is the standard pattern
775
+ * for job queues, task distribution, and any producer-consumer table.
776
+ *
777
+ * Requires the table to have a declared primary key (either via
778
+ * `column.uuid({ primaryKey: true })` or `defineTable({ primaryKey })`.
779
+ *
780
+ * @example Job queue claim
781
+ * ```ts
782
+ * const jobs = await client.claim({
783
+ * where: { status: 'pending', runAt: { lte: new Date() } },
784
+ * set: { status: 'processing', lockedBy: workerId, lockedAt: new Date() },
785
+ * setSql: { attempts: sql`${jobsTable.table.attempts} + 1` },
786
+ * orderBy: [{ column: 'priority', dir: 'desc' }, { column: 'createdAt', dir: 'asc' }],
787
+ * limit: 5,
788
+ * })
789
+ * ```
790
+ */
791
+ claim(options: ClaimOptions<TCols, TTable>): Promise<Row<TCols>[]>;
792
+ /**
793
+ * Run an aggregate query with GROUP BY.
794
+ *
795
+ * Each key in `select` is an output alias mapped to an aggregate
796
+ * specification. The `groupBy` columns are included automatically in
797
+ * the result. Returns typed rows with grouped column values plus the
798
+ * computed aggregates.
799
+ *
800
+ * @example Count jobs by status
801
+ * ```ts
802
+ * const rows = await client.aggregate({
803
+ * select: { count: { fn: 'count' } },
804
+ * groupBy: ['status'],
805
+ * })
806
+ * // → [{ status: 'pending', count: 12 }, { status: 'completed', count: 45 }]
807
+ * ```
808
+ *
809
+ * @example Average priority of open jobs
810
+ * ```ts
811
+ * const [row] = await client.aggregate({
812
+ * select: { avgPriority: { fn: 'avg', column: 'priority' } },
813
+ * where: { status: 'open' },
814
+ * })
815
+ * ```
816
+ */
817
+ aggregate<TResult extends Record<string, unknown> = Record<string, unknown>>(options: AggregateOptions<TCols>): Promise<TResult[]>;
818
+ /**
819
+ * Fast approximate row count using Postgres `reltuples` statistics.
820
+ *
821
+ * Returns the estimated row count from `pg_class` — updated by
822
+ * `ANALYZE` and autovacuum. Much faster than `COUNT(*)` on large
823
+ * tables (no sequential scan), but may be stale by up to a few
824
+ * percent. Returns 0 for tables that have never been analyzed.
825
+ *
826
+ * Use `count()` when you need an exact number. Use `countEstimate()`
827
+ * for UI display, pagination hints, or "is this table large?" checks
828
+ * where ±5% accuracy is fine.
829
+ *
830
+ * @example
831
+ * ```ts
832
+ * const approx = await client.countEstimate()
833
+ * if (approx > 100_000) {
834
+ * // Switch to keyset pagination instead of offset
835
+ * }
836
+ * ```
837
+ */
838
+ countEstimate(): Promise<number>;
839
+ /** Build a single aggregate SQL expression from a spec. */
840
+ private buildAggregateExpr;
674
841
  private buildWhereOrThrow;
675
842
  private validateLimit;
676
843
  }
@@ -680,14 +847,19 @@ declare class TableClient<TCols extends Record<string, ColumnFactory>> {
680
847
  * The result of `defineTable()`.
681
848
  *
682
849
  * @template TCols The columns record passed to `defineTable`.
850
+ * @template TTable The concrete Drizzle `pgTable` type produced by the
851
+ * definition. Defaulted to the non-generic `PgTable` for callers that
852
+ * pass `DefinedTable` around without propagating inference — inside
853
+ * `defineTable` itself, the actual `typeof table` is captured so
854
+ * `makeClient` returns a fully-typed `TableClient<TCols, typeof table>`.
683
855
  */
684
- interface DefinedTable<TCols extends Record<string, ColumnFactory>> {
856
+ interface DefinedTable<TCols extends Record<string, ColumnFactory>, TTable extends PgTable = PgTableWithColumns<TableConfig>> {
685
857
  /**
686
858
  * The underlying Drizzle `pgTable`. Exposed so callers can use it in
687
859
  * typed JOINs (the one sanctioned escape valve), and so it can be
688
- * referenced from `drizzle.config.ts` for migration generation.
860
+ * included in `Plugin.tables` for migration discovery by `lumi migrate`.
689
861
  */
690
- table: PgTable;
862
+ table: TTable;
691
863
  /**
692
864
  * The original schema metadata. Useful for introspection / tooling
693
865
  * that needs to know about indexes, unique constraints, etc.
@@ -698,6 +870,12 @@ interface DefinedTable<TCols extends Record<string, ColumnFactory>> {
698
870
  * by the where-builder for jsonb path validation.
699
871
  */
700
872
  columnKinds: Readonly<Record<string, ColumnKind>>;
873
+ /**
874
+ * Primary key column names (JS property names). Detected from either
875
+ * `schema.primaryKey` or column-level `{ primaryKey: true }`. Empty
876
+ * array if no PK is declared. Required by `claim()`.
877
+ */
878
+ primaryKeyColumns: readonly string[];
701
879
  /**
702
880
  * Build a typed CRUD client wired to the given database handle.
703
881
  *
@@ -705,7 +883,7 @@ interface DefinedTable<TCols extends Record<string, ColumnFactory>> {
705
883
  * cache it. The client is stateless aside from the db handle, so it
706
884
  * is safe to share.
707
885
  */
708
- makeClient(db: PostgresJsDatabase): TableClient<TCols>;
886
+ makeClient(db: PostgresJsDatabase): TableClient<TCols, TTable>;
709
887
  }
710
888
  /**
711
889
  * Define a typed table.
@@ -718,7 +896,59 @@ interface DefinedTable<TCols extends Record<string, ColumnFactory>> {
718
896
  * @throws if the table name doesn't match the snake_case regex, or if
719
897
  * the same name is registered twice with different table objects.
720
898
  */
721
- declare function defineTable<const TCols extends Record<string, ColumnFactory>>(def: TableDefinition<TCols>): DefinedTable<TCols>;
899
+ declare function defineTable<const TCols extends Record<string, ColumnFactory>>(def: TableDefinition<TCols>): {
900
+ table: PgTableWithColumns<{
901
+ name: string;
902
+ schema: undefined;
903
+ columns: {
904
+ [x: string]: _$drizzle_orm_pg_core0.PgColumn<{
905
+ name: string;
906
+ tableName: string;
907
+ dataType: _$drizzle_orm0.ColumnDataType;
908
+ columnType: string;
909
+ data: unknown;
910
+ driverParam: unknown;
911
+ notNull: false;
912
+ hasDefault: false;
913
+ isPrimaryKey: false;
914
+ isAutoincrement: false;
915
+ hasRuntimeDefault: false;
916
+ enumValues: string[] | undefined;
917
+ baseColumn: never;
918
+ identity: undefined;
919
+ generated: undefined;
920
+ }, {}, {}>;
921
+ };
922
+ dialect: "pg";
923
+ }>;
924
+ schema: TableDefinition<TCols>;
925
+ columnKinds: Readonly<Record<string, ColumnKind>>;
926
+ primaryKeyColumns: readonly string[];
927
+ makeClient: (db: PostgresJsDatabase) => TableClient<TCols, PgTableWithColumns<{
928
+ name: string;
929
+ schema: undefined;
930
+ columns: {
931
+ [x: string]: _$drizzle_orm_pg_core0.PgColumn<{
932
+ name: string;
933
+ tableName: string;
934
+ dataType: _$drizzle_orm0.ColumnDataType;
935
+ columnType: string;
936
+ data: unknown;
937
+ driverParam: unknown;
938
+ notNull: false;
939
+ hasDefault: false;
940
+ isPrimaryKey: false;
941
+ isAutoincrement: false;
942
+ hasRuntimeDefault: false;
943
+ enumValues: string[] | undefined;
944
+ baseColumn: never;
945
+ identity: undefined;
946
+ generated: undefined;
947
+ }, {}, {}>;
948
+ };
949
+ dialect: "pg";
950
+ }>>;
951
+ };
722
952
  //#endregion
723
953
  //#region src/table/registry.d.ts
724
954
  declare class TableRegistryImpl {
@@ -740,8 +970,8 @@ declare class TableRegistryImpl {
740
970
  /**
741
971
  * Return every registered table as a `{ [name]: PgTable }` map.
742
972
  *
743
- * Useful for `createTestDb().push(tableRegistry.allTables())` and for
744
- * exposing a single import target to `drizzle.config.ts`.
973
+ * Useful for `createTestDb().push(tableRegistry.allTables())` and
974
+ * runtime introspection.
745
975
  */
746
976
  allTables(): Record<string, PgTable>;
747
977
  /**
@@ -792,5 +1022,5 @@ declare class WhereBuilderError extends Error {
792
1022
  */
793
1023
  declare function isNonEmptyWhere(clause: unknown): boolean;
794
1024
  //#endregion
795
- export { type AnyPgColumn, 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 };
1025
+ 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 };
796
1026
  //# sourceMappingURL=index.d.mts.map