Docs Client library reference JavaScript
v0.2.6-alpha.3JavaScript: Fetch data
from(table).select(columns?, options?) returns a builder that reads local SQLite. Filters, .order(), .limit(), .range(), and the other modifiers chain on it, and awaiting the builder runs the read. The call signature matches .select() in supabase-js, including the count and head options.
Examples#
Basic#
// src/todo-list.ts
import { kizunasync } from './kizunasync'
const { data } = await kizunasync
.from('todos')
.select('id, title, done')
.eq('done', false)
.order('title', { ascending: true })Single row#
// src/todo-list.ts
import { kizunasync } from './kizunasync'
export async function loadTodo(todoId: string) {
const { data } = await kizunasync.from('todos').select('*').eq('id', todoId).single()
return data
}React: useQuery and Vue: useQuery take the same chain ending in .single() or .maybeSingle() and hold that row, or null, in data.
One page of rows#
// src/todo-list.ts
import { kizunasync } from './kizunasync'
const PAGE_SIZE = 20
export async function loadPage(page: number) {
const from = page * PAGE_SIZE
const { data } = await kizunasync
.from('todos')
.select('id, title, done')
.order('title')
.range(from, from + PAGE_SIZE - 1)
return data
}Both ends of range(from, to) are inclusive and count from zero after the sort, so page 1 holds rows 20 through 39.
Count the matches#
// src/todo-list.ts
import { kizunasync } from './kizunasync'
export async function loadOpenPage() {
const { data, count } = await kizunasync
.from('todos')
.select('id, title', { count: 'exact' })
.eq('done', false)
.range(0, 19)
return { todos: data, openTodos: count }
}count is the number of open todos before .range() cut the page. Pass head: true as well to get the count with data set to null.
Typed rows#
// src/todo-list.ts
import { kizunasync } from './kizunasync'
interface ITodo {
id: string
title: string
done: boolean
}
export async function loadTodos(): Promise<ITodo[]> {
const { data } = await kizunasync.from('todos').select('id, title, done').returns<ITodo[]>()
return data
}returns() and overrideTypes() change only the static type, as in supabase-js; the rows are the same rows.
CSV text#
// src/todo-list.ts
import { kizunasync } from './kizunasync'
export async function exportTodos(): Promise<string> {
const { data } = await kizunasync.from('todos').select('id, title, done').order('title').csv()
return data
}Stop a read the screen no longer needs#
// src/todo-list.ts
import { kizunasync } from './kizunasync'
export function loadTodosUntil(signal: AbortSignal) {
return kizunasync.from('todos').select('id, title').abortSignal(signal)
}When signal aborts before the read settles, the promise rejects with the signal's reason and the rows are discarded.
Parameters#
| Name | Type | Required | Description |
|---|---|---|---|
columns | string | No | Comma-separated projection. *, an empty string, and an omitted argument all return every stored column. A projected column the row does not carry comes back as null. Default: every column. |
options.count | 'exact' | 'planned' | 'estimated' | No | Returns, in count, how many rows the filters match before .range() and .limit(). The kernel counts every match, so all three modes return the exact count. Default: no count, and count is null. |
options.head | boolean | No | Returns the count without the rows: data is null. Default: false. |
The builder carries the filters on Using filters plus the modifiers below. All of them join the plan the kernel evaluates when the read executes.
| Name | Type | Required | Description |
|---|---|---|---|
.order(column, options?) | (column: string, options?: { ascending?: boolean; nullsFirst?: boolean }) => ILocalSelectBuilder | No | Sorts the matched rows. The first call in the chain is the primary sort and later calls break ties. ascending defaults to true, and nullsFirst (the wire key the plan carries) defaults to the negation of ascending. |
.limit(count) | (count: number) => ILocalSelectBuilder | No | Truncates after sorting. count must be an integer; a fractional count throws LOCAL_UNSUPPORTED synchronously, and a negative count is refused LOCAL_UNSUPPORTED when the read executes. |
.range(from, to) | (from: number, to: number) => ILocalSelectBuilder | No | Keeps the rows at indexes from through to after sorting, both ends inclusive and counted from zero, the way supabase-js reads them. The plan skips from rows and keeps to - from + 1, so a to one below from keeps no row, and a window past the last row matches nothing. A later .limit() replaces only the row count, and a later .range() replaces both. from and to must be integers of zero or more, with to at least from - 1; anything else throws LOCAL_UNSUPPORTED synchronously. |
.includeDeleted() | () => ILocalSelectBuilder | No | Keeps the rows a table's softDelete column marks. Without it a marked row is excluded before the plan runs, so .limit() counts only the rows you can see. No effect on a table with no softDelete column. |
.stripNulls() | () => ILocalSelectBuilder | No | Returns each row without its null-valued keys. The one-row terminals carry it too. |
.abortSignal(signal) | (signal: AbortSignal) => ILocalSelectBuilder | No | Rejects the read with signal.reason when the signal aborts before the read settles, and discards its rows. A signal with no reason rejects with an AbortError DOMException. |
.throwOnError() and .retry(enabled) | () => ILocalSelectBuilder | No | Return the builder unchanged. A local read already rejects on error, and it makes no network attempt to retry. |
.returns<TResult>() | () => ILocalSelectBuilder<TResult[number]> | No | Types the rows. TResult is the whole result type, an array for a list, as in supabase-js. Nothing changes at runtime. |
.overrideTypes<TResult, TOptions>() | () => ILocalSelectBuilder | No | Merges TResult into the row type, or replaces it with { merge: false }, as in supabase-js. Nothing changes at runtime. |
.csv() | () => ISelectCsvQuery | No | Terminal. Returns the rows as CSV text: a header of the selected columns in their order, or of every key the rows carry in the order it first appears for *, then one line per row. Fields holding a quote, a comma, or a line break are quoted per RFC 4180, null is an empty field, and an array or object is its JSON text. Lines end with \n. |
.single() | <TRow>() => ISelectSingleQuery<TRow> | No | Terminal. Yields the one matching row and throws when the match count is not exactly one. The type argument types that row. No further chaining is available after it. |
.maybeSingle() | <TRow>() => ISelectMaybeSingleQuery<TRow> | No | Terminal. Yields the matching row or null, and throws on more than one. |
Returns#
Promise<ISelectResult> by default, and the two terminals narrow it.
| Name | Type | Required | Description |
|---|---|---|---|
data | TColumnValues[] | — | The projected rows, sorted and truncated. .single() narrows this to one TColumnValues object, .maybeSingle() to TColumnValues | null, and .csv() to a string. null when options.head is true. |
error | null | — | Always null. A local read that cannot be served throws instead of returning an error envelope. |
count | number | null | — | The rows the filters matched before .range() and .limit() when options.count is set, else null. |
Errors#
| Code | Condition |
|---|---|
UNKNOWN_TABLE | from(table) names a table absent from Define config. |
LOCAL_UNSUPPORTED | The projection contains (, a relational embed, which the local store cannot follow because it holds each synced table without foreign-key joins (read related rows with a second query), or :, a rename. Both are refused when the read executes rather than when the column list is written. A negative .limit() count. .range() throws synchronously on a bound that is not an integer of zero or more, or on a to below from - 1. options.count outside exact, planned, and estimated throws synchronously. Also raised by the filters Using filters lists, such as a regexMatch() pattern the local regex engine cannot compile. .order(), .limit(), and .range() throw naming referencedTable or its deprecated alias foreignTable for the same reason as an embed. The postgrest-js methods with no local meaning are typed stubs that throw naming themselves and the reason, never a bare TypeError: rangeGt(), rangeGte(), rangeLt(), rangeLte(), and rangeAdjacent() (Postgres range types have no local representation), geojson() (PostGIS output has no local representation), explain() (it describes the server query planner), rollback() (there is no server transaction; local writes enter the outbox), setHeader() (a local read sends no HTTP request), and maxAffected() (it caps an update or a delete). stripNulls() after .csv() throws the same way, as it does in supabase-js. |
LOCAL_CONSTRAINT | .single() matched zero rows or more than one, or .maybeSingle() matched more than one. The kernel's message names the method and the count, for example query: single() requires exactly one row; got 0. |
This method throws its errors rather than returning them, so wrap the await in try when a query can violate one of the conditions above.
Notes#
Every read is served from the local database, and there is no network fallback: rows a peer wrote appear only after a pull commits a boundary, which the automatic loop runs on its own and Sync or Pull once runs on demand. Rows your own app wrote are visible immediately, before the outbox drains, which is the optimistic path Offline writes describes. How Kizuna works places the local read in the whole cycle.
The server decides Row Level Security, never the device. The local store holds what earlier pulls delivered, so the server decided visibility when those rows arrived. The policies themselves stay in Supabase.
Supported query operators lists every postgrest-js method and option with its status in the JavaScript, Swift, and Kotlin app clients, and the reason each unsupported one is refused. An unsupported call throws a typed TEngineError with code LOCAL_UNSUPPORTED, from kizunasync, and nothing falls back to the network.
The app client selects the engine when its first use opens it, as Initializing describes. It reads a driver-carried engine transport first, which is how the browser runs Rust as WebAssembly in its worker. Failing that, it runs Rust through a linked UniFFI binding or the N-API addon. In every case the driver reports databasePath. A file path lets the kernel open the same database. null or the literal ':memory:' creates a private in-memory database. Where no binding resolves, that first call rejects with a typed TEngineError with code ENGINE_UNAVAILABLE that names what to install, rather than downgrading to another engine, and so does every later call. The kernel runs the conformance corpus for the supported local subset, and Project status records which runtime reaches it how.
An update or delete whose filter matches no local row is a no-op with no error, matching supabase-js zero-row behavior. Every supported filter is evaluated by the kernel over the local SQLite store, and none of them reaches the network. Offline writes covers what happens to the mutation those builders queue.
Example: filters offline#
// src/todo-list.ts
import { kizunasync } from './kizunasync'
const { data } = await kizunasync
.from('todos')
.select('id, title, rank')
.gte('rank', 2)
.or('done.eq.false,title.ilike.%urgent%')
.order('rank')
.limit(20)Example: handling LOCAL_UNSUPPORTED#
// src/todo-list.ts
import { EEngineErrorCode, TEngineError } from 'kizunasync'
import { kizunasync } from './kizunasync'
try {
await kizunasync.from('todos').select().regexMatch('title', '(works) \\1')
} catch (error) {
if (error instanceof TEngineError && error.code === EEngineErrorCode.LOCAL_UNSUPPORTED) {
// the local regex engine has no backreferences, so match the title with a simpler pattern
}
}Related reference#
Found a problem? Edit this page on GitHub