Documentation

$db and @table

A table is a data class. A query is the list operation you would have written, lowered to SQL. A read in markup subscribes, so a write redraws whatever read it.

A table is a data class

// src/lib/models/Todo.wh
@table data class Todo(
  val id: Long = 0,            // the key; 0 is assigned on insert
  val title: String,
  val done: Boolean = false,
)

@table on a data class makes it a SQLite table. The fields are the columns, and id is the key: 0 means "not stored yet" and is replaced on insert. Reach the table as $db.Todo.

Queries are collection operations

var query = $state("")
val open = $db.Todo.filter { !it.done && it.title.contains(query, ignoreCase = true) }.sortedBy { it.title }

@for (todo in open, key = { it.id }) {
  <Checkbox checked={todo.done} onCheckedChange={(v) => $db.Todo.update(todo.copy(done = v))} />
}
<Button onClick={() => $db.Todo.insert(Todo(title = query))}>Add</Button>

The call is what you would write on a List; the compiler turns the part SQL can express into SQL:

CallSQL
$db.Todo.all(), .get(id)every row; one row or null
.filter { … }WHERE — it.col with == != < <= > >=, && || !, and .contains / .startsWith / .endsWith(…, ignoreCase = true)
.sortedBy { }, .sortedByDescending { }ORDER BY
.take(n), .count(), .first { }, .firstOrNull { }LIMIT, COUNT, LIMIT 1

A predicate SQL cannot express is a build error; nothing quietly loads every row. After the SQL part, the rest is Kotlin on the list.

Reads subscribe

Inside a composable a read subscribes: a write to Todo redraws whatever read it. There is no refresh call and no observer to register.

Writes

val id = $db.Todo.insert(Todo(title = "Buy milk"))   // returns the id
$db.Todo.update(todo.copy(done = true))
$db.Todo.upsert(todo)
$db.Todo.delete(todo)
$db.Todo.delete { it.done }
$db.Todo.clear()

insert(row) returns the id. update, upsert, delete(row), delete { … } and clear() are the rest.

Transactions

$db.transaction { … } groups writes:

$db.transaction {
  $db.Todo.delete { it.done }
  $db.Todo.insert(Todo(title = "Start over"))
}

Column types

Field typeStored as
the primitive types, String, ByteArraySQLite's own
an enumits name
anything elseJSON — readable, but not filterable

Migrations

Migrations are derived when the app opens, from the data class as it is now. A missing table or column is added; a dropped column is left unread. There is no migration file to write.

@was

A rename would look like a dropped column and a new one. @was says it is the same column:

@table data class Todo(
  val id: Long = 0,
  @was("name") val title: String,
  val done: Boolean = false,
)
A table is declared once, in a file of its own, and used from any screen or component as $db.Todo. Because the example above spans two files, it is shown as source rather than as a live snippet.

See Also