Skip to content

Commit

Permalink
EXPOSED-576 DAO Entity.new() fails if there is column with default va… (
Browse files Browse the repository at this point in the history
#2263)

* fix: EXPOSED-576 DAO Entity.new() fails if there is column with default value and transformation
  • Loading branch information
obabichevjb authored Oct 2, 2024
1 parent 232d4a3 commit c1282ad
Show file tree
Hide file tree
Showing 6 changed files with 128 additions and 12 deletions.
1 change: 1 addition & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 3 additions & 1 deletion exposed-core/api/exposed-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -536,12 +536,13 @@ public class org/jetbrains/exposed/sql/ColumnWithTransform : org/jetbrains/expos
public fun <init> (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;
}
Expand Down Expand Up @@ -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 <init> (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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -316,14 +316,39 @@ open class ColumnWithTransform<Unwrapped, Wrapped>(
val transformer: ColumnTransformer<Unwrapped, Wrapped>
) : ColumnType<Wrapped & Any>() {

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<Any, Unwrapped>).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<Any>
get() = when {
delegate is ColumnWithTransform<*, *> -> delegate.originalColumnType
else -> delegate as IColumnType<Any>
}

override fun sqlType(): String = delegate.sqlType()

override var nullable: Boolean
Expand Down Expand Up @@ -365,6 +390,26 @@ open class NullableColumnWithTransform<Unwrapped, Wrapped>(
delegate: IColumnType<Unwrapped & Any>,
transformer: ColumnTransformer<Unwrapped, Wrapped>
) : ColumnWithTransform<Unwrapped, Wrapped>(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<Any, Unwrapped>).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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,16 +165,24 @@ class ResultRow(
fun createAndFillDefaults(columns: List<Column<*>>): 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 <T> Column<T>.defaultValueOrNotInitialized(): Any? {
return when {
defaultValueFun != null -> when {
columnType is ColumnWithTransform<*, *> -> {
(columnType as ColumnWithTransform<Any, Any>).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])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1061,6 +1061,8 @@ interface ISqlExpressionBuilder {
@Suppress("UNCHECKED_CAST", "ComplexMethod")
fun <T, S : T?> ExpressionWithColumnType<S>.asLiteral(value: T): LiteralOp<T> = when {
value is ByteArray && columnType is BasicBinaryColumnType -> stringLiteral(value.toString(Charsets.UTF_8))
columnType is ColumnWithTransform<*, *> -> (columnType as ColumnWithTransform<Any, Any>)
.let { LiteralOp(it.originalColumnType, it.unwrapRecursive(value)) }
else -> LiteralOp(columnType as IColumnType<T & Any>, value)
} as LiteralOp<T>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {

Expand All @@ -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<Int, TransformDataHolder>
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<Int, TransformDataHolder?>
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<TransformDataHolder?, Int?>
assertNotNull(columnType3)
assertEquals(1, columnType3.unwrapRecursive(1))
assertEquals(0, columnType3.unwrapRecursive(null))
}

@Test
fun testSimpleTransforms() {
val tester = object : IntIdTable() {
Expand Down Expand Up @@ -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<Int>) : IntEntity(id) {
Expand All @@ -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<CustomId> {
override fun compareTo(other: CustomId): Int = id.compareTo(other.id)
}
Expand Down

0 comments on commit c1282ad

Please sign in to comment.