From 8f8499c7b6fcbb81d6e96fe2de46dac9fb9e0d7e Mon Sep 17 00:00:00 2001 From: bog-walk <82039410+bog-walk@users.noreply.github.com> Date: Wed, 18 Sep 2024 15:04:06 -0400 Subject: [PATCH] fix: EXPOSED-547 idParam() registers composite id value with a single 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. --- .../kotlin/org/jetbrains/exposed/sql/Op.kt | 13 ++++++- .../exposed/sql/SQLExpressionBuilder.kt | 38 ++++++++++--------- .../entities/CompositeIdTableEntityTest.kt | 19 ++++++++++ .../sql/tests/shared/entities/EntityTests.kt | 17 +++++---- 4 files changed, 61 insertions(+), 26 deletions(-) diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Op.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Op.kt index 91cd5fe161..637344066e 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Op.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Op.kt @@ -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 @@ -702,7 +703,17 @@ class QueryParameter( /** Returns the column type of this expression. */ val sqlType: IColumnType ) : Expression() { - 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]. */ 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 662fa7e011..4be37ac362 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 @@ -325,8 +325,9 @@ interface ISqlExpressionBuilder { } /** Checks if this expression is equal to some [other] expression. */ - infix fun Expression.eq(other: Expression): Op = when (other as Expression<*>) { - is Op.NULL -> isNull() + infix fun Expression.eq(other: Expression): Op = 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) } @@ -368,8 +369,9 @@ interface ISqlExpressionBuilder { } /** Checks if this expression is not equal to some [other] expression. */ - infix fun Expression.neq(other: Expression): Op = when (other as Expression<*>) { - is Op.NULL -> isNotNull() + infix fun Expression.neq(other: Expression): Op = 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) } @@ -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 Expression.isNull() = if (this is Column<*> && isEntityIdentifier()) { - table.mapIdOperator(::IsNullOp) - } else { - IsNullOp(this) + fun Expression.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 Expression.isNullOrEmpty() = if (this is Column<*> && isEntityIdentifier()) { - table.mapIdOperator(::IsNullOp) - } else { - IsNullOp(this) - }.or { this@isNullOrEmpty.charLength() eq 0 } + fun Expression.isNullOrEmpty() = IsNullOp(this).or { this@isNullOrEmpty.charLength() eq 0 } /** Returns `true` if this expression is not null, `false` otherwise. */ - fun Expression.isNotNull() = if (this is Column<*> && isEntityIdentifier()) { - table.mapIdOperator(::IsNotNullOp) - } else { - IsNotNullOp(this) + fun Expression.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 */ diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/entities/CompositeIdTableEntityTest.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/entities/CompositeIdTableEntityTest.kt index a34f2f49b8..f8e7df7a28 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/entities/CompositeIdTableEntityTest.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/entities/CompositeIdTableEntityTest.kt @@ -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) { diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/entities/EntityTests.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/entities/EntityTests.kt index c8f3647d08..11e1ea9d28 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/entities/EntityTests.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/entities/EntityTests.kt @@ -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 @@ -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] ) }