Compare

hypequery vs Raw SQL Strings in TypeScript

Raw SQL strings are the right call for one-off scripts and gnarly analytical queries. For the queries embedded in your app, hand-written SQL and interfaces drift from your schema. Here is where the line sits — and why hypequery keeps the raw-SQL escape hatch.

Decision type

Architecture and workflow fit

Audience

TypeScript teams building on ClickHouse

Outcome

Choose a path and move into implementation

Best for

Reused, app-embedded queries that must stay in sync with the schema

Schema drift

Rename a column and every affected query fails to compile

Refactoring

Find-references and rename work across the codebase

Injection safety

Parameterized by construction; values never interpolated

Escape hatch

selectExpr / rawAs / withCTE for the hard parts

What this page is for

Use this page when the real question is how one tool changes the shape of your ClickHouse application code, not just which syntax looks nicer.

What this page is not

It is not a broad ecosystem roundup. It stays narrow on the tradeoff a ClickHouse-heavy TypeScript team is actually deciding.

Recommended next move

If the tradeoff already looks clear, stop reading comparisons and test the fit against one real query in your own schema.

Dimension
hypequery
Alternative
Best for
Reused, app-embedded queries that must stay in sync with the schema
One-off scripts, migrations, and genuinely gnarly analytical SQL
Schema drift
Rename a column and every affected query fails to compile
Nothing fails until the query runs in production
Refactoring
Find-references and rename work across the codebase
String search, no IDE support, copy-paste reuse
Injection safety
Parameterized by construction; values never interpolated
Safe only if you never interpolate — easy to get wrong
Escape hatch
selectExpr / rawAs / withCTE for the hard parts
It is all raw SQL, all the time
Back to comparisons

Short answer

Do you even need a query builder? Sometimes not. If you're writing a one-off backfill script, a migration, or one genuinely gnarly analytical query — window functions, ASOF joins, three nested CTEs — a raw SQL string is the clearest thing you can write, and reaching for an abstraction just adds noise. hypequery agrees with this. It ships selectExpr, rawAs, and withCTE precisely so you can drop to raw SQL whenever the builder gets in the way. You never have to fight it.

The case for the builder isn't "raw SQL is bad." It's about the other SQL — the queries that live inside your application, get reused across endpoints, filtered by user input, and refactored as the schema changes. That's where raw SQL strings plus hand-written result interfaces quietly rot: rename a column and nothing fails until runtime. This page is about that practice — SQL strings wherever they live, in template literals, .sql files, or any client — not about any one library. (For the narrower comparison with the official transport client, see hypequery vs @clickhouse/client, which hypequery is actually built on.)

If you'd rather just see it run, the quick start goes from a live ClickHouse schema to a typed query in a few minutes.

Where raw SQL is genuinely the right call

Be honest about this up front, because it's where a lot of "just use a library" advice goes wrong:

  • One-off scripts and backfills. A script you run once and delete doesn't benefit from types. Write the SQL, run it, move on.
  • Migrations and DDL. CREATE TABLE, ALTER, engine and partition tuning — this is raw SQL territory and always will be.
  • Genuinely gnarly analytics. A query with several window functions, an ASOF JOIN, and correlated subqueries is often clearer as SQL than as any fluent chain. hypequery doesn't have an asofJoin method, and it's honest about that — ASOF JOIN is raw-SQL territory.

If that's the shape of your work, you may not need a builder at all. The builder earns its place when queries are repeated and embedded, not throwaway.

Where hand-written SQL strings start to hurt

The problems don't show up on day one. They show up three months later, in an app with fifty queries scattered across route handlers.

Schema drift is silent. You rename total to amount in ClickHouse. Your hand-written string SELECT sum(total) ... still compiles, still type-checks against your hand-written interface OrderRow, and still passes review. It fails at runtime, in production, when ClickHouse rejects the unknown column. Nothing in your toolchain warned you.

There's no refactoring support. Rename a column and you're doing a project-wide string search, hoping you catch every .sql file and every template literal. "Find all references" doesn't work on strings. Neither does rename-symbol.

Reuse becomes copy-paste. The same 20-line revenue query gets pasted into three endpoints with slightly different WHERE clauses. Now a fix has to land in three places, and they drift apart over time. (This exact pattern is the subject of stop writing the same query three times.)

Interpolation is an injection risk. The moment a raw string stops being static and starts including a runtime value, you're one careless template literal away from an injection bug:

Parameterization fixes this — but it's opt-in, and "remember to parameterize every value, forever, across the whole team" is not a safety model. It's a hope.

Before and after

Here's a realistic analytics query — daily revenue by region for one tenant — written both ways. First, the raw-string version with a hand-written interface:

That interface is a promise nobody enforces. revenue is typed as number, but sum() of a numeric column comes back from ClickHouse as a string in JS. Rename total and the string keeps compiling. The as RevenueRow[] cast papers over all of it.

Now the same query with hypequery:

No hand-written interface. The result type is inferred from the generated schema — including that revenue is a string, because that's what ClickHouse actually returns. tenant_id is parameterized by construction; there's no template literal to get wrong. Rename total in ClickHouse, re-run hypequery generate, and .sum('total', ...) stops compiling — in your editor, before the code ships.

The builder has edges, and it tells you where they are

This is the part that makes the tradeoff fair: hypequery doesn't pretend to cover every ClickHouse feature. Window functions aren't first-class builder methods. You express them with selectExpr, which drops a raw SQL expression into a typed .select():

And .withCTE() takes a raw SQL string when a subquery is easier written by hand than built. The point of these escape hatches is that adopting the builder is not an all-or-nothing bet. You get compile-time safety on the 90% of queries the builder covers cleanly, and raw SQL for the 10% where SQL is genuinely the better tool — in the same query, side by side. You never have to abandon the typed layer just because one clause is exotic.

The honest comparison

Raw SQL stringshypequery
Best forOne-off scripts, migrations, gnarly analyticsReused, app-embedded queries
Schema driftSilent until runtimeFails to compile
RefactoringString search onlyFind-references, rename-symbol
Result typesHand-written, unenforcedGenerated from live schema
Runtime type mappingYou remember it (UInt64 → string)Encoded in generated types
InjectionSafe only if you never interpolateParameterized by construction
ReuseCopy-pasteComposable query definitions
Exotic SQLNativeselectExpr / rawAs / withCTE

Getting started

If your queries are one-offs, keep writing SQL — that's the right tool, and hypequery would just be in the way. If they live in your app and keep changing, the quick start takes you from schema introspection to a typed query and a served endpoint in a few minutes. From there, the ClickHouse query builder guide covers filters, joins, and aggregations against generated types, and migrating raw SQL to hypequery walks through converting an existing string-based codebase one query at a time — keeping the raw-SQL escape hatch wherever it still makes sense.

Decision checkpoint

If the tradeoff is already clear, move into implementation

Most teams do not need another round of comparison content after this point. The better test is whether the workflow holds up on your own schema and your own query complexity.

Related content

Continue into implementation

FAQ

Is raw SQL ever the right choice over hypequery?

Yes. For a one-off backfill script, a migration, or a genuinely complex analytical query with window functions and ASOF joins, raw SQL is clearer than any builder. hypequery is aimed at the queries that live in your application and get reused, filtered, and refactored over time — not throwaway SQL.

Do I have to give up raw SQL entirely to use hypequery?

No. hypequery has escape hatches precisely so you never fight the builder: selectExpr and rawAs drop raw SQL expressions into a typed .select(), and .withCTE() takes a raw SQL subquery. You get compile-time safety on the parts the builder covers and raw SQL for the parts it doesn't.

How is this different from comparing hypequery to @clickhouse/client?

This page is about the practice of writing raw SQL strings and hand-written result interfaces wherever they live — template literals, .sql files, any client. The comparison with the official @clickhouse/client is about that specific transport library, which hypequery is actually built on top of.

Does the builder cover window functions and FINAL?

Partly. FINAL, LIMIT BY, CTEs, and array joins are first-class builder methods. Window functions are not — you express them with selectExpr('... OVER (...)', 'alias'). The builder is honest about its edges and hands you raw SQL where it stops.

Next step

Move from evaluation into a typed ClickHouse workflow

Generate schema types, define your first reusable query, and decide whether it should run locally or over HTTP.