Skip to content

Commit

Permalink
feat: Add ability to use aliases on expressions with column type to f…
Browse files Browse the repository at this point in the history
…ix EXPOSED-544 Coalesce mismatch error (#2308)

* feat: Add ability to use aliases on expressions with column type to fix EXPOSED-544 Coalesce mismatch error

Introduced a new class `ExpressionWithColumnTypeAlias` similar to `ExpressionAlias` to be able to use aliases with the Coalesce function

* chore: rewrite with interface to avoid code duplication
  • Loading branch information
joc-a authored Nov 28, 2024
1 parent 7e5d03f commit 33376e0
Show file tree
Hide file tree
Showing 10 changed files with 155 additions and 29 deletions.
34 changes: 30 additions & 4 deletions exposed-core/api/exposed-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ public final class org/jetbrains/exposed/sql/Alias : org/jetbrains/exposed/sql/T
public final class org/jetbrains/exposed/sql/AliasKt {
public static final fun alias (Lorg/jetbrains/exposed/sql/AbstractQuery;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/QueryAlias;
public static final fun alias (Lorg/jetbrains/exposed/sql/Expression;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/ExpressionAlias;
public static final fun alias (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/ExpressionWithColumnTypeAlias;
public static final fun alias (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/Alias;
public static final fun getLastQueryAlias (Lorg/jetbrains/exposed/sql/Join;)Lorg/jetbrains/exposed/sql/QueryAlias;
public static final fun joinQuery (Lorg/jetbrains/exposed/sql/Join;Lkotlin/jvm/functions/Function2;Lorg/jetbrains/exposed/sql/JoinType;ZLkotlin/jvm/functions/Function0;)Lorg/jetbrains/exposed/sql/Join;
Expand Down Expand Up @@ -950,11 +951,12 @@ public final class org/jetbrains/exposed/sql/Expression$Companion {
public final fun build (Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/sql/Expression;
}

public final class org/jetbrains/exposed/sql/ExpressionAlias : org/jetbrains/exposed/sql/Expression {
public final class org/jetbrains/exposed/sql/ExpressionAlias : org/jetbrains/exposed/sql/Expression, org/jetbrains/exposed/sql/IExpressionAlias {
public fun <init> (Lorg/jetbrains/exposed/sql/Expression;Ljava/lang/String;)V
public final fun aliasOnlyExpression ()Lorg/jetbrains/exposed/sql/Expression;
public final fun getAlias ()Ljava/lang/String;
public final fun getDelegate ()Lorg/jetbrains/exposed/sql/Expression;
public fun aliasOnlyExpression ()Lorg/jetbrains/exposed/sql/Expression;
public fun getAlias ()Ljava/lang/String;
public fun getDelegate ()Lorg/jetbrains/exposed/sql/Expression;
public fun queryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V
public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V
}

Expand All @@ -969,6 +971,17 @@ public abstract class org/jetbrains/exposed/sql/ExpressionWithColumnType : org/j
public abstract fun getColumnType ()Lorg/jetbrains/exposed/sql/IColumnType;
}

public final class org/jetbrains/exposed/sql/ExpressionWithColumnTypeAlias : org/jetbrains/exposed/sql/ExpressionWithColumnType, org/jetbrains/exposed/sql/IExpressionAlias {
public fun <init> (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/String;)V
public fun aliasOnlyExpression ()Lorg/jetbrains/exposed/sql/Expression;
public fun getAlias ()Ljava/lang/String;
public fun getColumnType ()Lorg/jetbrains/exposed/sql/IColumnType;
public synthetic fun getDelegate ()Lorg/jetbrains/exposed/sql/Expression;
public fun getDelegate ()Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;
public fun queryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V
public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V
}

public abstract interface class org/jetbrains/exposed/sql/FieldSet {
public abstract fun getFields ()Ljava/util/List;
public abstract fun getRealFields ()Ljava/util/List;
Expand Down Expand Up @@ -1079,6 +1092,18 @@ public abstract interface class org/jetbrains/exposed/sql/IDateColumnType {
public abstract fun getHasTimePart ()Z
}

public abstract interface class org/jetbrains/exposed/sql/IExpressionAlias {
public abstract fun aliasOnlyExpression ()Lorg/jetbrains/exposed/sql/Expression;
public abstract fun getAlias ()Ljava/lang/String;
public abstract fun getDelegate ()Lorg/jetbrains/exposed/sql/Expression;
public abstract fun queryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V
}

public final class org/jetbrains/exposed/sql/IExpressionAlias$DefaultImpls {
public static fun aliasOnlyExpression (Lorg/jetbrains/exposed/sql/IExpressionAlias;)Lorg/jetbrains/exposed/sql/Expression;
public static fun queryBuilder (Lorg/jetbrains/exposed/sql/IExpressionAlias;Lorg/jetbrains/exposed/sql/QueryBuilder;)V
}

public abstract interface class org/jetbrains/exposed/sql/ISqlExpressionBuilder {
public abstract fun asLiteral (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Object;)Lorg/jetbrains/exposed/sql/LiteralOp;
public abstract fun between (Lorg/jetbrains/exposed/sql/Column;Ljava/lang/Object;Ljava/lang/Object;)Lorg/jetbrains/exposed/sql/Between;
Expand Down Expand Up @@ -1965,6 +1990,7 @@ public final class org/jetbrains/exposed/sql/QueryAlias : org/jetbrains/exposed/
public fun fullJoin (Lorg/jetbrains/exposed/sql/ColumnSet;)Lorg/jetbrains/exposed/sql/Join;
public final fun get (Lorg/jetbrains/exposed/sql/Column;)Lorg/jetbrains/exposed/sql/Column;
public final fun get (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Expression;
public final fun get (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;)Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;
public final fun getAlias ()Ljava/lang/String;
public fun getColumns ()Ljava/util/List;
public fun getFields ()Ljava/util/List;
Expand Down
66 changes: 53 additions & 13 deletions exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Alias.kt
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,12 @@ class Alias<out T : Table>(val delegate: T, val alias: String) : Table() {
.orEmpty()
}

/** Represents a temporary SQL identifier, [alias], for a [delegate] expression. */
class ExpressionAlias<T>(val delegate: Expression<T>, val alias: String) : Expression<T>() {
override fun toQueryBuilder(queryBuilder: QueryBuilder) = queryBuilder {
interface IExpressionAlias<T> {
val delegate: Expression<T>

val alias: String

fun queryBuilder(queryBuilder: QueryBuilder) = queryBuilder {
if (delegate is ComparisonOp && (currentDialectIfAvailable is SQLServerDialect || currentDialectIfAvailable is OracleDialect)) {
+"(CASE WHEN "
append(delegate)
Expand All @@ -121,17 +124,30 @@ class ExpressionAlias<T>(val delegate: Expression<T>, val alias: String) : Expre
}

/** Returns an [Expression] containing only the string representation of this [alias]. */
fun aliasOnlyExpression(): Expression<T> {
return if (delegate is ExpressionWithColumnType<T>) {
object : Function<T>(delegate.columnType) {
override fun toQueryBuilder(queryBuilder: QueryBuilder) = queryBuilder { append(alias) }
}
} else {
object : Expression<T>() {
fun aliasOnlyExpression(): Expression<T> =
(delegate as? ExpressionWithColumnType<T>)?.columnType?.let { columnType ->
object : Function<T>(columnType) {
override fun toQueryBuilder(queryBuilder: QueryBuilder) = queryBuilder { append(alias) }
}
} ?: object : Expression<T>() {
override fun toQueryBuilder(queryBuilder: QueryBuilder) = queryBuilder { append(alias) }
}
}
}

/** Represents a temporary SQL identifier, [alias], for a [delegate] expression. */
class ExpressionAlias<T>(override val delegate: Expression<T>, override val alias: String) : Expression<T>(), IExpressionAlias<T> {
override fun toQueryBuilder(queryBuilder: QueryBuilder) = this.queryBuilder(queryBuilder)
}

/** Represents a temporary SQL identifier, [alias], for a [delegate] expression with column type. */
class ExpressionWithColumnTypeAlias<T>(
override val delegate: ExpressionWithColumnType<T>,
override val alias: String
) : ExpressionWithColumnType<T>(), IExpressionAlias<T> {
override val columnType: IColumnType<T & Any>
get() = delegate.columnType

override fun toQueryBuilder(queryBuilder: QueryBuilder) = this.queryBuilder(queryBuilder)
}

/** Represents a temporary SQL identifier, [alias], for a [query]. */
Expand All @@ -143,14 +159,18 @@ class QueryAlias(val query: AbstractQuery<*>, val alias: String) : ColumnSet() {
}

override val fields: List<Expression<*>> = query.set.fields.map { expression ->
(expression as? Column<*>)?.clone() ?: (expression as? ExpressionAlias<*>)?.aliasOnlyExpression() ?: expression
when (expression) {
is Column<*> -> expression.clone()
is IExpressionAlias<*> -> expression.aliasOnlyExpression()
else -> expression
}
}

internal val aliasedFields: List<Expression<*>>
get() = query.set.fields.map { expression ->
when (expression) {
is Column<*> -> expression.clone()
is ExpressionAlias<*> -> expression.delegate.alias("$alias.${expression.alias}").aliasOnlyExpression()
is IExpressionAlias<*> -> expression.delegate.alias("$alias.${expression.alias}").aliasOnlyExpression()
else -> expression
}
}
Expand All @@ -170,6 +190,16 @@ class QueryAlias(val query: AbstractQuery<*>, val alias: String) : ColumnSet() {
?: error("Field not found in original table fields")
}

operator fun <T : Any?> get(original: ExpressionWithColumnType<T>): ExpressionWithColumnType<T> {
val aliases = query.set.fields.filterIsInstance<ExpressionWithColumnTypeAlias<T>>()
return (
aliases.find { it == original }?.let {
it.delegate.alias("$alias.${it.alias}").aliasOnlyExpression()
} ?: aliases.find { it.delegate == original }?.aliasOnlyExpression()
) as? ExpressionWithColumnType<T>
?: error("Field not found in original table fields")
}

override fun join(
otherTable: ColumnSet,
joinType: JoinType,
Expand Down Expand Up @@ -223,6 +253,16 @@ fun <T : AbstractQuery<*>> T.alias(alias: String) = QueryAlias(this, alias)
*/
fun <T> Expression<T>.alias(alias: String) = ExpressionAlias(this, alias)

/**
* Creates a temporary identifier, [alias], for [this] expression with column type.
*
* The alias will be used on the database-side if the alias object is used to generate an SQL statement,
* instead of [this] expression with column type object.
*
* @sample org.jetbrains.exposed.sql.tests.shared.AliasesTests.testExpressionWithColumnTypeAlias
*/
fun <T> ExpressionWithColumnType<T>.alias(alias: String) = ExpressionWithColumnTypeAlias(this, alias)

/**
* Creates a join relation with a query.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ open class Query(override var set: FieldSet, where: Op<Boolean>?) : AbstractQuer
if (groupedByColumns.isNotEmpty()) {
append(" GROUP BY ")
groupedByColumns.appendTo {
+((it as? ExpressionAlias)?.aliasOnlyExpression() ?: it)
+((it as? IExpressionAlias<*>)?.aliasOnlyExpression() ?: it)
}
}

Expand Down Expand Up @@ -444,7 +444,12 @@ open class Query(override var set: FieldSet, where: Op<Boolean>?) : AbstractQuer
adjustSelect {
select(
originalSet.fields.map {
it as? ExpressionAlias<*> ?: ((it as? Column<*>)?.makeAlias() ?: it.alias("exp${expInx++}"))
when (it) {
is IExpressionAlias<*> -> it
is Column<*> -> it.makeAlias()
is ExpressionWithColumnType<*> -> it.alias("exp${expInx++}")
else -> it.alias("exp${expInx++}")
}
}
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ class ResultRow(
return when {
raw == null -> null
raw == NotInitializedValue -> error("$expression is not initialized yet")
expression is ExpressionWithColumnTypeAlias<T> -> rawToColumnValue(raw, expression.delegate)
expression is ExpressionAlias<T> -> rawToColumnValue(raw, expression.delegate)
expression is ExpressionWithColumnType<T> -> expression.columnType.valueFromDB(raw)
expression is Op.OpBoolean -> BooleanColumnType.INSTANCE.valueFromDB(raw)
Expand Down Expand Up @@ -121,7 +122,7 @@ class ResultRow(
?: fieldIndex.keys.firstOrNull { exp ->
when (exp) {
is Column<*> -> (exp.columnType as? EntityIDColumnType<*>)?.idColumn == expression
is ExpressionAlias<*> -> exp.delegate == expression
is IExpressionAlias<*> -> exp.delegate == expression
else -> false
}
}?.let { exp -> fieldIndex[exp] }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ sealed class SetOperation(
is Query -> {
val newSlice = _firstStatement.set.fields.mapIndexed { index, expression ->
when (expression) {
is Column<*>, is ExpressionAlias<*> -> expression
is Column<*>, is IExpressionAlias<*> -> expression
is ExpressionWithColumnType<*> -> expression.alias("exp$index")
else -> expression.alias("exp$index")
}
}
Expand All @@ -36,6 +37,7 @@ sealed class SetOperation(
else -> error("Unsupported statement type ${_firstStatement::class.simpleName} in $operationName")
}
private val rawStatements: List<AbstractQuery<*>> = listOf(firstStatement, secondStatement)

init {
require(rawStatements.isNotEmpty()) { "$operationName is empty" }
require(rawStatements.none { it is Query && it.isForUpdate() }) { "FOR UPDATE is not allowed within $operationName" }
Expand Down Expand Up @@ -192,10 +194,11 @@ class Except(
secondStatement: AbstractQuery<*>
) : SetOperation("EXCEPT", firstStatement, secondStatement) {

override val operationName: String get() = when {
currentDialect is OracleDialect || currentDialect.h2Mode == H2Dialect.H2CompatibilityMode.Oracle -> "MINUS"
else -> "EXCEPT"
}
override val operationName: String
get() = when {
currentDialect is OracleDialect || currentDialect.h2Mode == H2Dialect.H2CompatibilityMode.Oracle -> "MINUS"
else -> "EXCEPT"
}

override fun copy() = Intersect(firstStatement, secondStatement).also {
copyTo(it)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ class WindowFunctionDefinition<T>(
if (partitionExpressions.isNotEmpty()) {
+"PARTITION BY "
partitionExpressions.appendTo {
+((it as? ExpressionAlias)?.aliasOnlyExpression() ?: it)
+((it as? IExpressionAlias<*>)?.aliasOnlyExpression() ?: it)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ abstract class DataTypeProvider {

/** Returns the SQL representation of the specified [expression], to be used in an ORDER BY clause. */
open fun precessOrderByClause(queryBuilder: QueryBuilder, expression: Expression<*>, sortOrder: SortOrder) {
queryBuilder.append((expression as? ExpressionAlias<*>)?.alias ?: expression, " ", sortOrder.code)
queryBuilder.append((expression as? IExpressionAlias<*>)?.alias ?: expression, " ", sortOrder.code)
}

/** Returns the hex-encoded value to be inserted into the database. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ internal object MysqlDataTypeProvider : DataTypeProvider() {
SortOrder.ASC_NULLS_FIRST -> super.precessOrderByClause(queryBuilder, expression, SortOrder.ASC)
SortOrder.DESC_NULLS_LAST -> super.precessOrderByClause(queryBuilder, expression, SortOrder.DESC)
else -> {
val exp = (expression as? ExpressionAlias<*>)?.alias ?: expression
val exp = (expression as? IExpressionAlias<*>)?.alias ?: expression
val nullExp = if (sortOrder == SortOrder.ASC_NULLS_LAST) " IS NULL" else " IS NOT NULL"
val order = if (sortOrder == SortOrder.ASC_NULLS_LAST) SortOrder.ASC else SortOrder.DESC
queryBuilder.append(exp, nullExp, ", ", exp, " ", order.code)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,9 @@ internal object OracleFunctionProvider : FunctionProvider() {
+"UPDATE ("
val columnsToSelect = columnsAndValues.flatMap {
listOfNotNull(it.first, it.second as? Expression<*>)
}.mapIndexed { index, expression -> expression to expression.alias("c$index") }.toMap()
}.mapIndexed { index, expression ->
expression to ((expression as? ExpressionWithColumnType<*>)?.alias("c$index") ?: expression.alias("c$index"))
}.toMap()

val subQuery = targets.select(columnsToSelect.values.toList())
where?.let {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ import org.jetbrains.exposed.dao.id.LongIdTable
import org.jetbrains.exposed.dao.id.UUIDTable
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.tests.DatabaseTestsBase
import org.jetbrains.exposed.sql.tests.TestDB
import org.jetbrains.exposed.sql.tests.shared.dml.withCitiesAndUsers
import org.jetbrains.exposed.sql.tests.shared.entities.EntityTestsData
import org.junit.Test
import java.math.BigDecimal
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertNull
Expand Down Expand Up @@ -292,4 +294,51 @@ class AliasesTests : DatabaseTestsBase() {
assertEquals("foo", query.first()[internalQuery[fooAlias]])
}
}

@Test
fun testExpressionWithColumnTypeAlias() {
val subInvoices = object : Table("SubInvoices") {
val productId = long("product_id")
val mainAmount = decimal("main_amount", 4, 2)
val isDraft = bool("is_draft")
}

withTables(subInvoices) { testDb ->
subInvoices.insert {
it[productId] = 1
it[mainAmount] = 3.5.toBigDecimal()
it[isDraft] = false
}

val inputSum = SqlExpressionBuilder.coalesce(
subInvoices.mainAmount.sum(), decimalLiteral(BigDecimal.ZERO)
).alias("input_sum")

val input = subInvoices.select(subInvoices.productId, inputSum)
.where {
subInvoices.isDraft eq false
}.groupBy(subInvoices.productId).alias("input")

val sumTotal = Expression.build {
coalesce(input[inputSum], decimalLiteral(BigDecimal.ZERO))
}.alias("inventory")

val booleanValue = when (testDb) {
TestDB.SQLITE, in TestDB.ALL_ORACLE_LIKE, in TestDB.ALL_SQLSERVER_LIKE -> "0"
else -> "FALSE"
}

val expectedQuery = "SELECT COALESCE(input.input_sum, 0) inventory FROM " +
"""(SELECT ${subInvoices.nameInDatabaseCase()}.${subInvoices.productId.nameInDatabaseCase()}, """ +
"""COALESCE(SUM(${subInvoices.nameInDatabaseCase()}.${subInvoices.mainAmount.nameInDatabaseCase()}), 0) input_sum """ +
"""FROM ${subInvoices.nameInDatabaseCase()} """ +
"""WHERE ${subInvoices.nameInDatabaseCase()}.${subInvoices.isDraft.nameInDatabaseCase()} = $booleanValue """ +
"""GROUP BY ${subInvoices.nameInDatabaseCase()}.${subInvoices.productId.nameInDatabaseCase()}) input"""

assertEquals(
expectedQuery,
input.select(sumTotal).prepareSQL(QueryBuilder(false))
)
}
}
}

0 comments on commit 33376e0

Please sign in to comment.