Kotlin guide
Use typed queries, transactions, extensions, and backups in Android apps.
These recipes run in a coroutine with an open db from the Kotlin quickstart. Import dev.oliphaunt.* for the database types and query extension functions.
Query application data
db.execute("CREATE TABLE IF NOT EXISTS notes (id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, body text NOT NULL)")
val inserted = db.query(
"INSERT INTO notes (body) VALUES ($1) RETURNING id",
parameters = listOf(QueryParam.text("First note")),
)
val id = inserted.rows[0].value("id", PostgresDecoders.long)Bind values using QueryParam helpers and choose a decoder matching the PostgreSQL column type. SQL null returns null; decode by column index when a result has duplicate column names.
Run a transaction
db.transaction { tx ->
tx.execute(
"INSERT INTO notes (body) VALUES ($1)",
parameters = listOf(QueryParam.text("First")),
)
tx.execute(
"INSERT INTO notes (body) VALUES ($1)",
parameters = listOf(QueryParam.text("Second")),
)
}Returning commits; throwing rolls back. Use tx for all callback statements. Do not call the outer db or send manual BEGIN, COMMIT, or full ROLLBACK SQL. Use tx.rollback() for explicit rollback. Savepoints are supported.
Select extensions
Choose extensions in the app's Gradle configuration:
oliphaunt {
selectedExtensions.add("vector")
}Rebuild the app, then select its typed value when opening:
val db = Oliphaunt.open(
context,
OliphauntConfig(
storage = DatabaseStorage.Directory(context.filesDir.resolve("main.oliphaunt")),
extensions = listOf(OliphauntExtension.VECTOR),
),
)
db.execute("CREATE EXTENSION IF NOT EXISTS vector")The Gradle plugin packages selected native artifacts and required dependencies. Check the extension catalog for available names.
Choose process isolation
The quickstart uses direct mode. For a separate database process, follow mobile broker setup. Broker mode requires its own storage and restore path; use the recipes below for direct mode.
Back up and restore
Direct mode stays bound to one database root and configuration for the lifetime of the application process. Closing a handle does not let that process switch to another root. Prepare the restored data, then use it on the next launch.
val archive: ByteArray = db.backup()
db.close()
val destination = context.filesDir.resolve("restored.oliphaunt")
Oliphaunt.restore(context, destination, archive)On a subsequent application launch, open the restored destination:
val destination = context.filesDir.resolve("restored.oliphaunt")
val restored = Oliphaunt.open(
context,
OliphauntConfig(storage = DatabaseStorage.Directory(destination)),
)
restored.close()Restore requires a new or empty destination. Backups contain database state; the receiving app must also ship and select any required extensions. Use compatible native runtimes for physical restore.
Cancel and close
Coroutine cancellation does not replace the SDK's database interrupt. Call db.cancel() when your application needs to interrupt PostgreSQL, then observe the operation's outcome.
Await db.close() during orderly shutdown. It stops new work and drains accepted operations. A failed close leaves the handle terminal. Keep cancellation and shutdown in the application service that owns the handle, not in individual screen render paths.
Handle failures
PostgresException.postgresError.sqlstate identifies a PostgreSQL failure. A normal callback exception is rethrown after rollback. If rollback also fails, OliphauntTransactionRollbackException exposes both errors; an independent database failure is preserved by OliphauntTransactionDatabaseException.
| Symptom | Check |
|---|---|
| Runtime resource is missing | Plugin applied, repositories configured, native app rebuilt |
| Directory argument does not compile | Pass a File, not .absolutePath |
| Root already owned | Existing handle/process and stable app-private path |
| Extension cannot be created | Gradle selection and open-time selection agree |
| Decode failure | SQL column type, decoder, and null handling |