소스 정보
- 저장소
- G4brym/workers-qb
- 최근 소스 활동
- 2026년 1월 24일 16:20
- 감지된 SKILL.md 언어
- 영어
- 스타
- 401
- 포크
- 26
설치 방법
기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.
소스 파일 검토
설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.
메뉴
기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.
설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
직접 명령은 검토 Prompt를 거치지 않습니다. 실행하기 전에 소스를 확인하세요.
npx skills add https://github.com/G4brym/workers-qb --skill write-sql-queries명령은 한 줄로 유지됩니다. 복사하기 전에 가로로 스크롤해 전체 내용을 확인하세요.
로컬 사본을 원하시나요? SkillsMP에서 현재 제공할 수 있는 파일을 다운로드하세요.
SOC 직업 분류 기준
SKILL.md 표시 중
| name | write-sql-queries |
| description | 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().([]))
.();
combined = qb.()
.([])
.(qb.().([]))
.(qb.().([]))
.({ : })
.();
// 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').([, ]).(),
[, ]
)
.();
// 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();