Skip to content

Kotlin: Fetch data

query evaluates a plan against the rows the local database holds and returns the result as parsed JSON. It reaches no network, so the answer is the same offline as online, and it is the rows the last Sync committed plus every local write waiting in the outbox.

Examples#

Basic#

// app/src/main/kotlin/com/example/todo/TodoScreen.kt (excerpt)
import com.kizunasync.kizunasync.KizunaSyncQuery
import org.json.JSONArray

val raw = kizunasync.query(
    table = "todos",
    plan = KizunaSyncQuery.many(KizunaSyncQuery.eq("done", false)),
)
val rows = raw as? JSONArray ?: JSONArray()

query answers Any, because the shape follows the plan's cardinality. Cast it to the shape you asked for.

Count the matches#

// app/src/main/kotlin/com/example/todo/TodoScreen.kt (excerpt)
import com.kizunasync.kizunasync.KizunaSyncCount
import org.json.JSONObject

val page =
    kizunasync.from("todos")
        .select("id, title", count = KizunaSyncCount.Exact)
        .eq("done", false)
        .range(0, 19)
        .execute() as JSONObject
val openTodos = page.getInt("count")

With a count, execute(), single(), and maybeSingle() answer a JSONObject with rows and count, and count is every open todo, not only the 20 on the page. Every KizunaSyncCount returns the exact local count.

CSV text and stripped nulls#

// app/src/main/kotlin/com/example/todo/TodoScreen.kt (excerpt)
val csv = kizunasync.from("todos").select("id, title, done").order("title").csv()
val sparse = kizunasync.from("todos").select().eq("id", todoId).stripNulls().single()

csv() answers a String: a header of the selected columns in their order, or of every key the rows carry for *, then one line per row. A field holding a quote, a comma, or a line break is quoted per RFC 4180, null is an empty field, and an array or an object is its JSON text. stripNulls() drops each row's null-valued keys.

Ordered and limited#

// app/src/main/kotlin/com/example/todo/TodoScreen.kt (excerpt)
import com.kizunasync.kizunasync.KizunaSyncQuery
import org.json.JSONArray

val raw = kizunasync.query(
    table = "todos",
    plan = KizunaSyncQuery.plan(
        filters = JSONArray().put(KizunaSyncQuery.eq("done", false)),
        order = JSONArray().put(KizunaSyncQuery.order("title")),
        limit = 20,
    ),
)

One row#

// app/src/main/kotlin/com/example/todo/TodoScreen.kt (excerpt)
import com.kizunasync.kizunasync.KizunaSyncQuery
import org.json.JSONObject

val raw = kizunasync.query(
    table = "todos",
    plan = KizunaSyncQuery.single(KizunaSyncQuery.eq("id", todoId)),
)
val todo = raw as? JSONObject

Parameters#

NameTypeRequiredDescription
tableStringYesTable name, which must be one of the keys of tables in Initializing.
planJSONObjectNoThe query plan, built by the KizunaSyncQuery helpers or written out key by key. Default: an empty object, which reads every row of the table.
plan.filtersJSONArrayNoFilter objects, combined with and when there is more than one. Using filters lists the shapes. Default: none.
plan.ordersJSONArrayNoSort keys, applied in order. Each entry takes column, an optional ascending (default true), and an optional nulls_first, which defaults to sorting nulls first only on a descending key. Default: the order the store returns.
plan.limitIntNoRows kept after filtering and sorting. Default: no limit.
plan.offsetIntNoRows skipped after filtering and sorting, before limit counts the rest. An offset past the last row matches nothing, and a negative one throws LOCAL_UNSUPPORTED. from(table).select() sets it, with limit, through range(from, to). Default: no row skipped.
plan.projectionJSONArrayNoColumns kept on each row. A named column the row does not hold comes back as null. Default: every column.
plan.cardinalityStringNo"many", "single", or "maybeSingle". Default: "many".
plan.includeDeletedBooleanNoBrings back the rows the table's softDeleteColumn marks, which every read leaves out by default. A table with no such column is unaffected. from(table).select() chains it as includeDeleted(). Default: false.
plan.countBooleanNoAnswers a JSONObject with rows, the usual answer, and count, the rows the filters matched before offset and limit. from(table).select(columns, head, count) sets it for any KizunaSyncCount. Default: false.

Returns#

Any, parsed from the engine's JSON. A "many" plan answers a JSONArray of row objects, "single" answers a JSONObject, and "maybeSingle" answers a JSONObject or JSONObject.NULL when nothing matched. It is never null, which is the same non-optional shape Swift returns. A plan with count wraps that answer in a JSONObject with rows and count, and the select builder answers the same object for head = true, with rows set to JSONObject.NULL. The select builder's csv() answers a String.

Errors#

CodeCondition
UNKNOWN_TABLEtable is not one of the tables the client was created with.
LOCAL_UNSUPPORTEDThe plan cannot be evaluated locally: a filter whose shape or operand the local evaluator refuses, a regexMatch pattern the local regex engine cannot compile, an overlaps operand that is not an array, or a cardinality outside the three names. The select builder also throws it for a clause filter(column, operator, value) cannot decode and for dryRun(), geojson(), and explain(…), each named with its reason in Using filters.
LOCAL_CONSTRAINTA "single" plan did not match exactly one row, or a "maybeSingle" plan matched more than one.
JSONThe engine cannot parse the encoded plan.
ENGINE_UNAVAILABLEThe client has no engine because Initializing has not run.

A value org.json refuses never reaches the engine: the client raises a JSONException while encoding, so it surfaces as that exception rather than an engine code.

Every code in the table is the code on a thrown KizunaSyncError.Engine; message reads "CODE: message", but match on code rather than parsing the string.

Notes#

The operators and modifiers keep the meaning Supabase documents for select(), with three local-first differences: the rows come from local SQLite rather than from PostgREST, they already include writes that have not reached the server, and an operator the local evaluator does not implement throws LOCAL_UNSUPPORTED instead of being sent to the database. How Kizuna works explains why the read is local, and Supported query operators lists every postgrest-js method with its Kotlin name and status.

Row Level Security decides which rows ever arrive, but it does so during the pull, not during this call. A row the policy hides is never delivered, so it cannot be read here.

There is no live query on the native clients. Re-run query or from(table).select() after LOCAL_CHANGED from Subscribe to events to refresh a screen.

Found a problem? Edit this page on GitHub