| name | write-sql-queries |
| description | Guide for writing SELECT, INSERT, UPDATE, DELETE queries with workers-qb |
Write Queries Skill
When to Use
Use this skill when:
- Writing database queries for Cloudflare Workers
- Implementing CRUD operations (Create, Read, Update, Delete)
- Building complex queries with JOINs, subqueries, or aggregations
- Working with D1, Durable Objects SQLite, or PostgreSQL
Database Selection
| Database | Class | Sync/Async | Import |
|---|
| Cloudflare D1 | D1QB | async | import { D1QB } from 'workers-qb' |
| Durable Objects | DOQB | sync | import { DOQB } from 'workers-qb' |
| PostgreSQL | PGQB | async | import { PGQB } from 'workers-qb' |
Critical: Sync vs Async
const result = await qb.fetchAll({ tableName: 'users' }).execute();
const result = qb.fetchAll({ tableName: 'users' }).execute();
DOQB is synchronous. This is the most common mistake. Inside Durable Objects:
const users = qb.fetchAll({ tableName: 'users' }).execute();
const users = await qb.fetchAll({ tableName: 'users' }).execute();
Schema Definition
Define a schema type for autocomplete and type safety:
type Schema = {
users: {
id: number;
name: string;
email: string;
created_at: string;
};
posts: {
id: number;
user_id: number;
title: string;
content: string;
};
};
const qb = new D1QB<Schema>(env.DB);
SELECT Patterns
Object Syntax
const users = await qb.fetchAll({
tableName: 'users',
}).execute();
const user = await qb.fetchOne({
tableName: 'users',
where: {
conditions: 'id = ?',
params: [1],
},
}).execute();
const emails = await qb.fetchAll({
tableName: 'users',
fields: ['id', 'email'],
}).execute();
Fluent API (SelectBuilder)
const users = await qb.select('users')
.where('is_active = ?', true)
.orderBy({ name: 'ASC' })
.limit(10)
.all();
const user = await qb.select('users')
.where('id = ?', userId)
.one();
const count = await qb.select('users')
.where('is_active = ?', true)
.count();
console.log(count.results?.total);
WHERE Clauses
where: {
conditions: 'email = ?',
params: ['john@example.com'],
}
where: {
conditions: ['status = ?', 'role_id = ?'],
params: ['active', 2],
}
where: {
conditions: 'owner_id = ?1 OR assignee_id = ?1',
params: ['user123'],
}
where: 'is_active = true'
where: ['is_active = true', 'deleted_at IS NULL']
whereIn for Bulk Lookups
const users = await qb.select('users')
.whereIn('id', [1, 2, 3, 4, 5])
.all();
const records = await qb.select('assignments')
.whereIn(['user_id', 'project_id'], [[1, 101], [2, 102], [3, 103]])
.all();
DISTINCT
const uniqueEmails = await qb.select('users')
.distinct()
.fields(['email'])
.all();
const latestPerDepartment = await qb.select('employees')
.distinct(['department'])
.fields(['department', 'name', 'created_at'])
.orderBy({ department: 'ASC', created_at: 'DESC' })
.all();
JOINs
const usersWithRoles = await qb.fetchAll({
tableName: 'users',
fields: ['users.name', 'roles.name AS role_name'],
join: {
type: 'INNER',
table: 'roles',
on: 'users.role_id = roles.id',
},
}).execute();
join: {
type: 'LEFT',
table: 'profiles',
on: 'users.id = profiles.user_id',
}
join: [
{ type: 'INNER', table: 'roles', on: 'users.role_id = roles.id' },
{ type: 'LEFT', table: 'profiles', on: 'users.id = profiles.user_id' },
]
JOIN Convenience Methods
const result = await qb.select('users')
.innerJoin({ table: 'orders', on: 'users.id = orders.user_id' })
.leftJoin({ table: 'profiles', on: 'users.id = profiles.user_id' })
.rightJoin({ table: 'teams', on: 'users.team_id = teams.id' })
.fullJoin({ table: 'projects', on: 'users.id = projects.owner_id' })
.all();
const combinations = await qb.select('colors')
.crossJoin({ table: 'sizes' })
.all();
const combined = await qb.select('orders')
.naturalJoin('customers')
.all();
Subqueries
const activeProjectsQuery = qb
.select('projects')
.fields('id')
.where('status = ?', 'active');
const tasks = await qb.select('tasks')
.where('project_id IN ?', activeProjectsQuery)
.all();
const permissionQuery = qb
.select('permissions')
.where('user_id = ?', userId)
.where('action = ?', 'edit');
const docs = await qb.select('documents')
.where('EXISTS ?', permissionQuery)
.all();
Pagination (Manual)
const pageSize = 20;
const page = 2;
const users = await qb.fetchAll({
tableName: 'users',
orderBy: 'created_at DESC',
limit: pageSize,
offset: (page - 1) * pageSize,
}).execute();
Pagination Helper
const result = await qb.select('users')
.where('active = ?', true)
.orderBy({ created_at: 'DESC' })
.paginate({ page: 2, perPage: 20 });
UNION / INTERSECT / EXCEPT
const allUsers = await qb.select('active_users')
.fields(['id', 'name'])
.union(qb.select('archived_users').fields(['id', 'name']))
.all();
const allRecords = await qb.select('table1')
.fields(['id'])
.unionAll(qb.select('table2').fields(['id']))
.all();
const commonUsers = await qb.select('users')
.fields(['id'])
.intersect(qb.select('admins').fields(['user_id']))
.all();
const regularUsers = await qb.select('all_users')
.fields(['id'])
.except(qb.select('blocked_users').fields(['user_id']))
.all();
const combined = await qb.select('table1')
.fields(['id'])
.union(qb.select('table2').fields(['id']))
.union(qb.select('table3').fields(['id']))
.orderBy({ id: 'ASC' })
.all();
CTEs (Common Table Expressions)
const ordersWithActiveUsers = await qb.select('orders')
.with('active_users', qb.select('users').where('status = ?', 'active'))
.innerJoin({ table: 'active_users', on: 'orders.user_id = active_users.id' })
.all();
const result = await qb.select('summary')
.with('recent_orders', qb.select('orders').where('created_at > ?', lastWeek))
.with('top_customers', qb.select('customers').where('total_spent > ?', 1000))
.all();
const stats = await qb.select('user_counts')
.with(
'user_stats',
qb.select('users').fields(['department', 'COUNT(*) as cnt']).groupBy('department'),
['dept', 'count']
)
.all();
Order By
orderBy: 'name'
orderBy: { name: 'DESC' }
orderBy: [
{ created_at: 'DESC' },
'name ASC',
]
Group By and Having
const stats = await qb.fetchAll({
tableName: 'orders',
fields: ['customer_id', 'COUNT(*) as order_count', 'SUM(total) as total_spent'],
groupBy: 'customer_id',
having: 'SUM(total) > 1000',
}).execute();
Lazy Execution (Large Datasets)
const lazyResult = await qb.fetchAll({
tableName: 'large_table',
lazy: true,
}).execute();
for await (const row of lazyResult.results!) {
processRow(row);
}
const lazyResult = qb.fetchAll({
tableName: 'large_table',
lazy: true,
}).execute();
for (const row of lazyResult.results!) {
processRow(row);
}
INSERT Patterns
Single Row
const newUser = await qb.insert({
tableName: 'users',
data: {
name: 'John Doe',
email: 'john@example.com',
},
returning: '*',
}).execute();
Multiple Rows
const newUsers = await qb.insert({
tableName: 'users',
data: [
{ name: 'Alice', email: 'alice@example.com' },
{ name: 'Bob', email: 'bob@example.com' },
],
returning: ['id', 'name'],
}).execute();
ON CONFLICT - IGNORE
Skip insertion if conflict occurs:
await qb.insert({
tableName: 'users',
data: { email: 'existing@example.com', name: 'Ignored' },
onConflict: 'IGNORE',
}).execute();
ON CONFLICT - REPLACE
Replace existing row on conflict:
await qb.insert({
tableName: 'users',
data: { email: 'existing@example.com', name: 'Replaced' },
onConflict: 'REPLACE',
}).execute();
UPSERT (ON CONFLICT DO UPDATE)
import { Raw } from 'workers-qb';
await qb.insert({
tableName: 'users',
data: {
email: 'john@example.com',
name: 'John',
login_count: 1,
},
onConflict: {
column: 'email',
data: {
login_count: new Raw('login_count + 1'),
updated_at: new Raw('CURRENT_TIMESTAMP'),
},
where: 'excluded.updated_at > users.updated_at',
},
}).execute();
Using Raw for SQL Expressions
import { Raw } from 'workers-qb';
await qb.insert({
tableName: 'posts',
data: {
title: 'My Post',
created_at: new Raw('CURRENT_TIMESTAMP'),
slug: new Raw("LOWER(REPLACE('My Post', ' ', '-'))"),
},
}).execute();
UPDATE Patterns
Basic Update
await qb.update({
tableName: 'users',
data: {
name: 'Updated Name',
},
where: {
conditions: 'id = ?',
params: [userId],
},
}).execute();
Update with Raw Expressions
import { Raw } from 'workers-qb';
await qb.update({
tableName: 'posts',
data: {
view_count: new Raw('view_count + 1'),
updated_at: new Raw('CURRENT_TIMESTAMP'),
},
where: {
conditions: 'id = ?',
params: [postId],
},
}).execute();
Update with Returning
const updated = await qb.update({
tableName: 'users',
data: { status: 'verified' },
where: {
conditions: 'email = ?',
params: ['john@example.com'],
},
returning: ['id', 'status', 'updated_at'],
}).execute();
console.log(updated.results);
Multiple WHERE Conditions
await qb.update({
tableName: 'tasks',
data: { status: 'completed' },
where: {
conditions: ['project_id = ?', 'assignee_id = ?'],
params: [projectId, userId],
},
}).execute();
DELETE Patterns
Basic Delete
await qb.delete({
tableName: 'sessions',
where: {
conditions: 'user_id = ?',
params: [userId],
},
}).execute();
Delete with Returning
const deleted = await qb.delete({
tableName: 'users',
where: {
conditions: 'id = ?',
params: [userId],
},
returning: ['id', 'email'],
}).execute();
console.log('Deleted:', deleted.results);
Ordered Delete with Limit
await qb.delete({
tableName: 'sessions',
where: 'expires_at < CURRENT_TIMESTAMP',
orderBy: 'expires_at ASC',
limit: 100,
}).execute();
Raw Queries
For complex SQL not covered by the builder:
import { FetchTypes } from 'workers-qb';
const results = await qb.raw({
query: 'SELECT * FROM users WHERE email LIKE ?',
args: ['%@example.com'],
fetchType: FetchTypes.ALL,
}).execute();
const user = await qb.raw({
query: 'SELECT * FROM users WHERE id = ? LIMIT 1',
args: [userId],
fetchType: FetchTypes.ONE,
}).execute();
await qb.raw({
query: 'UPDATE users SET last_seen = CURRENT_TIMESTAMP WHERE id = ?',
args: [userId],
}).execute();
Query Debugging
toSQL() - Get Query Without Executing
const { sql, params } = qb.select('users')
.where('id = ?', 1)
.where('status = ?', 'active')
.toSQL();
console.log(sql);
console.log(params);
toDebugSQL() - Interpolated SQL (for logging only)
const debugSql = qb.select('users')
.where('id = ?', 1)
.where("name = ?", "O'Brien")
.toDebugSQL();
console.log(debugSql);
EXPLAIN - Query Plan Analysis
const plan = await qb.select('users')
.where('id = ?', 1)
.explain();
console.log(plan.results);
Query Hooks
Register middleware-style hooks for all queries:
qb.beforeQuery((query, type) => {
if (type !== 'INSERT' && type !== 'RAW') {
query.query = query.query.replace('WHERE', `WHERE tenant_id = ${tenantId} AND`)
}
return query
})
qb.afterQuery((result, query, duration) => {
console.log(`Query took ${duration}ms:`, query.query)
metrics.record(query.query, duration)
return result
})
Transactions
D1QB Transactions (async, batch-based)
const results = await qb.transaction(async (tx) => {
return [
tx.insert({ tableName: 'orders', data: { user_id: 1, total: 100 } }),
tx.update({
tableName: 'users',
data: { balance: new Raw('balance - 100') },
where: { conditions: 'id = ?', params: [1] }
}),
]
})
DOQB Transactions (sync, SQLite BEGIN/COMMIT)
this.ctx.blockConcurrencyWhile(() => {
qb.transaction((tx) => {
tx.insert({ tableName: 'orders', data: { user_id: 1, total: 100 } }).execute()
tx.update({
tableName: 'users',
data: { balance: new Raw('balance - 100') },
where: { conditions: 'id = ?', params: [1] }
}).execute()
})
})
Checklist
Before executing queries, verify:
Common Mistakes
const users = await qb.fetchAll({ tableName: 'users' });
const users = await qb.fetchAll({ tableName: 'users' }).execute();
where: `email = '${userEmail}'`
where: { conditions: 'email = ?', params: [userEmail] }
const result = await doqb.fetchAll({ tableName: 'users' }).execute();
const result = doqb.fetchAll({ tableName: 'users' }).execute();