From c1282addb174f4672697301c1c577fca0fb479f6 Mon Sep 17 00:00:00 2001 From: obabichevjb <166523824+obabichevjb@users.noreply.github.com> Date: Wed, 2 Oct 2024 13:06:49 +0200 Subject: [PATCH] =?UTF-8?q?EXPOSED-576=20DAO=20Entity.new()=20fails=20if?= =?UTF-8?q?=20there=20is=20column=20with=20default=20va=E2=80=A6=20(#2263)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: EXPOSED-576 DAO Entity.new() fails if there is column with default value and transformation --- .editorconfig | 1 + exposed-core/api/exposed-core.api | 4 +- .../org/jetbrains/exposed/sql/ColumnType.kt | 49 +++++++++++++- .../org/jetbrains/exposed/sql/ResultRow.kt | 20 ++++-- .../exposed/sql/SQLExpressionBuilder.kt | 2 + .../shared/dml/ColumnWithTransformTest.kt | 64 ++++++++++++++++++- 6 files changed, 128 insertions(+), 12 deletions(-) diff --git a/.editorconfig b/.editorconfig index 0fa7fc2648..1a8e06c1de 100644 --- a/.editorconfig +++ b/.editorconfig @@ -40,6 +40,7 @@ ij_kotlin_spaces_around_logical_operators = true ij_kotlin_spaces_around_equality_operators = true ij_any_align_group_field_declarations = false ij_java_align_group_field_declarations = false +ij_kotlin_line_break_after_multiline_when_entry = false [{.github/**/*.yml, .idea/*.xml}] indent_size = 2 diff --git a/exposed-core/api/exposed-core.api b/exposed-core/api/exposed-core.api index f8d9e9b781..b21738db24 100644 --- a/exposed-core/api/exposed-core.api +++ b/exposed-core/api/exposed-core.api @@ -536,12 +536,13 @@ public class org/jetbrains/exposed/sql/ColumnWithTransform : org/jetbrains/expos public fun (Lorg/jetbrains/exposed/sql/IColumnType;Lorg/jetbrains/exposed/sql/ColumnTransformer;)V public final fun getDelegate ()Lorg/jetbrains/exposed/sql/IColumnType; public fun getNullable ()Z + public final fun getOriginalColumnType ()Lorg/jetbrains/exposed/sql/IColumnType; public final fun getTransformer ()Lorg/jetbrains/exposed/sql/ColumnTransformer; public fun notNullValueToDB (Ljava/lang/Object;)Ljava/lang/Object; public fun setNullable (Z)V public fun setParameter (Lorg/jetbrains/exposed/sql/statements/api/PreparedStatementApi;ILjava/lang/Object;)V public fun sqlType ()Ljava/lang/String; - public final fun unwrapRecursive (Ljava/lang/Object;)Ljava/lang/Object; + public fun unwrapRecursive (Ljava/lang/Object;)Ljava/lang/Object; public fun valueFromDB (Ljava/lang/Object;)Ljava/lang/Object; public fun valueToDB (Ljava/lang/Object;)Ljava/lang/Object; } @@ -1693,6 +1694,7 @@ public final class org/jetbrains/exposed/sql/Ntile : org/jetbrains/exposed/sql/W public class org/jetbrains/exposed/sql/NullableColumnWithTransform : org/jetbrains/exposed/sql/ColumnWithTransform { public fun (Lorg/jetbrains/exposed/sql/IColumnType;Lorg/jetbrains/exposed/sql/ColumnTransformer;)V + public fun unwrapRecursive (Ljava/lang/Object;)Ljava/lang/Object; public fun valueFromDB (Ljava/lang/Object;)Ljava/lang/Object; public fun valueToDB (Ljava/lang/Object;)Ljava/lang/Object; public fun valueToString (Ljava/lang/Object;)Ljava/lang/String; diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/ColumnType.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/ColumnType.kt index 8a524e0108..d1ef2b826f 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/ColumnType.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/ColumnType.kt @@ -316,14 +316,39 @@ open class ColumnWithTransform( val transformer: ColumnTransformer ) : ColumnType() { - fun unwrapRecursive(value: Wrapped?): Any? { + /** + * Recursively unwraps the given value by applying the delegate's transformation. + * + * This method will recursively call unwrap on the inner delegate if the delegate + * is also an instance of [ColumnWithTransform]. This is useful for handling nested + * transformations. + * + * @param value The value to unwrap. Could be null. + * @return The unwrapped value. Returns the value transformed by the transformer if it's not null. + */ + open fun unwrapRecursive(value: Wrapped?): Any? { return if (delegate is ColumnWithTransform<*, *>) { (delegate as ColumnWithTransform).unwrapRecursive(transformer.unwrap(value as Wrapped)) } else { - transformer.unwrap(value!!) + value?.let { transformer.unwrap(value) } } } + /** + * Gets the original column type that this column with transformation wraps around. + * + * This property will recursively unwrap the delegate column type if the delegate + * is also an instance of [ColumnWithTransform]. This ensures that you get the + * original column type, regardless of the number of nested transformations. + * + * @return The original column's [IColumnType]. + */ + val originalColumnType: IColumnType + get() = when { + delegate is ColumnWithTransform<*, *> -> delegate.originalColumnType + else -> delegate as IColumnType + } + override fun sqlType(): String = delegate.sqlType() override var nullable: Boolean @@ -365,6 +390,26 @@ open class NullableColumnWithTransform( delegate: IColumnType, transformer: ColumnTransformer ) : ColumnWithTransform(delegate, transformer) { + /** + * Recursively unwraps the given value by applying the delegate's transformation. + * + * This method will recursively call unwrap on the inner delegate if the delegate + * is also an instance of [ColumnWithTransform]. This is useful for handling nested + * transformations. Unlike [ColumnWithTransform.unwrapRecursive], this method allows + * transformation involving `null` values. + * + * @param value The value to unwrap. Could be `null`. + * @return The unwrapped value. Returns the value transformed by the transformer, which + * could be `null` if the transformer design allows it. + */ + override fun unwrapRecursive(value: Wrapped?): Any? { + return if (delegate is ColumnWithTransform<*, *>) { + (delegate as ColumnWithTransform).unwrapRecursive(transformer.unwrap(value as Wrapped)) + } else { + transformer.unwrap(value as Wrapped) + } + } + override fun valueFromDB(value: Any): Wrapped? { return transformer.wrap(delegate.valueFromDB(value) as Unwrapped) } diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/ResultRow.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/ResultRow.kt index 02a4518cde..302084de9c 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/ResultRow.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/ResultRow.kt @@ -165,16 +165,24 @@ class ResultRow( fun createAndFillDefaults(columns: List>): ResultRow = ResultRow(columns.withIndex().associate { it.value to it.index }).apply { columns.forEach { - val value = when { - it.defaultValueFun != null -> it.defaultValueFun!!() - it.columnType.nullable -> null - else -> NotInitializedValue - } - setInternal(it, value) + setInternal(it, it.defaultValueOrNotInitialized()) } } } + private fun Column.defaultValueOrNotInitialized(): Any? { + return when { + defaultValueFun != null -> when { + columnType is ColumnWithTransform<*, *> -> { + (columnType as ColumnWithTransform).unwrapRecursive(defaultValueFun!!()) + } + else -> defaultValueFun!!() + } + columnType.nullable -> null + else -> NotInitializedValue + } + } + /** * [ResultRowCache] caches the values on reads by `expression`. The value cached by pair of `expression` itself and `columnType` of that expression. * It solves the problem of "equal" expression with different column type (like the same column with original type and [EntityIDColumnType]) diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/SQLExpressionBuilder.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/SQLExpressionBuilder.kt index 63ed874e17..574f07dfa1 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/SQLExpressionBuilder.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/SQLExpressionBuilder.kt @@ -1061,6 +1061,8 @@ interface ISqlExpressionBuilder { @Suppress("UNCHECKED_CAST", "ComplexMethod") fun ExpressionWithColumnType.asLiteral(value: T): LiteralOp = when { value is ByteArray && columnType is BasicBinaryColumnType -> stringLiteral(value.toString(Charsets.UTF_8)) + columnType is ColumnWithTransform<*, *> -> (columnType as ColumnWithTransform) + .let { LiteralOp(it.originalColumnType, it.unwrapRecursive(value)) } else -> LiteralOp(columnType as IColumnType, value) } as LiteralOp diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/ColumnWithTransformTest.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/ColumnWithTransformTest.kt index 7ec64b094b..de5f24aa2d 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/ColumnWithTransformTest.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/ColumnWithTransformTest.kt @@ -11,6 +11,9 @@ import org.jetbrains.exposed.sql.tests.shared.assertEqualLists import org.jetbrains.exposed.sql.tests.shared.assertEquals import org.junit.Test import java.util.* +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull class ColumnWithTransformTest : DatabaseTestsBase() { @@ -31,6 +34,43 @@ class ColumnWithTransformTest : DatabaseTestsBase() { override fun wrap(value: Int): TransformDataHolder? = if (value == 0) null else TransformDataHolder(value) } + @Test + fun testRecursiveUnwrap() { + val tester1 = object : IntIdTable() { + val value = integer("value") + .transform(DataHolderTransformer()) + .nullable() + } + + val columnType1 = tester1.value.columnType as? ColumnWithTransform + assertNotNull(columnType1) + assertEquals(1, columnType1.unwrapRecursive(TransformDataHolder(1))) + assertNull(columnType1.unwrapRecursive(null)) + + // Transform null into non-nullable value + val tester2 = object : IntIdTable() { + val value = integer("value") + .nullTransform(DataHolderNullTransformer()) + } + + val columnType2 = tester2.value.columnType as? ColumnWithTransform + assertNotNull(columnType2) + assertEquals(1, columnType2.unwrapRecursive(TransformDataHolder(1))) + assertEquals(0, columnType2.unwrapRecursive(null)) + + val tester3 = object : IntIdTable() { + val value = integer("value") + .transform(DataHolderTransformer()) + .nullable() + .transform(wrap = { it?.value ?: 0 }, unwrap = { TransformDataHolder(it ?: 0) }) + } + + val columnType3 = tester3.value.columnType as? ColumnWithTransform + assertNotNull(columnType3) + assertEquals(1, columnType3.unwrapRecursive(1)) + assertEquals(0, columnType3.unwrapRecursive(null)) + } + @Test fun testSimpleTransforms() { val tester = object : IntIdTable() { @@ -135,11 +175,14 @@ class ColumnWithTransformTest : DatabaseTestsBase() { } } - object TransformTable : IntIdTable("transform-table") { - val simple = integer("simple").transform(DataHolderTransformer()) - val chained = text("chained") + object TransformTable : IntIdTable("transform_table") { + val simple = integer("simple") + .default(1) + .transform(DataHolderTransformer()) + val chained = varchar("chained", length = 128) .transform(wrap = { it.toInt() }, unwrap = { it.toString() }) .transform(DataHolderTransformer()) + .default(TransformDataHolder(2)) } class TransformEntity(id: EntityID) : IntEntity(id) { @@ -166,6 +209,21 @@ class ColumnWithTransformTest : DatabaseTestsBase() { } } + @Test + fun testEntityWithDefaultValue() { + withTables(TransformTable) { + val entity = TransformEntity.new {} + + assertEquals(TransformDataHolder(1), entity.simple) + assertEquals(TransformDataHolder(2), entity.chained) + + val entry = TransformTable.selectAll().first() + + assertEquals(1, entry[TransformTable.simple].value) + assertEquals(2, entry[TransformTable.chained].value) + } + } + data class CustomId(val id: UUID) : Comparable { override fun compareTo(other: CustomId): Int = id.compareTo(other.id) }