Skip to content

Commit 59ea5fc

Browse files
committed
chore: wip
1 parent ab55b4c commit 59ea5fc

File tree

9 files changed

+302
-14
lines changed

9 files changed

+302
-14
lines changed

docs/advanced/api.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@ This page summarizes method signatures and brief descriptions. See feature pages
88

99
```ts
1010
import {
11+
buildDatabaseSchema,
12+
buildSchemaMeta, // types/config
13+
config,
1114
// builder
1215
createQueryBuilder,
16+
defaultConfig,
1317
// schema
14-
defineModel, defineModels,
15-
buildDatabaseSchema, buildSchemaMeta,
16-
// types/config
17-
config, defaultConfig,
18+
defineModel,
19+
defineModels,
1820
} from 'bun-query-builder'
1921
```
2022

docs/features/builder.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ See the Pagination page for details. Highlights:
235235
await db.selectFrom('users').paginate(25, 1)
236236
await db.selectFrom('users').simplePaginate(25)
237237
await db.selectFrom('users').cursorPaginate(100, undefined, 'id', 'asc')
238-
await db.selectFrom('users').chunkById(1000, 'id', async batch => { /* ... */ })
238+
await db.selectFrom('users').chunkById(1000, 'id', async (batch) => { /* ... */ })
239239
```
240240

241241
## CTEs and Recursive Queries

docs/features/pagination.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ Implementation details:
8787
Paginates using `paginate()` internally and invokes `handler(rows)` per page until exhausted.
8888

8989
```ts
90-
await db.selectFrom('users').chunk(1000, async rows => {
90+
await db.selectFrom('users').chunk(1000, async (rows) => {
9191
// process each page of 1000
9292
})
9393
```
@@ -97,7 +97,7 @@ await db.selectFrom('users').chunk(1000, async rows => {
9797
Paginates using `cursorPaginate()` for memory-friendly processing.
9898

9999
```ts
100-
await db.selectFrom('users').chunkById(1000, 'id', async rows => {
100+
await db.selectFrom('users').chunkById(1000, 'id', async (rows) => {
101101
// process 1000 rows at a time
102102
})
103103
```
@@ -107,7 +107,7 @@ await db.selectFrom('users').chunkById(1000, 'id', async rows => {
107107
Iterates row-by-row using `chunkById`.
108108

109109
```ts
110-
await db.selectFrom('users').eachById(500, 'id', async row => {
110+
await db.selectFrom('users').eachById(500, 'id', async (row) => {
111111
// process row
112112
})
113113
```
@@ -133,7 +133,7 @@ do {
133133
} while (cursor)
134134

135135
// background export
136-
await db.selectFrom('orders').chunkById(1000, 'id', async batch => {
136+
await db.selectFrom('orders').chunkById(1000, 'id', async (batch) => {
137137
// write batch to file
138138
})
139139
```

src/factory.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { DatabaseSchema, InferTableName, ModelRecord } from './schema'
1+
import type { DatabaseSchema, ModelRecord } from './schema'
22

33
export type BuildDatabaseSchema<MRecord extends ModelRecord> = DatabaseSchema<MRecord>
44

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@ export * from './client'
22
export * from './config'
33
export * from './factory'
44
export * from './loader'
5+
export * from './meta'
56
export * from './schema'
67
export * from './types'
7-
export * from './meta'

test/client.test.ts

Lines changed: 227 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,15 @@ function qb() {
3434
return createQueryBuilder<typeof schema>({ meta, schema })
3535
}
3636

37+
function toTextOf(q: any): string {
38+
const fn = (q as any)?.toText
39+
return typeof fn === 'function' ? (fn.call(q) ?? '') : ''
40+
}
41+
42+
function expectTextOutput(s: string) {
43+
expect(typeof s).toBe('string')
44+
}
45+
3746
describe('query builder - basics', () => {
3847
it('builds simple select returns a query object', () => {
3948
const q = qb().selectFrom('users').where({ active: true }).orderBy('created_at', 'desc').limit(10).offset(20).toSQL() as any
@@ -63,6 +72,11 @@ describe('query builder - basics', () => {
6372
const q = qb().selectFrom('users').forPage(3, 25).toSQL() as any
6473
expect(typeof q.execute).toBe('function')
6574
})
75+
76+
it('typed select(columns) returns a query object', () => {
77+
const q = qb().select('users', 'id', 'name as username').toSQL() as any
78+
expect(typeof q.execute).toBe('function')
79+
})
6680
})
6781

6882
describe('query builder - modifiers and raws', () => {
@@ -119,7 +133,7 @@ describe('query builder - subqueries and relations', () => {
119133
})
120134

121135
it('with() and selectAllRelations aliasing composes', () => {
122-
const q = qb().selectFrom('users').with('Project').selectAllRelations().toSQL() as any
136+
const q = (qb().selectFrom('users') as any).with('Project').selectAllRelations().toSQL() as any
123137
expect(typeof q.execute).toBe('function')
124138
})
125139

@@ -128,11 +142,18 @@ describe('query builder - subqueries and relations', () => {
128142
if (config.debug)
129143
config.debug.captureText = true
130144
const q = qb().selectFrom('users').where({ active: true })
131-
const s = (q as any).toText?.() ?? ''
132-
expect(typeof s).toBe('string')
145+
const s = toTextOf(q as any)
146+
expectTextOutput(s)
133147
if (config.debug)
134148
config.debug.captureText = prev as boolean
135149
})
150+
151+
it('unionAll composes and returns query object', () => {
152+
const a = qb().selectFrom('users').limit(1)
153+
const b = qb().selectFrom('users').limit(1)
154+
const q = a.unionAll(b).toSQL() as any
155+
expect(typeof q.execute).toBe('function')
156+
})
136157
})
137158

138159
describe('query builder - pagination helpers', () => {
@@ -143,3 +164,206 @@ describe('query builder - pagination helpers', () => {
143164
expect(typeof q.cursorPaginate).toBe('function')
144165
})
145166
})
167+
168+
describe('query builder - DML builders', () => {
169+
it('insertInto values returns query with execute and returning chain works', () => {
170+
const ins = qb().insertInto('users').values({ id: 1, name: 'a' })
171+
const q1 = ins.toSQL() as any
172+
expect(typeof q1.execute).toBe('function')
173+
const ret = ins.returning('id')
174+
const q2 = ret.toSQL() as any
175+
expect(typeof q2.execute).toBe('function')
176+
})
177+
178+
it('updateTable set/where and returning chain', () => {
179+
const upd = qb().updateTable('users').set({ name: 'b' }).where({ id: 1 })
180+
const q1 = upd.toSQL() as any
181+
expect(typeof q1.execute).toBe('function')
182+
const ret = upd.returning('id')
183+
const q2 = ret.toSQL() as any
184+
expect(typeof q2.execute).toBe('function')
185+
})
186+
187+
it('deleteFrom where and returning chain', () => {
188+
const del = qb().deleteFrom('users').where({ id: 1 })
189+
const q1 = del.toSQL() as any
190+
expect(typeof q1.execute).toBe('function')
191+
const ret = del.returning('id')
192+
const q2 = ret.toSQL() as any
193+
expect(typeof q2.execute).toBe('function')
194+
})
195+
196+
it('cancel() exists and is safe to call', () => {
197+
const q = qb().selectFrom('users').limit(1)
198+
expect(() => (q as any).cancel()).not.toThrow()
199+
})
200+
})
201+
202+
describe('query builder - SQL text for clauses and helpers', () => {
203+
let prevCapture: boolean | undefined
204+
beforeEach(() => {
205+
prevCapture = config.debug?.captureText
206+
if (config.debug)
207+
config.debug.captureText = true
208+
})
209+
afterEach(() => {
210+
if (config.debug && typeof prevCapture !== 'undefined')
211+
config.debug.captureText = prevCapture
212+
})
213+
214+
it('builds equality and object/array where', () => {
215+
const q1 = qb().selectFrom('users').where(['id', '=', 1]) as any
216+
const s1 = toTextOf(q1)
217+
expectTextOutput(s1)
218+
const q2 = qb().selectFrom('users').where({ id: 1, name: 'a' }) as any
219+
const s2 = toTextOf(q2)
220+
expectTextOutput(s2)
221+
const q3 = qb().selectFrom('users').where({ id: [1, 2, 3] }) as any
222+
const s3 = toTextOf(q3)
223+
expectTextOutput(s3)
224+
})
225+
226+
it('supports special operators in where tuple', () => {
227+
const ops: Array<[string, string, any]> = [
228+
['id', '!=', 1],
229+
['id', '<', 2],
230+
['id', '>', 2],
231+
['id', '<=', 2],
232+
['id', '>=', 2],
233+
['name', 'like', '%a%'],
234+
['id', 'in', [1, 2]],
235+
['id', 'not in', [1, 2]],
236+
['deleted_at', 'is', null],
237+
['deleted_at', 'is not', null],
238+
]
239+
for (const [col, op, val] of ops) {
240+
const s = toTextOf(qb().selectFrom('users').where([col as any, op as any, val]) as any)
241+
expectTextOutput(s)
242+
}
243+
})
244+
245+
it('null/between/exists/date helpers produce expected snippets', () => {
246+
const s1 = toTextOf((qb().selectFrom('users') as any).whereNull('deleted_at') as any)
247+
expectTextOutput(s1)
248+
const s2 = toTextOf((qb().selectFrom('users') as any).whereNotNull('deleted_at') as any)
249+
expectTextOutput(s2)
250+
const s3 = toTextOf(qb().selectFrom('users').whereBetween('id', 1, 5) as any)
251+
expectTextOutput(s3)
252+
const s4 = toTextOf(qb().selectFrom('users').whereNotBetween('id', 1, 5) as any)
253+
expectTextOutput(s4)
254+
const sub = qb().selectFrom('users').limit(1)
255+
const s5 = toTextOf((qb().selectFrom('projects') as any).whereExists(sub as any) as any)
256+
expectTextOutput(s5)
257+
const s6 = toTextOf(qb().selectFrom('users').whereDate('created_at', '>=', '2024-01-01') as any)
258+
expectTextOutput(s6)
259+
})
260+
261+
it('column comparisons and nested conditions', () => {
262+
const s1 = toTextOf(qb().selectFrom('users').whereColumn('users.id', '>=', 'projects.user_id') as any)
263+
expectTextOutput(s1)
264+
const nested = qb().selectFrom('users').where(['id', '>', 0])
265+
const s2 = toTextOf(qb().selectFrom('projects').whereNested(nested as any) as any)
266+
expectTextOutput(s2)
267+
const s3 = toTextOf(qb().selectFrom('projects').orWhereNested(nested as any) as any)
268+
expectTextOutput(s3)
269+
const s4 = toTextOf(qb().selectFrom('users').where(['id', '>', 0]).andWhere(['name', 'like', '%a%']) as any)
270+
expectTextOutput(s4)
271+
const s5 = toTextOf(qb().selectFrom('users').where(['id', '>', 0]).orWhere(['name', 'like', '%a%']) as any)
272+
expectTextOutput(s5)
273+
})
274+
275+
it('ordering, reordering, and random order', () => {
276+
const s1 = toTextOf(qb().selectFrom('users').orderBy('created_at', 'asc') as any)
277+
expectTextOutput(s1)
278+
const s2 = toTextOf(qb().selectFrom('users').orderByDesc('created_at') as any)
279+
expectTextOutput(s2)
280+
const s3 = toTextOf(qb().selectFrom('users').orderBy('created_at', 'desc').reorder('id', 'asc') as any)
281+
expectTextOutput(s3)
282+
const prev = config.sql.randomFunction
283+
config.sql.randomFunction = 'RANDOM()'
284+
const s4 = toTextOf(qb().selectFrom('users').inRandomOrder() as any)
285+
expectTextOutput(s4)
286+
config.sql.randomFunction = 'RAND()'
287+
const s5 = toTextOf(qb().selectFrom('users').inRandomOrder() as any)
288+
expectTextOutput(s5)
289+
config.sql.randomFunction = prev
290+
})
291+
292+
it('limit/offset and forPage', () => {
293+
const s1 = toTextOf(qb().selectFrom('users').limit(10) as any)
294+
expectTextOutput(s1)
295+
const s2 = toTextOf(qb().selectFrom('users').offset(20) as any)
296+
expectTextOutput(s2)
297+
const s3 = toTextOf(qb().selectFrom('users').forPage(2, 25) as any)
298+
expectTextOutput(s3)
299+
})
300+
301+
it('joins, join subs, and cross joins', () => {
302+
const s1 = toTextOf(qb().selectFrom('users').join('projects', 'users.id', '=', 'projects.user_id') as any)
303+
expectTextOutput(s1)
304+
const s2 = toTextOf(qb().selectFrom('users').innerJoin('projects', 'users.id', '=', 'projects.user_id') as any)
305+
expectTextOutput(s2)
306+
const s3 = toTextOf(qb().selectFrom('users').leftJoin('projects', 'users.id', '=', 'projects.user_id') as any)
307+
expectTextOutput(s3)
308+
const s4 = toTextOf(qb().selectFrom('users').rightJoin('projects', 'users.id', '=', 'projects.user_id') as any)
309+
expectTextOutput(s4)
310+
const sub = qb().selectFrom('users').limit(1)
311+
const s5 = toTextOf(qb().selectFrom('projects').joinSub(sub as any, 'u', 'u.id', '=', 'projects.user_id') as any)
312+
expectTextOutput(s5)
313+
const s6 = toTextOf(qb().selectFrom('projects').leftJoinSub(sub as any, 'u', 'u.id', '=', 'projects.user_id') as any)
314+
expectTextOutput(s6)
315+
const s7 = toTextOf(qb().selectFrom('projects').crossJoin('users') as any)
316+
expectTextOutput(s7)
317+
const s8 = toTextOf(qb().selectFrom('projects').crossJoinSub(sub as any, 'u') as any)
318+
expectTextOutput(s8)
319+
})
320+
321+
it('group by, group by raw, having and having raw', () => {
322+
const s1 = toTextOf(qb().selectFrom('users').groupBy('id') as any)
323+
expectTextOutput(s1)
324+
const s2 = toTextOf(qb().selectFrom('users').groupByRaw('id') as any)
325+
expectTextOutput(s2)
326+
const s3 = toTextOf(qb().selectFrom('users').groupBy('id').having(['id', '>', 0]) as any)
327+
expectTextOutput(s3)
328+
const s4 = toTextOf(qb().selectFrom('users').groupBy('id').havingRaw('1=1') as any)
329+
expectTextOutput(s4)
330+
})
331+
332+
it('with relations + selectAllRelations aliasing across formats', () => {
333+
// default table_column
334+
config.aliasing.relationColumnAliasFormat = 'table_column'
335+
const s1 = toTextOf((qb().selectFrom('users') as any).with('Project').selectAllRelations() as any)
336+
expectTextOutput(s1)
337+
// dot format
338+
config.aliasing.relationColumnAliasFormat = 'table.dot.column'
339+
const s2 = toTextOf((qb().selectFrom('users') as any).with('Project').selectAllRelations() as any)
340+
expectTextOutput(s2)
341+
// camelCase
342+
config.aliasing.relationColumnAliasFormat = 'camelCase'
343+
const s3 = toTextOf((qb().selectFrom('users') as any).with('Project').selectAllRelations() as any)
344+
expectTextOutput(s3)
345+
// reset default
346+
config.aliasing.relationColumnAliasFormat = 'table_column'
347+
})
348+
349+
it('locks and shared lock syntax selection', () => {
350+
const s1 = toTextOf(qb().selectFrom('users').lockForUpdate() as any)
351+
expectTextOutput(s1)
352+
const prev = config.sql.sharedLockSyntax
353+
config.sql.sharedLockSyntax = 'FOR SHARE'
354+
const s2 = toTextOf(qb().selectFrom('users').sharedLock() as any)
355+
expectTextOutput(s2)
356+
config.sql.sharedLockSyntax = 'LOCK IN SHARE MODE'
357+
const s3 = toTextOf(qb().selectFrom('users').sharedLock() as any)
358+
expectTextOutput(s3)
359+
config.sql.sharedLockSyntax = prev
360+
})
361+
362+
it('CTEs and recursive CTEs compose', () => {
363+
const sub = qb().selectFrom('users').limit(1)
364+
const s1 = toTextOf(qb().selectFrom('users').withCTE('one', sub as any) as any)
365+
expectTextOutput(s1)
366+
const s2 = toTextOf(qb().selectFrom('users').withRecursive('recur', sub as any) as any)
367+
expectTextOutput(s2)
368+
})
369+
})

test/config.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,11 @@ describe('config defaults', () => {
99
expect(config.pagination.defaultPerPage).toBeGreaterThan(0)
1010
expect(config.transactionDefaults.retries).toBeGreaterThanOrEqual(0)
1111
})
12+
13+
it('includes sql and feature toggles', () => {
14+
expect(['RANDOM()', 'RAND()']).toContain(config.sql.randomFunction)
15+
expect(['FOR SHARE', 'LOCK IN SHARE MODE']).toContain(config.sql.sharedLockSyntax)
16+
expect(['operator', 'function']).toContain(config.sql.jsonContainsMode)
17+
expect(typeof config.features.distinctOn).toBe('boolean')
18+
})
1219
})

test/schema_meta.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { describe, expect, it } from 'bun:test'
2+
import { buildDatabaseSchema, buildSchemaMeta, defineModels } from '../src'
3+
4+
const models = defineModels({
5+
User: {
6+
name: 'User',
7+
table: 'users',
8+
primaryKey: 'id',
9+
attributes: { id: { validation: { rule: {} } }, email: { validation: { rule: {} } } },
10+
},
11+
Project: {
12+
name: 'Project',
13+
table: 'projects',
14+
primaryKey: 'pid',
15+
attributes: { pid: { validation: { rule: {} } }, user_id: { validation: { rule: {} } } },
16+
},
17+
} as const)
18+
19+
describe('schema and meta builders', () => {
20+
it('buildDatabaseSchema maps attributes and primary keys', () => {
21+
const schema = buildDatabaseSchema(models)
22+
expect(Object.keys(schema)).toEqual(['users', 'projects'])
23+
expect(Object.keys(schema.users.columns)).toEqual(['id', 'email'])
24+
expect(schema.users.primaryKey).toBe('id')
25+
expect(Object.keys(schema.projects.columns)).toEqual(['pid', 'user_id'])
26+
expect(schema.projects.primaryKey).toBe('pid')
27+
})
28+
29+
it('buildSchemaMeta maps model<->table and primary keys', () => {
30+
const meta = buildSchemaMeta(models as any)
31+
expect(meta.modelToTable.User).toBe('users')
32+
expect(meta.tableToModel.users).toBe('User')
33+
expect(meta.primaryKeys.users).toBe('id')
34+
expect(meta.modelToTable.Project).toBe('projects')
35+
expect(meta.tableToModel.projects).toBe('Project')
36+
expect(meta.primaryKeys.projects).toBe('pid')
37+
})
38+
})

0 commit comments

Comments
 (0)