Skip to content

Commit

Permalink
fix: EXPOSED-547 idParam() registers composite id value with a single…
Browse files Browse the repository at this point in the history
… placeholder (#2242)

* fix: idParam with CompositeID value registers single argument

Current idParam registers the value from an entity id column with a single placeholder
marker. This fix ensures that a placeholder is registered for every component
value stored by CompositeID and that this argument registeration is overriden
and handled by the mapping operator as needed.
  • Loading branch information
bog-walk authored Sep 18, 2024
1 parent 851cbbd commit 8f8499c
Show file tree
Hide file tree
Showing 4 changed files with 61 additions and 26 deletions.
13 changes: 12 additions & 1 deletion exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Op.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.jetbrains.exposed.sql

import org.jetbrains.exposed.dao.id.CompositeID
import org.jetbrains.exposed.dao.id.EntityID
import org.jetbrains.exposed.sql.SqlExpressionBuilder.wrap
import org.jetbrains.exposed.sql.statements.api.ExposedBlob
Expand Down Expand Up @@ -702,7 +703,17 @@ class QueryParameter<T>(
/** Returns the column type of this expression. */
val sqlType: IColumnType<T & Any>
) : Expression<T>() {
override fun toQueryBuilder(queryBuilder: QueryBuilder): Unit = queryBuilder { registerArgument(sqlType, value) }
internal val compositeValue: CompositeID? = (value as? EntityID<*>)?.value as? CompositeID

override fun toQueryBuilder(queryBuilder: QueryBuilder) {
queryBuilder {
compositeValue?.let {
it.values.entries.appendTo { (column, value) ->
registerArgument(column.columnType, value)
}
} ?: registerArgument(sqlType, value)
}
}
}

/** Returns the specified [value] as a query parameter with the same type as [column]. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -325,8 +325,9 @@ interface ISqlExpressionBuilder {
}

/** Checks if this expression is equal to some [other] expression. */
infix fun <T, S1 : T?, S2 : T?> Expression<in S1>.eq(other: Expression<in S2>): Op<Boolean> = when (other as Expression<*>) {
is Op.NULL -> isNull()
infix fun <T, S1 : T?, S2 : T?> Expression<in S1>.eq(other: Expression<in S2>): Op<Boolean> = when {
(other as Expression<*>) is Op.NULL -> isNull()
(other as? QueryParameter)?.compositeValue != null -> (this as Column<*>).table.mapIdComparison(other.value, ::EqOp)
else -> EqOp(this, other)
}

Expand Down Expand Up @@ -368,8 +369,9 @@ interface ISqlExpressionBuilder {
}

/** Checks if this expression is not equal to some [other] expression. */
infix fun <T, S1 : T?, S2 : T?> Expression<in S1>.neq(other: Expression<in S2>): Op<Boolean> = when (other as Expression<*>) {
is Op.NULL -> isNotNull()
infix fun <T, S1 : T?, S2 : T?> Expression<in S1>.neq(other: Expression<in S2>): Op<Boolean> = when {
(other as Expression<*>) is Op.NULL -> isNotNull()
(other as? QueryParameter)?.compositeValue != null -> (this as Column<*>).table.mapIdComparison(other.value, ::NeqOp)
else -> NeqOp(this, other)
}

Expand Down Expand Up @@ -505,24 +507,26 @@ interface ISqlExpressionBuilder {
Between(this, wrap(EntityID(from, this.idTable())), wrap(EntityID(to, this.idTable())))

/** Returns `true` if this expression is null, `false` otherwise. */
fun <T> Expression<T>.isNull() = if (this is Column<*> && isEntityIdentifier()) {
table.mapIdOperator(::IsNullOp)
} else {
IsNullOp(this)
fun <T> Expression<T>.isNull() = when {
this is Column<*> && isEntityIdentifier() -> table.mapIdOperator(::IsNullOp)
this is QueryParameter && compositeValue != null -> {
val table = compositeValue.values.keys.first().table
table.mapIdOperator(::IsNullOp)
}
else -> IsNullOp(this)
}

/** Returns `true` if this string expression is null or empty, `false` otherwise. */
fun <T : String?> Expression<T>.isNullOrEmpty() = if (this is Column<*> && isEntityIdentifier()) {
table.mapIdOperator(::IsNullOp)
} else {
IsNullOp(this)
}.or { this@isNullOrEmpty.charLength() eq 0 }
fun <T : String?> Expression<T>.isNullOrEmpty() = IsNullOp(this).or { this@isNullOrEmpty.charLength() eq 0 }

/** Returns `true` if this expression is not null, `false` otherwise. */
fun <T> Expression<T>.isNotNull() = if (this is Column<*> && isEntityIdentifier()) {
table.mapIdOperator(::IsNotNullOp)
} else {
IsNotNullOp(this)
fun <T> Expression<T>.isNotNull() = when {
this is Column<*> && isEntityIdentifier() -> table.mapIdOperator(::IsNotNullOp)
this is QueryParameter && compositeValue != null -> {
val table = compositeValue.values.keys.first().table
table.mapIdOperator(::IsNotNullOp)
}
else -> IsNotNullOp(this)
}

/** Checks if this expression is equal to some [t] value, with `null` treated as a comparable value */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,25 @@ class CompositeIdTableEntityTest : DatabaseTestsBase() {
}
}

@Test
fun testIdParamWithCompositeValue() {
withTables(Towns) {
val townAValue = CompositeID {
it[Towns.zipCode] = "1A2 3B4"
it[Towns.name] = "Town A"
}
val townAId = Towns.insertAndGetId {
it[id] = townAValue
it[population] = 4
}

val query = Towns.selectAll().where { Towns.id eq idParam(townAId, Towns.id) }
val whereClause = query.prepareSQL(this, prepared = true).substringAfter("WHERE ")
assertEquals("(${fullIdentity(Towns.zipCode)} = ?) AND (${fullIdentity(Towns.name)} = ?)", whereClause)
assertEquals(4, query.single()[Towns.population])
}
}

@Test
fun testFlushingUpdatedEntity() {
withTables(Towns) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import org.jetbrains.exposed.dao.id.IntIdTable
import org.jetbrains.exposed.dao.id.LongIdTable
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.SqlExpressionBuilder.less
import org.jetbrains.exposed.sql.tests.DatabaseTestsBase
import org.jetbrains.exposed.sql.tests.TestDB
import org.jetbrains.exposed.sql.tests.currentDialectTest
Expand Down Expand Up @@ -1605,18 +1606,18 @@ class EntityTests : DatabaseTestsBase() {
@Test
fun testEntityIdParam() {
withTables(CreditCards) {
val creditCard = CreditCard.new {
val newCard = CreditCard.new {
number = "0000111122223333"
spendingLimit = 10000u
spendingLimit = 10000uL
}
val conditionalId = Case()
.When(CreditCards.spendingLimit less 500uL, CreditCards.id)
.Else(idParam(newCard.id, CreditCards.id))
assertEquals(newCard.id, CreditCards.select(conditionalId).single()[conditionalId])
assertEquals(
1,
CreditCards.select(idParam(creditCard.id, CreditCards.id)).count()
)
assertEquals(
10000u,
10000uL,
CreditCards.select(CreditCards.spendingLimit)
.where { CreditCards.id eq idParam(creditCard.id, CreditCards.id) }
.where { CreditCards.id eq idParam(newCard.id, CreditCards.id) }
.single()[CreditCards.spendingLimit]
)
}
Expand Down

0 comments on commit 8f8499c

Please sign in to comment.