tempest-db-js 0.1.0 → 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.cts CHANGED
@@ -18,12 +18,26 @@ interface OrderTerm {
18
18
  readonly column: string;
19
19
  readonly direction: SortDirection;
20
20
  }
21
+ /** One aggregate expression in a grouped SELECT (`COUNT(*) AS "n"`). */
22
+ interface AggregateTerm {
23
+ readonly fn: "count" | "sum" | "avg" | "min" | "max";
24
+ /** The column to aggregate, or `"*"` (only valid for `count`). */
25
+ readonly column: string | "*";
26
+ /** The result alias. */
27
+ readonly alias: string;
28
+ }
21
29
  /** Serializable AST for a SELECT. Dialects (Phase 4) compile this to SQL. */
22
30
  interface SelectNode {
23
31
  readonly kind: "select";
24
32
  readonly table: string;
25
33
  /** Projected columns, or "*" for the whole row. */
26
34
  readonly columns: readonly string[] | "*";
35
+ /** Emit `SELECT DISTINCT` when true. */
36
+ readonly distinct: boolean;
37
+ /** Aggregate expressions; when non-empty, this is a grouped/aggregate query. */
38
+ readonly aggregates: readonly AggregateTerm[];
39
+ /** `GROUP BY` columns. */
40
+ readonly groupBy: readonly string[];
27
41
  readonly where: CondNode | undefined;
28
42
  readonly orderBy: readonly OrderTerm[];
29
43
  readonly limit: number | undefined;
@@ -83,6 +97,29 @@ type WhereInput<Row = Record<string, unknown>> = {
83
97
  declare const OPERATORS: readonly ["eq", "ne", "gt", "gte", "lt", "lte", "like", "ilike", "in", "notIn", "between", "isNull"];
84
98
  /** One supported operator name. */
85
99
  type Operator = (typeof OPERATORS)[number];
100
+ /** An aggregate expression carrying its result type `T` as a phantom. */
101
+ declare class Agg<T> {
102
+ readonly fn: AggregateTerm["fn"];
103
+ readonly column: string | "*";
104
+ readonly __t: T;
105
+ constructor(fn: AggregateTerm["fn"], column: string | "*");
106
+ }
107
+ /** `COUNT(*)` — the number of rows in the group (never null). */
108
+ declare function count(): Agg<number>;
109
+ /** `SUM(column)` — null when the group has no non-null values. */
110
+ declare function sum(column: string): Agg<number | null>;
111
+ /** `AVG(column)` — null when the group has no non-null values. */
112
+ declare function avg(column: string): Agg<number | null>;
113
+ /** `MIN(column)` — numeric columns; null on an empty group. */
114
+ declare function min(column: string): Agg<number | null>;
115
+ /** `MAX(column)` — numeric columns; null on an empty group. */
116
+ declare function max(column: string): Agg<number | null>;
117
+ /** Extract the phantom result type of an aggregate expression. */
118
+ type AggResult<A> = A extends Agg<infer T> ? T : never;
119
+ /** Flatten an intersection into a single object literal. */
120
+ type SimplifyProj<T> = {
121
+ [K in keyof T]: T[K];
122
+ } & {};
86
123
  /**
87
124
  * Immutable, chainable SELECT builder.
88
125
  *
@@ -101,6 +138,27 @@ declare class SelectBuilder<Full, Proj = Full> {
101
138
  private with;
102
139
  /** Add a WHERE filter: the object form (keys typed) or an `and`/`or`/`not`. */
103
140
  where(input: WhereInput<Full> | Condition): SelectBuilder<Full, Proj>;
141
+ /** Emit `SELECT DISTINCT` — drop duplicate rows. */
142
+ distinct(): SelectBuilder<Full, Proj>;
143
+ /**
144
+ * Group by columns and compute aggregates. The result row is the grouped
145
+ * columns (typed from the model) plus one field per aggregate alias.
146
+ *
147
+ * @param groupBy The columns to group by (checked against the model). Pass `[]`
148
+ * for a whole-table aggregate.
149
+ * @param spec A map of result alias → aggregate expression ({@link count},
150
+ * {@link sum}, {@link avg}, {@link min}, {@link max}).
151
+ * @returns A builder whose row is `Pick<Full, K> & { [alias]: aggResult }`.
152
+ *
153
+ * @example
154
+ * ```ts
155
+ * select(Order).aggregate(["status"], { n: count(), total: sum("amount") });
156
+ * // rows: { status: string; n: number; total: number | null }[]
157
+ * ```
158
+ */
159
+ aggregate<K extends keyof Full & string, S extends Record<string, Agg<unknown>>>(groupBy: readonly K[], spec: S): SelectBuilder<Full, SimplifyProj<Pick<Full, K> & {
160
+ [A in keyof S]: AggResult<S[A]>;
161
+ }>>;
104
162
  /** Order by a column of `Full`. */
105
163
  orderBy(column: keyof Full & string, direction?: SortDirection): SelectBuilder<Full, Proj>;
106
164
  /** Limit the number of rows. */
@@ -177,12 +235,23 @@ declare function not<Row = Record<string, unknown>>(input: WhereArg<NoInfer<Row>
177
235
 
178
236
  /** Columns to return from a mutation, or "*" for the whole row. */
179
237
  type Returning = readonly string[] | "*" | null;
238
+ /**
239
+ * Conflict-resolution clause for an INSERT (`ON CONFLICT`). `target` is the
240
+ * conflicting column(s) (a unique/PK constraint); `update` is `"nothing"` for
241
+ * `DO NOTHING`, or the columns to overwrite for `DO UPDATE SET`.
242
+ */
243
+ interface OnConflict {
244
+ readonly target: readonly string[];
245
+ readonly update: Record<string, unknown> | "nothing";
246
+ }
180
247
  /** Serializable AST for an INSERT. */
181
248
  interface InsertNode {
182
249
  readonly kind: "insert";
183
250
  readonly table: string;
184
251
  readonly values: readonly Record<string, unknown>[];
185
252
  readonly returning: Returning;
253
+ /** Conflict handling (`ON CONFLICT ...`), or `undefined` for none. */
254
+ readonly onConflict?: OnConflict;
186
255
  }
187
256
  /**
188
257
  * INSERT builder.
@@ -202,6 +271,19 @@ declare class InsertBuilder<Full, Ins, Ret = number> {
202
271
  private with;
203
272
  /** Provide one row or many rows to insert, typed by the insert shape. */
204
273
  values(rows: Ins | readonly Ins[]): InsertBuilder<Full, Ins, Ret>;
274
+ /**
275
+ * On a unique/PK conflict on `target`, do nothing (skip the row).
276
+ *
277
+ * @param target The conflicting column(s) — a unique or primary key.
278
+ */
279
+ onConflictDoNothing(target: readonly (keyof Full & string)[]): InsertBuilder<Full, Ins, Ret>;
280
+ /**
281
+ * On a unique/PK conflict on `target`, overwrite the given columns (upsert).
282
+ *
283
+ * @param target The conflicting column(s) — a unique or primary key.
284
+ * @param set The columns to update with new values.
285
+ */
286
+ onConflictDoUpdate(target: readonly (keyof Full & string)[], set: Partial<Full>): InsertBuilder<Full, Ins, Ret>;
205
287
  /** Return the full inserted row(s). */
206
288
  returning(): InsertBuilder<Full, Ins, Full>;
207
289
  /** Return only the given columns of the inserted row(s). */
@@ -298,14 +380,14 @@ declare function del<C extends ModelClass>(model: C): DeleteBuilder<InferModel<C
298
380
  * detection, so URLs copied from a Python service still work here.
299
381
  */
300
382
  /** A database dialect tempest-db-js can target. */
301
- type Dialect = "sqlite" | "postgresql";
383
+ type Dialect = "sqlite" | "postgresql" | "mysql";
302
384
  /** A parsed database URL, dialect-neutral. */
303
385
  interface ParsedDatabaseUrl {
304
386
  /** The detected dialect. */
305
387
  readonly dialect: Dialect;
306
388
  /** Driver after the `+` in the scheme (e.g. `better-sqlite3`), or `null`. */
307
389
  readonly driver: string | null;
308
- /** Host (PostgreSQL), or `null` for SQLite. */
390
+ /** Host (PostgreSQL/MySQL), or `null` for SQLite. */
309
391
  readonly host: string | null;
310
392
  /** Port, or `null`. */
311
393
  readonly port: number | null;
@@ -420,8 +502,20 @@ interface JoinSelection {
420
502
  readonly alias: string;
421
503
  readonly column: string;
422
504
  }
423
- /** `where` filter for a join: keys are `alias.column` refs (object form). */
424
- type JoinWhereInput = Record<string, unknown>;
505
+ /** Collapse a union of object types into their intersection. */
506
+ type UnionToIntersection<U> = (U extends unknown ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
507
+ /**
508
+ * `where` filter for a join. Keys are `alias.column` refs; each value accepts a
509
+ * bare value (shorthand for `eq`) or an operator object restricted to the
510
+ * operators valid for that column's type — exactly like the single-table
511
+ * {@link WhereInput}, but qualified per source. `like` on a numeric join column,
512
+ * or `gt` on a string one, is a compile error.
513
+ */
514
+ type JoinWhereInput<S extends Sources> = Partial<UnionToIntersection<{
515
+ [A in keyof S]: {
516
+ [C in keyof NonNullable<S[A]> & string as `${A & string}.${C}`]: NonNullable<S[A]>[C] | OperatorsFor<NonNullable<NonNullable<S[A]>[C]>>;
517
+ };
518
+ }[keyof S]>>;
425
519
  /** Serializable AST for a multi-table SELECT. */
426
520
  interface JoinNode {
427
521
  readonly kind: "join_select";
@@ -476,7 +570,7 @@ declare class JoinBuilder<S extends Sources> {
476
570
  [K in A]: InferModel<C> | null;
477
571
  }>;
478
572
  /** Filter by `alias.column` references (object form) or an `and`/`or`/`not`. */
479
- where(input: Partial<Record<ColRef<S>, unknown>> | Condition): JoinBuilder<S>;
573
+ where(input: JoinWhereInput<S> | Condition): JoinBuilder<S>;
480
574
  /** Order by an `alias.column` reference. */
481
575
  orderBy(ref: ColRef<S>, direction?: SortDirection): JoinBuilder<S>;
482
576
  limit(n: number): JoinBuilder<S>;
@@ -516,12 +610,27 @@ type QueryNode = SelectNode | InsertNode | UpdateNode | DeleteNode | JoinNode;
516
610
  * actually differs between databases (placeholder syntax, `ILIKE` support).
517
611
  */
518
612
  declare abstract class BaseDialect {
519
- abstract readonly name: "sqlite" | "postgresql";
613
+ abstract readonly name: Dialect;
614
+ /**
615
+ * INSERT SQL templates keyed by structure (dialect|table|columns|rowCount|
616
+ * returning). Shared across dialect instances — the key namespaces by dialect
617
+ * name, and the placeholder text is dialect-specific but structure-determined.
618
+ */
619
+ private static readonly insertTemplates;
620
+ /** Quoted-identifier cache (see {@link quoteId}). Shared across dialects. */
621
+ private static readonly quotedIds;
520
622
  /** Render the n-th (1-based) placeholder. */
521
623
  protected abstract placeholder(index: number): string;
522
624
  /** Render a case-insensitive LIKE for the active dialect. */
523
625
  protected abstract ilike(column: string, param: string): string;
524
- /** Quote an identifier (column/table) for the active dialect. */
626
+ /**
627
+ * Quote an identifier (column/table) for the active dialect.
628
+ *
629
+ * Memoized: identifiers form a small, stable set (column/table names), but this
630
+ * runs for every identifier on every compile. Caching the quoted form removes a
631
+ * regex-replace + string allocation from the hot path. The standard double-quote
632
+ * form is identical across both dialects, so one shared cache is correct.
633
+ */
525
634
  protected quoteId(name: string): string;
526
635
  /** Compile any node to `{ sql, params }`. */
527
636
  compile(node: QueryNode): CompiledQuery;
@@ -529,10 +638,28 @@ declare abstract class BaseDialect {
529
638
  private qualify;
530
639
  private compileSelect;
531
640
  private compileInsert;
641
+ /**
642
+ * The INSERT SQL template for a given structure, cached across calls.
643
+ *
644
+ * The text depends only on (dialect, table, columns, row count, returning,
645
+ * conflict shape) — never on the bound values — and placeholder positions are
646
+ * deterministic from the counts (a fresh statement always starts binding at 1).
647
+ * So a per-row insert loop compiles the string once and reuses it every row.
648
+ */
649
+ private insertTemplate;
650
+ /**
651
+ * Render the conflict-handling clause. Standard SQL (SQLite/PostgreSQL) uses
652
+ * `ON CONFLICT (...) DO NOTHING | DO UPDATE SET ...`; MySQL overrides this.
653
+ *
654
+ * @param onConflict The conflict clause from the node.
655
+ * @param conflictCols The columns to overwrite on `DO UPDATE` (empty for nothing).
656
+ * @param nextPlaceholder Yields the next positional placeholder (advances the count).
657
+ */
658
+ protected renderConflict(onConflict: NonNullable<InsertNode["onConflict"]>, conflictCols: readonly string[], nextPlaceholder: () => string): string;
532
659
  private compileUpdate;
533
660
  private compileDelete;
534
661
  private compileJoin;
535
- private compileReturning;
662
+ protected compileReturning(returning: readonly string[] | "*" | null): string;
536
663
  /**
537
664
  * Compile a condition tree (fields / and / or / not) to SQL. `idFor` renders a
538
665
  * key to a quoted identifier — `quoteId` for single-table, `qualify` for joins —
@@ -554,8 +681,21 @@ declare class PostgresDialect extends BaseDialect {
554
681
  protected placeholder(index: number): string;
555
682
  protected ilike(column: string, param: string): string;
556
683
  }
684
+ /**
685
+ * MySQL dialect: `?` placeholders, backtick identifiers, `ON DUPLICATE KEY
686
+ * UPDATE` for upsert, and case-insensitive `LIKE` (default collation). MySQL has
687
+ * no `RETURNING`, so requesting it throws.
688
+ */
689
+ declare class MysqlDialect extends BaseDialect {
690
+ readonly name: "mysql";
691
+ protected placeholder(): string;
692
+ protected ilike(column: string, param: string): string;
693
+ protected quoteId(name: string): string;
694
+ protected renderConflict(onConflict: NonNullable<InsertNode["onConflict"]>, conflictCols: readonly string[], nextPlaceholder: () => string): string;
695
+ protected compileReturning(returning: readonly string[] | "*" | null): string;
696
+ }
557
697
  /** Get a dialect instance by name. */
558
- declare function getDialect(name: "sqlite" | "postgresql"): BaseDialect;
698
+ declare function getDialect(name: Dialect): BaseDialect;
559
699
 
560
700
  /** The outcome of running one statement. */
561
701
  interface DriverResult {
@@ -576,14 +716,35 @@ interface AsyncDriver {
576
716
  execute(sql: string, params: readonly unknown[]): Promise<DriverResult>;
577
717
  /** Lazily iterate rows (for `.stream()`), if the driver supports it. */
578
718
  iterate?(sql: string, params: readonly unknown[]): AsyncIterableIterator<Record<string, unknown>>;
719
+ /**
720
+ * Reserve a single pinned connection for the duration of a transaction.
721
+ * Pooled drivers (PostgreSQL) MUST implement this so `BEGIN`/`COMMIT` and the
722
+ * statements between them all run on the same connection. Single-connection
723
+ * drivers (SQLite) may omit it — `transaction` then runs on the shared handle.
724
+ */
725
+ reserve?(): Promise<ReservedAsyncDriver>;
579
726
  close(): Promise<void>;
580
727
  }
728
+ /** An {@link AsyncDriver} pinned to one connection, used inside a transaction. */
729
+ interface ReservedAsyncDriver extends AsyncDriver {
730
+ /** Return the pinned connection to the pool. */
731
+ release(): Promise<void>;
732
+ }
581
733
  /** SQLite driver backed by Node's built-in `node:sqlite` (zero install). */
582
734
  declare class NodeSqliteDriver implements SyncDriver {
583
735
  private readonly db;
736
+ /**
737
+ * Prepared-statement cache keyed by SQL text. tempest-db-js always
738
+ * parameterizes, so a query shape maps to one stable SQL string — reusing the
739
+ * compiled statement avoids re-`prepare()` on every call (the dominant cost of
740
+ * per-row inserts and point lookups).
741
+ */
742
+ private readonly statements;
584
743
  constructor(database: any);
585
744
  /** Open a `node:sqlite` database at the given path (or `:memory:`). */
586
745
  static open(path: string): NodeSqliteDriver;
746
+ /** Return the cached prepared statement for `sql`, preparing it on first use. */
747
+ private prepare;
587
748
  execute(sql: string, params: readonly unknown[]): DriverResult;
588
749
  iterate(sql: string, params: readonly unknown[]): IterableIterator<Record<string, unknown>>;
589
750
  close(): void;
@@ -607,6 +768,34 @@ type RowOf<B> = B extends {
607
768
  declare class NoResultError extends Error {
608
769
  constructor(message: string);
609
770
  }
771
+ /**
772
+ * Raised when the driver rejects a statement. Wraps the original driver error
773
+ * and attaches the offending SQL and its bound parameters, so a failure points
774
+ * at the exact query instead of an opaque driver message.
775
+ */
776
+ declare class QueryExecutionError extends Error {
777
+ /** The original error thrown by the driver. */
778
+ readonly cause: unknown;
779
+ /** The SQL that failed. */
780
+ readonly sql: string;
781
+ /** The bound parameters, in order. */
782
+ readonly params: readonly unknown[];
783
+ constructor(
784
+ /** The original error thrown by the driver. */
785
+ cause: unknown,
786
+ /** The SQL that failed. */
787
+ sql: string,
788
+ /** The bound parameters, in order. */
789
+ params: readonly unknown[]);
790
+ }
791
+ /**
792
+ * A hook invoked for every statement a session runs (query logging / tracing).
793
+ * Errors thrown by the logger are ignored so logging never breaks execution.
794
+ */
795
+ type QueryLogger = (event: {
796
+ readonly sql: string;
797
+ readonly params: readonly unknown[];
798
+ }) => void;
610
799
  /** Synchronous result view over already-fetched rows. */
611
800
  declare class SyncResult<Row> {
612
801
  private readonly rows;
@@ -636,7 +825,13 @@ declare class AsyncResult<Row> {
636
825
  declare class SyncSession {
637
826
  private readonly driver;
638
827
  private readonly dialect;
639
- constructor(driver: SyncDriver, dialect: BaseDialect);
828
+ /** Optional per-statement logger (query tracing). */
829
+ private readonly logger?;
830
+ constructor(driver: SyncDriver, dialect: BaseDialect,
831
+ /** Optional per-statement logger (query tracing). */
832
+ logger?: QueryLogger | undefined);
833
+ /** Log, run, and error-wrap one raw statement. */
834
+ private exec;
640
835
  /** Compile, run, and coerce a builder into a result. */
641
836
  execute<B extends Executable>(builder: B): SyncResult<RowOf<B>>;
642
837
  /** Run `fn` inside a transaction: commit on success, rollback on throw. */
@@ -649,17 +844,27 @@ declare class SyncSession {
649
844
  */
650
845
  stream<B extends Executable>(builder: B): IterableIterator<RowOf<B>>;
651
846
  close(): void;
847
+ /** `using session = ...` closes the driver when the scope exits. */
848
+ [Symbol.dispose](): void;
652
849
  }
653
850
  /** An asynchronous unit of work. */
654
851
  declare class AsyncSession {
655
852
  private readonly driver;
656
853
  private readonly dialect;
657
- constructor(driver: AsyncDriver, dialect: BaseDialect);
854
+ /** Optional per-statement logger (query tracing). */
855
+ private readonly logger?;
856
+ constructor(driver: AsyncDriver, dialect: BaseDialect,
857
+ /** Optional per-statement logger (query tracing). */
858
+ logger?: QueryLogger | undefined);
859
+ /** Log, run, and error-wrap one raw statement. */
860
+ private exec;
658
861
  execute<B extends Executable>(builder: B): AsyncResult<RowOf<B>>;
659
862
  /** Lazily iterate result rows. Uses driver streaming when available. */
660
863
  stream<B extends Executable>(builder: B): AsyncIterableIterator<RowOf<B>>;
661
864
  transaction<T>(fn: (tx: AsyncSession) => Promise<T>): Promise<T>;
662
865
  close(): Promise<void>;
866
+ /** `await using session = ...` closes the driver when the scope exits. */
867
+ [Symbol.asyncDispose](): Promise<void>;
663
868
  }
664
869
  /** Connection-pool tuning (PostgreSQL; ignored by SQLite). */
665
870
  interface PoolOptions {
@@ -676,24 +881,35 @@ interface EngineOptions {
676
881
  readonly driver?: string;
677
882
  /** Connection-pool tuning (PostgreSQL only). */
678
883
  readonly pool?: PoolOptions;
884
+ /**
885
+ * Called for every statement a session runs — SQL + bound params. Use for
886
+ * query logging/tracing. Thrown errors are swallowed so it never breaks a query.
887
+ */
888
+ readonly onQuery?: QueryLogger;
679
889
  }
680
890
  /** A synchronous engine (SQLite only). */
681
891
  declare class SyncEngine {
682
892
  private readonly driver;
893
+ private readonly logger?;
683
894
  readonly dialect: Dialect;
684
- constructor(driver: SyncDriver);
895
+ constructor(driver: SyncDriver, logger?: QueryLogger | undefined);
685
896
  session(): SyncSession;
686
897
  transaction<T>(fn: (tx: SyncSession) => T): T;
687
898
  close(): void;
899
+ /** `using engine = createSyncEngine(...)` closes the pool when the scope exits. */
900
+ [Symbol.dispose](): void;
688
901
  }
689
902
  /** An asynchronous engine. */
690
903
  declare class AsyncEngine {
691
904
  private readonly driver;
692
905
  readonly dialect: Dialect;
693
- constructor(driver: AsyncDriver, dialect: Dialect);
906
+ private readonly logger?;
907
+ constructor(driver: AsyncDriver, dialect: Dialect, logger?: QueryLogger | undefined);
694
908
  session(): AsyncSession;
695
909
  transaction<T>(fn: (tx: AsyncSession) => Promise<T>): Promise<T>;
696
910
  close(): Promise<void>;
911
+ /** `await using engine = createEngine(...)` closes the pool when the scope exits. */
912
+ [Symbol.asyncDispose](): Promise<void>;
697
913
  }
698
914
  /**
699
915
  * Create a **synchronous** engine from a database URL. SQLite only — PostgreSQL
@@ -785,6 +1001,91 @@ declare class BaseRepository<C extends ModelClass> {
785
1001
  paginate(filter?: PaginationFilter<InferModel<C>>): Promise<PaginationResult<InferModel<C>>>;
786
1002
  }
787
1003
 
1004
+ /**
1005
+ * tempest-db-js — opt-in active-record layer.
1006
+ *
1007
+ * The library's default return shape is a plain inferred object (a locked design
1008
+ * decision — see the roadmap). This module adds an **opt-in** wrapper for code
1009
+ * that prefers instance methods: `ActiveRecord` holds the plain row on `.data`
1010
+ * and exposes `save` / `update` / `delete` / `reload` over an async session. It
1011
+ * never replaces the default plain-object return — you reach for it explicitly.
1012
+ */
1013
+
1014
+ /**
1015
+ * An opt-in active-record wrapper around a single row.
1016
+ *
1017
+ * The current field values live on {@link data} (a plain, typed row object).
1018
+ * Mutating methods persist through the bound session and refresh `data`.
1019
+ *
1020
+ * @typeParam C - the model class.
1021
+ */
1022
+ declare class ActiveRecord<C extends ModelClass> {
1023
+ private readonly model;
1024
+ private readonly session;
1025
+ /** The current field values (a plain, typed row). */
1026
+ data: InferModel<C>;
1027
+ private readonly pk;
1028
+ constructor(model: C, session: AsyncSession,
1029
+ /** The current field values (a plain, typed row). */
1030
+ data: InferModel<C>);
1031
+ /** The primary-key value of the wrapped row. */
1032
+ private pkValue;
1033
+ private pkFilter;
1034
+ /**
1035
+ * Persist the current `data` — insert if new, otherwise overwrite the existing
1036
+ * row (upsert on the primary key). Refreshes `data` from the returned row.
1037
+ *
1038
+ * @returns This wrapper, for chaining.
1039
+ */
1040
+ save(): Promise<this>;
1041
+ /**
1042
+ * Update the given columns for this row and merge them into `data`.
1043
+ *
1044
+ * @param patch The columns to change.
1045
+ * @returns This wrapper, for chaining.
1046
+ */
1047
+ update(patch: Partial<InferModel<C>>): Promise<this>;
1048
+ /**
1049
+ * Delete this row.
1050
+ *
1051
+ * @returns The number of rows affected (0 or 1).
1052
+ */
1053
+ delete(): Promise<number>;
1054
+ /**
1055
+ * Re-fetch this row by primary key and refresh `data`.
1056
+ *
1057
+ * @returns This wrapper, for chaining.
1058
+ * @throws When the row no longer exists.
1059
+ */
1060
+ reload(): Promise<this>;
1061
+ }
1062
+ /** A small factory binding a model + session for producing {@link ActiveRecord}s. */
1063
+ interface ActiveRecordManager<C extends ModelClass> {
1064
+ /** Wrap an existing row (already loaded) as an active record. */
1065
+ wrap(row: InferModel<C>): ActiveRecord<C>;
1066
+ /** Build an unsaved active record from insert data; call `.save()` to persist. */
1067
+ create(data: InferInsert<C>): ActiveRecord<C>;
1068
+ /** Fetch a row by primary key and wrap it, or `null` if absent. */
1069
+ get(id: unknown): Promise<ActiveRecord<C> | null>;
1070
+ }
1071
+ /**
1072
+ * Create an {@link ActiveRecordManager} for a model over a session.
1073
+ *
1074
+ * @param model The model class.
1075
+ * @param session The async session to persist through.
1076
+ * @returns A manager that wraps/fetches rows as active records.
1077
+ *
1078
+ * @example
1079
+ * ```ts
1080
+ * const users = activeRecord(User, engine.session());
1081
+ * const u = users.create({ name: "Ana", age: 30 });
1082
+ * await u.save();
1083
+ * await u.update({ age: 31 });
1084
+ * await u.delete();
1085
+ * ```
1086
+ */
1087
+ declare function activeRecord<C extends ModelClass>(model: C, session: AsyncSession): ActiveRecordManager<C>;
1088
+
788
1089
  /**
789
1090
  * tempest-db-js — typed relations (hasMany / belongsTo) with eager loading.
790
1091
  *
@@ -1031,8 +1332,12 @@ declare abstract class Model {
1031
1332
  * Instantiates the class once and collects every field that is a `Column`. Used
1032
1333
  * by the serialization layer and (Phase 6) the migration schema reflector.
1033
1334
  *
1335
+ * The result is **memoized per class** — a model's columns never change at
1336
+ * runtime, and this is called once per row on hot read paths (coercion, joins),
1337
+ * so re-instantiating the class every time would dominate large result sets.
1338
+ *
1034
1339
  * @param model The model class (subclass of `Model`).
1035
- * @returns A record of column name → `Column` instance.
1340
+ * @returns A record of column name → `Column` instance (do not mutate).
1036
1341
  */
1037
1342
  declare function columnsOf(model: ModelClass): Record<string, Column<unknown>>;
1038
1343
  /** Pull the static type out of a Column. */
@@ -1081,4 +1386,4 @@ type InferInsert<C extends ModelClass> = Simplify<{
1081
1386
  [K in Exclude<ColumnKeys<InstanceType<C>>, OptionalInsertKeys<InstanceType<C>>>]: ColValue<InstanceType<C>[K]>;
1082
1387
  }>;
1083
1388
 
1084
- export { type AsyncDriver, AsyncEngine, AsyncResult, AsyncSession, BaseDialect, BaseRepository, type BelongsTo, type ColRef, type ColType, Column, type ColumnFlags, type ColumnType, type ColumnTypeKind, type ColumnTypeMeta, type CompiledQuery, type CondNode, type Condition, type DefaultValue, DeleteBuilder, type DeleteNode, type Dialect, type DriverResult, type EngineOptions, type Executable, type HasMany, type InferInsert, type InferModel, InsertBuilder, type InsertNode, InvalidDatabaseUrl, JoinBuilder, type JoinClause, type JoinNode, type JoinOn, type JoinSelection, type JoinWhereInput, Model, type ModelClass, NoResultError, NodeSqliteDriver, OPERATORS, type Operator, type OperatorsFor, type OrderTerm, type PaginationFilter, type PaginationResult, type ParsedDatabaseUrl, type PoolOptions, type PortableExpression, PostgresDialect, type QueryNode, RecordNotFound, type Relation, type RelationValue, type Returning, type RowOf, SelectBuilder, type SelectNode, type SortDirection, type Sources, SqliteDialect, type SyncDriver, SyncEngine, SyncResult, SyncSession, UpdateBuilder, type UpdateNode, ValidationError, type WhereArg, type WhereInput, type WithRelations, and, belongsTo, column, columnsOf, createEngine, createSyncEngine, del, detectDialect, fromDict, getDialect, hasMany, insert, isCondition, join, loadRelations, not, or, parse, parseDatabaseUrl, select, sql, stringify, toCondNode, toDict, toJSON, update };
1389
+ export { ActiveRecord, type ActiveRecordManager, Agg, type AggregateTerm, type AsyncDriver, AsyncEngine, AsyncResult, AsyncSession, BaseDialect, BaseRepository, type BelongsTo, type ColRef, type ColType, Column, type ColumnFlags, type ColumnType, type ColumnTypeKind, type ColumnTypeMeta, type CompiledQuery, type CondNode, type Condition, type DefaultValue, DeleteBuilder, type DeleteNode, type Dialect, type DriverResult, type EngineOptions, type Executable, type HasMany, type InferInsert, type InferModel, InsertBuilder, type InsertNode, InvalidDatabaseUrl, JoinBuilder, type JoinClause, type JoinNode, type JoinOn, type JoinSelection, type JoinWhereInput, Model, type ModelClass, MysqlDialect, NoResultError, NodeSqliteDriver, OPERATORS, type OnConflict, type Operator, type OperatorsFor, type OrderTerm, type PaginationFilter, type PaginationResult, type ParsedDatabaseUrl, type PoolOptions, type PortableExpression, PostgresDialect, QueryExecutionError, type QueryLogger, type QueryNode, RecordNotFound, type Relation, type RelationValue, type ReservedAsyncDriver, type Returning, type RowOf, SelectBuilder, type SelectNode, type SortDirection, type Sources, SqliteDialect, type SyncDriver, SyncEngine, SyncResult, SyncSession, UpdateBuilder, type UpdateNode, ValidationError, type WhereArg, type WhereInput, type WithRelations, activeRecord, and, avg, belongsTo, column, columnsOf, count, createEngine, createSyncEngine, del, detectDialect, fromDict, getDialect, hasMany, insert, isCondition, join, loadRelations, max, min, not, or, parse, parseDatabaseUrl, select, sql, stringify, sum, toCondNode, toDict, toJSON, update };