Mental model
Understand immutability, query modes, metadata, and result inference.
A few ideas make the rest of Kysoql much easier to reason about.
Kysely inspires the API, not the query language
Kysoql borrows fluent, immutable TypeScript patterns from Kysely where they fit. It does not try to turn SOQL into SQL.
| Familiar idea | Kysoql behaviour |
|---|---|
selectFrom, select, where, orderBy, limit, offset | Similar fluent names where SOQL has a matching concept. |
| SQL joins | Use Salesforce parent paths, child subqueries, and supported semi-joins or anti-joins. |
| SQL placeholders | Ordinary API values are escaped directly into compiled SOQL. |
| Mutations and transactions | Outside the scope of a SOQL query builder. |
| Kysely plugins | Kysoql has its own KysoqlPlugin contract. |
| Raw SQL | soql.raw() is Kysoql's explicit unsafe SOQL escape hatch. |
Apex binds are different from ordinary API values. They are available only in the compile-only Apex builders.
Your schema describes the org to TypeScript
new Kysoql<YourSchema>() uses the schema type to constrain what you can build. The usual schema comes from @kysoql/codegen and reflects Salesforce metadata such as fields, relationships, nullability, and query capabilities.
The TypeScript type itself is compile-time metadata. For features that need runtime metadata, such as Big Object index validation, pass the generated salesforceSchemaMetadata value as shown in the codegen guide.
Do not use the broad helper types from @kysoql/core as a replacement for an org-specific generated schema. They cannot know your custom objects or fields.
Builders are immutable
Every query method returns a new builder:
import { db } from "./db";
const base = db.selectFrom("Account").select(["Id", "Name"]);
const acme = base.where("Name", "like", "Acme%");
const firstPage = acme.orderBy("Name").limit(25);
console.log(firstPage.compile().soql);base still has no WHERE, ORDER BY, or LIMIT. That makes it safe to derive several queries from one starting point.
Query modes narrow the API
Kysoql changes the available methods as the query shape becomes more specific.
| Mode | How you enter it | Result or purpose |
|---|---|---|
| Record query | Select normal fields or supported SELECT functions | Readonly array of selected records. |
| Aggregate query | Select an aggregate or group first | Readonly array of aggregate rows. |
| Scalar count | Select bare fn.count() | One number. |
| Relationship subquery | Use selectSubquery() | Nested relationship result in the parent row. |
| Semi-join | Use an in or not in subquery callback | A filter only. |
| Apex | Call apex() or dynamicApex() at the supported point | Compile-only SOQL. |
Methods disappear when they would no longer describe valid SOQL. A bare count, for example, does not expose the same pagination API as a normal record query.
Result types follow selections
import { db } from "./db";
const query = db
.selectFrom("Account")
.select(["Id", "Name as name", "Owner.Name"]);The result type contains only what you selected.
Generated nullability carries through to field and relationship types. A field added inside $if becomes optional because it might not be selected at runtime. Aliases create the aliased property name in the result.
The bundled executors validate the Salesforce response envelope, not every selected field value at runtime. Types are not an authorization or runtime validation layer. See security and production use.
Compile or execute
compile().soql gives you the final SOQL string.
execute() needs a configured executor. The bundled executors follow root pagination for you. .executeAll() switches to Salesforce QueryAll mode rather than meaning "fetch all pages".
Use native REST execution for the normal transport and compile for Apex for Apex-only SOQL.
About the examples
Most guide snippets import db from the getting started guide.
Generate every object and field an example uses before running it. Names such as Office__c and Interests__c are illustrative custom metadata and should be replaced with API names from your org.