@bunyad/orm 0.2.0-beta.0 → 0.2.0-beta.1

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/README.md CHANGED
@@ -51,6 +51,28 @@ await User.query().count();
51
51
 
52
52
  Relations load lazily (`await user.posts().get()`) or eagerly (`User.with("posts")`).
53
53
 
54
+ ## Large tables
55
+
56
+ ```ts
57
+ for await (const user of User.where("active", true).cursor()) {
58
+ // hydrated one model at a time from a single streamed query
59
+ }
60
+
61
+ for await (const user of User.cursor()) {} // whole table
62
+ ```
63
+
64
+ `User.cursor()`, `User.lazy()`, `User.lazyById()` and `User.lazyByIdDesc()` are also available as statics. They return a `LazyCollection`, so you can chain without loading the table:
65
+
66
+ ```ts
67
+ const emails = await User.cursor()
68
+ .filter((user) => user.active)
69
+ .map((user) => user.email)
70
+ .take(100)
71
+ .toArray();
72
+ ```
73
+
74
+ `cursor()` keeps memory flat and fires `retrieved` events. `with()` relations are loaded per batch of `chunkSize` models (default 1000), not per row. Use `lazyById()` when the loop updates the rows it reads. See `@bunyad/database` for per-driver behavior.
75
+
54
76
  ## Drivers
55
77
 
56
78
  Pick one and install it next to `@bunyad/database`: `better-sqlite3`, `pg` or `mysql2`. Bun uses its built-in drivers.
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import type { DateInput, QueryBuilder } from "@bunyad/database";
5
5
  import { CursorPaginator, LengthAwarePaginator, Paginator } from "@bunyad/database";
6
- import { Collection } from "@bunyad/common";
6
+ import { Collection, LazyCollection } from "@bunyad/common";
7
7
  import { OrmCollection } from "./orm-collection.js";
8
8
  import { type CastDefinition } from "./casts.js";
9
9
  import { Model, type ModelClass, type ModelQueryOptions } from "./model.js";
@@ -259,14 +259,20 @@ export declare class ModelQuery<T extends Model = Model, TResult extends "model"
259
259
  }): Promise<Paginator<T>>;
260
260
  /** `chunk($count, $callback)`. */
261
261
  chunk(count: number, callback: (models: OrmCollection<T>) => void | Promise<void>): Promise<void>;
262
+ /**
263
+ * `cursor($chunkSize)` — hydrate models one at a time from a single streamed
264
+ * query, so memory stays flat on large tables. `with()` relations are loaded
265
+ * per batch of `chunkSize` models rather than per row.
266
+ */
267
+ cursor(chunkSize?: number): LazyCollection<T>;
262
268
  /** `lazy($chunkSize)`. */
263
- lazy(chunkSize?: number): AsyncGenerator<T, void, unknown>;
269
+ lazy(chunkSize?: number): LazyCollection<T>;
264
270
  /** `chunkById`. */
265
271
  chunkById(count: number, callback: (models: OrmCollection<T>) => void | boolean | Promise<void | boolean>, column?: string): Promise<boolean>;
266
272
  chunkByIdDesc(count: number, callback: (models: OrmCollection<T>) => void | boolean | Promise<void | boolean>, column?: string): Promise<boolean>;
267
273
  /** `lazyById`. */
268
- lazyById(chunkSize?: number, column?: string): AsyncGenerator<T, void, unknown>;
269
- lazyByIdDesc(chunkSize?: number, column?: string): AsyncGenerator<T, void, unknown>;
274
+ lazyById(chunkSize?: number, column?: string): LazyCollection<T>;
275
+ lazyByIdDesc(chunkSize?: number, column?: string): LazyCollection<T>;
270
276
  increment(column: string, amount?: number): Promise<void>;
271
277
  decrement(column: string, amount?: number): Promise<void>;
272
278
  /** `cursorPaginate`. */
@@ -1,5 +1,5 @@
1
1
  import { DatabaseManager as Db, CursorPaginator, LengthAwarePaginator, Paginator, dateTimeForStorage, hasQueryListeners, resolvePaginatorPage, wrapSqlName, } from "@bunyad/database";
2
- import { Collection } from "@bunyad/common";
2
+ import { Collection, LazyCollection } from "@bunyad/common";
3
3
  import { OrmCollection } from "./orm-collection.js";
4
4
  import { bootIfNotBooted, fireModelEvent, hasModelEventListeners, } from "./model-events.js";
5
5
  import { getGlobalScopes, hasGlobalScopes, } from "./scopes.js";
@@ -2922,8 +2922,45 @@ export class ModelQuery {
2922
2922
  page += 1;
2923
2923
  }
2924
2924
  }
2925
+ /**
2926
+ * `cursor($chunkSize)` — hydrate models one at a time from a single streamed
2927
+ * query, so memory stays flat on large tables. `with()` relations are loaded
2928
+ * per batch of `chunkSize` models rather than per row.
2929
+ */
2930
+ cursor(chunkSize = 1000) {
2931
+ return new LazyCollection(() => this.#cursorModels(chunkSize));
2932
+ }
2933
+ async *#cursorModels(chunkSize) {
2934
+ const size = Math.max(1, Math.floor(chunkSize));
2935
+ const fireRetrieved = hasModelEventListeners(this.model, "retrieved");
2936
+ const eager = this.#eagerLoad.length > 0;
2937
+ let batch = [];
2938
+ for await (const row of this.#buildQuery().cursor(size)) {
2939
+ const model = this.#makeModel(row);
2940
+ if (fireRetrieved) {
2941
+ await fireModelEvent(model, "retrieved");
2942
+ }
2943
+ if (!eager) {
2944
+ yield model;
2945
+ continue;
2946
+ }
2947
+ batch.push(model);
2948
+ if (batch.length >= size) {
2949
+ await this.#loadEager(batch);
2950
+ yield* batch;
2951
+ batch = [];
2952
+ }
2953
+ }
2954
+ if (batch.length > 0) {
2955
+ await this.#loadEager(batch);
2956
+ yield* batch;
2957
+ }
2958
+ }
2925
2959
  /** `lazy($chunkSize)`. */
2926
- async *lazy(chunkSize = 1000) {
2960
+ lazy(chunkSize = 1000) {
2961
+ return new LazyCollection(() => this.#lazyPages(chunkSize));
2962
+ }
2963
+ async *#lazyPages(chunkSize) {
2927
2964
  let page = 1;
2928
2965
  const fireRetrieved = hasModelEventListeners(this.model, "retrieved");
2929
2966
  for (;;) {
@@ -2976,11 +3013,11 @@ export class ModelQuery {
2976
3013
  }
2977
3014
  }
2978
3015
  /** `lazyById`. */
2979
- async *lazyById(chunkSize = 1000, column) {
2980
- yield* this.#lazyById(chunkSize, column ?? this.model.primaryKey, "asc");
3016
+ lazyById(chunkSize = 1000, column) {
3017
+ return new LazyCollection(() => this.#lazyById(chunkSize, column ?? this.model.primaryKey, "asc"));
2981
3018
  }
2982
- async *lazyByIdDesc(chunkSize = 1000, column) {
2983
- yield* this.#lazyById(chunkSize, column ?? this.model.primaryKey, "desc");
3019
+ lazyByIdDesc(chunkSize = 1000, column) {
3020
+ return new LazyCollection(() => this.#lazyById(chunkSize, column ?? this.model.primaryKey, "desc"));
2984
3021
  }
2985
3022
  async *#lazyById(chunkSize, column, direction) {
2986
3023
  const size = Math.max(1, Math.floor(chunkSize));
package/dist/model.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { Connection, DatabaseManager, DateInput } from "@bunyad/database";
2
2
  import { LengthAwarePaginator, CursorPaginator } from "@bunyad/database";
3
- import { Collection } from "@bunyad/common";
3
+ import { Collection, LazyCollection } from "@bunyad/common";
4
4
  import { OrmCollection } from "./orm-collection.js";
5
5
  import { type CastDefinition } from "./casts.js";
6
6
  import { type ModelEventListener, type ModelEventName, type ModelObserver } from "./model-events.js";
@@ -353,6 +353,14 @@ export declare class Model {
353
353
  path?: string;
354
354
  cursorName?: string;
355
355
  }): Promise<CursorPaginator<InstanceType<T>>>;
356
+ /** `Model::cursor()`: stream models from a single query. */
357
+ static cursor<T extends typeof Model>(this: T, chunkSize?: number): LazyCollection<InstanceType<T>>;
358
+ /** `Model::lazy()`: chunked iteration, one query per chunk. */
359
+ static lazy<T extends typeof Model>(this: T, chunkSize?: number): LazyCollection<InstanceType<T>>;
360
+ /** `Model::lazyById()`: chunked iteration by primary key (safe while updating). */
361
+ static lazyById<T extends typeof Model>(this: T, chunkSize?: number, column?: string): LazyCollection<InstanceType<T>>;
362
+ /** `Model::lazyByIdDesc()`. */
363
+ static lazyByIdDesc<T extends typeof Model>(this: T, chunkSize?: number, column?: string): LazyCollection<InstanceType<T>>;
356
364
  /** `Model::create` (respects `$fillable` / `$guarded`). */
357
365
  static create<T extends typeof Model>(this: T, attributes: Record<string, unknown>): InstanceType<T> | Promise<InstanceType<T>>;
358
366
  /** `Model::forceCreate` — insert every given attribute, ignoring `$fillable`. */
package/dist/model.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { DatabaseManager as Db, setDefaultConnection, connections, LengthAwarePaginator, CursorPaginator, Paginator, dateTimeForStorage, withoutQueryWriteHook, wrapSqlName, } from "@bunyad/database";
2
- import { Collection } from "@bunyad/common";
2
+ import { Collection, LazyCollection } from "@bunyad/common";
3
3
  import { OrmCollection } from "./orm-collection.js";
4
4
  import { Attribute, castFromStorage, castToStorage, hashCastValue, isAlreadyHashed, enumFromStorage, enumToStorage, isAttribute, isCastType, isEnumCast, } from "./casts.js";
5
5
  import { bootIfNotBooted, fireModelEvent, hasAnyModelEventListeners, hasModelEventListeners, observeModel, registerModelEvent, withoutModelEvents, } from "./model-events.js";
@@ -1138,6 +1138,22 @@ export class Model {
1138
1138
  static async cursorPaginate(perPage = 15, cursor = null, options = {}) {
1139
1139
  return this.newQuery().cursorPaginate(perPage, cursor, options);
1140
1140
  }
1141
+ /** `Model::cursor()`: stream models from a single query. */
1142
+ static cursor(chunkSize) {
1143
+ return this.newQuery().cursor(chunkSize);
1144
+ }
1145
+ /** `Model::lazy()`: chunked iteration, one query per chunk. */
1146
+ static lazy(chunkSize) {
1147
+ return this.newQuery().lazy(chunkSize);
1148
+ }
1149
+ /** `Model::lazyById()`: chunked iteration by primary key (safe while updating). */
1150
+ static lazyById(chunkSize, column) {
1151
+ return this.newQuery().lazyById(chunkSize, column);
1152
+ }
1153
+ /** `Model::lazyByIdDesc()`. */
1154
+ static lazyByIdDesc(chunkSize, column) {
1155
+ return this.newQuery().lazyByIdDesc(chunkSize, column);
1156
+ }
1141
1157
  /** `Model::create` (respects `$fillable` / `$guarded`). */
1142
1158
  static create(attributes) {
1143
1159
  this.bootIfNotBooted();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bunyad/orm",
3
- "version": "0.2.0-beta.0",
3
+ "version": "0.2.0-beta.1",
4
4
  "description": "Active-record ORM for Bunyad: models, relations, casts, scopes, events, factories.",
5
5
  "license": "MIT",
6
6
  "author": "Shah Khalid",
@@ -36,8 +36,8 @@
36
36
  }
37
37
  },
38
38
  "dependencies": {
39
- "@bunyad/common": "^0.2.0-beta.0",
40
- "@bunyad/database": "^0.2.0-beta.0"
39
+ "@bunyad/common": "^0.2.0-beta.1",
40
+ "@bunyad/database": "^0.2.0-beta.1"
41
41
  },
42
42
  "publishConfig": {
43
43
  "access": "public"