Data source wrapper

This chapter introduces the data source wrapper in Kronos.

KronosDataSourceWrapper connects Kronos SQL tasks to a database execution engine. The built-in JDBC implementation is KronosJdbcWrapper, and custom wrappers can delegate execution to Spring JDBC, JDBI, MyBatis, or another data access layer.

Native drivers

Implement KronosExecutionDataSource for a platform driver and wrap it with KronosExecutionWrapper to execute shared ORM queries. See 📚Native SQLite for the complete connection example.

Configure the default data source

Kronos.connect(...) configures Kronos.dataSource and returns the KronosJdbcWrapper.

kotlin

import com.kotlinorm.Kronos
import com.kotlinorm.connect

Kronos.connect(
    url = "jdbc:postgresql://localhost:5432/kronos",
    userName = "postgres",
    password = "******",
    driverClassName = "org.postgresql.Driver"
)

Pass databaseType when the dialect needs to be explicit, and use the configuration block for JDBC wrapper settings.

Create wrappers for multiple data sources

Use DriverManagerDataSource to create an independent wrapper for a reader database, writer database, or tenant.

kotlin

import com.kotlinorm.enums.DBType
import com.kotlinorm.wrappers.DriverManagerDataSource
import com.kotlinorm.wrappers.KronosJdbcWrapper

val reportingWrapper = KronosJdbcWrapper(
    dataSource = DriverManagerDataSource(
        url = "jdbc:postgresql://localhost:5432/kronos",
        userName = "postgres",
        password = "******",
        driverClassName = "org.postgresql.Driver"
    ),
    databaseType = DBType.Postgres
)

Use Kronos.transact(reportingWrapper) when a transaction must run on that data source. For application services, add a connection-pool and JDBC driver, then pass the pool's DataSource to KronosJdbcWrapper.

Android SQLite

For Android/JVM SQLiteDatabase setup and a complete wrapper reference, see 📚Android SQLite.

Properties

namedescriptiontype
urlDatabase connection URLString
userNameDatabase usernameString
dbTypeDatabase type used by SQL rendering and DDL statementsDBType
sqlDialectSQL dialect resolved from dbTypeSqlDialect

Use these properties to inspect the configured wrapper.

kotlin

val wrapper = KronosJdbcWrapper(dataSource)

println(wrapper.url)
println(wrapper.userName)
println(wrapper.dbType)
println(wrapper.sqlDialect.family)

output

jdbc:postgresql://localhost:5432/kronos
postgres
Postgres
PostgreSql

Configure the built-in JDBC wrapper

KronosJdbcWrapper accepts a DataSource, including DriverManagerDataSource or a connection-pool data source, an optional DBType, and a configuration block. Pass databaseType when the dialect should be explicit.

database type

import com.kotlinorm.enums.DBType
import com.kotlinorm.wrappers.KronosJdbcWrapper

val wrapper = KronosJdbcWrapper(
    dataSource = dataSource,
    databaseType = DBType.Mysql
)

Use the configuration block for JDBC statement settings and SQL warning handling.

statement

import com.kotlinorm.wrappers.KronosDuplicateKeyException
import com.kotlinorm.wrappers.KronosJdbcWrapper
import com.kotlinorm.wrappers.KronosSqlWarningPolicy
import com.kotlinorm.wrappers.SqlStateSQLExceptionTranslator

val wrapper = KronosJdbcWrapper(dataSource) {
    statement.fetchSize = 1000
    statement.maxRows = 5000
    statement.queryTimeoutSeconds = 30
    warningPolicy = KronosSqlWarningPolicy.THROW
    exceptionTranslator = { sql, params, exception ->
        if (exception.errorCode == 1062) {
            KronosDuplicateKeyException("Duplicate key", sql, params, exception)
        } else {
            SqlStateSQLExceptionTranslator().translate(sql, params, exception)
        }
    }
}

The built-in wrapper also exposes registries for custom parameter binding and result mapping.

mapping

import com.kotlinorm.wrappers.KronosArgument
import com.kotlinorm.wrappers.KronosArgumentFactory
import com.kotlinorm.wrappers.KronosJdbcWrapper
import com.kotlinorm.wrappers.KronosPhysicalReadResult
import com.kotlinorm.wrappers.KronosPhysicalValueReader
import java.sql.Types
import java.util.UUID

val wrapper = KronosJdbcWrapper(dataSource) {
    arguments.register(KronosArgumentFactory { value, _ ->
        if (value is UUID) {
            KronosArgument { position, statement, _ ->
                statement.setObject(position, value, Types.OTHER)
            }
        } else {
            null
        }
    })

    columnMappers.register(KronosPhysicalValueReader { resultSet, position, _ ->
        val jdbcTypeName = resultSet.metaData.getColumnTypeName(position)
        if (jdbcTypeName.equals("jsonb", ignoreCase = true)) {
            KronosPhysicalReadResult.Handled(resultSet.getString(position))
        } else {
            KronosPhysicalReadResult.NotHandled
        }
    })
}

Physical readers inspect JDBC metadata or vendor behavior before ResultSet.getObject(position). Return Handled(null) when the column was handled and its value is SQL NULL; return NotHandled to continue with the default JDBC read. Register a custom value mapping when the resulting string should become a domain value such as UUID.

Query lists with toList(task)

toList(task) executes a select task and maps every row according to task.targetType. The KType keeps generic arguments and nullability, so wrappers do not need separate KClass, isKPojo, or supertype parameters.

signature

fun toList(task: KAtomicQueryTask): List<Any?>

kotlin

val users = wrapper.toList(
    KronosAtomicQueryTask(
        sql = "SELECT id, name FROM user WHERE age > :age",
        paramMap = mapOf("age" to 18),
        targetType = typeOf<User>()
    )
)

The built-in JDBC wrapper recognizes scalar types, typed row maps, and KPojo types from this target KType. JDBC row mapping materializes each row as a LinkedHashMap, so the supported map containers are only direct Map and MutableMap declarations (including a nullable top-level target). Fixed, reordered, and custom Map subtypes are rejected because Kronos cannot return their declared container safely. Map keys must be String, String?, or *; Any, Any?, and * values remain raw, while a typed value argument is converted using its complete KType.

Query one value with first(task)

first(task) uses the same target type and returns null for an empty result. Higher-level first<T>() decides whether an empty result should throw based on the requested type's nullability.

signature

fun first(task: KAtomicQueryTask): Any?

kotlin

val user = wrapper.first(
    KronosAtomicQueryTask(
        sql = "SELECT id, name FROM user WHERE id = :id",
        paramMap = mapOf("id" to 1),
        targetType = typeOf<User?>()
    )
)

Execute writes with update(task)

update(task) executes insert, update, delete, and DDL tasks and returns affected rows.

signature

fun update(task: KAtomicActionTask): Int

kotlin

val affectedRows = wrapper.update(
    KronosAtomicActionTask(
        sql = "UPDATE user SET name = :name WHERE id = :id",
        paramMap = mapOf("name" to "Ada", "id" to 1)
    )
)

result

1

Execute batch writes with batchUpdate(task)

batchUpdate(task) executes one SQL statement with multiple parameter maps and returns the JDBC update count for each batch item.

signature

fun batchUpdate(task: KronosAtomicBatchTask): IntArray

kotlin

val affectedRows = wrapper.batchUpdate(
    KronosAtomicBatchTask(
        sql = "UPDATE user SET name = :name WHERE id = :id",
        paramMapArr = arrayOf(
            mapOf("name" to "Ada", "id" to 1),
            mapOf("name" to "Linus", "id" to 2)
        )
    )
)

result

[1, 1]

Execute transactions with transact

transact(...) runs the block with a TransactionScope receiver. JDBC wrappers can use savepoint, rollbackToSavepoint, and releaseSavepoint inside the block.

signature

fun transact(
    isolation: TransactionIsolation? = null,
    timeout: Int? = null,
    block: TransactionScope.() -> Any?
): Any?

kotlin

val result = wrapper.transact(
    isolation = TransactionIsolation.READ_COMMITTED,
    timeout = 30
) {
    val point = savepoint("before_user_update")

    try {
        wrapper.update(
            KronosAtomicActionTask(
                sql = "UPDATE user SET name = :name WHERE id = :id",
                paramMap = mapOf("name" to "Ada", "id" to 1)
            )
        )
        releaseSavepoint(point)
    } catch (e: Exception) {
        rollbackToSavepoint(point)
        throw e
    }
}

Convert named parameters for JDBC

task.parsed() converts :name parameters to JDBC ? placeholders. KronosAtomicBatchTask.parsedArr() returns the same shape for every batch item.

query task

val task = KronosAtomicQueryTask(
    sql = "SELECT id, name FROM user WHERE id = :id AND status = :status",
    paramMap = mapOf("id" to 1, "status" to "ACTIVE"),
    targetType = typeOf<Map<String, Any?>>()
)

val parsed = task.parsed()

println(parsed.jdbcSql)
println(parsed.jdbcParamList.toList())

output

SELECT id, name FROM user WHERE id = ? AND status = ?
[1, ACTIVE]

batch task

val batchTask = KronosAtomicBatchTask(
    sql = "UPDATE user SET status = :status WHERE id = :id",
    paramMapArr = arrayOf(
        mapOf("status" to "ACTIVE", "id" to 1),
        mapOf("status" to "LOCKED", "id" to 2)
    )
)

val (jdbcSql, batchParams) = batchTask.parsedArr()

println(jdbcSql)
println(batchParams)

batch output

UPDATE user SET status = ? WHERE id = ?
[[ACTIVE, 1], [LOCKED, 2]]

For a Spring JDBC wrapper example, see 📚Data Sources and Third-party Framework.