$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:
| Call | SQL |
|---|---|
$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 type | Stored as |
|---|---|
the primitive types, String, ByteArray | SQLite's own |
| an enum | its name |
| anything else | JSON — 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,
) $db.Todo. Because the example above spans two files, it is shown as source
rather than as a live snippet.See Also
- Data Classes — what a
@tableis underneath - $data — loaders, for data from the network
- $fetch — HTTP requests
- Reactivity — what "subscribes" means