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
4747import { 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
4950import { mysql } from " @coderbuzz/sql/mysql" ;
5051import { mssql } from " @coderbuzz/sql/mssql" ;
5152import { 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
144199const 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
10991154const 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...
11021157const [{ 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` ` `
12681532Package: @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