Skip to content

Commit 2eecb89

Browse files
docs: sync from coderbuzz/codex@4eb7d4a
1 parent f48af2e commit 2eecb89

2 files changed

Lines changed: 369 additions & 27 deletions

File tree

‎AI_KNOWLEDGE.md‎

Lines changed: 111 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<!-- docs: sync from coderbuzz/codex@b1e2bde -->
1+
<!-- docs: sync from coderbuzz/codex@4eb7d4a -->
22

33
# @coderbuzz/sql — AI Expert Knowledge Reference
44

@@ -206,7 +206,7 @@ const users = pg.table("users", {
206206
email: pg.text().notNull().unique().index(),
207207
name: pg.varchar(120).notNull(),
208208
role: pg.varchar(20).default("user"),
209-
score: pg.decimal(5, 2).default(0),
209+
score: pg.decimal(5, 2).default(0), // → string (exact); use float() for approximate
210210
bio: pg.text().nullable(),
211211
metadata: pg.jsonb<Record<string, unknown>>().nullable(),
212212
created_at: pg.timestamptz().defaultNow(),
@@ -559,12 +559,14 @@ const batcher = db.batchInsert("events", {
559559
wait: 50, // ms of inactivity before flush (REQUIRED)
560560
max: 5_000, // flush when pending reaches this count
561561
timeout: 2_000, // force flush after this many ms from first write
562+
maxInflight: 4, // concurrent flushes allowed before write() waits
562563
settings: { async_insert: "1" }, // engine-specific (ClickHouse)
564+
onError: (err, rows) => { /* REQUIRED — retry or dead-letter these rows */ },
563565
});
564566

565-
// Write rows
566-
batcher.write({ id: 1, val: "a" });
567-
batcher.write([{ id: 2, val: "b" }, { id: 3, val: "c" }]);
567+
// Write rows — write() returns a promise; awaiting it applies backpressure
568+
await batcher.write({ id: 1, val: "a" });
569+
await batcher.write([{ id: 2, val: "b" }, { id: 3, val: "c" }]);
568570

569571
// Inspect state
570572
batcher.pendingCount; // rows waiting to flush
@@ -578,8 +580,15 @@ await batcher.close(); // flush + drain + seal (throws if write() called after)
578580

579581
**Critical rules:**
580582

583+
- `onError` is REQUIRED. The constructor throws without it. Rows leave the
584+
pending queue before the insert runs, so a failed auto-flush has no other way
585+
to be observed.
581586
- Always `await batcher.close()` at the end — never fire-and-forget.
582-
- After `close()`, calling `write()` throws.
587+
- After `close()`, calling `write()` rejects.
588+
- Every row in a batch must have the SAME keys — a batched INSERT has one
589+
column list. A differing row rejects. Pass `heterogeneousRows: "union"` to
590+
insert the union of all columns with NULL for absent ones.
591+
- `await` each `write()` in a bulk import; that is what keeps memory flat.
583592
- Auto-flushes are fire-and-forget internally but tracked — `drain()` waits for
584593
them.
585594

@@ -655,16 +664,64 @@ Placeholder styles per dialect:
655664

656665
## 14. Transactions
657666

667+
`transaction()` holds ONE connection for the whole callback. Use `tx` for every
668+
statement inside — `db` is a pool and would run the statement on a different
669+
connection, outside the transaction.
670+
658671
```ts
659672
await db.transaction(async (tx) => {
660-
// Use `tx` exactly like `db` inside the transaction
661673
await users.insert(tx).values([...]).execute();
662674
await posts.insert(tx).values([...]).execute();
663-
// Throws? → auto ROLLBACK
675+
// Throws? → ROLLBACK
676+
});
677+
// Success → COMMIT
678+
```
679+
680+
### Options
681+
682+
```ts
683+
await db.transaction(fn, {
684+
isolation: "SERIALIZABLE", // also READ COMMITTED / REPEATABLE READ / READ UNCOMMITTED
685+
readOnly: true, // PostgreSQL / MySQL only
686+
setup: [ // runs inside the transaction, on its connection
687+
{ sql: `SELECT set_config('app.tenant_id', $1, true)`, params: [tenantId] },
688+
],
664689
});
665-
// Success → auto COMMIT
666690
```
667691

692+
`setup` is where `SET LOCAL` belongs — it is the mechanism PostgreSQL
693+
row-level security depends on, and it is correct only inside a
694+
single-connection transaction.
695+
696+
### Savepoints
697+
698+
```ts
699+
await db.transaction(async (tx) => {
700+
await postHeader(tx);
701+
try {
702+
await tx.savepoint(async (sp) => reserveStock(sp)); // rolls back alone
703+
} catch { /* header survives */ }
704+
});
705+
```
706+
707+
`tx.transaction(...)` inside a transaction becomes a savepoint, not a second
708+
`BEGIN`. `db.savepoint(...)` outside a transaction throws.
709+
710+
### Row locking
711+
712+
```ts
713+
tx.select("last_no").from("nomor_faktur").where({ seri: "A" }).forUpdate()
714+
// .forShare(), .forUpdate({ noWait: true }), .forUpdate({ skipLocked: true })
715+
```
716+
717+
PostgreSQL / MySQL / Oracle only. SQLite, MSSQL and ClickHouse throw.
718+
719+
### Errors
720+
721+
If the callback fails AND the `ROLLBACK` also fails, a
722+
`TransactionRollbackError` is thrown carrying both `cause` and `rollbackError`.
723+
The transaction's outcome is undetermined — reconcile, do not just retry.
724+
668725
---
669726

670727
## 15. Middleware
@@ -727,6 +784,12 @@ max<number>("score", "topScore"); // MAX(score) AS topScore
727784

728785
## 18. Column Types by Dialect
729786

787+
> **TypeScript type of exact numerics.** `decimal(p,s)`, `numeric(p,s)`,
788+
> `bigint()`, `bigserial()` and MSSQL `money()` infer as **`string`**, not
789+
> `number` — float64 cannot represent them exactly, and `pg`/`mysql2` return
790+
> them as strings anyway. `integer`, `smallint`, `int`, `serial`, `float`,
791+
> `real` and `doublePrecision` remain `number`.
792+
730793
### SQLite
731794

732795
```ts
@@ -931,9 +994,46 @@ ClickHouse — throws `"RETURNING is not supported by this dialect"`.
931994
**DO NOT** use `.full_join()` with SQLite, MySQL, or ClickHouse — throws at
932995
compile time.
933996
934-
**DO NOT** call `batcher.write()` after `batcher.close()` — throws.
997+
**DO NOT** call `batcher.write()` after `batcher.close()` — rejects.
998+
999+
**DO NOT** forget `await batcher.close()` — rows may be left unwritten.
1000+
1001+
**DO NOT** use the pooled engine inside a transaction callback. Use `tx`:
1002+
1003+
```ts
1004+
// WRONG — this INSERT runs on a different connection, outside the transaction
1005+
await db.transaction(async (tx) => { await db.execute(insertSql); });
1006+
1007+
// CORRECT
1008+
await db.transaction(async (tx) => { await tx.execute(insertSql); });
1009+
```
1010+
1011+
**DO NOT** pass user input to `.order_by()`, `.group_by()`, `.select()`,
1012+
`.from()`, or a join `ON` condition. These cannot be bound parameters, so they
1013+
are interpolated. They are validated and will throw `UnsafeIdentifierError` on
1014+
anything dangerous, but that is a guard, not a licence — map a sort parameter
1015+
through a fixed allow-list of column names:
1016+
1017+
```ts
1018+
// WRONG
1019+
query.order_by(req.query.sort);
1020+
1021+
// CORRECT
1022+
const SORTS = { date: "created_at DESC", amount: "total DESC" } as const;
1023+
query.order_by(SORTS[req.query.sort as keyof typeof SORTS] ?? SORTS.date);
1024+
```
9351025
936-
**DO NOT** forget `await batcher.close()` — rows may be silently lost.
1026+
**DO NOT** treat `decimal`/`numeric`/`bigint`/`bigserial` values as numbers.
1027+
They are typed `string` because float64 cannot hold them exactly. `Number(x)`
1028+
on a money column loses cents:
1029+
1030+
```ts
1031+
// WRONG — reintroduces the precision loss the string type exists to prevent
1032+
const total = rows.reduce((a, r) => a + Number(r.debit), 0);
1033+
1034+
// CORRECT — sum in SQL, or use a decimal library
1035+
const [{ total }] = await db.sql`SELECT SUM(debit)::text AS total FROM jurnal`.execute();
1036+
```
9371037
9381038
**DO NOT** use `DELETE` or `UPDATE` without `.where()` unless you intend to
9391039
affect all rows. Add a middleware guard in production code.

0 commit comments

Comments
 (0)