Skip to main content

Overview

@sqb/builder is SQB's query builder: a standalone, driverless way to compose Select, Insert, Update, and Delete statements as plain JS/TS objects, then serialize them to the correct SQL text for whichever database you target. It has no driver and makes no network connection — installing just @sqb/builder is enough to build and print SQL.

Why a query builder instead of raw strings

Concatenating SQL strings by hand gets error-prone fast — quoting identifiers correctly, building a WHERE clause out of conditions that may or may not be present, keeping parameter placeholders in sync with their values, and doing all of that differently for every database you target. A hand-rolled string template also doesn't compose: adding one more optional filter usually means restructuring the whole query.

@sqb/builder replaces that with a small set of chainable calls that read like the SQL they produce:

import { Select, Eq } from '@sqb/builder';

const query = Select('id', 'given_name', 'family_name')
.from('customers')
.where(Eq('active', true))
.orderBy('id')
.limit(10);

Conditions, columns, and joins are values you can build up conditionally in plain JavaScript — push another Eq()/In() into .where() only when a filter is actually present, no string surgery required.

Why a query builder instead of a bespoke query DSL

Some ORMs invent their own filter syntax ({ where: { age: { gt: 18 } } }-style objects) that you have to learn on top of SQL itself, and that inevitably can't express everything SQL can. @sqb/builder's API stays close to SQL's own vocabulary — Select, .from(), .join(), .where(), .groupBy(), .orderBy() — so what you already know about SQL mostly carries over directly.

Why it matters that it's dialect-aware

The same query object can be serialized for different databases, and SQB accounts for the SQL differences between them. Take one simple query:

import { Select } from '@sqb/builder';

const query = Select('*').from('customers').limit(10);

With no dialect plugin imported at all, .generate() still works, but pagination isn't rendered — .limit() is meaningless without dialect-specific knowledge of how to express it:

query.generate().sql;
// select * from customers

Import a dialect plugin and pass its name, and the exact same query object renders that database's real pagination syntax:

import '@sqb/postgres-dialect';

query.generate({ dialect: 'postgres' }).sql;
// select * from customers LIMIT 10
import '@sqb/oracle-dialect';

// Oracle < 12c has no FETCH FIRST — the dialect plugin wraps the query in a rownum subquery instead
query.generate({ dialect: 'oracle', dialectVersion: '11' }).sql;
// select * from (select * from customers) where rownum <= 10

// Oracle 12c+ gained real FETCH FIRST syntax
query.generate({ dialect: 'oracle', dialectVersion: '12' }).sql;
// select * from customers FETCH FIRST 10 ROWS ONLY
import '@sqb/mssql-dialect';

query.generate({ dialect: 'mssql' }).sql;
// select * from customers ORDER BY (SELECT NULL) OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY

Nothing about query changed between these calls — only which dialect plugins were imported and which dialect/dialectVersion were passed to .generate(). @sqb/builder never bundles dialect logic itself, and .generate() doesn't validate the dialect name — forgetting the import doesn't throw, it just silently falls back to the generic form shown first. See The Dialect Plugin System for the full mechanism, and Generating SQL per Dialect for the .generate() API itself.

Where to go next