Add DB decode annotations and preserved declaration metadata
This commit is contained in:
1 parent
2abe7e2f96
commit
50e34e520e
23 files changed
+1690
-55
No files matched your search
@@ -18,21 +18,33 @@
|
||||
package net.sergeych.lyng.io.db
|
||||
|
||||
import net.sergeych.lyng.Arguments
|
||||
import net.sergeych.lyng.DeclAnnotation
|
||||
import net.sergeych.lyng.ModuleScope
|
||||
import net.sergeych.lyng.Scope
|
||||
import net.sergeych.lyng.ScopeFacade
|
||||
import net.sergeych.lyng.TypeDecl
|
||||
import net.sergeych.lyng.obj.Obj
|
||||
import net.sergeych.lyng.obj.ObjBool
|
||||
import net.sergeych.lyng.obj.ObjBuffer
|
||||
import net.sergeych.lyng.obj.ObjBitBuffer
|
||||
import net.sergeych.lyng.obj.ObjClass
|
||||
import net.sergeych.lyng.obj.ObjEnumClass
|
||||
import net.sergeych.lyng.obj.ObjEnumEntry
|
||||
import net.sergeych.lyng.obj.ObjException
|
||||
import net.sergeych.lyng.obj.ObjImmutableList
|
||||
import net.sergeych.lyng.obj.ObjInstance
|
||||
import net.sergeych.lyng.obj.ObjInt
|
||||
import net.sergeych.lyng.obj.ObjNull
|
||||
import net.sergeych.lyng.obj.ObjRecord
|
||||
import net.sergeych.lyng.obj.ObjString
|
||||
import net.sergeych.lyng.obj.ObjTypeExpr
|
||||
import net.sergeych.lyng.obj.ObjVoid
|
||||
import net.sergeych.lyng.obj.thisAs
|
||||
import net.sergeych.lyng.requireScope
|
||||
import net.sergeych.lyng.serialization.ObjJsonClass
|
||||
import net.sergeych.lynon.BitArray
|
||||
import net.sergeych.lynon.ObjLynonClass
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlin.collections.List
|
||||
import kotlin.collections.Map
|
||||
import kotlin.collections.MutableList
|
||||
@@ -43,11 +55,14 @@ import kotlin.collections.forEachIndexed
|
||||
import kotlin.collections.getOrNull
|
||||
import kotlin.collections.getOrPut
|
||||
import kotlin.collections.indices
|
||||
import kotlin.collections.LinkedHashMap
|
||||
import kotlin.collections.linkedMapOf
|
||||
import kotlin.collections.listOf
|
||||
import kotlin.collections.map
|
||||
import kotlin.collections.mutableListOf
|
||||
import kotlin.collections.set
|
||||
import kotlin.text.lowercase
|
||||
import kotlin.text.substringAfterLast
|
||||
|
||||
internal data class SqlColumnMeta(
|
||||
val name: String,
|
||||
@@ -90,6 +105,9 @@ internal class SqlCoreModule private constructor(
|
||||
val sqlConstraintException: ObjException.Companion.ExceptionClass,
|
||||
val sqlUsageException: ObjException.Companion.ExceptionClass,
|
||||
val rollbackException: ObjException.Companion.ExceptionClass,
|
||||
val iterableClass: ObjClass,
|
||||
val iteratorClass: ObjClass,
|
||||
val dbFieldAdapterClass: ObjClass,
|
||||
val sqlTypes: SqlTypeEntries,
|
||||
) {
|
||||
companion object {
|
||||
@@ -106,6 +124,11 @@ internal class SqlCoreModule private constructor(
|
||||
sqlConstraintException = module.requireClass("SqlConstraintException") as ObjException.Companion.ExceptionClass,
|
||||
sqlUsageException = module.requireClass("SqlUsageException") as ObjException.Companion.ExceptionClass,
|
||||
rollbackException = module.requireClass("RollbackException") as ObjException.Companion.ExceptionClass,
|
||||
iterableClass = module.importProvider.rootScope.get("Iterable")?.value as? ObjClass
|
||||
?: error("lyng.stdlib.Iterable declaration is missing"),
|
||||
iteratorClass = module.importProvider.rootScope.get("Iterator")?.value as? ObjClass
|
||||
?: error("lyng.stdlib.Iterator declaration is missing"),
|
||||
dbFieldAdapterClass = module.requireClass("DbFieldAdapter"),
|
||||
sqlTypes = SqlTypeEntries.resolve(module),
|
||||
)
|
||||
}
|
||||
@@ -148,6 +171,8 @@ internal class SqlRuntimeTypes private constructor(
|
||||
val rowClass: ObjClass,
|
||||
val columnClass: ObjClass,
|
||||
val executionResultClass: ObjClass,
|
||||
val decodedIterableClass: ObjClass,
|
||||
val decodedIteratorClass: ObjClass,
|
||||
) {
|
||||
companion object {
|
||||
fun create(prefix: String, core: SqlCoreModule): SqlRuntimeTypes {
|
||||
@@ -157,6 +182,8 @@ internal class SqlRuntimeTypes private constructor(
|
||||
val rowClass = object : ObjClass("${prefix}Row", core.rowClass) {}
|
||||
val columnClass = object : ObjClass("${prefix}Column", core.columnClass) {}
|
||||
val executionResultClass = object : ObjClass("${prefix}ExecutionResult", core.executionResultClass) {}
|
||||
val decodedIterableClass = object : ObjClass("${prefix}DecodedIterable", core.iterableClass) {}
|
||||
val decodedIteratorClass = object : ObjClass("${prefix}DecodedIterator", core.iteratorClass) {}
|
||||
val runtime = SqlRuntimeTypes(
|
||||
core = core,
|
||||
databaseClass = databaseClass,
|
||||
@@ -165,6 +192,8 @@ internal class SqlRuntimeTypes private constructor(
|
||||
rowClass = rowClass,
|
||||
columnClass = columnClass,
|
||||
executionResultClass = executionResultClass,
|
||||
decodedIterableClass = decodedIterableClass,
|
||||
decodedIteratorClass = decodedIteratorClass,
|
||||
)
|
||||
runtime.bind()
|
||||
return runtime
|
||||
@@ -251,6 +280,19 @@ internal class SqlRuntimeTypes private constructor(
|
||||
self.lifetime.ensureActive(this)
|
||||
ObjImmutableList(self.rows)
|
||||
}
|
||||
resultSetClass.addFn(
|
||||
"decodeAs",
|
||||
callSignature = core.resultSetClass.getInstanceMemberOrNull("decodeAs")?.callSignature
|
||||
) {
|
||||
val self = thisAs<SqlResultSetObj>()
|
||||
self.lifetime.ensureActive(this)
|
||||
SqlDecodedIterableObj(
|
||||
self.types,
|
||||
self.lifetime,
|
||||
self.rows.map { it as SqlRowObj },
|
||||
resolveDecodeTargetType(requireScope())
|
||||
)
|
||||
}
|
||||
|
||||
rowClass.addProperty("size", getter = {
|
||||
val self = thisAs<SqlRowObj>()
|
||||
@@ -260,6 +302,12 @@ internal class SqlRuntimeTypes private constructor(
|
||||
val self = thisAs<SqlRowObj>()
|
||||
ObjImmutableList(self.values)
|
||||
})
|
||||
rowClass.addFn(
|
||||
"decodeAs",
|
||||
callSignature = core.rowClass.getInstanceMemberOrNull("decodeAs")?.callSignature
|
||||
) {
|
||||
decodeSqlRow(requireScope(), thisAs(), resolveDecodeTargetType(requireScope()))
|
||||
}
|
||||
|
||||
columnClass.addProperty("name", getter = { ObjString(thisAs<SqlColumnObj>().meta.name) })
|
||||
columnClass.addProperty("sqlType", getter = { thisAs<SqlColumnObj>().meta.sqlType })
|
||||
@@ -276,6 +324,27 @@ internal class SqlRuntimeTypes private constructor(
|
||||
self.lifetime.ensureActive(this)
|
||||
SqlResultSetObj(self.types, self.lifetime, self.result.generatedKeys)
|
||||
}
|
||||
|
||||
decodedIterableClass.addFn("iterator") {
|
||||
val self = thisAs<SqlDecodedIterableObj>()
|
||||
self.lifetime.ensureActive(this)
|
||||
SqlDecodedIteratorObj(self.types, self.lifetime, self.rows.iterator(), self.targetType)
|
||||
}
|
||||
|
||||
decodedIteratorClass.addFn("hasNext") {
|
||||
val self = thisAs<SqlDecodedIteratorObj>()
|
||||
self.lifetime.ensureActive(this)
|
||||
ObjBool(self.rows.hasNext())
|
||||
}
|
||||
decodedIteratorClass.addFn("next") {
|
||||
val self = thisAs<SqlDecodedIteratorObj>()
|
||||
self.lifetime.ensureActive(this)
|
||||
decodeSqlRow(requireScope(), self.rows.next(), self.targetType)
|
||||
}
|
||||
decodedIteratorClass.addFn("cancelIteration") {
|
||||
thisAs<SqlDecodedIteratorObj>().lifetime.ensureActive(this)
|
||||
ObjVoid
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -319,6 +388,7 @@ internal class SqlResultSetObj(
|
||||
val lifetime: SqlTransactionLifetime,
|
||||
data: SqlResultSetData,
|
||||
) : Obj() {
|
||||
val columnMeta: List<SqlColumnMeta> = data.columns
|
||||
val columns: List<Obj> = data.columns.map { SqlColumnObj(types, it) }
|
||||
val rows: List<Obj> = buildRows(types, data)
|
||||
|
||||
@@ -334,13 +404,14 @@ internal class SqlResultSetObj(
|
||||
indexByName.getOrPut(column.name.lowercase()) { mutableListOf() }.add(index)
|
||||
}
|
||||
return data.rows.map { rowValues ->
|
||||
SqlRowObj(types, rowValues, indexByName)
|
||||
SqlRowObj(types, data.columns, rowValues, indexByName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class SqlRowObj(
|
||||
val types: SqlRuntimeTypes,
|
||||
val columns: List<SqlColumnMeta>,
|
||||
val values: List<Obj>,
|
||||
private val indexByName: Map<String, List<Int>>,
|
||||
) : Obj() {
|
||||
@@ -381,6 +452,26 @@ internal class SqlRowObj(
|
||||
}
|
||||
}
|
||||
|
||||
internal class SqlDecodedIterableObj(
|
||||
val types: SqlRuntimeTypes,
|
||||
val lifetime: SqlTransactionLifetime,
|
||||
val rows: List<SqlRowObj>,
|
||||
val targetType: TypeDecl,
|
||||
) : Obj() {
|
||||
override val objClass: ObjClass
|
||||
get() = types.decodedIterableClass
|
||||
}
|
||||
|
||||
internal class SqlDecodedIteratorObj(
|
||||
val types: SqlRuntimeTypes,
|
||||
val lifetime: SqlTransactionLifetime,
|
||||
val rows: Iterator<SqlRowObj>,
|
||||
val targetType: TypeDecl,
|
||||
) : Obj() {
|
||||
override val objClass: ObjClass
|
||||
get() = types.decodedIteratorClass
|
||||
}
|
||||
|
||||
internal class SqlColumnObj(
|
||||
val types: SqlRuntimeTypes,
|
||||
val meta: SqlColumnMeta,
|
||||
@@ -397,3 +488,339 @@ internal class SqlExecutionResultObj(
|
||||
override val objClass: ObjClass
|
||||
get() = types.executionResultClass
|
||||
}
|
||||
|
||||
private fun resolveDecodeTargetType(scope: Scope): TypeDecl {
|
||||
val explicit = scope.args.explicitTypeArgs.singleOrNull()
|
||||
if (explicit != null) return explicit
|
||||
val bound = scope["T"]?.value
|
||||
return when (bound) {
|
||||
is ObjTypeExpr -> bound.typeDecl
|
||||
is ObjClass -> TypeDecl.Simple(bound.className, false)
|
||||
else -> scope.raiseIllegalArgument("decodeAs requires exactly one type argument")
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun decodeSqlRow(scope: Scope, row: SqlRowObj, targetType: TypeDecl): Obj {
|
||||
val targetClass = resolveTypeDeclClass(scope, targetType)
|
||||
if (targetClass != null && shouldUseStructuredRowDecoding(row, targetClass)) {
|
||||
return decodeStructuredRow(scope, row, targetType, targetClass)
|
||||
}
|
||||
if (row.values.size == 1) {
|
||||
return decodeSqlValue(scope, row.types, row, row.columns[0], row.values[0], targetType)
|
||||
}
|
||||
scope.raiseError(
|
||||
ObjException(
|
||||
row.types.core.sqlUsageException,
|
||||
scope,
|
||||
ObjString("Can't decode SQL row with ${row.values.size} columns as ${renderTypeName(targetType)}")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun shouldUseStructuredRowDecoding(row: SqlRowObj, targetClass: ObjClass): Boolean {
|
||||
if (row.values.size > 1) return true
|
||||
val memberNames = linkedMapOf<String, Int>()
|
||||
targetClass.constructorMeta?.params?.forEach { memberNames[it.name.lowercase()] = 1 }
|
||||
row.columns.forEach { column ->
|
||||
if (column.name.lowercase() in memberNames) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private suspend fun decodeStructuredRow(
|
||||
scope: Scope,
|
||||
row: SqlRowObj,
|
||||
targetType: TypeDecl,
|
||||
targetClass: ObjClass,
|
||||
): Obj {
|
||||
val meta = targetClass.constructorMeta
|
||||
?: raiseSqlUsage(scope, row.types, "Can't decode SQL row as ${targetClass.className}: target class has no constructor metadata")
|
||||
|
||||
val normalizedColumns = buildColumnLookup(scope, row)
|
||||
val consumed = mutableSetOf<String>()
|
||||
val namedArgs = LinkedHashMap<String, Obj>()
|
||||
|
||||
for (param in meta.params) {
|
||||
if (param.isTransient) continue
|
||||
val lowered = param.name.lowercase()
|
||||
val column = normalizedColumns[lowered]
|
||||
if (column == null) {
|
||||
if (param.defaultValue == null && !param.type.isNullable) {
|
||||
raiseSqlUsage(scope, row.types, "Missing SQL column '${param.name}' for ${targetClass.className}")
|
||||
}
|
||||
continue
|
||||
}
|
||||
namedArgs[param.name] = decodeSqlValue(
|
||||
scope,
|
||||
row.types,
|
||||
row,
|
||||
column.first,
|
||||
row.values[column.second],
|
||||
param.type,
|
||||
param.annotations
|
||||
)
|
||||
consumed += lowered
|
||||
}
|
||||
|
||||
val callScope = scope.createChildScope(args = Arguments(list = emptyList(), named = namedArgs))
|
||||
val instance = targetClass.callOn(callScope)
|
||||
if (instance !is ObjInstance) {
|
||||
return instance
|
||||
}
|
||||
val knownFields = collectSerializableFieldTargets(scope, row, targetClass, instance)
|
||||
|
||||
for ((lowered, target) in knownFields) {
|
||||
if (consumed.contains(lowered)) continue
|
||||
val column = normalizedColumns[lowered] ?: continue
|
||||
val targetTypeDecl = target.record.typeDecl ?: TypeDecl.TypeAny
|
||||
target.record.value = decodeSqlValue(
|
||||
scope,
|
||||
row.types,
|
||||
row,
|
||||
column.first,
|
||||
row.values[column.second],
|
||||
targetTypeDecl,
|
||||
target.annotations
|
||||
)
|
||||
consumed += lowered
|
||||
}
|
||||
|
||||
val allowedNames = meta.params.filter { !it.isTransient }.map { it.name.lowercase() }.toMutableSet()
|
||||
allowedNames += knownFields.keys
|
||||
for (column in row.columns) {
|
||||
val lowered = column.name.lowercase()
|
||||
if (lowered !in allowedNames) {
|
||||
raiseSqlUsage(scope, row.types, "Unknown SQL result column '${column.name}' while decoding ${renderTypeName(targetType)}")
|
||||
}
|
||||
}
|
||||
|
||||
instance.invokeInstanceMethod(scope, "onDeserialized", Arguments.EMPTY) { ObjVoid }
|
||||
return instance
|
||||
}
|
||||
|
||||
private data class FieldDecodeTarget(
|
||||
val record: ObjRecord,
|
||||
val annotations: List<DeclAnnotation>,
|
||||
)
|
||||
|
||||
private fun collectSerializableFieldTargets(
|
||||
scope: Scope,
|
||||
row: SqlRowObj,
|
||||
targetClass: ObjClass,
|
||||
instance: ObjInstance,
|
||||
): Map<String, FieldDecodeTarget> {
|
||||
val result = linkedMapOf<String, FieldDecodeTarget>()
|
||||
for ((name, record) in instance.serializingVars) {
|
||||
val simpleName = name.substringAfterLast("::")
|
||||
val lowered = simpleName.lowercase()
|
||||
val classAnnotations = targetClass.getInstanceMemberOrNull(simpleName)?.annotations ?: emptyList()
|
||||
val previous = result.put(lowered, FieldDecodeTarget(record, classAnnotations))
|
||||
if (previous != null) {
|
||||
raiseSqlUsage(scope, row.types, "Ambiguous serializable target field '$lowered' in ${targetClass.className}")
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun buildColumnLookup(scope: Scope, row: SqlRowObj): Map<String, Pair<SqlColumnMeta, Int>> {
|
||||
val result = linkedMapOf<String, Pair<SqlColumnMeta, Int>>()
|
||||
row.columns.forEachIndexed { index, column ->
|
||||
val lowered = column.name.lowercase()
|
||||
if (result.containsKey(lowered)) {
|
||||
raiseSqlUsage(scope, row.types, "Ambiguous SQL result column: ${column.name}")
|
||||
}
|
||||
result[lowered] = column to index
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private suspend fun decodeSqlValue(
|
||||
scope: Scope,
|
||||
types: SqlRuntimeTypes,
|
||||
row: SqlRowObj,
|
||||
column: SqlColumnMeta,
|
||||
value: Obj,
|
||||
targetType: TypeDecl,
|
||||
annotations: List<DeclAnnotation> = emptyList(),
|
||||
): Obj {
|
||||
val adapterAnnotation = findDbDecodeWithAnnotation(scope, types, annotations)
|
||||
if (adapterAnnotation != null) {
|
||||
val adapted = applyDbFieldAdapter(scope, types, row, column, value, targetType, adapterAnnotation)
|
||||
if (adapted === ObjNull) {
|
||||
if (targetType.isNullable || targetType == TypeDecl.TypeNullableAny) return ObjNull
|
||||
raiseSqlUsage(scope, types, "SQL column '${column.name}' is null but target type ${renderTypeName(targetType)} is non-null")
|
||||
}
|
||||
if (!matchesTypeDeclCompat(scope, adapted, targetType)) {
|
||||
raiseSqlUsage(
|
||||
scope,
|
||||
types,
|
||||
"DB adapter result for column '${column.name}' does not match target type ${renderTypeName(targetType)}"
|
||||
)
|
||||
}
|
||||
return adapted
|
||||
}
|
||||
if (value === ObjNull) {
|
||||
if (targetType.isNullable || targetType == TypeDecl.TypeNullableAny) return ObjNull
|
||||
raiseSqlUsage(scope, types, "SQL column '${column.name}' is null but target type ${renderTypeName(targetType)} is non-null")
|
||||
}
|
||||
if (matchesTypeDeclCompat(scope, value, targetType)) {
|
||||
return value
|
||||
}
|
||||
if (value is ObjBuffer) {
|
||||
return try {
|
||||
val decoded = ObjLynonClass.decodeAny(scope, ObjBitBuffer(BitArray(value.byteArray, 8)))
|
||||
if (!matchesTypeDeclCompat(scope, decoded, targetType)) {
|
||||
raiseSqlUsage(
|
||||
scope,
|
||||
types,
|
||||
"Lynon-decoded SQL column '${column.name}' does not match target type ${renderTypeName(targetType)}"
|
||||
)
|
||||
}
|
||||
decoded
|
||||
} catch (e: Throwable) {
|
||||
raiseSqlUsage(
|
||||
scope,
|
||||
types,
|
||||
"Failed to decode Lynon column '${column.name}' as ${renderTypeName(targetType)}: ${e.message ?: e::class.simpleName}"
|
||||
)
|
||||
}
|
||||
}
|
||||
if (isJsonLikeNativeType(column.nativeType) && value is ObjString) {
|
||||
return try {
|
||||
ObjJsonClass.decodeFromJsonElement(scope, Json.parseToJsonElement(value.value), targetType)
|
||||
} catch (e: Throwable) {
|
||||
raiseSqlUsage(
|
||||
scope,
|
||||
types,
|
||||
"Failed to decode JSON column '${column.name}' as ${renderTypeName(targetType)}: ${e.message ?: e::class.simpleName}"
|
||||
)
|
||||
}
|
||||
}
|
||||
raiseSqlUsage(
|
||||
scope,
|
||||
types,
|
||||
"SQL column '${column.name}' of native type ${column.nativeType} can't be decoded as ${renderTypeName(targetType)}"
|
||||
)
|
||||
}
|
||||
|
||||
private fun findDbDecodeWithAnnotation(
|
||||
scope: Scope,
|
||||
types: SqlRuntimeTypes,
|
||||
annotations: List<DeclAnnotation>,
|
||||
): DeclAnnotation? {
|
||||
val matches = annotations.filter { it.name == "DbDecodeWith" }
|
||||
if (matches.size > 1) {
|
||||
raiseSqlUsage(scope, types, "Only one @DbDecodeWith(...) annotation is allowed per declaration")
|
||||
}
|
||||
return matches.singleOrNull()
|
||||
}
|
||||
|
||||
private suspend fun applyDbFieldAdapter(
|
||||
scope: Scope,
|
||||
types: SqlRuntimeTypes,
|
||||
row: SqlRowObj,
|
||||
column: SqlColumnMeta,
|
||||
value: Obj,
|
||||
targetType: TypeDecl,
|
||||
annotation: DeclAnnotation,
|
||||
): Obj {
|
||||
if (annotation.named.isNotEmpty() || annotation.positional.size != 1) {
|
||||
raiseSqlUsage(scope, types, "@DbDecodeWith(...) expects exactly one adapter instance argument")
|
||||
}
|
||||
val adapter = annotation.positional.first()
|
||||
if (!adapter.isInstanceOf(types.core.dbFieldAdapterClass)) {
|
||||
raiseSqlUsage(scope, types, "@DbDecodeWith(...) argument must implement DbFieldAdapter")
|
||||
}
|
||||
return try {
|
||||
adapter.invokeInstanceMethod(
|
||||
scope,
|
||||
"decode",
|
||||
Arguments(
|
||||
value,
|
||||
SqlColumnObj(types, column),
|
||||
row,
|
||||
runtimeTargetTypeObject(scope, targetType)
|
||||
)
|
||||
)
|
||||
} catch (e: Throwable) {
|
||||
raiseSqlUsage(
|
||||
scope,
|
||||
types,
|
||||
"Failed to decode SQL column '${column.name}' with @DbDecodeWith(...): ${e.message ?: e::class.simpleName}"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun runtimeTargetTypeObject(scope: Scope, targetType: TypeDecl): Obj {
|
||||
return resolveTypeDeclClass(scope, targetType) ?: ObjTypeExpr(targetType)
|
||||
}
|
||||
|
||||
private fun isJsonLikeNativeType(nativeType: String): Boolean {
|
||||
val normalized = nativeType.trim().substringBefore('(').lowercase()
|
||||
return normalized == "json" || normalized == "jsonb"
|
||||
}
|
||||
|
||||
private fun resolveTypeDeclClass(scope: Scope, type: TypeDecl): ObjClass? = when (type) {
|
||||
is TypeDecl.Simple -> {
|
||||
val direct = scope[type.name]?.value as? ObjClass
|
||||
direct ?: scope[type.name.substringAfterLast('.')]?.value as? ObjClass
|
||||
}
|
||||
is TypeDecl.Generic -> {
|
||||
val direct = scope[type.name]?.value as? ObjClass
|
||||
direct ?: scope[type.name.substringAfterLast('.')]?.value as? ObjClass
|
||||
}
|
||||
is TypeDecl.Ellipsis -> resolveTypeDeclClass(scope, type.elementType)
|
||||
is TypeDecl.TypeVar -> when (val bound = scope[type.name]?.value) {
|
||||
is ObjClass -> bound
|
||||
is ObjTypeExpr -> resolveTypeDeclClass(scope, bound.typeDecl)
|
||||
else -> null
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
|
||||
private fun matchesTypeDeclCompat(scope: Scope, value: Obj, typeDecl: TypeDecl): Boolean {
|
||||
if (value === ObjNull) return typeDecl.isNullable || typeDecl == TypeDecl.TypeNullableAny
|
||||
fun resolve(typeName: String): ObjClass? {
|
||||
val direct = scope[typeName]?.value as? ObjClass
|
||||
return direct ?: scope[typeName.substringAfterLast('.')]?.value as? ObjClass
|
||||
}
|
||||
return when (typeDecl) {
|
||||
TypeDecl.TypeAny, TypeDecl.TypeNullableAny -> true
|
||||
is TypeDecl.TypeVar -> {
|
||||
val cls = resolve(typeDecl.name)
|
||||
if (cls != null) value.isInstanceOf(cls) else value.isInstanceOf(typeDecl.name)
|
||||
}
|
||||
is TypeDecl.Simple -> {
|
||||
val cls = resolve(typeDecl.name)
|
||||
if (cls != null) value.isInstanceOf(cls) else value.isInstanceOf(typeDecl.name.substringAfterLast('.'))
|
||||
}
|
||||
is TypeDecl.Generic -> {
|
||||
val cls = resolve(typeDecl.name)
|
||||
if (cls != null) value.isInstanceOf(cls) else value.isInstanceOf(typeDecl.name.substringAfterLast('.'))
|
||||
}
|
||||
is TypeDecl.Function -> value.isInstanceOf("Callable")
|
||||
is TypeDecl.Ellipsis -> matchesTypeDeclCompat(scope, value, typeDecl.elementType)
|
||||
is TypeDecl.Union -> typeDecl.options.any { matchesTypeDeclCompat(scope, value, it) }
|
||||
is TypeDecl.Intersection -> typeDecl.options.all { matchesTypeDeclCompat(scope, value, it) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderTypeName(type: TypeDecl): String = when (type) {
|
||||
TypeDecl.TypeAny -> "Object"
|
||||
TypeDecl.TypeNullableAny -> "Object?"
|
||||
is TypeDecl.Simple -> type.name + if (type.isNullable) "?" else ""
|
||||
is TypeDecl.Generic -> type.name + "<" + type.args.joinToString(",") { renderTypeName(it) } + ">" + if (type.isNullable) "?" else ""
|
||||
is TypeDecl.Function -> "Callable"
|
||||
is TypeDecl.Ellipsis -> renderTypeName(type.elementType) + "..."
|
||||
is TypeDecl.TypeVar -> type.name + if (type.isNullable) "?" else ""
|
||||
is TypeDecl.Union -> type.options.joinToString(" | ") { renderTypeName(it) } + if (type.isNullable) "?" else ""
|
||||
is TypeDecl.Intersection -> type.options.joinToString(" & ") { renderTypeName(it) } + if (type.isNullable) "?" else ""
|
||||
}
|
||||
|
||||
private fun raiseSqlUsage(scope: Scope, types: SqlRuntimeTypes?, message: String): Nothing {
|
||||
val exClass = types?.core?.sqlUsageException
|
||||
if (exClass != null) {
|
||||
scope.raiseError(ObjException(exClass, scope, ObjString(message)))
|
||||
}
|
||||
scope.raiseIllegalArgument(message)
|
||||
}
|
||||
@@ -137,6 +137,230 @@ class LyngSqliteModuleTest {
|
||||
assertEquals(2L, result.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDecodeAsProjectsJsonColumnIntoObjectField() = runTest {
|
||||
val scope = Script.newScope()
|
||||
createSqliteModule(scope.importManager)
|
||||
|
||||
val code = """
|
||||
import lyng.io.db.sqlite
|
||||
|
||||
class Point(x: Int, y: Int)
|
||||
class Row(id: Int, payload: Point)
|
||||
|
||||
val db = openSqlite(":memory:")
|
||||
db.transaction { tx ->
|
||||
tx.execute("create table data(id integer not null, payload json not null)")
|
||||
tx.execute("insert into data(id, payload) values(?, ?)", 7, "{\"x\":4,\"y\":5}")
|
||||
val row = tx.select("select id, payload from data").decodeAs<Row>().first
|
||||
assertEquals(7, row.id)
|
||||
assertEquals(4, row.payload.x)
|
||||
assertEquals(5, row.payload.y)
|
||||
row.payload.y
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
val result = Compiler.compile(Source("<sqlite-decode-json-field>", code), scope.importManager).execute(scope) as ObjInt
|
||||
assertEquals(5L, result.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDecodeAsSupportsSingleJsonColumnProjection() = runTest {
|
||||
val scope = Script.newScope()
|
||||
createSqliteModule(scope.importManager)
|
||||
|
||||
val code = """
|
||||
import lyng.io.db.sqlite
|
||||
|
||||
class Point(x: Int, y: Int)
|
||||
|
||||
val db = openSqlite(":memory:")
|
||||
db.transaction { tx ->
|
||||
tx.execute("create table data(payload json not null)")
|
||||
tx.execute("insert into data(payload) values(?)", "{\"x\":9,\"y\":11}")
|
||||
val point = tx.select("select payload from data").decodeAs<Point>().first
|
||||
assertEquals(9, point.x)
|
||||
assertEquals(11, point.y)
|
||||
point.x + point.y
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
val result = Compiler.compile(Source("<sqlite-decode-json-single>", code), scope.importManager).execute(scope) as ObjInt
|
||||
assertEquals(20L, result.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDecodeAsDoesNotAutoDecodePlainTextAsJson() = runTest {
|
||||
val scope = Script.newScope()
|
||||
createSqliteModule(scope.importManager)
|
||||
|
||||
val code = """
|
||||
import lyng.io.db.sqlite
|
||||
|
||||
class Point(x: Int, y: Int)
|
||||
|
||||
val db = openSqlite(":memory:")
|
||||
db.transaction { tx ->
|
||||
tx.execute("create table data(payload text not null)")
|
||||
tx.execute("insert into data(payload) values(?)", "{\"x\":1,\"y\":2}")
|
||||
tx.select("select payload from data").decodeAs<Point>().first
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
val error = assertFailsWith<ExecutionError> {
|
||||
Compiler.compile(Source("<sqlite-decode-json-text-guard>", code), scope.importManager).execute(scope)
|
||||
}
|
||||
assertEquals("SqlUsageException", error.errorObject.objClass.className)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDecodeAsSupportsSingleLynonBinaryProjection() = runTest {
|
||||
val scope = Script.newScope()
|
||||
createSqliteModule(scope.importManager)
|
||||
|
||||
val code = """
|
||||
import lyng.io.db.sqlite
|
||||
import lyng.serialization
|
||||
|
||||
class Point(x: Int, y: Int)
|
||||
|
||||
val db = openSqlite(":memory:")
|
||||
db.transaction { tx ->
|
||||
tx.execute("create table data(payload blob not null)")
|
||||
tx.execute("insert into data(payload) values(?)", Lynon.encode(Point(6, 8)).toBuffer())
|
||||
val point = tx.select("select payload from data").decodeAs<Point>().first
|
||||
assertEquals(6, point.x)
|
||||
assertEquals(8, point.y)
|
||||
point.x + point.y
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
val result = Compiler.compile(Source("<sqlite-decode-lynon-single>", code), scope.importManager).execute(scope) as ObjInt
|
||||
assertEquals(14L, result.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDecodeAsSupportsDbDecodeWithOnConstructorParamsAndFields() = runTest {
|
||||
val scope = Script.newScope()
|
||||
createSqliteModule(scope.importManager)
|
||||
|
||||
val code = """
|
||||
import lyng.io.db
|
||||
import lyng.io.db.sqlite
|
||||
|
||||
object TrimmedStringAdapter: DbFieldAdapter {
|
||||
override fun decode(rawValue, column, row, targetType) =
|
||||
when(rawValue) {
|
||||
null -> null
|
||||
else -> rawValue.toString().trim()
|
||||
}
|
||||
}
|
||||
|
||||
class User(
|
||||
id: Int,
|
||||
@DbDecodeWith(TrimmedStringAdapter) name: String
|
||||
) {
|
||||
@DbDecodeWith(TrimmedStringAdapter)
|
||||
var note: String = ""
|
||||
}
|
||||
|
||||
val db = openSqlite(":memory:")
|
||||
db.transaction { tx ->
|
||||
tx.execute("create table data(id integer not null, name text not null, note text not null)")
|
||||
tx.execute("insert into data(id, name, note) values(?, ?, ?)", 10, " Alice ", " hello ")
|
||||
val user = tx.select("select id, name, note from data").decodeAs<User>().first
|
||||
assertEquals(10, user.id)
|
||||
assertEquals("Alice", user.name)
|
||||
assertEquals("hello", user.note)
|
||||
user.note.size
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
val result = Compiler.compile(Source("<sqlite-decode-dbdecodewith>", code), scope.importManager).execute(scope) as ObjInt
|
||||
assertEquals(5L, result.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDecodeAsFailsWhenDbDecodeWithReturnsWrongType() = runTest {
|
||||
val scope = Script.newScope()
|
||||
createSqliteModule(scope.importManager)
|
||||
|
||||
val code = """
|
||||
import lyng.io.db
|
||||
import lyng.io.db.sqlite
|
||||
|
||||
object BadAdapter: DbFieldAdapter {
|
||||
override fun decode(rawValue, column, row, targetType) = 42
|
||||
}
|
||||
|
||||
class User(@DbDecodeWith(BadAdapter) name: String)
|
||||
|
||||
val db = openSqlite(":memory:")
|
||||
db.transaction { tx ->
|
||||
tx.execute("create table data(name text not null)")
|
||||
tx.execute("insert into data(name) values(?)", "Alice")
|
||||
tx.select("select name from data").decodeAs<User>().first
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
val error = assertFailsWith<ExecutionError> {
|
||||
Compiler.compile(Source("<sqlite-decode-dbdecodewith-bad-type>", code), scope.importManager).execute(scope)
|
||||
}
|
||||
assertEquals("SqlUsageException", error.errorObject.objClass.className)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDecodeAsKeepsRawBufferForBufferTarget() = runTest {
|
||||
val scope = Script.newScope()
|
||||
createSqliteModule(scope.importManager)
|
||||
|
||||
val code = """
|
||||
import lyng.io.db.sqlite
|
||||
import lyng.buffer
|
||||
import lyng.serialization
|
||||
|
||||
class Point(x: Int, y: Int)
|
||||
|
||||
val db = openSqlite(":memory:")
|
||||
db.transaction { tx ->
|
||||
tx.execute("create table data(payload blob not null)")
|
||||
val encoded = Lynon.encode(Point(1, 2)).toBuffer()
|
||||
tx.execute("insert into data(payload) values(?)", encoded)
|
||||
val payload = tx.select("select payload from data").decodeAs<Buffer>().first
|
||||
assertEquals(encoded.size, payload.size)
|
||||
payload.size
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
val result = Compiler.compile(Source("<sqlite-decode-buffer-raw>", code), scope.importManager).execute(scope) as ObjInt
|
||||
assertTrue(result.value > 0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDecodeAsFailsForNonLynonBinaryTypedProjection() = runTest {
|
||||
val scope = Script.newScope()
|
||||
createSqliteModule(scope.importManager)
|
||||
|
||||
val code = """
|
||||
import lyng.io.db.sqlite
|
||||
import lyng.buffer
|
||||
|
||||
class Point(x: Int, y: Int)
|
||||
|
||||
val db = openSqlite(":memory:")
|
||||
db.transaction { tx ->
|
||||
tx.execute("create table data(payload blob not null)")
|
||||
tx.execute("insert into data(payload) values(?)", "hello".encodeUtf8())
|
||||
tx.select("select payload from data").decodeAs<Point>().first
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
val error = assertFailsWith<ExecutionError> {
|
||||
Compiler.compile(Source("<sqlite-decode-lynon-binary-guard>", code), scope.importManager).execute(scope)
|
||||
}
|
||||
assertEquals("SqlUsageException", error.errorObject.objClass.className)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNestedTransactionRollbackUsesSavepoint() = runTest {
|
||||
val scope = Script.newScope()
|
||||
|
||||
+226
@@ -19,11 +19,13 @@ package net.sergeych.lyng.io.db.sqlite
|
||||
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.datetime.TimeZone
|
||||
import net.sergeych.lyng.Compiler
|
||||
import net.sergeych.lyng.ExecutionError
|
||||
import net.sergeych.lyng.ModuleScope
|
||||
import net.sergeych.lyng.Pos
|
||||
import net.sergeych.lyng.Scope
|
||||
import net.sergeych.lyng.Script
|
||||
import net.sergeych.lyng.Source
|
||||
import net.sergeych.lyng.obj.Obj
|
||||
import net.sergeych.lyng.obj.ObjBool
|
||||
import net.sergeych.lyng.obj.ObjBuffer
|
||||
@@ -231,6 +233,230 @@ class LyngSqliteModuleNativeTest {
|
||||
assertEquals("beta", stringValue(scope, rows.getAt(scope, ObjInt.of(1)).getAt(scope, ObjString("name"))))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDecodeAsProjectsJsonColumnIntoObjectField() = runTest {
|
||||
val scope = Script.newScope()
|
||||
createSqliteModule(scope.importManager)
|
||||
|
||||
val code = """
|
||||
import lyng.io.db.sqlite
|
||||
|
||||
class Point(x: Int, y: Int)
|
||||
class Row(id: Int, payload: Point)
|
||||
|
||||
val db = openSqlite(":memory:")
|
||||
db.transaction { tx ->
|
||||
tx.execute("create table data(id integer not null, payload json not null)")
|
||||
tx.execute("insert into data(id, payload) values(?, ?)", 7, "{\"x\":4,\"y\":5}")
|
||||
val row = tx.select("select id, payload from data").decodeAs<Row>().first
|
||||
assertEquals(7, row.id)
|
||||
assertEquals(4, row.payload.x)
|
||||
assertEquals(5, row.payload.y)
|
||||
row.payload.y
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
val result = Compiler.compile(Source("<sqlite-native-decode-json-field>", code), scope.importManager).execute(scope) as ObjInt
|
||||
assertEquals(5L, result.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDecodeAsSupportsSingleJsonColumnProjection() = runTest {
|
||||
val scope = Script.newScope()
|
||||
createSqliteModule(scope.importManager)
|
||||
|
||||
val code = """
|
||||
import lyng.io.db.sqlite
|
||||
|
||||
class Point(x: Int, y: Int)
|
||||
|
||||
val db = openSqlite(":memory:")
|
||||
db.transaction { tx ->
|
||||
tx.execute("create table data(payload json not null)")
|
||||
tx.execute("insert into data(payload) values(?)", "{\"x\":9,\"y\":11}")
|
||||
val point = tx.select("select payload from data").decodeAs<Point>().first
|
||||
assertEquals(9, point.x)
|
||||
assertEquals(11, point.y)
|
||||
point.x + point.y
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
val result = Compiler.compile(Source("<sqlite-native-decode-json-single>", code), scope.importManager).execute(scope) as ObjInt
|
||||
assertEquals(20L, result.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDecodeAsDoesNotAutoDecodePlainTextAsJson() = runTest {
|
||||
val scope = Script.newScope()
|
||||
createSqliteModule(scope.importManager)
|
||||
|
||||
val code = """
|
||||
import lyng.io.db.sqlite
|
||||
|
||||
class Point(x: Int, y: Int)
|
||||
|
||||
val db = openSqlite(":memory:")
|
||||
db.transaction { tx ->
|
||||
tx.execute("create table data(payload text not null)")
|
||||
tx.execute("insert into data(payload) values(?)", "{\"x\":1,\"y\":2}")
|
||||
tx.select("select payload from data").decodeAs<Point>().first
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
val error = assertFailsWith<ExecutionError> {
|
||||
Compiler.compile(Source("<sqlite-native-decode-json-text-guard>", code), scope.importManager).execute(scope)
|
||||
}
|
||||
assertEquals("SqlUsageException", error.errorObject.objClass.className)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDecodeAsSupportsSingleLynonBinaryProjection() = runTest {
|
||||
val scope = Script.newScope()
|
||||
createSqliteModule(scope.importManager)
|
||||
|
||||
val code = """
|
||||
import lyng.io.db.sqlite
|
||||
import lyng.serialization
|
||||
|
||||
class Point(x: Int, y: Int)
|
||||
|
||||
val db = openSqlite(":memory:")
|
||||
db.transaction { tx ->
|
||||
tx.execute("create table data(payload blob not null)")
|
||||
tx.execute("insert into data(payload) values(?)", Lynon.encode(Point(6, 8)).toBuffer())
|
||||
val point = tx.select("select payload from data").decodeAs<Point>().first
|
||||
assertEquals(6, point.x)
|
||||
assertEquals(8, point.y)
|
||||
point.x + point.y
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
val result = Compiler.compile(Source("<sqlite-native-decode-lynon-single>", code), scope.importManager).execute(scope) as ObjInt
|
||||
assertEquals(14L, result.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDecodeAsSupportsDbDecodeWithOnConstructorParamsAndFields() = runTest {
|
||||
val scope = Script.newScope()
|
||||
createSqliteModule(scope.importManager)
|
||||
|
||||
val code = """
|
||||
import lyng.io.db
|
||||
import lyng.io.db.sqlite
|
||||
|
||||
object TrimmedStringAdapter: DbFieldAdapter {
|
||||
override fun decode(rawValue, column, row, targetType) =
|
||||
when(rawValue) {
|
||||
null -> null
|
||||
else -> rawValue.toString().trim()
|
||||
}
|
||||
}
|
||||
|
||||
class User(
|
||||
id: Int,
|
||||
@DbDecodeWith(TrimmedStringAdapter) name: String
|
||||
) {
|
||||
@DbDecodeWith(TrimmedStringAdapter)
|
||||
var note: String = ""
|
||||
}
|
||||
|
||||
val db = openSqlite(":memory:")
|
||||
db.transaction { tx ->
|
||||
tx.execute("create table data(id integer not null, name text not null, note text not null)")
|
||||
tx.execute("insert into data(id, name, note) values(?, ?, ?)", 10, " Alice ", " hello ")
|
||||
val user = tx.select("select id, name, note from data").decodeAs<User>().first
|
||||
assertEquals(10, user.id)
|
||||
assertEquals("Alice", user.name)
|
||||
assertEquals("hello", user.note)
|
||||
user.note.size
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
val result = Compiler.compile(Source("<sqlite-native-decode-dbdecodewith>", code), scope.importManager).execute(scope) as ObjInt
|
||||
assertEquals(5L, result.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDecodeAsFailsWhenDbDecodeWithReturnsWrongType() = runTest {
|
||||
val scope = Script.newScope()
|
||||
createSqliteModule(scope.importManager)
|
||||
|
||||
val code = """
|
||||
import lyng.io.db
|
||||
import lyng.io.db.sqlite
|
||||
|
||||
object BadAdapter: DbFieldAdapter {
|
||||
override fun decode(rawValue, column, row, targetType) = 42
|
||||
}
|
||||
|
||||
class User(@DbDecodeWith(BadAdapter) name: String)
|
||||
|
||||
val db = openSqlite(":memory:")
|
||||
db.transaction { tx ->
|
||||
tx.execute("create table data(name text not null)")
|
||||
tx.execute("insert into data(name) values(?)", "Alice")
|
||||
tx.select("select name from data").decodeAs<User>().first
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
val error = assertFailsWith<ExecutionError> {
|
||||
Compiler.compile(Source("<sqlite-native-decode-dbdecodewith-bad-type>", code), scope.importManager).execute(scope)
|
||||
}
|
||||
assertEquals("SqlUsageException", error.errorObject.objClass.className)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDecodeAsKeepsRawBufferForBufferTarget() = runTest {
|
||||
val scope = Script.newScope()
|
||||
createSqliteModule(scope.importManager)
|
||||
|
||||
val code = """
|
||||
import lyng.io.db.sqlite
|
||||
import lyng.buffer
|
||||
import lyng.serialization
|
||||
|
||||
class Point(x: Int, y: Int)
|
||||
|
||||
val db = openSqlite(":memory:")
|
||||
db.transaction { tx ->
|
||||
tx.execute("create table data(payload blob not null)")
|
||||
val encoded = Lynon.encode(Point(1, 2)).toBuffer()
|
||||
tx.execute("insert into data(payload) values(?)", encoded)
|
||||
val payload = tx.select("select payload from data").decodeAs<Buffer>().first
|
||||
assertEquals(encoded.size, payload.size)
|
||||
payload.size
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
val result = Compiler.compile(Source("<sqlite-native-decode-buffer-raw>", code), scope.importManager).execute(scope) as ObjInt
|
||||
assertTrue(result.value > 0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDecodeAsFailsForNonLynonBinaryTypedProjection() = runTest {
|
||||
val scope = Script.newScope()
|
||||
createSqliteModule(scope.importManager)
|
||||
|
||||
val code = """
|
||||
import lyng.io.db.sqlite
|
||||
import lyng.buffer
|
||||
|
||||
class Point(x: Int, y: Int)
|
||||
|
||||
val db = openSqlite(":memory:")
|
||||
db.transaction { tx ->
|
||||
tx.execute("create table data(payload blob not null)")
|
||||
tx.execute("insert into data(payload) values(?)", "hello".encodeUtf8())
|
||||
tx.select("select payload from data").decodeAs<Point>().first
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
val error = assertFailsWith<ExecutionError> {
|
||||
Compiler.compile(Source("<sqlite-native-decode-lynon-binary-guard>", code), scope.importManager).execute(scope)
|
||||
}
|
||||
assertEquals("SqlUsageException", error.errorObject.objClass.className)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testExecuteRejectsReturningButSelectSupportsIt() = runTest {
|
||||
val scope = Script.newScope()
|
||||
|
||||
@@ -20,6 +20,32 @@ extern class SqlColumn {
|
||||
val nativeType: String
|
||||
}
|
||||
|
||||
/*
|
||||
Adapter interface for custom DB field projection.
|
||||
|
||||
Use it with `@DbDecodeWith(adapter)` on class constructor parameters or
|
||||
class-body fields/properties participating in `decodeAs<T>()` projection.
|
||||
|
||||
`targetType` is the requested Lyng target type represented as a runtime
|
||||
type object, such as a class or a type expression.
|
||||
|
||||
Default methods throw `NotImplementedException`.
|
||||
*/
|
||||
interface DbFieldAdapter {
|
||||
/*
|
||||
Decode one raw database field value into a Lyng value suitable for the
|
||||
requested target type.
|
||||
*/
|
||||
fun decode(rawValue: Object?, column: SqlColumn, row: SqlRow, targetType: Object): Object? =
|
||||
throw NotImplementedException("DB field adapter decode is not implemented")
|
||||
|
||||
/*
|
||||
Encode one Lyng value into a database field representation.
|
||||
*/
|
||||
fun encode(value: Object?, targetType: Object): Object? =
|
||||
throw NotImplementedException("DB field adapter encode is not implemented")
|
||||
}
|
||||
|
||||
extern class SqlRow {
|
||||
/* Number of columns in the row */
|
||||
val size: Int
|
||||
@@ -34,6 +60,22 @@ extern class SqlRow {
|
||||
names and invalid indexes should also fail.
|
||||
*/
|
||||
override fun getAt(indexOrName: String | Int): Object?
|
||||
|
||||
/*
|
||||
Decode this row into a typed Lyng value.
|
||||
|
||||
For object/class targets, constructor parameters are matched by column
|
||||
label first and then remaining matching serializable mutable fields are
|
||||
assigned.
|
||||
|
||||
If a constructor parameter or class-body field/property has
|
||||
`@DbDecodeWith(adapter)`, the adapter is applied first and its result
|
||||
must match the target member type.
|
||||
|
||||
For single-column rows, the column value may also be decoded directly to
|
||||
the requested target type.
|
||||
*/
|
||||
fun decodeAs<T>(): T
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -69,6 +111,16 @@ extern class ResultSet : Iterable<SqlRow> {
|
||||
internally, but this must not change visible later iteration behavior.
|
||||
*/
|
||||
override fun isEmpty(): Bool
|
||||
|
||||
/*
|
||||
Return a transaction-scoped iterable view that decodes each row with
|
||||
`SqlRow.decodeAs<T>()`.
|
||||
|
||||
The returned iterable itself must not be used after the owning
|
||||
transaction ends. Materialized decoded objects may outlive the
|
||||
transaction.
|
||||
*/
|
||||
fun decodeAs<T>(): Iterable<T>
|
||||
}
|
||||
|
||||
extern class ExecutionResult {
|
||||
|
||||
Reference in new issue
Block a user