# Native REST execution (/docs/rest/execution)



<SalesforceReference title="REST API Query resource" href="https://developer.salesforce.com/docs/platform/api-rest/guide/resources-query.html" />

`@kysoql/rest` is the native `fetch` executor for Kysoql. Use it when you want Salesforce REST execution without adding JSforce to your application.

## Configure an executor [#configure-an-executor]

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

export function createDatabase(instanceUrl: string, accessToken: string) {
  const executor = createRestExecutor(
    {
      instanceUrl,
      accessToken,
      apiVersion: "65.0",
      timeoutMs: 30_000,
    },
    {
      batchSize: 1000,
      maxPages: 100,
      maxRecords: 100_000,
    },
  );

  return {
    db: new Kysoql<SalesforceSchema>({ executor }),
    executor,
  };
}
```

Use the authenticated org's HTTPS instance URL. `batchSize`, `maxPages`, and `maxRecords` are client-side controls, not changes to your SOQL `LIMIT`.

The REST client defaults to API version `65.0`. Set the version explicitly when your application needs a different supported Salesforce API version. Runtime options do not read `kysoql.config.ts` or environment variables automatically.

## Execute records and counts [#execute-records-and-counts]

Once the executor is attached, normal builder methods can run directly:

```ts
import { db } from "./db";

const accounts = await db
  .selectFrom("Account")
  .select(["Id", "Name"])
  .where("Name", "like", "Acme%")
  .limit(100)
  .execute();

const count = await db
  .selectFrom("Account")
  .select(({ fn }) => fn.count())
  .execute();
```

Record and grouped aggregate queries return readonly arrays. Bare `COUNT()` returns a number.

The executor follows Salesforce root continuation pages automatically. Selected child relationship queries are completed too, including supported nested child subqueries. Each compiled `LIMIT` remains the record ceiling for that query level.

No matches return `[]` for row queries or `0` for a bare count. Errors are never converted into empty success results.

## QueryAll is a separate query mode [#queryall-is-a-separate-query-mode]

Use `.executeAll()` only when you need Salesforce QueryAll semantics:

```ts
import { db } from "./db";

const recordsIncludingDeleted = await db
  .selectFrom("Account")
  .select(["Id", "Name", "IsDeleted"])
  .limit(100)
  .executeAll();
```

`execute()` starts with Salesforce `/query`. `executeAll()` starts with `/queryAll` so eligible deleted records and archived activities can be included.

Both modes paginate. QueryAll does not bypass permissions, and it does not recover permanently removed records. Apex `ALL ROWS` is a separate compile-only feature.

## Iterate large root results [#iterate-large-root-results]

`.execute()` collects the root result in memory. When you want to process root rows progressively, call the executor with a compiled query:

```ts
import type { RestExecutor } from "@kysoql/rest";
import { db } from "./db";

export async function processAccounts(
  executor: RestExecutor,
  signal: AbortSignal,
) {
  const query = db.selectFrom("Account").select(["Id", "Name"]).compile();

  for await (const account of executor.iterateQuery(query, { signal })) {
    console.log(account.Id);
  }
}
```

`queryPages()` yields complete root response pages. `iterateQuery()` yields root records.

Before a root page is yielded, any selected child relationship continuations in that page are completed. This keeps relationship results convenient, but it also means one root page can involve several Salesforce requests.

Stopping iteration prevents later root pages from being fetched. Rows already yielded cannot be rolled back if a later request fails.

## Cancellation and timeouts [#cancellation-and-timeouts]

Builder execution accepts `{ signal }`:

```ts
import { db } from "./db";

const accounts = await db
  .selectFrom("Account")
  .select(["Id", "Name"])
  .execute({ signal: AbortSignal.timeout(60_000) });
```

The executor also accepts `signal` and `timeoutMs` on direct calls.

The configured `timeoutMs` applies to each HTTP request rather than the whole multi-page query. Use an overall `AbortSignal` when you need a deadline for the complete operation.

## Child relationship pagination [#child-relationship-pagination]

<SalesforceReference title="Relationship query results" href="https://developer.salesforce.com/docs/platform/salesforce-soql-sosl/guide/sforce-api-calls-soql-relationships-query-results.html" />

You do not need to follow child `nextRecordsUrl` values yourself when using the native executor:

```ts
import { db } from "./db";

const accounts = await db
  .selectFrom("Account")
  .select("Id")
  .selectSubquery("Contacts", (contacts) =>
    contacts
      .select(["Id", "LastName"])
      .selectSubquery("Cases", (cases) =>
        cases.select(["Id", "Subject"]).limit(50),
      )
      .limit(500),
  )
  .limit(25)
  .execute();
```

Put deliberate limits on relationships that can grow large. Without a child `LIMIT`, the executor drains that selected relationship until Salesforce marks it complete.

## Resource limits [#resource-limits]

The default page budget is 10,000 requests across root and relationship continuation pages. There is no root record cap unless you set `maxRecords`.

A budget overflow throws `SalesforceQueryLimitError` rather than returning a truncated result.

`batchSize` can request a Salesforce query batch size from 200 to 2000. Salesforce may still return fewer root rows, especially when relationship rows contribute to the response size.

<Accordions type="single">
  <Accordion title="What does the page budget count?">
    Every Salesforce query page fetched for one execution, including root pages and selected child relationship continuations.
  </Accordion>

  <Accordion title="What does the record budget count?">
    Root records only. Child relationship sizes are controlled by their own query `LIMIT` values or by Salesforce completion when no limit is present.
  </Accordion>

  <Accordion title="Is this Bulk API streaming?">
    No. This is Salesforce Query REST. Async iteration avoids collecting every root row at once, but each response page is still received and parsed as a normal REST response.
  </Accordion>
</Accordions>

## Errors and retries [#errors-and-retries]

| Error                       | What it means                                                        |
| --------------------------- | -------------------------------------------------------------------- |
| `SalesforceRestError`       | Salesforce returned a non-success HTTP response.                     |
| `SalesforceResponseError`   | Salesforce returned malformed, incomplete, or unsafe response data.  |
| `SalesforceQueryLimitError` | A configured page or record budget would be exceeded.                |
| `SalesforceOAuthError`      | Token acquisition or renewal failed.                                 |
| Native fetch or abort error | The network request failed, timed out, redirected, or was cancelled. |

When an access-token provider is configured, the only automatic replay is one request after `401 INVALID_SESSION_ID`. Rate limits, network failures, 5xx responses, and general query errors are not retried automatically.

Structured error messages avoid placing credentials or SOQL in the main message. Salesforce error details can still contain business data, so treat them as sensitive when logging.

## Share a REST client [#share-a-rest-client]

Codegen and query execution can share one native client:

```ts
import { createRestDescribeClient } from "@kysoql/codegen";
import { createRestClient, createRestExecutor } from "@kysoql/rest";

export function createClients(instanceUrl: string, accessToken: string) {
  const client = createRestClient({ instanceUrl, accessToken });

  return {
    executor: createRestExecutor(client),
    describe: createRestDescribeClient(client),
  };
}
```

The lower-level `RestClient.request()` is GET-only and returns `unknown`. Prefer the typed executor and Describe client for supported operations unless you have a specific reason to work at the raw REST layer.
