diff --git a/build.gradle b/build.gradle index eb316fa4..154fcd24 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ buildscript { ext { - kotlinVersion = "1.4.21" + kotlinVersion = "1.4.32" detektVersion = "1.15.0" } repositories { diff --git a/ktorm-core/src/main/kotlin/org/ktorm/dsl/Dml.kt b/ktorm-core/src/main/kotlin/org/ktorm/dsl/Dml.kt index 21d6d622..ad226c97 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/dsl/Dml.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/dsl/Dml.kt @@ -352,7 +352,7 @@ public open class AssignmentsBuilder { if (method.returnType == Void.TYPE || !method.returnType.isPrimitive) { null } else { - method.returnType.kotlin.defaultValue + method.returnType.defaultValue } } diff --git a/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityDml.kt b/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityDml.kt index 4c6e619b..33e08c1c 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityDml.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityDml.kt @@ -246,7 +246,7 @@ private fun EntityImplementation.findChangedColumns(fromTable: Table<*>): Map { if (binding.onProperty.name in changedProperties) { - val child = this.getProperty(binding.onProperty.name) as Entity<*>? + val child = this.getProperty(binding.onProperty) as Entity<*>? assignments[column] = child?.implementation?.getPrimaryKeyValue(binding.referenceTable as Table<*>) } } @@ -265,7 +265,7 @@ private fun EntityImplementation.findChangedColumns(fromTable: Table<*>): Map) } } - curr = curr.getProperty(prop.name) + curr = curr.getProperty(prop) } } } diff --git a/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityExtensions.kt b/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityExtensions.kt index d78469ec..c4a4620b 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityExtensions.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityExtensions.kt @@ -33,18 +33,18 @@ internal fun EntityImplementation.getPrimaryKeyValue(fromTable: Table<*>): Any? internal fun EntityImplementation.getColumnValue(binding: ColumnBinding): Any? { when (binding) { is ReferenceBinding -> { - val child = this.getProperty(binding.onProperty.name) as Entity<*>? + val child = this.getProperty(binding.onProperty) as Entity<*>? return child?.implementation?.getPrimaryKeyValue(binding.referenceTable as Table<*>) } is NestedBinding -> { var curr: EntityImplementation? = this for ((i, prop) in binding.properties.withIndex()) { if (i != binding.properties.lastIndex) { - val child = curr?.getProperty(prop.name) as Entity<*>? + val child = curr?.getProperty(prop) as Entity<*>? curr = child?.implementation } } - return curr?.getProperty(binding.properties.last().name) + return curr?.getProperty(binding.properties.last()) } } } @@ -72,14 +72,14 @@ internal fun EntityImplementation.setPrimaryKeyValue( internal fun EntityImplementation.setColumnValue(binding: ColumnBinding, value: Any?, forceSet: Boolean = false) { when (binding) { is ReferenceBinding -> { - var child = this.getProperty(binding.onProperty.name) as Entity<*>? + var child = this.getProperty(binding.onProperty) as Entity<*>? if (child == null) { child = Entity.create( entityClass = binding.onProperty.returnType.jvmErasure, fromDatabase = this.fromDatabase, fromTable = binding.referenceTable as Table<*> ) - this.setProperty(binding.onProperty.name, child, forceSet) + this.setProperty(binding.onProperty, child, forceSet) } val refTable = binding.referenceTable as Table<*> @@ -89,17 +89,17 @@ internal fun EntityImplementation.setColumnValue(binding: ColumnBinding, value: var curr: EntityImplementation = this for ((i, prop) in binding.properties.withIndex()) { if (i != binding.properties.lastIndex) { - var child = curr.getProperty(prop.name) as Entity<*>? + var child = curr.getProperty(prop) as Entity<*>? if (child == null) { child = Entity.create(prop.returnType.jvmErasure, parent = curr) - curr.setProperty(prop.name, child, forceSet) + curr.setProperty(prop, child, forceSet) } curr = child.implementation } } - curr.setProperty(binding.properties.last().name, value, forceSet) + curr.setProperty(binding.properties.last(), value, forceSet) } } } diff --git a/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityImplementation.kt b/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityImplementation.kt index dcb0789f..9feb7f4b 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityImplementation.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityImplementation.kt @@ -29,6 +29,7 @@ import kotlin.collections.LinkedHashMap import kotlin.collections.LinkedHashSet import kotlin.reflect.KClass import kotlin.reflect.KProperty1 +import kotlin.reflect.jvm.javaGetter import kotlin.reflect.jvm.jvmErasure import kotlin.reflect.jvm.jvmName import kotlin.reflect.jvm.kotlinFunction @@ -65,8 +66,8 @@ internal class EntityImplementation( "flushChanges" -> this.doFlushChanges() "discardChanges" -> this.doDiscardChanges() "delete" -> this.doDelete() - "get" -> this.getProperty(args!![0] as String) - "set" -> this.setProperty(args!![0] as String, args[1]) + "get" -> this.doGetProperty(args!![0] as String) + "set" -> this.doSetProperty(args!![0] as String, args[1]) "copy" -> this.copy() else -> throw IllegalStateException("Unrecognized method: $method") } @@ -83,14 +84,14 @@ internal class EntityImplementation( val (prop, isGetter) = ktProp if (prop.isAbstract) { if (isGetter) { - val result = this.getProperty(prop.name) + val result = this.getProperty(prop, unboxInlineValues = true) if (result != null || prop.returnType.isMarkedNullable) { return result } else { - return prop.defaultValue.also { cacheDefaultValue(prop, it) } + return method.defaultReturnValue.also { cacheDefaultValue(prop, it) } } } else { - this.setProperty(prop.name, args!![0]) + this.setProperty(prop, args!![0]) return null } } else { @@ -106,9 +107,9 @@ internal class EntityImplementation( } } - private val KProperty1<*, *>.defaultValue: Any get() { + private val Method.defaultReturnValue: Any get() { try { - return returnType.jvmErasure.defaultValue + return returnType.defaultValue } catch (e: Throwable) { val msg = "" + "The value of non-null property [$this] doesn't exist, " + @@ -119,19 +120,19 @@ internal class EntityImplementation( } private fun cacheDefaultValue(prop: KProperty1<*, *>, value: Any) { - val type = prop.returnType.jvmErasure + val type = prop.javaGetter!!.returnType // Skip for primitive types, enums and string, because their default values always share the same instance. - if (type == Boolean::class) return - if (type == Char::class) return - if (type == Byte::class) return - if (type == Short::class) return - if (type == Int::class) return - if (type == Long::class) return - if (type == String::class) return - if (type.java.isEnum) return + if (type == Boolean::class.javaPrimitiveType) return + if (type == Char::class.javaPrimitiveType) return + if (type == Byte::class.javaPrimitiveType) return + if (type == Short::class.javaPrimitiveType) return + if (type == Int::class.javaPrimitiveType) return + if (type == Long::class.javaPrimitiveType) return + if (type == String::class.java) return + if (type.isEnum) return - setProperty(prop.name, value) + setProperty(prop, value) } @Suppress("SwallowedException") @@ -152,11 +153,58 @@ internal class EntityImplementation( } } - fun getProperty(name: String): Any? { + @OptIn(ExperimentalUnsignedTypes::class) + fun getProperty(prop: KProperty1<*, *>, unboxInlineValues: Boolean = false): Any? { + if (!unboxInlineValues) { + return doGetProperty(prop.name) + } + + val returnType = prop.javaGetter!!.returnType + val value = doGetProperty(prop.name) + + // Unbox inline class values if necessary. + // In principle, we need to check for all inline classes, but kotlin-reflect is still unable to determine + // whether a class is inline, so as a workaround, we have to enumerate some common-used types here. + return when { + value is UByte && returnType == Byte::class.javaPrimitiveType -> value.toByte() + value is UShort && returnType == Short::class.javaPrimitiveType -> value.toShort() + value is UInt && returnType == Int::class.javaPrimitiveType -> value.toInt() + value is ULong && returnType == Long::class.javaPrimitiveType -> value.toLong() + value is UByteArray && returnType == ByteArray::class.java -> value.toByteArray() + value is UShortArray && returnType == ShortArray::class.java -> value.toShortArray() + value is UIntArray && returnType == IntArray::class.java -> value.toIntArray() + value is ULongArray && returnType == LongArray::class.java -> value.toLongArray() + else -> value + } + } + + private fun doGetProperty(name: String): Any? { return values[name] } - fun setProperty(name: String, value: Any?, forceSet: Boolean = false) { + @OptIn(ExperimentalUnsignedTypes::class) + fun setProperty(prop: KProperty1<*, *>, value: Any?, forceSet: Boolean = false) { + val propType = prop.returnType.jvmErasure + + // For inline classes, always box the underlying values as wrapper types. + // In principle, we need to check for all inline classes, but kotlin-reflect is still unable to determine + // whether a class is inline, so as a workaround, we have to enumerate some common-used types here. + val boxedValue = when { + propType == UByte::class && value is Byte -> value.toUByte() + propType == UShort::class && value is Short -> value.toUShort() + propType == UInt::class && value is Int -> value.toUInt() + propType == ULong::class && value is Long -> value.toULong() + propType == UByteArray::class && value is ByteArray -> value.toUByteArray() + propType == UShortArray::class && value is ShortArray -> value.toUShortArray() + propType == UIntArray::class && value is IntArray -> value.toUIntArray() + propType == ULongArray::class && value is LongArray -> value.toULongArray() + else -> value + } + + doSetProperty(prop.name, boxedValue, forceSet) + } + + private fun doSetProperty(name: String, value: Any?, forceSet: Boolean = false) { if (!forceSet && isPrimaryKey(name) && name in values) { val msg = "Cannot modify the primary key `$name` because it's already set to ${values[name]}" throw UnsupportedOperationException(msg) diff --git a/ktorm-core/src/main/kotlin/org/ktorm/schema/ColumnBindingHandler.kt b/ktorm-core/src/main/kotlin/org/ktorm/schema/ColumnBindingHandler.kt index d67864df..bf507565 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/schema/ColumnBindingHandler.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/schema/ColumnBindingHandler.kt @@ -24,15 +24,12 @@ import java.util.* import kotlin.reflect.KClass import kotlin.reflect.KMutableProperty import kotlin.reflect.KProperty1 -import kotlin.reflect.full.createInstance import kotlin.reflect.full.declaredMemberProperties import kotlin.reflect.full.isSubclassOf import kotlin.reflect.jvm.javaGetter import kotlin.reflect.jvm.javaSetter -import kotlin.reflect.jvm.jvmErasure @PublishedApi -@OptIn(ExperimentalUnsignedTypes::class) internal class ColumnBindingHandler(val properties: MutableList>) : InvocationHandler { override fun invoke(proxy: Any, method: Method, args: Array?): Any? { @@ -51,14 +48,10 @@ internal class ColumnBindingHandler(val properties: MutableList properties += prop - val returnType = prop.returnType.jvmErasure + val returnType = method.returnType return when { - returnType.isSubclassOf(Entity::class) -> createProxy(returnType, properties) - returnType.java.isPrimitive -> returnType.defaultValue - returnType == UByte::class -> 0.toByte() - returnType == UShort::class -> 0.toShort() - returnType == UInt::class -> 0 - returnType == ULong::class -> 0L + returnType.kotlin.isSubclassOf(Entity::class) -> createProxy(returnType.kotlin, properties) + returnType.isPrimitive -> returnType.defaultValue else -> null } } @@ -90,39 +83,43 @@ internal val Method.kotlinProperty: Pair, Boolean>? get() { return null } -// should use java Class instead of KClass to avoid inline class issues. -internal val KClass<*>.defaultValue: Any get() { +@OptIn(ExperimentalUnsignedTypes::class) +internal val Class<*>.defaultValue: Any get() { val value = when { - this == Boolean::class -> false - this == Char::class -> 0.toChar() - this == Byte::class -> 0.toByte() - this == Short::class -> 0.toShort() - this == Int::class -> 0 - this == Long::class -> 0L - this == Float::class -> 0.0F - this == Double::class -> 0.0 - this == String::class -> "" - this.isSubclassOf(Entity::class) -> Entity.create(this) - this.java.isEnum -> this.java.enumConstants[0] - this.java.isArray -> this.java.componentType.createArray(0) - this == Set::class || this == MutableSet::class -> LinkedHashSet() - this == List::class || this == MutableList::class -> ArrayList() - this == Collection::class || this == MutableCollection::class -> ArrayList() - this == Map::class || this == MutableMap::class -> LinkedHashMap() - this == Queue::class || this == Deque::class -> LinkedList() - this == SortedSet::class || this == NavigableSet::class -> TreeSet() - this == SortedMap::class || this == NavigableMap::class -> TreeMap() - else -> this.createInstance() + this == Boolean::class.javaPrimitiveType -> false + this == Char::class.javaPrimitiveType -> 0.toChar() + this == Byte::class.javaPrimitiveType -> 0.toByte() + this == Short::class.javaPrimitiveType -> 0.toShort() + this == Int::class.javaPrimitiveType -> 0 + this == Long::class.javaPrimitiveType -> 0L + this == Float::class.javaPrimitiveType -> 0.0F + this == Double::class.javaPrimitiveType -> 0.0 + this == String::class.java -> "" + this == UByte::class.java -> 0.toUByte() + this == UShort::class.java -> 0.toUShort() + this == UInt::class.java -> 0U + this == ULong::class.java -> 0UL + this == UByteArray::class.java -> ubyteArrayOf() + this == UShortArray::class.java -> ushortArrayOf() + this == UIntArray::class.java -> uintArrayOf() + this == ULongArray::class.java -> ulongArrayOf() + this == Set::class.java -> LinkedHashSet() + this == List::class.java -> ArrayList() + this == Collection::class.java -> ArrayList() + this == Map::class.java -> LinkedHashMap() + this == Queue::class.java || this == Deque::class.java -> LinkedList() + this == SortedSet::class.java || this == NavigableSet::class.java -> TreeSet() + this == SortedMap::class.java || this == NavigableMap::class.java -> TreeMap() + this.isEnum -> this.enumConstants[0] + this.isArray -> java.lang.reflect.Array.newInstance(this.componentType, 0) + this.kotlin.isSubclassOf(Entity::class) -> Entity.create(this.kotlin) + else -> this.newInstance() } - if (this.isInstance(value)) { + if (this.kotlin.isInstance(value)) { return value } else { // never happens... throw AssertionError("$value must be instance of $this") } } - -private fun Class<*>.createArray(length: Int): Any { - return java.lang.reflect.Array.newInstance(this, length) -} diff --git a/ktorm-core/src/test/kotlin/org/ktorm/database/DatabaseTest.kt b/ktorm-core/src/test/kotlin/org/ktorm/database/DatabaseTest.kt index 3979487c..27d373da 100644 --- a/ktorm-core/src/test/kotlin/org/ktorm/database/DatabaseTest.kt +++ b/ktorm-core/src/test/kotlin/org/ktorm/database/DatabaseTest.kt @@ -3,10 +3,7 @@ package org.ktorm.database import org.junit.Test import org.ktorm.BaseTest import org.ktorm.dsl.* -import org.ktorm.entity.Entity -import org.ktorm.entity.count -import org.ktorm.entity.mapColumns -import org.ktorm.entity.sequenceOf +import org.ktorm.entity.* import org.ktorm.schema.* import java.sql.PreparedStatement import java.sql.ResultSet @@ -108,21 +105,90 @@ class DatabaseTest : BaseTest() { }) } - interface Emp : Entity { - companion object : Entity.Factory() - val id: ULong + interface TestUnsigned : Entity { + companion object : Entity.Factory() + var id: ULong } @Test - fun testULong() { - val t = object : Table("t_employee") { - val id = ulong("id").primaryKey().bindTo { it.id } + fun testUnsigned() { + val t = object : Table("T_TEST_UNSIGNED") { + val id = ulong("ID").primaryKey().bindTo { it.id } } - val ids = database.sequenceOf(t).mapColumns { it.id } + database.useConnection { conn -> + conn.createStatement().use { statement -> + val sql = """CREATE TABLE T_TEST_UNSIGNED(ID BIGINT UNSIGNED NOT NULL PRIMARY KEY)""" + statement.executeUpdate(sql) + } + } + + val unsigned = TestUnsigned { id = 5UL } + assert(unsigned.id == 5UL) + database.sequenceOf(t).add(unsigned) + + val ids = database.sequenceOf(t).toList().map { it.id } println(ids) - assert(ids == listOf(1.toULong(), 2.toULong(), 3.toULong(), 4.toULong())) + assert(ids == listOf(5UL)) - assert(Emp().id == 0.toULong()) + database.insert(t) { + set(it.id, 6UL) + } + + val ids2 = database.from(t).select(t.id).map { row -> row[t.id] } + println(ids2) + assert(ids2 == listOf(5UL, 6UL)) + + assert(TestUnsigned().id == 0UL) + } + + interface TestUnsignedNullable : Entity { + companion object : Entity.Factory() + var id: ULong? + } + + @Test + fun testUnsignedNullable() { + val t = object : Table("T_TEST_UNSIGNED_NULLABLE") { + val id = ulong("ID").primaryKey().bindTo { it.id } + } + + database.useConnection { conn -> + conn.createStatement().use { statement -> + val sql = """CREATE TABLE T_TEST_UNSIGNED_NULLABLE(ID BIGINT UNSIGNED NOT NULL PRIMARY KEY)""" + statement.executeUpdate(sql) + } + } + + val unsigned = TestUnsignedNullable { id = 5UL } + assert(unsigned.id == 5UL) + database.sequenceOf(t).add(unsigned) + + val ids = database.sequenceOf(t).toList().map { it.id } + println(ids) + assert(ids == listOf(5UL)) + + assert(TestUnsignedNullable().id == null) + } + + @Test + fun testDefaultValueReferenceEquality() { + assert(Boolean::class.javaPrimitiveType!!.defaultValue === Boolean::class.javaPrimitiveType!!.defaultValue) + assert(Char::class.javaPrimitiveType!!.defaultValue === Char::class.javaPrimitiveType!!.defaultValue) + assert(Byte::class.javaPrimitiveType!!.defaultValue === Byte::class.javaPrimitiveType!!.defaultValue) + assert(Short::class.javaPrimitiveType!!.defaultValue === Short::class.javaPrimitiveType!!.defaultValue) + assert(Int::class.javaPrimitiveType!!.defaultValue === Int::class.javaPrimitiveType!!.defaultValue) + assert(Long::class.javaPrimitiveType!!.defaultValue === Long::class.javaPrimitiveType!!.defaultValue) + assert(Float::class.javaPrimitiveType!!.defaultValue !== Float::class.javaPrimitiveType!!.defaultValue) + assert(Double::class.javaPrimitiveType!!.defaultValue !== Double::class.javaPrimitiveType!!.defaultValue) + assert(String::class.java.defaultValue === String::class.java.defaultValue) + assert(UByte::class.java.defaultValue !== UByte::class.java.defaultValue) + assert(UShort::class.java.defaultValue !== UShort::class.java.defaultValue) + assert(UInt::class.java.defaultValue !== UInt::class.java.defaultValue) + assert(ULong::class.java.defaultValue !== ULong::class.java.defaultValue) + assert(UByteArray::class.java.defaultValue !== UByteArray::class.java.defaultValue) + assert(UShortArray::class.java.defaultValue !== UShortArray::class.java.defaultValue) + assert(UIntArray::class.java.defaultValue !== UIntArray::class.java.defaultValue) + assert(ULongArray::class.java.defaultValue !== ULongArray::class.java.defaultValue) } } \ No newline at end of file