lyng/docs/lyng.io.db.md

241 lines
5.9 KiB
Markdown

### lyng.io.db — SQL database access for Lyng scripts
This module provides the portable SQL database contract for Lyng. The first shipped provider is SQLite via `lyng.io.db.sqlite`.
> **Note:** `lyngio` is a separate library module. It must be explicitly added as a dependency to your host application and initialized in your Lyng scopes.
---
#### Install the module into a Lyng session
For SQLite-backed database access, install both the generic DB module and the SQLite provider:
```kotlin
import net.sergeych.lyng.EvalSession
import net.sergeych.lyng.Scope
import net.sergeych.lyng.io.db.createDbModule
import net.sergeych.lyng.io.db.sqlite.createSqliteModule
suspend fun bootstrapDb() {
val session = EvalSession()
val scope: Scope = session.getScope()
createDbModule(scope)
createSqliteModule(scope)
session.eval("""
import lyng.io.db
import lyng.io.db.sqlite
""".trimIndent())
}
```
`createSqliteModule(...)` also registers the `sqlite:` scheme for generic `openDatabase(...)`.
---
#### Using from Lyng scripts
Typed SQLite open helper:
```lyng
import lyng.io.db.sqlite
val db = openSqlite(":memory:")
val userCount = db.transaction { tx ->
tx.execute("create table user(id integer primary key autoincrement, name text not null)")
tx.execute("insert into user(name) values(?)", "Ada")
tx.execute("insert into user(name) values(?)", "Linus")
tx.select("select count(*) as count from user").toList()[0]["count"]
}
assertEquals(2, userCount)
```
Generic provider-based open:
```lyng
import lyng.io.db
import lyng.io.db.sqlite
val db = openDatabase(
"sqlite:./app.db",
Map(
"foreignKeys" => true,
"busyTimeoutMillis" => 5000
)
)
```
Nested transactions use real savepoint semantics:
```lyng
import lyng.io.db
import lyng.io.db.sqlite
val db = openSqlite(":memory:")
db.transaction { tx ->
tx.execute("create table item(id integer primary key autoincrement, name text not null)")
tx.execute("insert into item(name) values(?)", "outer")
try {
tx.transaction { inner ->
inner.execute("insert into item(name) values(?)", "inner")
throw IllegalStateException("rollback nested")
}
} catch (_: IllegalStateException) {
}
assertEquals(1, tx.select("select count(*) as count from item").toList()[0]["count"])
}
```
Intentional rollback without treating it as a backend failure:
```lyng
import lyng.io.db
import lyng.io.db.sqlite
val db = openSqlite(":memory:")
assertThrows(RollbackException) {
db.transaction { tx ->
tx.execute("create table item(id integer primary key autoincrement, name text not null)")
tx.execute("insert into item(name) values(?)", "temporary")
throw RollbackException("stop here")
}
}
```
---
#### Portable API
##### `Database`
- `transaction(block)` — opens a transaction, commits on normal exit, rolls back on uncaught failure.
##### `SqlTransaction`
- `select(clause, params...)` — execute a statement whose primary result is a row set.
- `execute(clause, params...)` — execute a side-effect statement and return `ExecutionResult`.
- `transaction(block)` — nested transaction with real savepoint semantics.
##### `ResultSet`
- `columns` — positional `SqlColumn` metadata, available before iteration.
- `size()` — result row count.
- `isEmpty()` — fast emptiness check where possible.
- `iterator()` / `toList()` — normal row iteration.
##### `SqlRow`
- `row[index]` — zero-based positional access.
- `row["columnName"]` — case-insensitive lookup by output column label.
Name-based access fails with `SqlUsageException` if the name is missing or ambiguous.
##### `ExecutionResult`
- `affectedRowsCount`
- `getGeneratedKeys()`
Statements that return rows directly, such as `... returning ...`, should use `select(...)`, not `execute(...)`.
---
#### Value mapping
Portable bind values:
- `null`
- `Bool`
- `Int`, `Double`, `Decimal`
- `String`
- `Buffer`
- `Date`, `DateTime`, `Instant`
Unsupported parameter values fail with `SqlUsageException`.
Portable result metadata categories:
- `Binary`
- `String`
- `Int`
- `Double`
- `Decimal`
- `Bool`
- `Date`
- `DateTime`
- `Instant`
For temporal types, see [time functions](time.md).
---
#### SQLite provider
`lyng.io.db.sqlite` currently provides the first concrete backend.
Typed helper:
```lyng
openSqlite(
path: String,
readOnly: Bool = false,
createIfMissing: Bool = true,
foreignKeys: Bool = true,
busyTimeoutMillis: Int = 5000
): Database
```
Accepted generic URL forms:
- `sqlite::memory:`
- `sqlite:relative/path.db`
- `sqlite:/absolute/path.db`
Supported `openDatabase(..., extraParams)` keys for SQLite:
- `readOnly: Bool`
- `createIfMissing: Bool`
- `foreignKeys: Bool`
- `busyTimeoutMillis: Int`
SQLite write/read policy in v1:
- `Bool` writes as `0` / `1`
- `Decimal` writes as canonical text
- `Date` writes as `YYYY-MM-DD`
- `DateTime` writes as ISO local timestamp text without timezone
- `Instant` writes as ISO UTC timestamp text with explicit timezone marker
- `TIME*` values stay `String`
- `TIMESTAMP` / `DATETIME` reject timezone-bearing stored text
Open-time validation failures:
- malformed URL or bad option shape -> `IllegalArgumentException`
- runtime open failure -> `DatabaseException`
---
#### Lifetime rules
Result sets and rows are valid only while their owning transaction is active.
This means:
- do not keep `ResultSet` or `SqlRow` objects after the transaction block returns
- copy the values you need into ordinary Lyng objects inside the transaction
The same lifetime rule applies to generated keys returned by `ExecutionResult.getGeneratedKeys()`.
---
#### Platform support
- `lyng.io.db` — generic contract, available when host code installs it
- `lyng.io.db.sqlite` — implemented on JVM and Linux Native in the current release tree
For the broader I/O overview, see [lyngio overview](lyngio.md).