Approach
What Quarry is, what it is not, and the design principles that shape the public API.
Goals
Quarry is a ClickHouse-native TypeScript query builder. It is a query builder first — not an ORM, and not a generic multi-dialect SQL abstraction.
Inspiration
Quarry is not a port of Kysely or Drizzle, but both projects are useful reference points.
- Kysely influenced the overall query-builder architecture, the immutable chaining style, and a lot of the type-level thinking around source scope and selected output types.
- Drizzle is a useful reference point for ergonomics, schema-first thinking, and keeping the API approachable.
The goal is to learn from those projects without forcing ClickHouse into abstractions that were designed for other databases.
Why not a Kysely dialect or Drizzle adapter?
Because ClickHouse differs in ways that are not just syntax deep:
FINALbelongs to table sourcesPREWHEREis a distinct clauseSETTINGSis a query-level ClickHouse featureLEFT JOINruntime behavior differs from the null-default assumptions many SQL tools make- typed ClickHouse placeholders such as
{p0:UInt64}matter directly in the SQL text - ClickHouse result shapes and runtime value types do not always line up with the defaults expected by more generic SQL builders
Those differences make it possible to borrow ideas from Kysely and Drizzle without wanting to force Quarry into a dialect or adapter layer on top of them.
Quarry is not trying to be:
- a generic SQL abstraction across many databases
- an ORM
The current focus is a query builder that compiles to ClickHouse SQL cleanly and stays honest about runtime behavior.
Design principles
ClickHouse first
The API should model ClickHouse concepts directly instead of pretending every database behaves like Postgres.
That is why the current surface already includes things like:
final()prewhere(...)settings(...)
Runtime-honest typing
The builder should type execute() based on what @clickhouse/client actually
returns.
Examples from the current integration tests:
| ClickHouse type | TypeScript type at runtime |
|---|---|
UInt64 | string |
count() | string |
DateTime64(3) | string |
Decimal(18, 2) | number |
The goal is to avoid pretending values are safer or more transformed than they really are. See Runtime semantics for the details that matter when reading query results.
Builder and compiler stay separate
Internally, the library is still organized around:
builder -> AST -> compiler -> SQL + paramsThe AST is an implementation detail. The public API should feel like a query builder, not like AST construction.
Small explicit API over magical inference
When ClickHouse semantics are ambiguous, Quarry prefers explicit APIs over guessing.
Examples:
whereNull(...)/whereNotNull(...)instead of rawnullequalityparam(value, "Type")when the placeholder type should be explicit- first-class table sources like
db.table("event_logs").as("e").final()for advanced cases
Schema story today
Quarry keeps the schema story intentionally small.
Plain TypeScript DB types
This is the main way to use the query builder.
interface DB {
event_logs: {
user_id: number;
event_type: string;
created_at: string;
};
}
const db = createClickHouseDB<DB>({ client });This mode is great when you want:
- typed table and column names
- typed query results
- minimal setup
The important design point is that Quarry stays honest about what it is. It is not trying to become a separate schema-definition system or ORM layer.
That is why introspection now generates plain TypeScript DB types instead of a second Quarry-specific schema DSL. The query builder needs typed source names and typed selected rows. Plain TS covers that directly without asking you to model your database twice.