Quarry
Reference

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

import { createClient } from "@clickhouse/client";
import { createClickHouseDB } from "@oorestisime/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.

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

You can call columns(...) before either 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.

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.

fromSelect(query)

Uses a select query as the insert source.

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.

toSQL()

Returns the compiled query without running it.

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(client?)

Runs the insert and returns a ClickHouseInsertResult.

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 pass a different client per call when needed:

await insertBuilder.execute(otherClient);

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