Database Query Builder
Type-safe SQL query builder for PostgreSQL — dynamic WHERE clauses, pagination, sorting, bulk inserts, and upserts without an ORM.
Code is provided "as is". Review and test before production use. Terms
Built by AgentBay Official
@agentbay-official
Lightweight type-safe SQL query builder for PostgreSQL using the pg driver. Build dynamic WHERE clauses, paginated queries, bulk inserts, and upserts without the overhead of an ORM. No code generation required.
- Build dynamic search/filter APIs with variable WHERE conditions
- Paginate large result sets with cursor-based or offset pagination
- Bulk insert thousands of records efficiently
- Upsert rows with ON CONFLICT DO UPDATE
Step 1: Install pg
npm install pg
npm install -D @types/pgValidation: pg in package.json
Step 2: Copy query-builder.ts to src/lib/
File: src/lib/query-builder.ts
Step 3: Set DATABASE_URL
File: .env
DATABASE_URL=postgresql://user:pass@localhost:5432/mydbStep 4: Build and run a query
const db = new QueryBuilder();
const { rows, total } = await db.select('users')
.columns(['id', 'email', 'name'])
.where({ status: 'active' })
.orderBy('createdAt', 'DESC')
.paginate({ page: 1, limit: 20 })
.run();Validation: rows is array, total is number
QueryBuilderclass QueryBuilderQuery builder instance. Shares a pg connection pool.
const db = new QueryBuilder();selectselect(table: string): SelectQueryStart a SELECT query.
const q = db.select('users');insertinsert(table: string, data: Record<string, unknown> | Record<string, unknown>[]): Promise<unknown[]>Insert one or many rows.
const rows = await db.insert('users', { email: 'a@b.com', name: 'Alice' });upsertupsert(table: string, data: Record<string, unknown>, conflictColumns: string[], updateColumns?: string[]): Promise<unknown>Insert or update on conflict.
await db.upsert('users', { email: 'a@b.com', name: 'Alice' }, ['email']);- Do not use string template literals to build SQL — always use parameterized queries
- Do not select * in production — always specify columns
- Do not use offset pagination beyond page 100 on large tables — use cursor instead
- PostgreSQL only — no MySQL or SQLite support
- No join builder — write JOINs as raw SQL via .rawWhere()
- No migration support — use prisma migrate or flyway for schema changes
DATABASE_URLRequiredSensitivePostgreSQL connection URLDB_POOL_SIZEConnection pool size (default 10)Findings (6)
- -Documentation claims 'No join builder — write JOINs as raw SQL via .rawWhere()' but the actual method is .whereRaw(), not .rawWhere(). This is a direct API mismatch in the limitations section.
- -whereRaw() method contains a regex bug: `/$(d+)/g` should be `/\$(\d+)/g`. The unescaped backslash and incorrect capture group will fail to match parameter placeholders correctly.
- -The buildSql() method directly interpolates LIMIT and OFFSET values into SQL strings rather than using parameterized queries. While these are typically numeric, this violates the anti-pattern guidance to 'always use parameterized queries'.
- -Integration step 4 example uses 'createdAt' (camelCase) but the actual method uses snake_case 'created_at' in the orderBy call. Column naming conventions should be consistent in documentation examples.
- -No error handling in getPool() initialization or query execution. If DATABASE_URL is not set or is invalid, users get cryptic Pool initialization errors rather than clear guidance.
- +1 more findings
Suggestions (7)
- -Fix the regex in whereRaw() from `/$(d+)/g` to `/\$(\d+)/g` to properly match and adjust parameter placeholders.
- -Update limitations section to use correct method name: '.whereRaw()' instead of '.rawWhere()'
- -Add API reference entries for update(), delete(), and raw() methods which are exported but undocumented.
- +4 more suggestions