lyng/docs/lyng.io.db.md

5.9 KiB

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:

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:

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:

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:

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:

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.


SQLite provider

lyng.io.db.sqlite currently provides the first concrete backend.

Typed helper:

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.