diff --git a/exposed-core/api/exposed-core.api b/exposed-core/api/exposed-core.api index 26a7a383e4..fa6f0174e8 100644 --- a/exposed-core/api/exposed-core.api +++ b/exposed-core/api/exposed-core.api @@ -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; @@ -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 (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 } @@ -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 (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; @@ -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; @@ -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; diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Alias.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Alias.kt index 2b547f808c..db49c31f76 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Alias.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Alias.kt @@ -107,9 +107,12 @@ class Alias(val delegate: T, val alias: String) : Table() { .orEmpty() } -/** Represents a temporary SQL identifier, [alias], for a [delegate] expression. */ -class ExpressionAlias(val delegate: Expression, val alias: String) : Expression() { - override fun toQueryBuilder(queryBuilder: QueryBuilder) = queryBuilder { +interface IExpressionAlias { + val delegate: Expression + + val alias: String + + fun queryBuilder(queryBuilder: QueryBuilder) = queryBuilder { if (delegate is ComparisonOp && (currentDialectIfAvailable is SQLServerDialect || currentDialectIfAvailable is OracleDialect)) { +"(CASE WHEN " append(delegate) @@ -121,17 +124,30 @@ class ExpressionAlias(val delegate: Expression, val alias: String) : Expre } /** Returns an [Expression] containing only the string representation of this [alias]. */ - fun aliasOnlyExpression(): Expression { - return if (delegate is ExpressionWithColumnType) { - object : Function(delegate.columnType) { - override fun toQueryBuilder(queryBuilder: QueryBuilder) = queryBuilder { append(alias) } - } - } else { - object : Expression() { + fun aliasOnlyExpression(): Expression = + (delegate as? ExpressionWithColumnType)?.columnType?.let { columnType -> + object : Function(columnType) { override fun toQueryBuilder(queryBuilder: QueryBuilder) = queryBuilder { append(alias) } } + } ?: object : Expression() { + override fun toQueryBuilder(queryBuilder: QueryBuilder) = queryBuilder { append(alias) } } - } +} + +/** Represents a temporary SQL identifier, [alias], for a [delegate] expression. */ +class ExpressionAlias(override val delegate: Expression, override val alias: String) : Expression(), IExpressionAlias { + override fun toQueryBuilder(queryBuilder: QueryBuilder) = this.queryBuilder(queryBuilder) +} + +/** Represents a temporary SQL identifier, [alias], for a [delegate] expression with column type. */ +class ExpressionWithColumnTypeAlias( + override val delegate: ExpressionWithColumnType, + override val alias: String +) : ExpressionWithColumnType(), IExpressionAlias { + override val columnType: IColumnType + get() = delegate.columnType + + override fun toQueryBuilder(queryBuilder: QueryBuilder) = this.queryBuilder(queryBuilder) } /** Represents a temporary SQL identifier, [alias], for a [query]. */ @@ -143,14 +159,18 @@ class QueryAlias(val query: AbstractQuery<*>, val alias: String) : ColumnSet() { } override val fields: List> = 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> 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 } } @@ -170,6 +190,16 @@ class QueryAlias(val query: AbstractQuery<*>, val alias: String) : ColumnSet() { ?: error("Field not found in original table fields") } + operator fun get(original: ExpressionWithColumnType): ExpressionWithColumnType { + val aliases = query.set.fields.filterIsInstance>() + return ( + aliases.find { it == original }?.let { + it.delegate.alias("$alias.${it.alias}").aliasOnlyExpression() + } ?: aliases.find { it.delegate == original }?.aliasOnlyExpression() + ) as? ExpressionWithColumnType + ?: error("Field not found in original table fields") + } + override fun join( otherTable: ColumnSet, joinType: JoinType, @@ -223,6 +253,16 @@ fun > T.alias(alias: String) = QueryAlias(this, alias) */ fun Expression.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 ExpressionWithColumnType.alias(alias: String) = ExpressionWithColumnTypeAlias(this, alias) + /** * Creates a join relation with a query. * diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Query.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Query.kt index 61cb509c97..394c2f196b 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Query.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Query.kt @@ -246,7 +246,7 @@ open class Query(override var set: FieldSet, where: Op?) : AbstractQuer if (groupedByColumns.isNotEmpty()) { append(" GROUP BY ") groupedByColumns.appendTo { - +((it as? ExpressionAlias)?.aliasOnlyExpression() ?: it) + +((it as? IExpressionAlias<*>)?.aliasOnlyExpression() ?: it) } } @@ -444,7 +444,12 @@ open class Query(override var set: FieldSet, where: Op?) : 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++}") + } } ) } 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 e2c38aca4c..5a86c77aa2 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 @@ -92,6 +92,7 @@ class ResultRow( return when { raw == null -> null raw == NotInitializedValue -> error("$expression is not initialized yet") + expression is ExpressionWithColumnTypeAlias -> rawToColumnValue(raw, expression.delegate) expression is ExpressionAlias -> rawToColumnValue(raw, expression.delegate) expression is ExpressionWithColumnType -> expression.columnType.valueFromDB(raw) expression is Op.OpBoolean -> BooleanColumnType.INSTANCE.valueFromDB(raw) @@ -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] } diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/SetOperations.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/SetOperations.kt index 96a5eba9bd..6aa446e4c0 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/SetOperations.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/SetOperations.kt @@ -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") } } @@ -36,6 +37,7 @@ sealed class SetOperation( else -> error("Unsupported statement type ${_firstStatement::class.simpleName} in $operationName") } private val rawStatements: List> = 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" } @@ -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) diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/WindowFunction.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/WindowFunction.kt index 05f9ec4426..1c54a943fa 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/WindowFunction.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/WindowFunction.kt @@ -130,7 +130,7 @@ class WindowFunctionDefinition( if (partitionExpressions.isNotEmpty()) { +"PARTITION BY " partitionExpressions.appendTo { - +((it as? ExpressionAlias)?.aliasOnlyExpression() ?: it) + +((it as? IExpressionAlias<*>)?.aliasOnlyExpression() ?: it) } } } diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/DataTypeProvider.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/DataTypeProvider.kt index fbd20a0486..a418386fa6 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/DataTypeProvider.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/DataTypeProvider.kt @@ -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. */ diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/MysqlDialect.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/MysqlDialect.kt index 16c10c25f9..a5b445ac69 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/MysqlDialect.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/MysqlDialect.kt @@ -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) diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/OracleDialect.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/OracleDialect.kt index 54c9e6d298..64de7bc0c0 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/OracleDialect.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/OracleDialect.kt @@ -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 { diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/AliasesTests.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/AliasesTests.kt index 91e8f4875d..113f234274 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/AliasesTests.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/AliasesTests.kt @@ -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 @@ -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)) + ) + } + } }