Skip to content

Commit 0376ea6

Browse files
docs: sync from coderbuzz/codex@60ca8c4
1 parent c942962 commit 0376ea6

2 files changed

Lines changed: 346 additions & 10 deletions

File tree

‎AI_KNOWLEDGE.md‎

Lines changed: 271 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<!-- docs: sync from coderbuzz/codex@200be78 -->
1+
<!-- docs: sync from coderbuzz/codex@60ca8c4 -->
22

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

@@ -45,7 +45,8 @@ SqlTable<S>
4545

4646
```ts
4747
import { sqlite } from "@coderbuzz/sql/sqlite";
48-
import { pg } from "@coderbuzz/sql/postgres";
48+
import { pg } from "@coderbuzz/sql/postgres"; // driver: `pg` package
49+
import { pg as pgBun } from "@coderbuzz/sql/postgres-bun"; // driver: Bun built-in, none to install
4950
import { mysql } from "@coderbuzz/sql/mysql";
5051
import { mssql } from "@coderbuzz/sql/mssql";
5152
import { ch } from "@coderbuzz/sql/clickhouse";
@@ -100,9 +101,42 @@ import {
100101
sum,
101102
UpdateQuery,
102103
type WhereClause,
104+
// Exact decimal arithmetic (also at @coderbuzz/sql/decimal)
105+
add,
106+
subtract,
107+
multiply,
108+
divide,
109+
negate,
110+
absDecimal,
111+
sumDecimals,
112+
roundDecimal,
113+
normalizeDecimal,
114+
compareDecimals,
115+
equalsDecimal,
116+
lessThanDecimal,
117+
greaterThanDecimal,
118+
isZeroDecimal,
119+
isNegativeDecimal,
120+
maxDecimal,
121+
minDecimal,
122+
isDecimalString,
123+
toMinorUnits,
124+
fromMinorUnits,
125+
allocate,
126+
splitEvenly,
127+
DecimalError,
128+
type DecimalInput,
129+
type RoundingMode,
130+
type DecimalOpOptions,
103131
} from "@coderbuzz/sql";
104132
```
105133

134+
### 2.2b Decimal subpath (no engine, no dialect)
135+
136+
```ts
137+
import { add, multiply, roundDecimal, allocate } from "@coderbuzz/sql/decimal";
138+
```
139+
106140
### 2.3 Type-only subpaths (column factories without engine)
107141

108142
```ts
@@ -140,6 +174,27 @@ const db = pg.connect({
140174
max: 10,
141175
});
142176

177+
// PostgreSQL on Bun: same namespace shape, no driver package
178+
import { pg as pgBun } from "@coderbuzz/sql/postgres-bun";
179+
const db = pgBun.connect({ connectionString: process.env.DATABASE_URL, max: 10 });
180+
const db = pgBun.connect({
181+
host: "localhost",
182+
port: 5432,
183+
database: "app",
184+
user: "app",
185+
password: "secret",
186+
max: 10,
187+
// Bun-specific, all optional:
188+
bigint: false, // true → int8 arrives as a JS bigint instead of a string
189+
prepare: true, // false → required behind PgBouncer in transaction mode
190+
idleTimeout: 30, // seconds
191+
connectionTimeout: 30, // seconds
192+
maxLifetime: 0, // seconds; 0 = unlimited
193+
sslMode: "prefer", // 'disable' | 'prefer' | 'require' | 'verify-ca' | 'verify-full'
194+
streamBatchSize: 1000, // rows per round trip in stream()
195+
tenantSetting: "app.tenant_id",
196+
});
197+
143198
// MySQL
144199
const db = mysql.connect({
145200
host: "localhost",
@@ -1098,8 +1153,12 @@ on a money column loses cents:
10981153
// WRONG: reintroduces the precision loss the string type exists to prevent
10991154
const total = rows.reduce((a, r) => a + Number(r.debit), 0);
11001155

1101-
// CORRECT: sum in SQL, or use a decimal library
1156+
// CORRECT: sum in SQL...
11021157
const [{ total }] = await db.sql`SELECT SUM(debit)::text AS total FROM jurnal`.execute();
1158+
1159+
// ...or with the exact helpers this package ships (BigInt, no dependency)
1160+
import { sumDecimals } from "@coderbuzz/sql/decimal";
1161+
const total = sumDecimals(rows.map(r => r.debit));
11031162
```
11041163
11051164
**DO validate incoming amounts with `decimal()` from `@coderbuzz/veta`**, not
@@ -1125,7 +1184,212 @@ affect all rows. Add a middleware guard in production code.
11251184
11261185
---
11271186
1128-
## 25. Quick Code Patterns
1187+
## 25. Exact Decimal Arithmetic (`@coderbuzz/sql/decimal`)
1188+
1189+
Every function takes and returns **decimal strings**, the same representation
1190+
`NUMERIC`/`DECIMAL`/`BIGINT` columns produce and `decimal()` from
1191+
`@coderbuzz/veta` validates. Internally each value parses to a scaled `BigInt`
1192+
(`'12.34'` → `1234n` at scale 2), so no float64 is ever involved. Zero
1193+
dependencies: there is no `decimal.js` or `big.js` under this.
1194+
1195+
### Input types
1196+
1197+
```ts
1198+
type DecimalInput = string | bigint | number;
1199+
```
1200+
1201+
- `string`: the normal case. Must match `/^-?\d+(\.\d+)?$/`. No exponents, no
1202+
thousands separators, no currency symbols, at least one integer digit
1203+
(`'.5'` and `'1.'` are rejected).
1204+
- `bigint`: accepted at scale 0.
1205+
- `number`: accepted **only** when `Number.isSafeInteger(n)`. `12.34` throws;
1206+
it has already lost precision before the call.
1207+
1208+
Anything else throws `DecimalError`.
1209+
1210+
### Rounding modes
1211+
1212+
```ts
1213+
type RoundingMode =
1214+
| "half-up" // default; ties away from zero: 0.125 → 0.13, -0.125 → -0.13
1215+
| "half-even" // banker's; ties to even: 0.125 → 0.12, 0.135 → 0.14
1216+
| "half-down" // ties toward zero
1217+
| "up" // always away from zero
1218+
| "down" // always toward zero (truncate)
1219+
| "ceil" // toward +Infinity
1220+
| "floor"; // toward -Infinity
1221+
```
1222+
1223+
### Full signatures
1224+
1225+
| Function | Signature | Result scale |
1226+
| --- | --- | --- |
1227+
| `add` | `(a: DecimalInput, b: DecimalInput, options?: DecimalOpOptions) => string` | `max(scaleA, scaleB)` |
1228+
| `subtract` | `(a, b, options?) => string` | `max(scaleA, scaleB)` |
1229+
| `multiply` | `(a, b, options?) => string` | `scaleA + scaleB` (exact product) |
1230+
| `divide` | `(a, b, options: DecimalOpOptions & { scale: number }) => string` | `options.scale` (required) |
1231+
| `negate` | `(value: DecimalInput) => string` | unchanged |
1232+
| `absDecimal` | `(value: DecimalInput) => string` | unchanged |
1233+
| `sumDecimals` | `(values: readonly DecimalInput[], options?) => string` | largest input scale; `'0'` for `[]` |
1234+
| `roundDecimal` | `(value, scale: number, rounding?: RoundingMode) => string` | `scale` |
1235+
| `normalizeDecimal` | `(value, scale?: number, rounding?: RoundingMode) => string` | `scale`, or unchanged |
1236+
| `compareDecimals` | `(a, b) => -1 \| 0 \| 1` | n/a |
1237+
| `equalsDecimal` | `(a, b) => boolean` | n/a |
1238+
| `lessThanDecimal` | `(a, b) => boolean` | n/a |
1239+
| `greaterThanDecimal` | `(a, b) => boolean` | n/a |
1240+
| `isZeroDecimal` | `(value) => boolean` | n/a |
1241+
| `isNegativeDecimal` | `(value) => boolean` | `'-0.00'` is **not** negative |
1242+
| `maxDecimal` / `minDecimal` | `(a, b) => string` | n/a |
1243+
| `isDecimalString` | `(value: unknown) => value is string` | n/a |
1244+
| `toMinorUnits` | `(value, scale: number, rounding?: RoundingMode) => bigint` | n/a |
1245+
| `fromMinorUnits` | `(units: bigint \| number, scale: number) => string` | `scale` |
1246+
| `allocate` | `(total, weights: readonly DecimalInput[], options: { scale: number }) => string[]` | `scale` |
1247+
| `splitEvenly` | `(total, parts: number, options: { scale: number }) => string[]` | `scale` |
1248+
1249+
```ts
1250+
type DecimalOpOptions = {
1251+
scale?: number; // 0..100; omitted = keep the exact scale
1252+
rounding?: RoundingMode; // default 'half-up'
1253+
};
1254+
```
1255+
1256+
### Behavioural notes
1257+
1258+
- **Nothing rounds implicitly.** `add`, `subtract` and `multiply` return the
1259+
exact result and let the scale grow; pass `{ scale }` to round. `divide`
1260+
requires `scale` because no default is honest.
1261+
- **Normalisation is canonical.** Leading zeros are dropped, `-0` becomes `0`,
1262+
and `{ scale }` pads with zeros. Two equal amounts are therefore equal
1263+
strings, usable as map keys and with `===`.
1264+
- **Comparison is by value, not by string order.** `compareDecimals('2.00',
1265+
'10.00')` is `-1`; `'2.00' < '10.00'` as strings is `false`.
1266+
- **`sumDecimals` is order-independent**, which is what makes a
1267+
`debit === credit` check meaningful.
1268+
- **`toMinorUnits` refuses to lose a digit** unless a rounding mode is passed:
1269+
`toMinorUnits('1234.565', 2)` throws, `toMinorUnits('1234.565', 2, 'half-up')`
1270+
is `123457n`. Trailing zeros do not count as a lost digit.
1271+
- **`allocate` always sums to the total.** It floors each share, then hands out
1272+
the leftover minor units to the largest discarded fractions, ties to the
1273+
earlier index (so it is deterministic). Negative totals allocate their
1274+
magnitude and carry the sign. Weights must be non-negative and must not all
1275+
be zero; an empty weight list throws.
1276+
- **`splitEvenly(total, n, { scale })`** is `allocate` with equal weights: the
1277+
earliest parts absorb the odd minor units, the instalment convention.
1278+
- **Errors** are always `DecimalError`, never a silent `NaN`. Division by zero
1279+
throws rather than returning `Infinity`.
1280+
1281+
### Worked ERP line
1282+
1283+
```ts
1284+
import { multiply, subtract, add, allocate, sumDecimals } from "@coderbuzz/sql/decimal";
1285+
1286+
const gross = multiply(row.price, row.qty); // '139.93' exactly
1287+
const discount = multiply(gross, "0.15", { scale: 2 }); // 20.9895 → '20.99'
1288+
const net = subtract(gross, discount); // '118.94'
1289+
const tax = multiply(net, "0.11", { scale: 2 }); // 13.0834 → '13.08'
1290+
const total = add(net, tax); // '132.02'
1291+
1292+
const perCentre = allocate(tax, ["1", "1", "1"], { scale: 2 });
1293+
sumDecimals(perCentre) === tax; // true, always
1294+
```
1295+
1296+
---
1297+
1298+
## 26. PostgreSQL on Bun (`@coderbuzz/sql/postgres-bun`)
1299+
1300+
`BunPostgresEngine` drives PostgreSQL through Bun's built-in SQL client
1301+
(`Bun.SQL`, Bun 1.2+; the suite is run on Bun 1.3 and 1.4). No peer dependency:
1302+
the protocol is in the runtime.
1303+
Everything `PostgresEngine` does, this does, with the same semantics. Only the
1304+
import path differs.
1305+
1306+
```ts
1307+
import { pg } from "@coderbuzz/sql/postgres-bun";
1308+
const db = pg.connect({ connectionString: process.env.DATABASE_URL, max: 10 });
1309+
```
1310+
1311+
On a runtime without `Bun.SQL` the constructor throws with a message pointing at
1312+
`@coderbuzz/sql/postgres`; it does not fail at import time.
1313+
1314+
### Parity with the `pg` engine
1315+
1316+
| Capability | `@coderbuzz/sql/postgres` | `@coderbuzz/sql/postgres-bun` |
1317+
| --- | --- | --- |
1318+
| `transaction(fn, { isolation, readOnly, setup })` | one pooled client | one reserved connection |
1319+
| `tx.savepoint()` | yes | yes |
1320+
| `forTenant(tenantId)` + RLS binding lock | yes | yes |
1321+
| `unsafeCrossTenant(reason, fn)` | yes (separate BYPASSRLS pool) | yes (separate BYPASSRLS client) |
1322+
| `stream()` | `DECLARE CURSOR` + `FETCH FORWARD` | same |
1323+
| `prepare()` | named statement + `DEALLOCATE` | statement cached on a held connection |
1324+
| `withAdvisoryLock(key, fn, wait?)` | yes | yes |
1325+
| Middleware sees `BEGIN`/`COMMIT`/`ROLLBACK` | yes | yes |
1326+
| Connection destroyed after a failed `ROLLBACK` | `release(true)` | `connection.close()` |
1327+
| Driver | `pg` peer dependency | none |
1328+
1329+
### Config (`BunPostgresConfig`)
1330+
1331+
| Option | Default | Meaning |
1332+
| --- | --- | --- |
1333+
| `connectionString` | none | `postgres://user:pass@host:port/db`; an explicit `user`/`password` overrides the credentials inside it (this is what lets `unsafeCrossTenant` swap in the BYPASSRLS role) |
1334+
| `host` / `port` / `database` / `user` / `password` | `localhost` / `5432` / none / none / none | connection parts |
1335+
| `max` | `10` | pool size |
1336+
| `idleTimeout` | driver default | seconds a pooled connection may idle |
1337+
| `connectionTimeout` | driver default | seconds to wait for a connection |
1338+
| `maxLifetime` | driver default | seconds before a connection is recycled |
1339+
| `bigint` | `false` | `true` → `int8` arrives as a JS `bigint` |
1340+
| `prepare` | `true` | `false` for PgBouncer in transaction mode |
1341+
| `tls` / `sslMode` | none | passed through to `Bun.SQL` |
1342+
| `streamBatchSize` | `1000` | rows per `FETCH FORWARD` in `stream()` |
1343+
| `tenantSetting` | `'app.tenant_id'` | run-time parameter RLS policies read |
1344+
| `crossTenant` | none | `{ user, password, max? }` for a `BYPASSRLS` role |
1345+
| `onCrossTenantAccess` | none | called with the reason on every `unsafeCrossTenant()` |
1346+
1347+
### Type mapping (verified against PostgreSQL 16 via Bun)
1348+
1349+
| PostgreSQL type | JS value from the driver | Column factory | Declared TS type |
1350+
| --- | --- | --- | --- |
1351+
| `numeric` / `decimal` | `string` (exact, any precision) | `pg.numeric(p,s)` / `pg.decimal(p,s)` | `string` |
1352+
| `bigint` / `int8` | `string`, or `bigint` with `{ bigint: true }` | `pg.bigint()` | `string` |
1353+
| `bigserial` | `string` | `pg.bigserial()` | `string` |
1354+
| `count(*)`, any `int8` aggregate | `string` | n/a | n/a |
1355+
| `integer` / `smallint` / `serial` | `number` | `pg.integer()` … | `number` |
1356+
| `double precision` / `real` | `number` | `pg.doublePrecision()` | `number` |
1357+
| `boolean` | `boolean` | `pg.boolean()` | `boolean` |
1358+
| `timestamptz` / `timestamp` / `date` | `Date` | `pg.timestamptz()` … | `Date` |
1359+
| `json` / `jsonb` | parsed object | `pg.jsonb()` | `object` |
1360+
| `bytea` | `Buffer` | `pg.bytea()` | n/a |
1361+
| `uuid` / `text` / `varchar` | `string` | `pg.uuid()` … | `string` |
1362+
| `money` | `string`, **locale-formatted** (`'$1,234.56'`) | none offered | n/a |
1363+
1364+
Gotchas that follow from the table:
1365+
1366+
- `count(*)` is `int8`, so `rows[0].n` is `'41'`, not `41`. Cast in SQL
1367+
(`count(*)::int`) or read it as a decimal string.
1368+
- `money` is not a decimal string under any driver. Use `numeric(p, s)`.
1369+
- Binding a JS `number` to a `numeric` column silently goes through float64:
1370+
`${0.1 + 0.2}` lands as `0.30`. Bind the string.
1371+
- `{ bigint: true }` changes what the driver returns, not what a column
1372+
promises: the typed path still yields `string` for `bigint()` columns.
1373+
1374+
### Methods beyond the shared `Sql` surface
1375+
1376+
```ts
1377+
db.tenantSetting; // string
1378+
db.client; // the raw Bun.SQL object (LISTEN/NOTIFY, file(), beginDistributed)
1379+
db.forTenant(tenantId(id)); // TenantScopedSql
1380+
db.unsafeCrossTenant(reason, async admin => ...); // needs config.crossTenant
1381+
db.withAdvisoryLock(key, fn, wait?); // Promise<R | undefined>
1382+
db.stream(compiledQuery); // AsyncIterable<row>
1383+
db.prepare(compiledQuery); // { execute(params?), close() }
1384+
await db.close(); // closes both clients
1385+
```
1386+
1387+
`db.client` bypasses middleware and is not part of any transaction the engine
1388+
opened. Use it for Bun features this engine does not wrap, not for queries.
1389+
1390+
---
1391+
1392+
## 27. Quick Code Patterns
11291393

11301394
### Full CRUD (SQLite)
11311395

@@ -1262,7 +1526,7 @@ const rows = await db
12621526

12631527
---
12641528

1265-
## 26. Package Metadata
1529+
## 28. Package Metadata
12661530

12671531
```
12681532
Package: @coderbuzz/sql
@@ -1280,6 +1544,8 @@ Runtime dep: @coderbuzz/veta (internal, schema coercion)
12801544
| `@coderbuzz/sql` | Core classes, helpers, ANSI types |
12811545
| `@coderbuzz/sql/sqlite` | `sqlite` namespace + `SQLiteEngine` |
12821546
| `@coderbuzz/sql/postgres` | `pg` namespace + `PostgresEngine` |
1547+
| `@coderbuzz/sql/postgres-bun` | `pg` namespace + `BunPostgresEngine` |
1548+
| `@coderbuzz/sql/decimal` | Exact decimal arithmetic helpers |
12831549
| `@coderbuzz/sql/mysql` | `mysql` namespace + `MySQLEngine` |
12841550
| `@coderbuzz/sql/mssql` | `mssql` namespace + `MSSQLEngine` |
12851551
| `@coderbuzz/sql/clickhouse` | `ch` namespace + `ClickHouseEngine` |

0 commit comments

Comments
 (0)