Quarry

InsertQueryBuilder

Reference for every method on InsertQueryBuilder.

InsertQueryBuilder is what you get back from db.insertInto(table). It is much smaller than SelectQueryBuilder because inserts in Quarry still have a deliberately narrow shape:

  • you pick a table,
  • you optionally pick target columns,
  • you choose one source,
  • you execute.

That source can be either:

  • a batch of rows via values([...]), or
  • a select query via fromSelect(...).

For row inserts, the compiled query is INSERT INTO <table> FORMAT JSONEachRow and the actual values are sent to the driver via its native insert() API. For fromSelect(...), the compiled query is INSERT INTO <table> SELECT ... and executes through the driver's command API.

How you get one

TypeScript
import { createClient } from "@clickhouse/client";
import { createClickHouseDB } from "quarry";

interface DB {
  users: {
    id: number;
    email: string;
    status: string;
  };
}

const db = createClickHouseDB<DB>({
  client: createClient({ url: "http://localhost:8123" }),
});

const insert = db.insertInto("users");

db.insertInto<Table>(table) is type-checked against your schema — passing a table name that does not exist on DB is a compile error, and the returned builder is parameterised by that table's row type.

columns(...columns)

Narrows the target columns for the insert.

TypeScript
db.insertInto("users").columns("id", "email");

This works with both insert shapes:

  • with values([...]), it narrows the accepted row shape at the type level
  • with fromSelect(...), it defines the target-side column list in the generated INSERT INTO table (col, col, ...) SELECT ... statement

Call columns(...) before values(...) or fromSelect(...).

values(rows)

Sets the rows to be inserted. Accepts a readonly Row[] and returns a new builder. Each row is type-checked against the table's row type from your schema.

TypeScript
db
  .insertInto("users")
  .values([
    { id: 1, email: "alice@example.com", status: "active" },
    { id: 2, email: "bob@example.com",   status: "active" },
  ]);

values() and fromSelect() are mutually exclusive. Once either one has set the insert source, trying to set it again throws:

Insert source has already been set for this query.

Insert builders model one insert operation, not an accumulating buffer. If you want to insert two batches, or use both patterns, build two queries.

Automatic value normalization

Quarry normalizes bound query parameter values when it has an explicit ClickHouse type, but insertInto(...).values(...) rows are otherwise sent as you provide them.

If you need exact ClickHouse date/time formatting in inserts, pass explicit strings or use INSERT INTO ... SELECT with typed expressions.

fromSelect(query)

Uses a select query as the insert source.

TypeScript
db
  .insertInto("daily_aggregates")
  .columns("user_id", "event_date", "event_count")
  .fromSelect(
    db
      .selectFrom("event_logs as e")
      .selectExpr((eb) => [
        "e.user_id",
        eb.fn.toDate("e.created_at").as("event_date"),
        eb.fn.count().as("event_count"),
      ])
      .where("e.created_at", ">=", param("2025-01-01", "Date"))
      .groupBy("e.user_id", (eb) => eb.fn.toDate("e.created_at")),
  );

fromSelect(...) accepts a normal SelectQueryBuilder, so the source query can use CTEs, joins, filters, aggregates, and query parameters.

ClickHouse maps source values to target columns by position. Quarry requires explicit .columns(...) and explicit source selections, and checks the ordered types and column count. Aliases do not change insertion order; selecting email, id for targets id, email fails typechecking when their types differ. Columns with the same TypeScript type still require care: types cannot distinguish two different string columns. Table stars are rejected because their order is not represented by the schema interface.

toSQL()

Returns the compiled query without running it.

TypeScript
const compiled = db
  .insertInto("users")
  .values([{ id: 1, email: "alice@example.com", status: "active" }])
  .toSQL();

compiled.query;
// INSERT INTO users FORMAT JSONEachRow
compiled.params;
// {}
compiled.values;
// [{ id: 1, email: "alice@example.com", status: "active" }]

For fromSelect(...), compiled.values is undefined and compiled.params contains any bound parameters from the nested select query.

The returned object shape is:

Prop

Type

For values(...), the query string never contains the row data — the values travel separately through the driver's insert() API. This is how Quarry keeps large batch inserts efficient.

toSQL() throws if no source has been set:

Cannot compile an insert without a source

execute(options?)

Runs the insert and returns a ClickHouseInsertResult.

TypeScript
const result = await db
  .insertInto("users")
  .values([{ id: 1, email: "alice@example.com", status: "active" }])
  .execute();

If you passed a client to createClickHouseDB({ client }), that client is used by default. You can override it per call and pass driver-level execution settings when needed:

TypeScript
await insertBuilder.execute({
  client: otherClient,
  queryId: "users-insert",
  clickhouse_settings: {
    async_insert: 1,
  },
});

Execution depends on the insert source:

  • values(...) uses the driver's insert() method with format: "JSONEachRow"
  • fromSelect(...) uses the driver's command() method with the compiled INSERT INTO ... SELECT ... SQL and parameter map

execute() throws in these cases:

Error messageWhen it happens
Cannot execute an insert without a sourceYou called execute() before values() or fromSelect().
No ClickHouse insert client configured. Pass one to execute() or createClickHouseDB().The insert uses values(...), but no insert-capable client is available.
No ClickHouse command client configured. Pass one to execute() or createClickHouseDB().The insert uses fromSelect(...), but no command-capable client is available.

toAST()

Returns a structured clone of the internal insert AST node. This is an escape hatch for tooling and tests — you do not need it for normal use.

What InsertQueryBuilder is not

It does not have:

  • a where(...) method — ClickHouse inserts cannot be conditional at the query level. If you need filtering or transformations, build them into the select you pass to fromSelect(...).
  • a returning(...) method — ClickHouse does not support INSERT ... RETURNING.
  • an as(...) method — insert queries cannot be used as subqueries.
  • an executeTakeFirst(...) variant — inserts return a single ClickHouseInsertResult, not a row list.

If you need any of those, you are reaching for the wrong builder — either use SelectQueryBuilder for query work, or fall through to the underlying @clickhouse/client for things Quarry does not model.

Type parameters

InsertQueryBuilder<Table, Row> carries two parameters that encode the target of the insert:

ParameterWhat it represents
TableThe table name as a string literal type. Used to format the INSERT INTO clause and to track which table this builder is bound to.
RowThe row type for that table, derived from the schema you passed to createClickHouseDB.

You almost never write these by hand — they are inferred from the db.insertInto(...) call.

On this page