Guide for writing SELECT, INSERT, UPDATE, DELETE queries with workers-qb
Use this skill when:
| 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' |
// D1QB/PGQB - ALWAYS use await
const result = await qb.fetchAll({ tableName: 'users' }).execute();
// DOQB - NEVER use await (synchronous)
const result = qb.fetchAll({ tableName: 'users' }).execute();
DOQB is synchronous. This is the most common mistake. Inside Durable Objects:
// CORRECT
const users = qb.fetchAll({ tableName: 'users' }).execute();
// WRONG - don't await DOQB
const users = await qb.fetchAll({ tableName: 'users' }).execute();
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);
// Now tableName, fields, returning all have autocomplete
// Fetch all rows
const users = await qb.fetchAll({
tableName: 'users',
}).execute();
// Fetch one row
const user = await qb.fetchOne({
tableName: 'users',
where: {
conditions: 'id = ?',
params: [1],
},
}).execute();
// Select specific fields
const emails = await qb.fetchAll({
tableName: 'users',
fields: ['id', 'email'],
}).execute();
// Basic chain
const users = await qb.select('users')
.where('is_active = ?', true)
.orderBy({ name: 'ASC' })
.limit(10)
.all();
// Single row
const user = await qb.select('users')
.where('id = ?', userId)
.one();
// Count query
const count = await qb.select('users')
.where('is_active = ?', true)
.count();
console.log(count.results?.total);
// Single condition
where: {
conditions: 'email = ?',
params: ['john@example.com'],
}
// Multiple conditions (AND)
where: {
conditions: ['status = ?', 'role_id = ?'],
params: ['active', 2],
}
// Numbered parameters (for reuse)
where: {
conditions: 'owner_id = ?1 OR assignee_id = ?1',
params: ['user123'],
}
// Simple string (no params)
where: 'is_active = true'
// String array (AND, no params)
where: ['is_active = true', 'deleted_at IS NULL']
// Single column
const users = await qb.select('users')
.whereIn('id', [1, 2, 3, 4, 5])
.all();
// Multiple columns (composite key)
const records = await qb.select('assignments')
.whereIn(['user_id', 'project_id'], [[1, 101], [2, 102], [3, 103]])
.all();
// Simple DISTINCT
const uniqueEmails = await qb.select('users')
.distinct()
.fields(['email'])
.all();
// SELECT DISTINCT email FROM users
// DISTINCT ON (PostgreSQL only)
const latestPerDepartment = await qb.select('employees')
.distinct(['department'])
.fields(['department', 'name', 'created_at'])
.orderBy({ department: 'ASC', created_at: 'DESC' })
.all();
// SELECT DISTINCT ON (department) department, name, created_at FROM employees
// INNER JOIN
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();
// LEFT JOIN
join: {
type: 'LEFT',
table: 'profiles',
on: 'users.id = profiles.user_id',
}
// Multiple JOINs
join: [
{ type: 'INNER', table: 'roles', on: 'users.role_id = roles.id' },
{ type: 'LEFT', table: 'profiles', on: 'users.id = profiles.user_id' },
]
// Using convenience methods (fluent API)
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();
// CROSS JOIN
const combinations = await qb.select('colors')
.crossJoin({ table: 'sizes' })
.all();
// NATURAL JOIN (auto-matches columns with same name)
const combined = await qb.select('orders')
.naturalJoin('customers')
.all();
// IN with subquery
const activeProjectsQuery = qb
.select('projects')
.fields('id')
.where('status = ?', 'active');
const tasks = await qb.select('tasks')
.where('project_id IN ?', activeProjectsQuery)
.all();
// EXISTS with subquery
const permissionQuery = qb
.select('permissions')
.where('user_id = ?', userId)
.where('action = ?', 'edit');
const docs = await qb.select('documents')
.where('EXISTS ?', permissionQuery)
.all();
const pageSize = 20;
const page = 2;
const users = await qb.fetchAll({
tableName: 'users',
orderBy: 'created_at DESC',
limit: pageSize,
offset: (page - 1) * pageSize,
}).execute();
// Use .paginate() for automatic pagination metadata
const result = await qb.select('users')
.where('active = ?', true)
.orderBy({ created_at: 'DESC' })
.paginate({ page: 2, perPage: 20 });
// Returns:
// {
// results: [...],
// pagination: {
// page: 2,
// perPage: 20,
// total: 150,
// totalPages: 8,
// hasNext: true,
// hasPrev: true
// }
// }
// UNION - combine results, remove duplicates
const allUsers = await qb.select('active_users')
.fields(['id', 'name'])
.union(qb.select('archived_users').fields(['id', 'name']))
.all();
// UNION ALL - keep duplicates
const allRecords = await qb.select('table1')
.fields(['id'])
.unionAll(qb.select('table2').fields(['id']))
.all();
// INTERSECT - only common rows
const commonUsers = await qb.select('users')
.fields(['id'])
.intersect(qb.select('admins').fields(['user_id']))
.all();
// EXCEPT - rows in first but not second
const regularUsers = await qb.select('all_users')
.fields(['id'])
.except(qb.select('blocked_users').fields(['user_id']))
.all();
// Chain multiple set operations
const combined = await qb.select('table1')
.fields(['id'])
.union(qb.select('table2').fields(['id']))
.union(qb.select('table3').fields(['id']))
.orderBy({ id: 'ASC' }) // ORDER BY applies to combined result
.all();
// Simple CTE - WITH clause
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();
// WITH active_users AS (SELECT * FROM users WHERE status = ?)
// SELECT * FROM orders INNER JOIN active_users ON orders.user_id = active_users.id
// Multiple CTEs
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();
// CTE with explicit column names
const stats = await qb.select('user_counts')
.with(
'user_stats',
qb.select('users').fields(['department', 'COUNT(*) as cnt']).groupBy('department'),
['dept', 'count'] // Column aliases for the CTE
)
.all();
// WITH user_stats(dept, count) AS (SELECT department, COUNT(*) as cnt FROM users GROUP BY department)
// Simple
orderBy: 'name'
// With direction
orderBy: { name: 'DESC' }
// Multiple columns
orderBy: [
{ created_at: 'DESC' },
'name ASC',
]
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();
// D1/PostgreSQL - AsyncIterable
const lazyResult = await qb.fetchAll({
tableName: 'large_table',
lazy: true,
}).execute();
for await (const row of lazyResult.results!) {
processRow(row);
}
// DOQB - Iterable (sync)
const lazyResult = qb.fetchAll({
tableName: 'large_table',
lazy: true,
}).execute();
for (const row of lazyResult.results!) {
processRow(row);
}
const newUser = await qb.insert({
tableName: 'users',
data: {
name: 'John Doe',
email: 'john@example.com',
},
returning: '*',
}).execute();
const newUsers = await qb.insert({
tableName: 'users',
data: [
{ name: 'Alice', email: 'alice@example.com' },
{ name: 'Bob', email: 'bob@example.com' },
],
returning: ['id', 'name'],
}).execute();
Skip insertion if conflict occurs:
await qb.insert({
tableName: 'users',
data: { email: 'existing@example.com', name: 'Ignored' },
onConflict: 'IGNORE',
}).execute();
Replace existing row on conflict:
await qb.insert({
tableName: 'users',
data: { email: 'existing@example.com', name: 'Replaced' },
onConflict: 'REPLACE',
}).execute();
import { Raw } from 'workers-qb';
await qb.insert({
tableName: 'users',
data: {
email: 'john@example.com',
name: 'John',
login_count: 1,
},
onConflict: {
column: 'email', // or ['email', 'tenant_id'] for composite
data: {
login_count: new Raw('login_count + 1'),
updated_at: new Raw('CURRENT_TIMESTAMP'),
},
// Optional: conditional update
where: 'excluded.updated_at > users.updated_at',
},
}).execute();
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();
await qb.update({
tableName: 'users',
data: {
name: 'Updated Name',
},
where: {
conditions: 'id = ?',
params: [userId],
},
}).execute();
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();
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);
await qb.update({
tableName: 'tasks',
data: { status: 'completed' },
where: {
conditions: ['project_id = ?', 'assignee_id = ?'],
params: [projectId, userId],
},
}).execute();
await qb.delete({
tableName: 'sessions',
where: {
conditions: 'user_id = ?',
params: [userId],
},
}).execute();
const deleted = await qb.delete({
tableName: 'users',
where: {
conditions: 'id = ?',
params: [userId],
},
returning: ['id', 'email'],
}).execute();
console.log('Deleted:', deleted.results);
// Delete oldest 100 expired sessions
await qb.delete({
tableName: 'sessions',
where: 'expires_at < CURRENT_TIMESTAMP',
orderBy: 'expires_at ASC',
limit: 100,
}).execute();
For complex SQL not covered by the builder:
import { FetchTypes } from 'workers-qb';
// Fetch multiple rows
const results = await qb.raw({
query: 'SELECT * FROM users WHERE email LIKE ?',
args: ['%@example.com'],
fetchType: FetchTypes.ALL, // or 'ALL'
}).execute();
// Fetch single row
const user = await qb.raw({
query: 'SELECT * FROM users WHERE id = ? LIMIT 1',
args: [userId],
fetchType: FetchTypes.ONE, // or 'ONE'
}).execute();
// Execute without fetching (INSERT/UPDATE/DELETE)
await qb.raw({
query: 'UPDATE users SET last_seen = CURRENT_TIMESTAMP WHERE id = ?',
args: [userId],
}).execute();
// Get the SQL and parameters without executing
const { sql, params } = qb.select('users')
.where('id = ?', 1)
.where('status = ?', 'active')
.toSQL();
console.log(sql); // SELECT * FROM users WHERE (id = ?) AND (status = ?)
console.log(params); // [1, 'active']
// Get SQL with parameters interpolated - NEVER use for execution
const debugSql = qb.select('users')
.where('id = ?', 1)
.where("name = ?", "O'Brien")
.toDebugSQL();
console.log(debugSql); // SELECT * FROM users WHERE (id = 1) AND (name = 'O''Brien')
// Get the query execution plan
const plan = await qb.select('users')
.where('id = ?', 1)
.explain();
// Returns array of plan rows showing how the database will execute the query
console.log(plan.results);
// [{ id: 0, parent: 0, notused: 0, detail: 'SCAN users' }]
Register middleware-style hooks for all queries:
// beforeQuery - modify queries before execution
qb.beforeQuery((query, type) => {
// Add tenant filter to all queries
if (type !== 'INSERT' && type !== 'RAW') {
query.query = query.query.replace('WHERE', `WHERE tenant_id = ${tenantId} AND`)
}
return query
})
// afterQuery - log, modify results, record metrics
qb.afterQuery((result, query, duration) => {
console.log(`Query took ${duration}ms:`, query.query)
metrics.record(query.query, duration)
return result
})
// D1 uses batching - all queries succeed or all fail together
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 uses SQLite's native transaction support
// Should be called within blockConcurrencyWhile for proper isolation
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()
// Automatically commits on success, rolls back on error
})
})
Before executing queries, verify:
.execute() on the query (or .all(), .one(), .paginate())await for D1QB/PGQB, no await for DOQB? placeholders), not string interpolationRaw for SQL expressions (not strings) in data objects.toSQL() or .toDebugSQL() for debugging, not for execution// WRONG: Forgot .execute()
const users = await qb.fetchAll({ tableName: 'users' });
// CORRECT
const users = await qb.fetchAll({ tableName: 'users' }).execute();
// WRONG: String interpolation (SQL injection risk)
where: `email = '${userEmail}'`
// CORRECT: Parameterized
where: { conditions: 'email = ?', params: [userEmail] }
// WRONG: Using await with DOQB
const result = await doqb.fetchAll({ tableName: 'users' }).execute();
// CORRECT: No await with DOQB
const result = doqb.fetchAll({ tableName: 'users' }).execute();
下载完整 Skill 目录,包含 SKILL.md 及所有相关文件
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer