Skip to content

Commit

Permalink
fix!: EXPOSED-288 Extend ANY and ALL operators to use ArrayColumnType (
Browse files Browse the repository at this point in the history
…#1992)

* feat: EXPOSED-288 Extend ANY and ALL operators to use ArrayColumnType

- Support using ANY and ALL operators with array column or expression.
- Remove UntypedAndUnsizedArrayColumnType and replace it in AllAnyFromArrayOp
with ArrayColumnType. Remove associated unused dialect data types.
- Introduce function that resolves a column type based on type reflection. Refactor
existing array and json functions to use this, while still having a nullable override.

* fix!: EXPOSED-288 Extend ANY and ALL operators to use ArrayColumnType

Introduce InternalApi annotation and add it to resolveColumnType().
Move other existing annotation classes to dedicated file.
  • Loading branch information
bog-walk authored Feb 20, 2024
1 parent 971d90b commit 705e106
Show file tree
Hide file tree
Showing 14 changed files with 183 additions and 84 deletions.
12 changes: 6 additions & 6 deletions exposed-core/api/exposed-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,8 @@ public abstract class org/jetbrains/exposed/sql/ColumnType : org/jetbrains/expos
public final class org/jetbrains/exposed/sql/ColumnTypeKt {
public static final fun getAutoIncColumnType (Lorg/jetbrains/exposed/sql/Column;)Lorg/jetbrains/exposed/sql/AutoIncColumnType;
public static final fun isAutoInc (Lorg/jetbrains/exposed/sql/IColumnType;)Z
public static final fun resolveColumnType (Lkotlin/reflect/KClass;Lorg/jetbrains/exposed/sql/ColumnType;)Lorg/jetbrains/exposed/sql/ColumnType;
public static synthetic fun resolveColumnType$default (Lkotlin/reflect/KClass;Lorg/jetbrains/exposed/sql/ColumnType;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/ColumnType;
}

public abstract class org/jetbrains/exposed/sql/ComparisonOp : org/jetbrains/exposed/sql/Op, org/jetbrains/exposed/sql/ComplexExpression, org/jetbrains/exposed/sql/Op$OpBoolean {
Expand Down Expand Up @@ -1206,6 +1208,9 @@ public final class org/jetbrains/exposed/sql/IntegerColumnType : org/jetbrains/e
public synthetic fun valueFromDB (Ljava/lang/Object;)Ljava/lang/Object;
}

public abstract interface annotation class org/jetbrains/exposed/sql/InternalApi : java/lang/annotation/Annotation {
}

public final class org/jetbrains/exposed/sql/Intersect : org/jetbrains/exposed/sql/SetOperation {
public fun <init> (Lorg/jetbrains/exposed/sql/AbstractQuery;Lorg/jetbrains/exposed/sql/AbstractQuery;)V
public fun copy ()Lorg/jetbrains/exposed/sql/Intersect;
Expand Down Expand Up @@ -1535,8 +1540,6 @@ public final class org/jetbrains/exposed/sql/OpKt {
public static final fun andIfNotNull (Lorg/jetbrains/exposed/sql/Op;Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/sql/Op;
public static final fun andIfNotNull (Lorg/jetbrains/exposed/sql/Op;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Op;
public static final fun andNot (Lorg/jetbrains/exposed/sql/Expression;Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/sql/Op;
public static final fun arrayLiteral (Ljava/util/List;Lorg/jetbrains/exposed/sql/ColumnType;)Lorg/jetbrains/exposed/sql/LiteralOp;
public static final fun arrayParam (Ljava/util/List;Lorg/jetbrains/exposed/sql/ColumnType;)Lorg/jetbrains/exposed/sql/Expression;
public static final fun blobParam (Lorg/jetbrains/exposed/sql/statements/api/ExposedBlob;Z)Lorg/jetbrains/exposed/sql/Expression;
public static synthetic fun blobParam$default (Lorg/jetbrains/exposed/sql/statements/api/ExposedBlob;ZILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Expression;
public static final fun booleanLiteral (Z)Lorg/jetbrains/exposed/sql/LiteralOp;
Expand Down Expand Up @@ -1805,11 +1808,9 @@ public final class org/jetbrains/exposed/sql/SQLExpressionBuilderKt {
public static final fun allFrom (Lorg/jetbrains/exposed/sql/AbstractQuery;)Lorg/jetbrains/exposed/sql/Op;
public static final fun allFrom (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Op;
public static final fun allFrom (Lorg/jetbrains/exposed/sql/Table;)Lorg/jetbrains/exposed/sql/Op;
public static final fun allFrom ([Ljava/lang/Object;)Lorg/jetbrains/exposed/sql/Op;
public static final fun anyFrom (Lorg/jetbrains/exposed/sql/AbstractQuery;)Lorg/jetbrains/exposed/sql/Op;
public static final fun anyFrom (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Op;
public static final fun anyFrom (Lorg/jetbrains/exposed/sql/Table;)Lorg/jetbrains/exposed/sql/Op;
public static final fun anyFrom ([Ljava/lang/Object;)Lorg/jetbrains/exposed/sql/Op;
public static final fun avg (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;I)Lorg/jetbrains/exposed/sql/Avg;
public static synthetic fun avg$default (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;IILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Avg;
public static final fun castTo (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/IColumnType;)Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;
Expand Down Expand Up @@ -2694,7 +2695,7 @@ public final class org/jetbrains/exposed/sql/functions/math/TanFunction : org/je
}

public final class org/jetbrains/exposed/sql/ops/AllAnyFromArrayOp : org/jetbrains/exposed/sql/ops/AllAnyFromBaseOp {
public fun <init> (Z[Ljava/lang/Object;)V
public fun <init> (Z[Ljava/lang/Object;Lorg/jetbrains/exposed/sql/ColumnType;)V
public synthetic fun registerSubSearchArgument (Lorg/jetbrains/exposed/sql/QueryBuilder;Ljava/lang/Object;)V
public fun registerSubSearchArgument (Lorg/jetbrains/exposed/sql/QueryBuilder;[Ljava/lang/Object;)V
}
Expand Down Expand Up @@ -3317,7 +3318,6 @@ public abstract class org/jetbrains/exposed/sql/vendors/DataTypeProvider {
public fun uintegerType ()Ljava/lang/String;
public fun ulongAutoincType ()Ljava/lang/String;
public fun ulongType ()Ljava/lang/String;
public fun untypedAndUnsizedArrayType ()Ljava/lang/String;
public fun ushortType ()Ljava/lang/String;
public fun uuidToDB (Ljava/util/UUID;)Ljava/lang/Object;
public fun uuidType ()Ljava/lang/String;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package org.jetbrains.exposed.sql

/**
* API marked with this annotation is experimental.
* Any behavior associated with its use is not guaranteed to be stable.
*/
@RequiresOptIn(
message = "This database migration API is experimental. " +
"Its usage must be marked with '@OptIn(org.jetbrains.exposed.sql.ExperimentalDatabaseMigrationApi::class)' " +
"or '@org.jetbrains.exposed.sql.ExperimentalDatabaseMigrationApi'."
)
@Target(AnnotationTarget.FUNCTION)
annotation class ExperimentalDatabaseMigrationApi

/**
* API marked with this annotation is experimental.
* Any behavior associated with its use is not guaranteed to be stable.
*/
@RequiresOptIn(
message = "This API is experimental and the behavior defined by setting this value to 'true' is now the default. " +
"Its usage must be marked with '@OptIn(org.jetbrains.exposed.sql.ExperimentalKeywordApi::class)' " +
"or '@org.jetbrains.exposed.sql.ExperimentalKeywordApi'."
)
@Target(AnnotationTarget.PROPERTY)
annotation class ExperimentalKeywordApi

/**
* API marked with this annotation is internal and should not be used outside Exposed.
* It may be changed or removed in the future without notice.
* Using it outside Exposed may result in undefined and unexpected behaviour.
*/
@RequiresOptIn(
level = RequiresOptIn.Level.ERROR,
message = "This API is internal in Exposed and should not be used. It may be changed or removed in the future without notice."
)
@Target(
AnnotationTarget.CLASS,
AnnotationTarget.CONSTRUCTOR,
AnnotationTarget.FUNCTION,
AnnotationTarget.PROPERTY,
AnnotationTarget.PROPERTY_SETTER,
AnnotationTarget.TYPEALIAS
)
annotation class InternalApi
Original file line number Diff line number Diff line change
Expand Up @@ -1035,18 +1035,6 @@ class CustomEnumerationColumnType<T : Enum<T>>(

// Array columns

/**
* Array column for storing arrays of any size and type.
*
* This column type only exists to allow registering an array as a valid SQL type for statement clauses generated
* using `anyFrom(array)` and `allFrom(array)`. It does not correctly process arrays for use in `nonNullValueToString()`
* and will be replaced with a full implementation of ArrayColumnType.
*/
internal object UntypedAndUnsizedArrayColumnType : ColumnType() {
override fun sqlType(): String =
currentDialect.dataTypeProvider.untypedAndUnsizedArrayType()
}

/**
* Array column for storing a collection of elements.
*/
Expand Down Expand Up @@ -1078,8 +1066,9 @@ class ArrayColumnType(
else -> value
}

override fun valueToString(value: Any?): String = when {
value is List<*> -> nonNullValueToString(value)
override fun valueToString(value: Any?): String = when (value) {
is List<*> -> nonNullValueToString(value)
is Array<*> -> nonNullValueToString(value.toList())
else -> super.valueToString(value)
}

Expand Down Expand Up @@ -1118,3 +1107,35 @@ interface IDateColumnType {
interface JsonColumnMarker {
val usesBinaryFormat: Boolean
}

/**
* Returns the [ColumnType] commonly associated with storing values of type [T], or the [defaultType] if a mapping
* does not exist for type [T].
*
* @throws IllegalStateException If no column type mapping is found and a [defaultType] is not provided.
*/
@InternalApi
fun <T : Any> resolveColumnType(
klass: KClass<T>,
defaultType: ColumnType? = null
): ColumnType = when (klass) {
Boolean::class -> BooleanColumnType()
Byte::class -> ByteColumnType()
UByte::class -> UByteColumnType()
Short::class -> ShortColumnType()
UShort::class -> UShortColumnType()
Int::class -> IntegerColumnType()
UInt::class -> UIntegerColumnType()
Long::class -> LongColumnType()
ULong::class -> ULongColumnType()
Float::class -> FloatColumnType()
Double::class -> DoubleColumnType()
String::class -> TextColumnType()
Char::class -> CharacterColumnType()
ByteArray::class -> BasicBinaryColumnType()
BigDecimal::class -> DecimalColumnType.INSTANCE
UUID::class -> UUIDColumnType()
else -> defaultType ?: error(
"A column type could not be associated with ${klass.qualifiedName}. Provide an explicit column type argument."
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -141,11 +141,3 @@ class DatabaseConfig private constructor(
}
}
}

@RequiresOptIn(
message = "This API is experimental and the behavior defined by setting this value to 'true' is now the default. " +
"Its usage must be marked with '@OptIn(org.jetbrains.exposed.sql.ExperimentalKeywordApi::class)' " +
"or '@org.jetbrains.exposed.sql.ExperimentalKeywordApi'."
)
@Target(AnnotationTarget.PROPERTY)
annotation class ExperimentalKeywordApi
30 changes: 24 additions & 6 deletions exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Op.kt
Original file line number Diff line number Diff line change
Expand Up @@ -684,9 +684,18 @@ fun stringLiteral(value: String): LiteralOp<String> = LiteralOp(TextColumnType()
/** Returns the specified [value] as a decimal literal. */
fun decimalLiteral(value: BigDecimal): LiteralOp<BigDecimal> = LiteralOp(DecimalColumnType(value.precision(), value.scale()), value)

/** Returns the specified [value] as an array literal, with elements parsed by the [delegateType]. */
fun <T> arrayLiteral(value: List<T>, delegateType: ColumnType): LiteralOp<List<T>> =
LiteralOp(ArrayColumnType(delegateType), value)
/**
* Returns the specified [value] as an array literal, with elements parsed by the [delegateType] if provided.
*
* **Note** If [delegateType] is left `null`, the associated column type will be resolved according to the
* internal mapping of the element's type in [resolveColumnType].
*
* @throws IllegalStateException If no column type mapping is found and a [delegateType] is not provided.
*/
inline fun <reified T : Any> arrayLiteral(value: List<T>, delegateType: ColumnType? = null): LiteralOp<List<T>> {
@OptIn(InternalApi::class)
return LiteralOp(ArrayColumnType(delegateType ?: resolveColumnType(T::class)), value)
}

// Query Parameters

Expand Down Expand Up @@ -754,9 +763,18 @@ fun decimalParam(value: BigDecimal): Expression<BigDecimal> = QueryParameter(val
fun blobParam(value: ExposedBlob, useObjectIdentifier: Boolean = false): Expression<ExposedBlob> =
QueryParameter(value, BlobColumnType(useObjectIdentifier))

/** Returns the specified [value] as an array query parameter, with elements parsed by the [delegateType]. */
fun <T> arrayParam(value: List<T>, delegateType: ColumnType): Expression<List<T>> =
QueryParameter(value, ArrayColumnType(delegateType))
/**
* Returns the specified [value] as an array query parameter, with elements parsed by the [delegateType] if provided.
*
* **Note** If [delegateType] is left `null`, the associated column type will be resolved according to the
* internal mapping of the element's type in [resolveColumnType].
*
* @throws IllegalStateException If no column type mapping is found and a [delegateType] is not provided.
*/
inline fun <reified T : Any> arrayParam(value: List<T>, delegateType: ColumnType? = null): Expression<List<T>> {
@OptIn(InternalApi::class)
return QueryParameter(value, ArrayColumnType(delegateType ?: resolveColumnType(T::class)))
}

// Misc.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,20 @@ fun <T : Any?> ExpressionWithColumnType<T>.varSamp(scale: Int = 2): VarSamp<T> =
/** Returns this subquery wrapped in the `ANY` operator. This function is not supported by the SQLite dialect. */
fun <T> anyFrom(subQuery: AbstractQuery<*>): Op<T> = AllAnyFromSubQueryOp(true, subQuery)

/** Returns this array of data wrapped in the `ANY` operator. This function is only supported by PostgreSQL and H2 dialects. */
fun <T> anyFrom(array: Array<T>): Op<T> = AllAnyFromArrayOp(true, array)
/**
* Returns this array of data wrapped in the `ANY` operator. This function is only supported by PostgreSQL and H2 dialects.
*
* **Note** If [delegateType] is left `null`, the base column type associated with storing elements of type [T] will be
* resolved according to the internal mapping of the element's type in [resolveColumnType].
*
* @throws IllegalStateException If no column type mapping is found and a [delegateType] is not provided.
*/
inline fun <reified T : Any> anyFrom(array: Array<T>, delegateType: ColumnType? = null): Op<T> {
// emptyArray() without type info generates ARRAY[]
@OptIn(InternalApi::class)
val columnType = delegateType ?: resolveColumnType(T::class, if (array.isEmpty()) TextColumnType() else null)
return AllAnyFromArrayOp(true, array, columnType)
}

/** Returns this table wrapped in the `ANY` operator. This function is only supported by MySQL, PostgreSQL, and H2 dialects. */
fun <T> anyFrom(table: Table): Op<T> = AllAnyFromTableOp(true, table)
Expand All @@ -128,8 +140,20 @@ fun <E, T : List<E>?> anyFrom(expression: Expression<T>): Op<E> = AllAnyFromExpr
/** Returns this subquery wrapped in the `ALL` operator. This function is not supported by the SQLite dialect. */
fun <T> allFrom(subQuery: AbstractQuery<*>): Op<T> = AllAnyFromSubQueryOp(false, subQuery)

/** Returns this array of data wrapped in the `ALL` operator. This function is only supported by PostgreSQL and H2 dialects. */
fun <T> allFrom(array: Array<T>): Op<T> = AllAnyFromArrayOp(false, array)
/**
* Returns this array of data wrapped in the `ALL` operator. This function is only supported by PostgreSQL and H2 dialects.
*
* **Note** If [delegateType] is left `null`, the base column type associated with storing elements of type [T] will be
* resolved according to the internal mapping of the element's type in [resolveColumnType].
*
* @throws IllegalStateException If no column type mapping is found and a [delegateType] is not provided.
*/
inline fun <reified T : Any> allFrom(array: Array<T>, delegateType: ColumnType? = null): Op<T> {
// emptyArray() without type info generates ARRAY[]
@OptIn(InternalApi::class)
val columnType = delegateType ?: resolveColumnType(T::class, if (array.isEmpty()) TextColumnType() else null)
return AllAnyFromArrayOp(false, array, columnType)
}

/** Returns this table wrapped in the `ALL` operator. This function is only supported by MySQL, PostgreSQL, and H2 dialects. */
fun <T> allFrom(table: Table): Op<T> = AllAnyFromTableOp(false, table)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -888,11 +888,3 @@ object SchemaUtils {
}
}
}

@RequiresOptIn(
message = "This database migration API is experimental. " +
"Its usage must be marked with '@OptIn(org.jetbrains.exposed.sql.ExperimentalDatabaseMigrationApi::class)' " +
"or '@org.jetbrains.exposed.sql.ExperimentalDatabaseMigrationApi'."
)
@Target(AnnotationTarget.FUNCTION)
annotation class ExperimentalDatabaseMigrationApi
20 changes: 20 additions & 0 deletions exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Table.kt
Original file line number Diff line number Diff line change
Expand Up @@ -829,6 +829,26 @@ open class Table(name: String = "") : ColumnSet(), DdlAware {
fun <T> array(name: String, columnType: ColumnType, maximumCardinality: Int? = null): Column<List<T>> =
registerColumn(name, ArrayColumnType(columnType.apply { nullable = true }, maximumCardinality))

/**
* Creates an array column, with the specified [name], for storing elements of a `List`.
*
* **Note** This column type is only supported by H2 and PostgreSQL dialects.
*
* **Note** The base column type associated with storing elements of type [T] will be resolved according to
* the internal mapping in [resolveColumnType]. To avoid this type reflection, or if a mapping does not exist
* for the elements being stored, please provide an explicit column type to the [array] overload. If the elements
* to be stored are nullable, an explicit column type will also need to be provided.
*
* @param name Name of the column.
* @param maximumCardinality The maximum amount of allowed elements. **Note** Providing an array size limit
* when using the PostgreSQL dialect is allowed, but this value will be ignored by the database.
* @throws IllegalStateException If no column type mapping is found.
*/
inline fun <reified T : Any> array(name: String, maximumCardinality: Int? = null): Column<List<T>> {
@OptIn(InternalApi::class)
return array(name, resolveColumnType(T::class), maximumCardinality)
}

// Auto-generated values

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
package org.jetbrains.exposed.sql.ops

import org.jetbrains.exposed.sql.AbstractQuery
import org.jetbrains.exposed.sql.Expression
import org.jetbrains.exposed.sql.Op
import org.jetbrains.exposed.sql.QueryBuilder
import org.jetbrains.exposed.sql.Table
import org.jetbrains.exposed.sql.UntypedAndUnsizedArrayColumnType
import org.jetbrains.exposed.sql.*

/**
* Represents an SQL operator that checks a value, based on the preceding comparison operator,
Expand Down Expand Up @@ -47,9 +42,14 @@ class AllAnyFromSubQueryOp<T>(
*
* **Note** This operation is only supported by PostgreSQL and H2 dialects.
*/
class AllAnyFromArrayOp<T>(isAny: Boolean, array: Array<T>) : AllAnyFromBaseOp<T, Array<T>>(isAny, array) {
override fun QueryBuilder.registerSubSearchArgument(subSearch: Array<T>) =
registerArgument(UntypedAndUnsizedArrayColumnType, subSearch)
class AllAnyFromArrayOp<T : Any>(
isAny: Boolean,
array: Array<T>,
private val delegateType: ColumnType
) : AllAnyFromBaseOp<T, Array<T>>(isAny, array) {
override fun QueryBuilder.registerSubSearchArgument(subSearch: Array<T>) {
registerArgument(ArrayColumnType(delegateType), subSearch)
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,11 +137,6 @@ abstract class DataTypeProvider {
open fun jsonBType(): String =
throw UnsupportedByDialectException("This vendor does not support binary JSON data type", currentDialect)

/** Data type for arrays with no specified size or element type, used only as types of [QueryParameter]s for PostgreSQL and H2.
* An array with no element type cannot be used for storing arrays in a column in either PostgreSQL or H2. */
open fun untypedAndUnsizedArrayType(): String =
throw UnsupportedByDialectException("This vendor does not support array data type", currentDialect)

// Misc.

/** Returns the SQL representation of the specified expression, for it to be used as a column default value. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ internal object H2DataTypeProvider : DataTypeProvider() {
override fun timestampWithTimeZoneType(): String = "TIMESTAMP(9) WITH TIME ZONE"

override fun jsonBType(): String = "JSON"
override fun untypedAndUnsizedArrayType(): String = "ARRAY[]"

override fun hexToDb(hexString: String): String = "X'$hexString'"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@ internal object PostgreSQLDataTypeProvider : DataTypeProvider() {
override fun dateTimeType(): String = "TIMESTAMP"
override fun jsonBType(): String = "JSONB"

override fun untypedAndUnsizedArrayType(): String = "ARRAY"

override fun processForDefaultValue(e: Expression<*>): String = when {
e is LiteralOp<*> && e.columnType is JsonColumnMarker && (currentDialect as? H2Dialect) == null -> {
val cast = if (e.columnType.usesBinaryFormat) "::jsonb" else "::json"
Expand Down
Loading

0 comments on commit 705e106

Please sign in to comment.