Kysoql
Compile for Apex

Apex bindings and expressions

Build typed Apex variable references without dropping down to raw SOQL.

The helpers on this page are exported by @kysoql/core and become available after .apex() or .dynamicApex(). They describe expressions that your Apex caller will provide at runtime.

Scalar and collection binds

import { apexBind } from "@kysoql/core";
import { db } from "./db";

const compiled = db
  .selectFrom("Account")
  .select(["Id", "Name"])
  .apex()
  .where((eb) =>
    eb.or([
      eb("Name", "=", apexBind<string>("filters.accountName")),
      eb("Id", "in", apexBind<readonly string[]>("accountIds")),
    ]),
  )
  .compile();

apexBind<T>(expression) accepts an Apex identifier or dotted member path. Leave off the leading colon. Calls, indexing, whitespace, and arbitrary expression text are rejected.

The generic T checks compatibility with the surrounding query. It cannot prove that the Apex variable exists or has a valid value at runtime.

Limits and offsets

import { apexBind } from "@kysoql/core";
import { db } from "./db";

const compiled = db
  .selectFrom("Account")
  .select("Id")
  .apex()
  .limit(apexBind<number>("page.rowLimit"))
  .offset(apexBind<number>("page.rowOffset"))
  .compile();

Literal limits and offsets are validated by Kysoql. Bound values only exist in Apex, so validate their runtime range there.

Addition and concatenation

Use apexAdd() for supported string concatenation or numeric addition:

import { apexAdd, apexBind } from "@kysoql/core";
import { db } from "./db";

const compiled = db
  .selectFrom("Account")
  .select("Id")
  .apex()
  .where("Name", "=", apexAdd("Ac", "me"))
  .limit(apexAdd(apexBind<number>("page.baseLimit"), 1))
  .compile();

This helper is intentionally small. It is not an arbitrary Apex expression parser.

String substring

import { apexAdd, apexBind, apexSubstring } from "@kysoql/core";
import { db } from "./db";

const compiled = db
  .selectFrom("Account")
  .select("Id")
  .apex()
  .where(
    "Name",
    "like",
    apexAdd(apexSubstring(apexBind<string>("filters.name"), 0, 2), "%"),
  )
  .compile();

apexSubstring() accepts a string literal or string-valued bind expression and non-negative literal indices. Runtime string length still belongs to the Apex caller.

Read a field from a single-record query

import { apexBind, apexQueryField } from "@kysoql/core";
import { db } from "./db";

const sourceAccount = db
  .selectFrom("Account")
  .select("Name")
  .apex()
  .where("Id", "=", apexBind<string>("sourceAccount.Id"));

const compiled = db
  .selectFrom("Account")
  .select("Id")
  .apex()
  .where("Name", "=", apexQueryField(sourceAccount, "Name"))
  .compile();

apexQueryField() models Apex bracket-query field access. Kysoql does not add LIMIT 1, so make sure the inner query is constrained to exactly one record.

Bind-left INCLUDES

Apex also supports Kysoql's bind-left includes overload:

import { apexBind } from "@kysoql/core";
import { db } from "./db";

const compiled = db
  .selectFrom("Account")
  .select("Id")
  .apex()
  .where(apexBind<string>("account.Type"), "includes", [
    "Customer - Direct; Customer - Channel",
  ])
  .compile();

This is a specific overload for includes, not a general bind-left filter API.

Binds in child subqueries

Enter Apex before creating the child subquery when its filters need binds:

import { apexBind } from "@kysoql/core";
import { db } from "./db";

const compiled = db
  .selectFrom("Account")
  .select("Id")
  .apex()
  .selectSubquery("Contacts", (contacts) =>
    contacts
      .select(["Id", "LastName"])
      .where("LastName", "like", apexBind<string>("filters.lastName")),
  )
  .compile();

Child builders remain child builders. They do not gain root-only Apex features. Knowledge article Apex queries also reject bind expressions.

See Compile for Apex for context rules and support boundaries for broader restrictions.

On this page