Skip to content

Commit 7fbc97f

Browse files
docs: sync from coderbuzz/codex@b37bd48
1 parent 4c926a4 commit 7fbc97f

2 files changed

Lines changed: 134 additions & 68 deletions

File tree

‎AI_KNOWLEDGE.md‎

Lines changed: 112 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
<!-- docs: sync from coderbuzz/codex@9a7a8a5 -->
1+
<!-- docs: sync from coderbuzz/codex@b37bd48 -->
22

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

5-
**Package:** `@coderbuzz/sql` v0.1.3\
5+
**Package:** `@coderbuzz/sql` v0.8.1\
66
**Purpose:** Comprehensive reference for AI agents generating application code
77
with the `@coderbuzz/sql` library.\
88
**Distribution:** ESM only (`dist/` folder). No source `.ts` files in the
@@ -53,44 +53,32 @@ import { mssql } from "@coderbuzz/sql/mssql";
5353
import { ch } from "@coderbuzz/sql/clickhouse";
5454
```
5555

56+
`@coderbuzz/sql/sqlite` resolves by export condition: `bun` → `bun:sqlite`,
57+
`deno` → `@db/sqlite`, `default` (Node.js) → `better-sqlite3`, falling back to
58+
the built-in `node:sqlite` (Node 22+) when `better-sqlite3` is not installed.
59+
5660
### 2.2 Root package: shared helpers and types
5761

5862
```ts
5963
import {
60-
and,
6164
avg,
6265
type BatchOptions,
6366
// Types
6467
type CompiledQuery,
6568
// Aggregate helpers
6669
count,
6770
DeleteQuery,
68-
// Expression helpers (also in each dialect namespace)
69-
eq,
7071
// Expression factory
7172
expr,
72-
gt,
73-
gte,
74-
ilike,
7573
type InferRow,
7674
type InferSelect,
77-
inList,
7875
InsertBatcher,
7976
type InsertOptions,
8077
InsertQuery,
81-
isNotNull,
82-
isNull,
83-
like,
84-
lt,
85-
lte,
8678
max,
8779
type Middleware,
8880
min,
89-
ne,
90-
not,
9181
type OnConflictClause,
92-
or,
93-
raw,
9482
SelectQuery,
9583
// Classes (for advanced use)
9684
Sql,
@@ -129,6 +117,11 @@ import {
129117
} from "@coderbuzz/sql";
130118
```
131119

120+
The expression helpers (`eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `like`, `ilike`,
121+
`inList`, `isNull`, `isNotNull`, `and`, `or`, `not`, `raw`) are **not**
122+
exported from the root package. Use them from a dialect namespace:
123+
`pg.eq(...)`, `sqlite.and(...)`, and so on.
124+
132125
### 2.2b Decimal subpath (no engine, no dialect)
133126

134127
```ts
@@ -315,18 +308,37 @@ await db.execute(users.dropTable()); // "DROP TABLE IF EXISTS users;"
315308
import { introspect } from "@coderbuzz/sql/dist/migration/introspect";
316309
import { diff } from "@coderbuzz/sql/dist/migration/diff";
317310
import { applyDiff } from "@coderbuzz/sql/dist/migration/apply";
318-
import { sqliteCompiler } from "@coderbuzz/sql/dist/dialects/sqlite";
319311

320312
const live = await introspect(db); // query live schema
321313
const diffs = diff(live, [usersV2.toAst()]); // compute diffs
322-
const stmts = applyDiff(diffs, sqliteCompiler); // ALTER TABLE statements
314+
const stmts = applyDiff(diffs, db); // ALTER TABLE statements; a compiler also works
323315

324316
for (const stmt of stmts) {
325317
await db.execute(stmt);
326318
}
327319
```
328320

329-
`applyDiff()` supports: RENAME COLUMN (all dialects), ADD COLUMN (all dialects),
321+
> **Not importable yet.** `introspect`, `diff` and `applyDiff` live in
322+
> `src/migration/` but are not a tsup entry and not in the `exports` map, so
323+
> the `@coderbuzz/sql/dist/migration/*` paths above do not resolve in the
324+
> published package. Do not generate code that imports them until a
325+
> `./migration` subpath exists.
326+
327+
Signatures: `introspect(db: Sql<any>): Promise<CreateTableNode[]>` (SQLite,
328+
PostgreSQL, MySQL, MSSQL), `diff(live, target): TableDiff[]`,
329+
`applyDiff(diffs, target: BaseCompiler | engine, options?: { allowDestructive?: boolean }): string[]`.
330+
`DROP COLUMN` is omitted (with a `console.warn`) unless
331+
`{ allowDestructive: true }` is passed.
332+
333+
On PostgreSQL, run the statements inside `db.transaction()` (DDL is
334+
transactional, so a failure leaves the schema untouched) and wrap the whole
335+
thing in `db.withAdvisoryLock(key, fn)` so two instances of a rolling deploy
336+
do not migrate at once. The version ledger and file loading belong in the
337+
application.
338+
339+
`applyDiff()` supports: RENAME COLUMN (emitted for all dialects as
340+
`ALTER TABLE ... RENAME COLUMN`, which SQL Server rejects: it needs
341+
`sp_rename`), ADD COLUMN (all dialects),
330342
DROP COLUMN (PG/MySQL/MSSQL), ALTER COLUMN (PG/MySQL/MSSQL). SQLite skips
331343
DROP/ALTER with a `console.warn`.
332344

@@ -493,7 +505,7 @@ const { sql, params } = users.from(db)
493505
.fields("id", "email")
494506
.where({ active: true })
495507
.toSQL();
496-
// sql: 'SELECT "id", "email" FROM users WHERE active = ?'
508+
// sql: 'SELECT id, email FROM users WHERE "active" = ?;'
497509
// params: [true]
498510
```
499511

@@ -504,10 +516,14 @@ const { sql, params } = users.from(db)
504516
const plan = await users.from(db).where({ id: 1 }).explain().execute();
505517
const { sql } = users.from(db).where({ id: 1 }).explain();
506518
// SQLite: "EXPLAIN QUERY PLAN ..."
507-
// PostgreSQL: "EXPLAIN ANALYZE ..."
519+
// PostgreSQL: "EXPLAIN ANALYZE ..." (ANALYZE runs the query for real)
508520
// Others: "EXPLAIN ..."
509521
```
510522

523+
`explain_analyze()` returns a plain `CompiledQuery` (not executable) with a
524+
literal `EXPLAIN ANALYZE` prefix on every dialect, which SQLite does not
525+
accept.
526+
511527
---
512528

513529
## 8. WHERE Conditions: Complete Reference
@@ -555,12 +571,13 @@ const { sql } = users.from(db).where({ id: 1 }).explain();
555571
### Expression helpers (preferred for complex conditions)
556572

557573
```ts
558-
import { eq, ne, gt, gte, lt, lte, like, ilike, inList, isNull, isNotNull, and, or, not, raw } from "@coderbuzz/sql";
574+
// Not exported from the root: take them from the dialect namespace
575+
const { eq, ne, gt, gte, lt, lte, like, ilike, inList, isNull, isNotNull, and, or, not, raw } = pg;
559576

560577
.where(eq("id", 5))
561578
.where(gte("score", 90))
562579
.where(like("email", "%@example.com"))
563-
.where(ilike("name", "%alice%")) // case-insensitive, PostgreSQL
580+
.where(ilike("name", "%alice%")) // emits ILIKE on every dialect; only PostgreSQL accepts it
564581
.where(inList("id", [1, 2, 3]))
565582
.where(isNull("deleted_at"))
566583
.where(isNotNull("email"))
@@ -569,7 +586,9 @@ import { eq, ne, gt, gte, lt, lte, like, ilike, inList, isNull, isNotNull, and,
569586
.where(and(eq("active", true), gte("score", 90), not(isNull("email"))))
570587
.where(or(eq("role", "admin"), eq("role", "owner")))
571588

572-
// Raw fragment with params
589+
// Raw fragment with params. The SQL is inserted verbatim: use `?`, which the
590+
// pg/mssql engines rewrite to $N/@pN at execute time. On PostgreSQL/MSSQL this
591+
// is only correct when no other parameter precedes it (see section 24).
573592
.where(raw("created_at > NOW() - INTERVAL ? DAY", [7]))
574593

575594
// Raw fragment (no params)
@@ -645,9 +664,10 @@ done.
645664
```ts
646665
const batcher = db.batchInsert("events", {
647666
wait: 50, // ms of inactivity before flush (REQUIRED)
648-
max: 5_000, // flush when pending reaches this count
649-
timeout: 2_000, // force flush after this many ms from first write
650-
maxInflight: 4, // concurrent flushes allowed before write() waits
667+
max: 5_000, // flush when pending reaches this count (default 1000)
668+
timeout: 2_000, // force flush after this many ms from first write (default 5000)
669+
maxInflight: 4, // concurrent flushes allowed before write() waits (default 4)
670+
heterogeneousRows: "reject", // default; "union" fills absent keys with NULL
651671
settings: { async_insert: "1" }, // engine-specific (ClickHouse)
652672
onError: (err, rows) => { /* REQUIRED: retry or dead-letter these rows */ },
653673
});
@@ -778,7 +798,9 @@ await db.transaction(fn, {
778798

779799
`setup` is where `SET LOCAL` belongs: it is the mechanism PostgreSQL
780800
row-level security depends on, and it is correct only inside a
781-
single-connection transaction.
801+
single-connection transaction. For shared-schema multi-tenancy prefer
802+
`engine.forTenant(tenantId(id))` (both PostgreSQL engines); the full guide is
803+
`docs/multi-tenancy.md` in the monorepo.
782804

783805
### Savepoints
784806

@@ -833,7 +855,7 @@ db.use(async (query, next) => {
833855

834856
## 16. Streaming and Prepared Queries
835857

836-
### Streaming (SQLite + PostgreSQL only)
858+
### Streaming (SQLite on Bun + PostgreSQL only)
837859

838860
```ts
839861
for await (const row of users.from(db).where({ active: true }).stream()) {
@@ -842,12 +864,15 @@ for await (const row of users.from(db).where({ active: true }).stream()) {
842864
// Throws on unsupported dialects: "Streaming is not supported by this dialect."
843865
```
844866

845-
### Prepared queries (SQLite + PostgreSQL only)
867+
### Prepared queries (SQLite on Bun + PostgreSQL only)
868+
869+
`sqlite-node` and `sqlite-deno` do not override `stream()`/`prepare()`, so on
870+
Node.js and Deno `@coderbuzz/sql/sqlite` throws for both.
846871

847872
```ts
848873
const prepared = users.from(db).where({ id: 1 }).prepare();
849874
const rows = await prepared.execute();
850-
prepared.close();
875+
await prepared.close(); // Promise on PostgreSQL: DEALLOCATEs and releases the held connection
851876
// Throws on unsupported dialects: "Prepared statements are not supported by this dialect."
852877
```
853878

@@ -1030,7 +1055,7 @@ ch.uuid() ch.ipv4() ch.ipv6() ch.lowCardinality(type)
10301055
| **Identifier quoting** | `"id"` (PG, SQLite) · `` `id` `` (MySQL, CH) · `[id]` (MSSQL) |
10311056
| **Placeholders** | `?` (SQLite/MySQL/CH) · `$N` (PG) · `@pN` (MSSQL) |
10321057
| **RETURNING** | PostgreSQL + SQLite only. Others throw `"RETURNING is not supported by this dialect"` |
1033-
| **FULL OUTER JOIN** | PostgreSQL + ANSI only. SQLite/MySQL/ClickHouse throw at compile time |
1058+
| **FULL OUTER JOIN** | PostgreSQL, MSSQL and ANSI. SQLite/MySQL/ClickHouse throw at compile time |
10341059
| **ClickHouse params** | Values inlined into SQL (HTTP API has no native binding). Safe via `escapeClickHouseValue()` |
10351060
| **ClickHouse CREATE INDEX** | Not emitted. Indexes are defined via the ENGINE / ORDER BY clause |
10361061
| **ClickHouse UNIQUE** | Not supported. Throws if `.unique()` is used in a ClickHouse table |
@@ -1079,7 +1104,7 @@ const compiled = users.from(db)
10791104
.toSQL();
10801105

10811106
console.log(compiled.sql);
1082-
// SELECT "id", "name" FROM users WHERE (active = ? AND score >= ?) ORDER BY id ASC LIMIT 10
1107+
// SELECT id, name FROM users WHERE ("active" = ? AND "score" >= ?) ORDER BY id ASC LIMIT 10;
10831108
console.log(compiled.params);
10841109
// [true, 90]
10851110
```
@@ -1135,8 +1160,8 @@ const rows = await db.execute(`SELECT * FROM users WHERE name = '${name}'`);
11351160
const rows = await db.sql`SELECT * FROM users WHERE name = ${name}`.execute();
11361161
```
11371162
1138-
**DO NOT** call `.stream()` or `.prepare()` on MySQL, MSSQL, or ClickHouse
1139-
engines: they throw.
1163+
**DO NOT** call `.stream()` or `.prepare()` on MySQL (either engine), MSSQL,
1164+
ClickHouse, or SQLite on Node.js/Deno: they throw.
11401165
11411166
**DO NOT** use `.returning()` on MySQL, MSSQL, or ClickHouse: throws `"RETURNING is not supported by this dialect"`.
11421167
@@ -1147,6 +1172,12 @@ compile time.
11471172
11481173
**DO NOT** forget `await batcher.close()`: rows may be left unwritten.
11491174
1175+
**DO NOT** put `raw(sql, params)` after another parameterised condition on
1176+
PostgreSQL or MSSQL. The fragment's `?` is left as-is at compile time while the
1177+
other placeholders are numbered, so `and(eq('a', 1), raw('b > ?', [2]))`
1178+
compiles to `"a" = $1 AND b > ?`, and the engine's `?`-to-`$N` rewrite then
1179+
turns it into `b > $1`, binding `1` instead of `2`.
1180+
11501181
**DO NOT** use the pooled engine inside a transaction callback. Use `tx`:
11511182
11521183
```ts
@@ -1161,7 +1192,8 @@ await db.transaction(async (tx) => { await tx.execute(insertSql); });
11611192
`.from()`, or a join `ON` condition. These cannot be bound parameters, so they
11621193
are interpolated. They are validated and will throw `UnsafeIdentifierError` on
11631194
anything dangerous, but that is a guard, not a licence: map a sort parameter
1164-
through a fixed allow-list of column names:
1195+
through a fixed allow-list of column names. `assertSafeFragment()` (root
1196+
export) runs the same check on a fragment you build yourself:
11651197
11661198
```ts
11671199
// WRONG
@@ -1194,7 +1226,7 @@ so there is no conversion at the HTTP boundary, and conversions are where
11941226
precision goes:
11951227

11961228
```ts
1197-
import { decimal, object } from "@coderbuzz/veta";
1229+
import { decimal, object, string } from "@coderbuzz/veta";
11981230

11991231
const postJournal = object({
12001232
ref: string(),
@@ -1212,9 +1244,10 @@ returns a float64. Only the typed path rewrites it. In raw SQL, select
12121244
column on SQLite; they run on text or floats. Fetch and use
12131245
`@coderbuzz/sql/decimal` (`sumDecimals`, `compareDecimals`).
12141246

1215-
**Expect `BIGINT` and `COUNT(*)` as strings in raw MySQL results.** The engine
1216-
enables `bigNumberStrings`, matching PostgreSQL. `count()` on the typed path
1217-
still returns a number.
1247+
**Expect `BIGINT` and `COUNT(*)` as strings in raw `mysql2` results.** The
1248+
`@coderbuzz/sql/mysql` engine enables `bigNumberStrings`, matching PostgreSQL.
1249+
The `mysql-bun` engine returns them as numbers when they fit a float64 (see
1250+
section 27). `count()` on the typed path returns a number on both.
12181251

12191252
**DO NOT** use `DELETE` or `UPDATE` without `.where()` unless you intend to
12201253
affect all rows. Add a middleware guard in production code.
@@ -1453,7 +1486,7 @@ db.tenantSetting; // string
14531486
db.client; // the raw Bun.SQL object (LISTEN/NOTIFY, file(), beginDistributed)
14541487
db.forTenant(tenantId(id)); // TenantScopedSql
14551488
db.unsafeCrossTenant(reason, async admin => ...); // needs config.crossTenant
1456-
db.withAdvisoryLock(key, fn, wait?); // Promise<R | undefined>
1489+
db.withAdvisoryLock(key, fn, wait = true); // Promise<R | undefined>; wait=false → undefined if held
14571490
db.stream(compiledQuery); // AsyncIterable<row>
14581491
db.prepare(compiledQuery); // { execute(params?), close() }
14591492
await db.close(); // closes both clients
@@ -1656,9 +1689,13 @@ const rows = await db.sql<{ id: number; name: string }[]>`
16561689
### Batch write + close
16571690

16581691
```ts
1659-
const batcher = db.batchInsert("logs", { wait: 100, max: 1000 });
1692+
const batcher = db.batchInsert("logs", {
1693+
wait: 100,
1694+
max: 1000,
1695+
onError: (err, rows) => deadLetter.push({ err, rows }), // required
1696+
});
16601697
for (const log of logBuffer) {
1661-
batcher.write(log);
1698+
await batcher.write(log);
16621699
}
16631700
await batcher.close();
16641701
```
@@ -1668,10 +1705,10 @@ await batcher.close();
16681705
```ts
16691706
try {
16701707
await db.transaction(async (tx) => {
1671-
await accounts.update(tx).set({ balance: db.sql`balance - ${amount}` })
1672-
.where({ id: fromId }).execute();
1673-
await accounts.update(tx).set({ balance: db.sql`balance + ${amount}` })
1674-
.where({ id: toId }).execute();
1708+
// .set() binds every value as a parameter, so an expression such as
1709+
// `balance - x` has to go through the tagged template instead.
1710+
await tx.sql`UPDATE accounts SET balance = balance - ${amount} WHERE id = ${fromId}`.execute();
1711+
await tx.sql`UPDATE accounts SET balance = balance + ${amount} WHERE id = ${toId}`.execute();
16751712
});
16761713
} catch (e) {
16771714
// transaction was rolled back automatically
@@ -1716,10 +1753,11 @@ const rows = await db
17161753

17171754
```
17181755
Package: @coderbuzz/sql
1719-
Version: 0.1.3
1756+
Version: 0.8.1
17201757
License: MIT
17211758
Type: ESM only (type: "module")
1722-
Peer deps (all optional): pg, mysql2, mssql, better-sqlite3, @db/sqlite
1759+
Peer deps (all optional): pg, mysql2, mssql, better-sqlite3
1760+
(@db/sqlite, used on Deno, is loaded at runtime but not declared as a peer dep)
17231761
Runtime dep: @coderbuzz/veta (internal, schema coercion)
17241762
```
17251763

@@ -1742,3 +1780,26 @@ Runtime dep: @coderbuzz/veta (internal, schema coercion)
17421780
| `@coderbuzz/sql/mssql-types` | MSSQL column factories only |
17431781
| `@coderbuzz/sql/clickhouse-types` | ClickHouse column factories only |
17441782
| `@coderbuzz/sql/ansi` | ANSI column factories only |
1783+
1784+
---
1785+
1786+
## 30. Benchmarks
1787+
1788+
Machine-readable source of truth: `https://raw.githubusercontent.com/coderbuzz/benchmarks/main/results/latest.json` (each entry has `winner`, `factorVsNext`, `higherIsBetter`). Quote numbers from there, not from memory.
1789+
1790+
Full results at **[github.com/coderbuzz/benchmarks](https://github.com/coderbuzz/benchmarks)**.
1791+
1792+
SQL query compilation throughput on Apple M-series, Bun runtime. Higher is better.
1793+
1794+
| Scenario | @coderbuzz/sql | Kysely | Factor vs Kysely | Drizzle ORM | Factor vs Drizzle |
1795+
|---|---|---|---|---|---|
1796+
| SELECT simple | **1,700,391 ops/s** | 561,869 | **3.0x** | 34,239 | **49.7x** |
1797+
| SELECT JOIN (2 tables) | **2,208,322 ops/s** | 310,690 | **7.1x** | 16,801 | **131.4x** |
1798+
| INSERT single row | **2,954,261 ops/s** | 393,159 | **7.5x** | 55,085 | **53.6x** |
1799+
| INSERT batch 100 rows | **134,187 ops/s** | 17,634 | **7.6x** | 914 | **146.8x** |
1800+
| CTE (WITH clause) | **831,703 ops/s** | 224,893 | **3.7x** | 12,222 | **68.0x** |
1801+
| 10 nested WHERE conditions | **656,309 ops/s** | 117,064 | **5.6x** | 12,692 | **51.7x** |
1802+
1803+
`@coderbuzz/sql` is 3-8x faster than Kysely and 50-147x faster than Drizzle ORM across every query type. The gap widens with query complexity (batch, JOIN, conditions) due to `@coderbuzz/sql`'s zero-overhead string compilation strategy vs Kysely's AST-based approach and Drizzle's ORM abstraction layer.
1804+
1805+
These measure query **compilation** only (building the SQL string), not database round trips.

0 commit comments

Comments
 (0)