Skip to content

Kotlin: Write with filters

applyWhere evaluates the filters against the local rows of one table and applies the same write to each row that matches, one mutation per row. It returns the primary keys it touched, so the caller knows the size of the change before the first sync run carries it.

Examples#

Mark every open todo done#

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

val pks = kizunasync.applyWhere(
    table = "todos",
    op = KizunaSyncOp.Update,
    filters = JSONArray().put(KizunaSyncQuery.eq("done", false)),
    columns = mapOf("done" to true),
)

Delete the todos of one list#

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

val pks = kizunasync.applyWhere(
    table = "todos",
    op = KizunaSyncOp.Delete,
    filters = JSONArray().put(KizunaSyncQuery.eq("list_id", listId)),
)

Count every matching row up#

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

val pks = kizunasync.applyWhere(
    table = "todos",
    op = KizunaSyncOp.Update,
    filters = JSONArray().put(KizunaSyncQuery.eq("done", false)),
    transforms = mapOf("views" to mapOf("op" to "increment", "by" to 1)),
)

Parameters#

NameTypeRequiredDescription
tableStringYesTable name, which must be one of the keys of tables in Initializing.
opKizunaSyncOpYesThe operation applied to each match. KizunaSyncOp.Update and KizunaSyncOp.Delete are the meaningful values, because every target is a row that already exists.
filtersJSONArrayYesFilters that choose the rows, combined with and when there is more than one. The shapes are the ones in Using filters. An empty array is refused rather than treated as every row.
columnsMap<String, Any?>NoColumns assigned to each matched row, encoded to a JSONObject. Default: empty.
transformsMap<String, Any?>?NoField transforms applied to each matched row on top of the assigned columns. See Using transforms. Default: null.
preconditionMap<String, Any?>?NoColumn values the server compares per row before it applies that row's mutation. A row whose values moved is rejected with reason PRECONDITION while the others apply. Default: null.

Returns#

List<String>, the primary keys that matched, in the order the local store returned them. An empty list means nothing matched and nothing was queued.

Errors#

CodeCondition
UNKNOWN_TABLEtable is not one of the tables the client was created with.
LOCAL_CONSTRAINTA malformed transform on one of the matched rows: an entry that is not an object, a missing op, an op outside increment, arrayUnion, and arrayRemove, an increment without by or with a by that is not a signed integer, or an arrayUnion or arrayRemove without a values array.
LOCAL_UNSUPPORTEDfilters is empty, because an unfiltered update or delete would rewrite the whole table, or a filter the local evaluator refuses, such as an unusable like pattern.
JSONA filter kind or operand the engine cannot deserialize.
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 rows are applied one at a time, so a failure part of the way through leaves the earlier rows written and queued. Re-running the same call after the cause is fixed is safe for an update and for a delete, because both are idempotent per row.

The filter vocabulary follows Supabase Using filters, with the local-first difference that the filters run over local SQLite rather than reaching PostgREST: the match is decided on the device, including rows written while offline. Each matched row becomes its own outbox entry with its own verdict, so a policy that refuses one row does not revert the others. Offline writes shows how a rejection surfaces.

A filtered write is a convenience over Update data and Delete data, not a server-side statement. Rows a peer has but this device has not pulled are not matched, so run it after a Sync when the set has to be complete.

Filter matching skips a row the table's softDeleteColumn already marks. Fetch data applies the same exclusion to a read. A write has no includeDeleted option, so reach an already-marked row with the low-level apply(table, pk, op, ...) by primary key instead.

On such a table, a Delete that does match a row never removes it. It becomes an Update that stamps the column with one timestamp for the whole call. SOFT_DELETE_VIOLATION therefore never reaches this method; the low-level apply(op = KizunaSyncOp.Delete) is where that code is raised. See Delete data.

Found a problem? Edit this page on GitHub