ReviewOS

also looking at this

stacks/bun-query-builder

test(docs): check the docs against the code, and fix what that found

#1073
Merged glennmichael123 wants to merge chore/repo-hygiene into main
13 files +325 -21
.gitignoremodified+6-2
Changes to .gitignore
@@ -18,8 +18,12 @@ generated/
1818bin/qbx
1919packages/bun-query-builder/bin/qbx
2020/packages/bun-query-builder/sql
21packages/benchmark/benchmark.db-shm
22packages/benchmark/benchmark.db-wal
21# SQLite scratch databases, wherever a test or a manual run happens to drop
22# them. These were previously listed one path at a time, so `other.db` and its
23# -shm/-wal siblings ended up committed at the repo root.
24*.db
25*.db-shm
26*.db-wal
2327*.sql
2428*.csv
2529users.json
README.mdmodified+2-2
Changes to README.md
@@ -386,8 +386,8 @@ query-builder wait-ready --attempts 30 --delay 250
386386
387387# Execute SQL
388388query-builder file ./migrations/seed.sql
389query-builder unsafe "SELECT _ FROM users WHERE id = $1" --params "[1]"
390query-builder explain "SELECT _ FROM users WHERE active = true"
389query-builder unsafe "SELECT * FROM users WHERE id = $1" --params "[1]"
390query-builder explain "SELECT * FROM users WHERE active = true"
391391
392392# Diagrams & Visualization
393393query-builder relation:diagram # Generate Mermaid ER diagram
docs/aggregations.mdmodified+64-0
Changes to docs/aggregations.md
@@ -3,6 +3,70 @@ title: Aggregations
33description: Perform aggregate calculations with the query builder.
44---
55
6# Aggregations
7
8Perform aggregate calculations like COUNT, SUM, AVG, MIN, and MAX with full type safety.
9
10## Count
11
12Count records in a table:
13
14```typescript
15import { createQueryBuilder } from 'bun-query-builder'
16
17const db = createQueryBuilder<typeof schema>({ schema, meta })
18
19// Count all records
20const totalUsers = await db.selectFrom('users').count()
21console.log(totalUsers) // 150
22
23// Count with conditions
24const activeUsers = await db
25 .selectFrom('users')
26 .where({ active: true })
27 .count()
28
29// Count specific column (excludes NULL)
30const usersWithEmail = await db.selectFrom('users').count('email')
31
32// Count distinct values
33const uniqueCountries = await db.selectFrom('users').countDistinct('country')
34```
35
36## Sum
37
38Calculate the sum of a column:
39
40```typescript
41// Sum of all order amounts
42const totalRevenue = await db.selectFrom('orders').sum('amount')
43console.log(totalRevenue) // 125000.50
44
45// Sum with conditions
46const yearlyRevenue = await db
47 .selectFrom('orders')
48 .where('created_at', '>=', '2024-01-01')
49 .sum('amount')
50```
51
52## Average
53
54Calculate the average of a column:
55
56```typescript
57// Average age of users
58const avgAge = await db.selectFrom('users').avg('age')
59console.log(avgAge) // 32.5
60
61// Average with conditions
62const avgActiveUserAge = await db
63 .selectFrom('users')
64 .where({ active: true })
65 .avg('age')
66```
67
68## Max
69
670Get the maximum value:
771
872```typescript
docs/features/cli.mdmodified+6-2
Changes to docs/features/cli.md
@@ -872,9 +872,13 @@ query-builder migrate:status # Verify migration state
872872- **Access Restrictions**: Limit who can use `unsafe` command in production
873873
874874```bash
875# Good: Use environment variables for sensitive data
875# Good: Use environment variables for sensitive data.
876# DB_CONNECTION must name the dialect — it is what arms the rest of the
877# DB_* connection variables. Without it they are ignored.
878export DB_CONNECTION=postgres
876879export DB_HOST=production.db.company.com
877export DB_USER=readonly_user
880export DB_USERNAME=readonly_user
881export DB_PASSWORD="$PGPASSWORD"
878882query-builder ping
879883
880884# Good: Validate parameters before use
docs/usage.mdmodified+27-13
Changes to docs/usage.md
@@ -465,20 +465,34 @@ HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
465465
466466### Environment-Specific Configurations
467467
468These are every environment variable the library reads:
469
468470```bash
469# Development environment
470export DB_TIMEOUT=10000
471export DB_RETRY_ATTEMPTS=3
472
473# Production environment
474export DB_TIMEOUT=5000
475export DB_RETRY_ATTEMPTS=1
476export DB_HEALTH_CHECK_INTERVAL=30
477
478# Testing environment
479export DB_TIMEOUT=15000
480export DB_RETRY_ATTEMPTS=5
481export DB_WAIT_READY_ATTEMPTS=30
471# Which dialect to connect with. This one is load-bearing: it also ARMS the
472# connection variables below, which are ignored unless it is set and matches
473# the dialect in use. DB_DIALECT is an accepted alias.
474export DB_CONNECTION=postgres
475
476# Connection details, honoured only when DB_CONNECTION is set as above
477export DB_HOST=localhost
478export DB_PORT=5432
479export DB_USERNAME=postgres
480export DB_PASSWORD=secret
481export DB_DATABASE=my_app
482
483# Require TLS ('true' or '1'). Read on its own, no DB_CONNECTION needed.
484export DB_SSL=true
485```
486
487Anything not on this list has no effect. Timeouts, retry counts and health-check
488intervals are configuration, not environment — set them in
489`query-builder.config.ts` or via `setConfig()`:
490
491```ts
492setConfig({
493 transactionDefaults: { retries: 3, backoff: { baseMs: 100, factor: 2, maxMs: 2000, jitter: true } },
494 database: { pool: { acquireTimeoutMs: 5000 } },
495})
482496```
483497
484498The CLI is designed to integrate seamlessly with modern development workflows, from local development to production monitoring. Choose the right commands and options for your specific use case and environment.
other.dbdeleted+0-0

Binary file. Nothing to show line by line.

other.db-shmdeleted+0-0

Binary file. Nothing to show line by line.

other.db-waldeleted+0-0

No changes to show.

packages/bun-query-builder/other.dbdeleted+0-0

Binary file. Nothing to show line by line.

packages/bun-query-builder/other.db-shmdeleted+0-0

Binary file. Nothing to show line by line.

packages/bun-query-builder/other.db-waldeleted+0-0

No changes to show.

packages/bun-query-builder/test/docs-lint.test.tsadded+220-0
Changes to packages/bun-query-builder/test/docs-lint.test.ts
@@ -0,0 +1,220 @@
1/**
2 * Mechanical checks on the documentation.
3 *
4 * Three classes of doc breakage reached `main` and survived months, because
5 * nothing verified the docs against the code:
6 *
7 * - ~40 documented calls to builder methods that do not exist (`db.update`,
8 * `db.insert`, `db.delete`, `db.execute`, `db.beginTransaction`, …). Every
9 * one threw on its first line, on every dialect, and `docs/api/reference.md`
10 * listed one of them in its method table.
11 * - Six pages truncated mid-code-block by a lint-fix commit, each starting on
12 * an orphaned expression with no H1 and an unbalanced fence.
13 * - 132 identifiers whose `_` had been rewritten to `*` — `created*at`,
14 * `user*id` — and the multiplication in `7 * 24 * 60 * 60 * 1000` rewritten
15 * the other way. One line read `60 _ 60 * 1000`, half converted.
16 *
17 * None of these need a database or a network, so they are cheap to assert.
18 * The builder-method check is the load-bearing one: it compares every
19 * documented call against the live export list rather than a hand-kept copy,
20 * so it cannot drift as the API grows.
21 */
22
23import { readdirSync, readFileSync, statSync } from 'node:fs'
24import { extname, join, relative, resolve } from 'node:path'
25import { describe, expect, it } from 'bun:test'
26import { buildDatabaseSchema, createQueryBuilder } from '../src/index'
27
28const REPO_ROOT = resolve(import.meta.dir, '../../..')
29const DOCS_DIR = join(REPO_ROOT, 'docs')
30
31function markdownFiles(dir: string): string[] {
32 const out: string[] = []
33 for (const entry of readdirSync(dir, { withFileTypes: true })) {
34 const full = join(dir, entry.name)
35 // `public/` holds images and other binaries.
36 if (entry.isDirectory()) {
37 if (entry.name !== 'public')
38 out.push(...markdownFiles(full))
39 continue
40 }
41 if (extname(entry.name) === '.md')
42 out.push(full)
43 }
44 return out.sort()
45}
46
47const DOC_FILES = markdownFiles(DOCS_DIR)
48const READMES = [join(REPO_ROOT, 'README.md'), join(REPO_ROOT, 'packages/bun-query-builder/README.md')]
49 .filter(p => statSync(p, { throwIfNoEntry: false }))
50const ALL_FILES = [...DOC_FILES, ...READMES]
51
52const rel = (p: string) => relative(REPO_ROOT, p)
53
54describe('docs lint', () => {
55 it('finds documentation to check', () => {
56 // Guards the guard: a bad glob would make every assertion below vacuous.
57 expect(DOC_FILES.length).toBeGreaterThan(20)
58 })
59
60 it('closes every code fence', () => {
61 // An odd count means a page is truncated mid-block — the exact signature of
62 // the six pages that lost their heads.
63 const offenders: string[] = []
64 for (const file of ALL_FILES) {
65 const fences = readFileSync(file, 'utf8').split('\n').filter(l => l.startsWith('```')).length
66 if (fences % 2 !== 0)
67 offenders.push(`${rel(file)} (${fences} fences)`)
68 }
69 expect(offenders).toEqual([])
70 })
71
72 it('starts every prose page with a heading', () => {
73 // The truncated pages opened on an orphaned `.get()` instead. This is what
74 // caught `docs/aggregations.md`, a seventh casualty of the same commit that
75 // the first repair pass missed.
76 const offenders: string[] = []
77 for (const file of DOC_FILES) {
78 const raw = readFileSync(file, 'utf8')
79 const lines = raw.split('\n')
80 let i = 0
81 let frontmatter = ''
82 if (lines[0]?.trim() === '---') {
83 i = 1
84 while (i < lines.length && lines[i].trim() !== '---') {
85 frontmatter += `${lines[i]}\n`
86 i++
87 }
88 i++
89 }
90 // `layout: home` / `layout: page` pages (index, team) render from
91 // frontmatter and legitimately have no heading in the body.
92 if (/^layout:/m.test(frontmatter))
93 continue
94
95 while (i < lines.length && lines[i].trim() === '') i++
96 // A leading HTML block — the centred logo on intro.md — is fine, so long
97 // as a heading follows it.
98 if (lines[i]?.startsWith('<')) {
99 while (i < lines.length && !lines[i].startsWith('#') && lines[i].trim() !== '') i++
100 while (i < lines.length && lines[i].trim() === '') i++
101 }
102 if (!lines[i]?.startsWith('#'))
103 offenders.push(`${rel(file)} starts with: ${JSON.stringify(lines[i]?.slice(0, 60) ?? '')}`)
104 }
105 expect(offenders).toEqual([])
106 })
107
108 it('has no identifiers whose underscore was rewritten as an asterisk', () => {
109 // `created*at`, `user*id`. Deliberately narrow so `SELECT *`, `COUNT(*)`
110 // and `**bold**` are untouched: the asterisk must sit between two
111 // alphanumerics with no spaces.
112 const offenders: string[] = []
113 for (const file of ALL_FILES) {
114 readFileSync(file, 'utf8').split('\n').forEach((line, n) => {
115 const hit = line.match(/[A-Za-z0-9]\*[A-Za-z0-9]/)
116 if (hit)
117 offenders.push(`${rel(file)}:${n + 1} ${hit[0]}`)
118 })
119 }
120 expect(offenders).toEqual([])
121 })
122
123 it('has no multiplication rewritten as an underscore', () => {
124 // `7 _ 24 _ 60 _ 60 _ 1000` was `7 * 24 * 60 * 60 * 1000`.
125 const offenders: string[] = []
126 for (const file of ALL_FILES) {
127 readFileSync(file, 'utf8').split('\n').forEach((line, n) => {
128 const hit = line.match(/[A-Za-z0-9_)\]] _ [A-Za-z0-9(]/)
129 if (hit)
130 offenders.push(`${rel(file)}:${n + 1} ${hit[0]}`)
131 })
132 }
133 expect(offenders).toEqual([])
134 })
135
136 it('only documents builder methods that exist', () => {
137 // Compared against the live builder rather than a hand-kept list, so the
138 // check keeps working as the API grows.
139 const schema = buildDatabaseSchema({
140 User: { name: 'User', table: 'users', primaryKey: 'id', attributes: { id: { validation: { rule: {} } } } },
141 } as any)
142 const builder = createQueryBuilder<typeof schema>({ schema }) as Record<string, unknown>
143 const methods = new Set(Object.keys(builder))
144 // The same receiver names hold chain objects in the docs' examples
145 // (`const q = db.selectFrom('users'); q.where(...)`), so a chain's methods
146 // are equally legitimate. Collect them from a real chain rather than
147 // listing them, for the same reason as the builder itself.
148 for (const chain of [
149 (builder as any).selectFrom('users'),
150 (builder as any).insertInto('users'),
151 (builder as any).updateTable('users'),
152 (builder as any).deleteFrom('users'),
153 ]) {
154 let obj = chain
155 while (obj && obj !== Object.prototype) {
156 for (const k of Object.getOwnPropertyNames(obj)) methods.add(k)
157 obj = Object.getPrototypeOf(obj)
158 }
159 }
160 expect(methods.size).toBeGreaterThan(40)
161
162 // Receivers that are a query builder in the docs' own examples.
163 const CALL = /\b(?:db|trx|tx|sp|innerTrx|qb)\.([a-zA-Z]+)\(/g
164 const offenders: string[] = []
165 for (const file of ALL_FILES) {
166 readFileSync(file, 'utf8').split('\n').forEach((line, n) => {
167 for (const m of line.matchAll(CALL)) {
168 if (!methods.has(m[1]))
169 offenders.push(`${rel(file)}:${n + 1} .${m[1]}()`)
170 }
171 })
172 }
173 expect(offenders).toEqual([])
174 })
175
176 it('only documents environment variables the library reads', () => {
177 // `DB_USER`, `DB_TIMEOUT`, `DB_RETRY_ATTEMPTS` and friends were documented
178 // for a long time and read nowhere, so following the docs did nothing and
179 // reported no error.
180 const srcDir = resolve(import.meta.dir, '../src')
181 const sources: string[] = []
182 const walk = (dir: string): void => {
183 for (const entry of readdirSync(dir, { withFileTypes: true })) {
184 const full = join(dir, entry.name)
185 if (entry.isDirectory())
186 walk(full)
187 else if (extname(entry.name) === '.ts')
188 sources.push(readFileSync(full, 'utf8'))
189 }
190 }
191 walk(srcDir)
192 const source = sources.join('\n')
193
194 // `createConnectionString` does `const e = process.env` and then reads
195 // `e.DB_HOST`, so a scan for `process.env.DB_*` alone reports variables as
196 // unread when they are not. Match the property access on any receiver, and
197 // strip comments first so a prose mention does not count as a read.
198 const code = source
199 .replace(/\/\*[\s\S]*?\*\//g, '')
200 .split('\n')
201 .map(l => l.replace(/\/\/.*$/, ''))
202 .join('\n')
203
204 const read = new Set<string>()
205 for (const m of code.matchAll(/\.(DB_[A-Z0-9_]+)\b/g)) read.add(m[1])
206 for (const m of code.matchAll(/\[['"](DB_[A-Z0-9_]+)['"]\]/g)) read.add(m[1])
207 expect(read.size).toBeGreaterThan(0)
208
209 const offenders: string[] = []
210 for (const file of ALL_FILES) {
211 readFileSync(file, 'utf8').split('\n').forEach((line, n) => {
212 for (const m of line.matchAll(/\b(DB_[A-Z0-9_]{2,})\b/g)) {
213 if (!read.has(m[1]))
214 offenders.push(`${rel(file)}:${n + 1} ${m[1]}`)
215 }
216 })
217 }
218 expect(offenders).toEqual([])
219 })
220})
qbxdeleted+0-2
Changes to qbx
@@ -1,2 +0,0 @@
1#!/usr/bin/env bun
2import('./bin/cli')