sonamu 0.9.19 → 0.9.20

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sonamu",
3
- "version": "0.9.19",
3
+ "version": "0.9.20",
4
4
  "description": "Sonamu — TypeScript Fullstack API Framework",
5
5
  "keywords": [
6
6
  "framework",
@@ -130,8 +130,8 @@
130
130
  "vite": "8.0.5",
131
131
  "vitest": "^4.1.2",
132
132
  "@sonamu-kit/hmr-runner": "^0.2.0",
133
+ "@sonamu-kit/tasks": "^0.3.1",
133
134
  "@sonamu-kit/ts-loader": "^2.2.0",
134
- "@sonamu-kit/tasks": "^0.3.0",
135
135
  "@sonamu-kit/hmr-hook": "^0.5.1"
136
136
  },
137
137
  "devDependencies": {
@@ -436,6 +436,37 @@ SQL expressions per source column type:
436
436
  }
437
437
  ```
438
438
 
439
+ ### Partial index (`where`)
440
+
441
+ `where` declares a PostgreSQL partial index predicate. Provide a raw SQL condition **without** the `WHERE` keyword; it is appended to the generated `CREATE INDEX`. Works for every index type (`index`, `unique`, `hnsw`, `ivfflat`, pgroonga).
442
+
443
+ ```json
444
+ {
445
+ "name": "uniq_users_email_active",
446
+ "type": "unique",
447
+ "columns": [{ "name": "email" }],
448
+ "where": "deleted_at IS NULL"
449
+ }
450
+ ```
451
+
452
+ → `CREATE UNIQUE INDEX uniq_users_email_active ON users (email) WHERE deleted_at IS NULL;`
453
+ (Enforces email uniqueness only among non-deleted rows.)
454
+
455
+ ### `nullsNotDistinct` (unique only)
456
+
457
+ By default PostgreSQL treats `NULL`s as distinct, so a unique index allows multiple `NULL` rows. Set `nullsNotDistinct: true` to emit `NULLS NOT DISTINCT`, treating `NULL`s as equal (at most one `NULL` allowed).
458
+
459
+ ```json
460
+ {
461
+ "name": "uniq_accounts_external_id",
462
+ "type": "unique",
463
+ "columns": [{ "name": "external_id" }],
464
+ "nullsNotDistinct": true
465
+ }
466
+ ```
467
+
468
+ → `CREATE UNIQUE INDEX uniq_accounts_external_id ON accounts (external_id) NULLS NOT DISTINCT;`
469
+
439
470
  ### IMPORTANT: Use the actual DB column name in indexes
440
471
 
441
472
  **The way FK columns are referenced differs between indexes and subsets. Do not confuse them.**
@@ -180,6 +180,28 @@ db.table("orders")
180
180
  db.orderBy("created_at", "desc").limit(20).offset(40); // Page 3
181
181
  ```
182
182
 
183
+ ### NULLS position & array form
184
+
185
+ `orderBy` has two overloads:
186
+
187
+ 1. Single column: `orderBy(column, direction?, nulls?)` — `nulls` is `"first" | "last"`
188
+ 2. Array: `orderBy(entries[])` — each entry is a column string, a `Puri.raw*` SQL expression, or an object `{ column, order?, nulls? }`
189
+
190
+ ```typescript
191
+ // Single column with NULLS position
192
+ db.orderBy("published_at", "desc", "last");
193
+
194
+ // Array form: multiple sort keys, per-key direction and NULLS
195
+ db.orderBy([
196
+ { column: "is_pinned", order: "desc" },
197
+ { column: "published_at", order: "desc", nulls: "last" },
198
+ "title", // bare string defaults to asc
199
+ ]);
200
+
201
+ // Sort by a SQL expression
202
+ db.orderBy([Puri.rawNumber("view_count * 2"), { column: "id", order: "desc" }]);
203
+ ```
204
+
183
205
  ## GROUP BY & HAVING
184
206
 
185
207
  ```typescript
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: sonamu-upsert
3
- description: Saving complex relational data with Sonamu UpsertBuilder. ubRegister, ubUpsert, insertOnly, updateBatch patterns, FK ordering, cleanOrphans. Use when saving related data with foreign key dependencies.
3
+ description: Saving complex relational data with Sonamu UpsertBuilder. ubRegister, ubUpsert, ubInsertOnly, ubUpdateBatch, bulk insert/upsert patterns, FK ordering, cleanOrphans. Use when saving related data with foreign key dependencies.
4
4
  ---
5
5
 
6
6
  # UpsertBuilder
@@ -178,15 +178,62 @@ await wdb.transaction(async (trx) => {
178
178
  });
179
179
  ```
180
180
 
181
- ## insertOnly (INSERT Only)
181
+ ## Bulk Insert / Bulk Upsert (Large Datasets)
182
+
183
+ **Principle (same as regular saves):** call `ubRegister` for every row **outside** the transaction, then call only `ubUpsert` / `ubInsertOnly` **inside** the transaction. Split large datasets with `chunkSize`. Calling `ubRegister` inside the transaction is reserved for the special case where a row depends on the real id produced by a preceding `ubUpsert`.
184
+
185
+ ### (a) Bulk insert inside a Model
186
+
187
+ ```typescript
188
+ async createMany(params: PostCreateParams[]): Promise<number[]> {
189
+ const wdb = this.getPuri("w");
190
+ params.forEach((p) => wdb.ubRegister("posts", p));
191
+
192
+ return wdb.transaction(async (trx) => {
193
+ return trx.ubInsertOnly("posts", { chunkSize: 5000 });
194
+ });
195
+ }
196
+ ```
197
+
198
+ ### (b) Standalone Puri (seed / batch scripts outside a Model)
199
+
200
+ Outside a Model there is no `this.getPuri("w")`, so build a `PuriWrapper` directly. Use `DB.getDB("w")` only in this script context — inside Models always use `getPuri("w")`.
201
+
202
+ ```typescript
203
+ import { DB, PuriWrapper, UpsertBuilder } from "sonamu";
204
+
205
+ const puri = new PuriWrapper(DB.getDB("w"), new UpsertBuilder());
206
+ for (const row of rows) puri.ubRegister("audit_logs", row);
207
+
208
+ await puri.transaction(async (trx) => {
209
+ await trx.ubInsertOnly("audit_logs", { chunkSize: 5000 }); // or ubUpsert
210
+ });
211
+ ```
212
+
213
+ ### (c) Multiple tables in one transaction (FK order)
214
+
215
+ Register every table outside the transaction, then upsert parents before children.
216
+
217
+ ```typescript
218
+ const puri = new PuriWrapper(DB.getDB("w"), new UpsertBuilder());
219
+ for (const row of postRows) puri.ubRegister("posts", row);
220
+ for (const row of tagRows) puri.ubRegister("post_tags", row);
221
+
222
+ await puri.transaction(async (trx) => {
223
+ await trx.ubInsertOnly("posts", { chunkSize: 5000 }); // parent first
224
+ await trx.ubInsertOnly("post_tags", { chunkSize: 5000 }); // child next
225
+ });
226
+ ```
227
+
228
+ ## ubInsertOnly (INSERT Only)
182
229
 
183
230
  Perform INSERT without UPDATE:
184
231
 
185
232
  ```typescript
186
- await trx.insertOnly("logs", { chunkSize: 1000 });
233
+ await trx.ubInsertOnly("logs", { chunkSize: 1000 });
187
234
  ```
188
235
 
189
- ## updateBatch (Batch Update)
236
+ ## ubUpdateBatch (Batch Update)
190
237
 
191
238
  Bulk UPDATE operations:
192
239
 
@@ -197,14 +244,14 @@ wdb.ubRegister("users", { id: 2, status: "active" });
197
244
  wdb.ubRegister("users", { id: 3, status: "inactive" });
198
245
 
199
246
  await wdb.transaction(async (trx) => {
200
- await trx.updateBatch("users", {
247
+ await trx.ubUpdateBatch("users", {
201
248
  chunkSize: 500, // batch size (default: 500)
202
249
  where: "id", // WHERE condition column (default: "id")
203
250
  });
204
251
  });
205
252
 
206
253
  // Composite key for WHERE condition
207
- await trx.updateBatch("user_settings", {
254
+ await trx.ubUpdateBatch("user_settings", {
208
255
  where: ["user_id", "setting_key"],
209
256
  });
210
257
  ```