# Custom executors and testing (/docs/core/reference/custom-executors)



Most applications can use [`@kysoql/rest`](/docs/rest) or
[`@kysoql/jsforce`](/docs/jsforce). Build a custom executor when you already have
a transport layer or need tighter control over execution.

## Executor contract [#executor-contract]

Only `executeQuery` is required:

```ts
import type {
  AbortableQueryOptions,
  CompiledQuery,
} from "@kysoql/core";

interface ExampleExecutor {
  executeQuery<O>(
    query: CompiledQuery<O>,
    options?: AbortableQueryOptions,
  ): Promise<readonly O[]>;

  executeAllQuery?<O>(
    query: CompiledQuery<O>,
    options?: AbortableQueryOptions,
  ): Promise<readonly O[]>;

  executeCountQuery?(
    query: CompiledQuery<number>,
    options?: AbortableQueryOptions,
  ): Promise<number>;

  executeAllCountQuery?(
    query: CompiledQuery<number>,
    options?: AbortableQueryOptions,
  ): Promise<number>;
}
```

Expose an optional method only when the transport genuinely supports it. Kysoql
will not silently turn QueryAll into a normal query or reinterpret record arrays
as scalar counts.

A custom executor owns authentication, network calls, paging, response
validation, and transport errors. The generic output type is erased at runtime,
so casting arbitrary JSON to `O[]` is not validation.

## Wrap an existing executor [#wrap-an-existing-executor]

Wrappers are useful for tracing, metrics, or policy checks. Preserve optional
capabilities instead of replacing missing methods with fake fallbacks:

```ts
import type { QueryExecutor } from "@kysoql/core";

export function timedExecutor(
  inner: QueryExecutor,
  observe: (method: string, milliseconds: number) => void,
): QueryExecutor {
  const wrapped: QueryExecutor = {
    async executeQuery(query, options) {
      const started = performance.now();
      try {
        return await inner.executeQuery(query, options);
      } finally {
        observe("executeQuery", performance.now() - started);
      }
    },
  };

  if (inner.executeCountQuery) {
    wrapped.executeCountQuery = inner.executeCountQuery.bind(inner);
  }

  if (inner.executeAllQuery) {
    wrapped.executeAllQuery = inner.executeAllQuery.bind(inner);
  }

  if (inner.executeAllCountQuery) {
    wrapped.executeAllCountQuery = inner.executeAllCountQuery.bind(inner);
  }

  return wrapped;
}
```

Avoid logging raw tokens or query text when SOQL literals may contain sensitive
values.

## Test without Salesforce [#test-without-salesforce]

For most builder tests, compile the query and assert the SOQL. For adapter tests,
inject a fake `fetch` or a small executor that returns the exact response your
test needs.

```ts
import assert from "node:assert/strict";
import { Kysoql } from "@kysoql/core";
import { createRestExecutor } from "@kysoql/rest";
import type { SalesforceSchema } from "./kysoql/salesforce.generated";

const fetch: typeof globalThis.fetch = async () =>
  Response.json({
    done: true,
    totalSize: 1,
    records: [{ Id: "001000000000001AAA", Name: "Example account" }],
  });

const db = new Kysoql<SalesforceSchema>({
  executor: createRestExecutor({
    instanceUrl: "https://example.my.salesforce.com",
    accessToken: "test-only-token",
    fetch,
  }),
});

const rows = await db.selectFrom("Account").select(["Id", "Name"]).limit(1).execute();
assert.equal(rows[0]?.Name, "Example account");
```

Add transport-focused cases for pagination, malformed responses, count results,
QueryAll, abort signals, and error propagation when those behaviours matter to
your executor.

## Custom compilers [#custom-compilers]

`DefaultQueryCompiler` is the normal compiler. Replacing it is an advanced
extension point for callers that deliberately own their query serialization.
A custom compiler is responsible for preserving the escaping and structural
validation guarantees your application depends on.
