Skip to content

Commit

Permalink
Review issues: use only one column type for both 1 dimentional and mu…
Browse files Browse the repository at this point in the history
…ltidimentional arrays
  • Loading branch information
obabichevjb committed Sep 26, 2024
1 parent f0673d0 commit ef054cb
Show file tree
Hide file tree
Showing 11 changed files with 140 additions and 190 deletions.
7 changes: 7 additions & 0 deletions documentation-website/Writerside/topics/Breaking-Changes.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Breaking Changes

## 0.56.0

* `ArrayColumnType` supports multidimensional arrays now and has one more generic parameter.
If the was used directly for one dimensional arrays with parameter `T` like `ArrayColumnType<T>`, now it should
be defined as `ArrayColumnType<T, List<T>>`, for example `ArrayColumnType<Int>` -> `ArrayColumnType<Int, List<Int>>`


## 0.55.0
* The `DeleteStatement` property `table` is now deprecated in favor of `targetsSet`, which holds a `ColumnSet` that may be a `Table` or `Join`.
This enables the use of the new `Join.delete()` function, which performs a delete operation on a specific table from the join relation.
Expand Down
10 changes: 5 additions & 5 deletions documentation-website/Writerside/topics/Data-Types.topic
Original file line number Diff line number Diff line change
Expand Up @@ -334,18 +334,18 @@

<code-block lang="kotlin">
object Teams : Table("teams") {
val memberIds = multi2Array<UUID>("member_ids")
val memberNames = multi3Array<String>("member_names")
val budgets = multi2Array<Double>("budgets")
val memberIds = array2<UUID>("member_ids")
val memberNames = array3<String>("member_names")
val budgets = array2<Double>("budgets")
}
</code-block>
<p>If more control is needed over the base content type, or if the latter is user-defined or from a non-core
module, the explicit type should be provided to the function:</p>

<code-block lang="kotlin">
object Teams : Table("teams") {
val memberIds = multi2Array<UUID>("member_ids")
val memberNames = multi3Array<String>("member_names", VarCharColumnType(colLength = 32))
val memberIds = array2<UUID>("member_ids")
val memberNames = array3<String>("member_names", VarCharColumnType(colLength = 32))
}
</code-block>

Expand Down
31 changes: 6 additions & 25 deletions exposed-core/api/exposed-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -203,13 +203,12 @@ public final class org/jetbrains/exposed/sql/AndOp : org/jetbrains/exposed/sql/C
}

public final class org/jetbrains/exposed/sql/ArrayColumnType : org/jetbrains/exposed/sql/ColumnType {
public fun <init> (Lorg/jetbrains/exposed/sql/ColumnType;Ljava/lang/Integer;)V
public synthetic fun <init> (Lorg/jetbrains/exposed/sql/ColumnType;Ljava/lang/Integer;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (Lorg/jetbrains/exposed/sql/ColumnType;ILjava/util/List;)V
public synthetic fun <init> (Lorg/jetbrains/exposed/sql/ColumnType;ILjava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun getDelegate ()Lorg/jetbrains/exposed/sql/ColumnType;
public final fun getDelegateType ()Ljava/lang/String;
public final fun getMaximumCardinality ()Ljava/lang/Integer;
public synthetic fun nonNullValueAsDefaultString (Ljava/lang/Object;)Ljava/lang/String;
public fun nonNullValueAsDefaultString (Ljava/util/List;)Ljava/lang/String;
public final fun getDimensions ()I
public final fun getMaximumCardinality ()Ljava/util/List;
public synthetic fun nonNullValueToString (Ljava/lang/Object;)Ljava/lang/String;
public fun nonNullValueToString (Ljava/util/List;)Ljava/lang/String;
public synthetic fun notNullValueToDB (Ljava/lang/Object;)Ljava/lang/Object;
Expand All @@ -219,8 +218,6 @@ public final class org/jetbrains/exposed/sql/ArrayColumnType : org/jetbrains/exp
public fun sqlType ()Ljava/lang/String;
public synthetic fun valueFromDB (Ljava/lang/Object;)Ljava/lang/Object;
public fun valueFromDB (Ljava/lang/Object;)Ljava/util/List;
public synthetic fun valueToString (Ljava/lang/Object;)Ljava/lang/String;
public fun valueToString (Ljava/util/List;)Ljava/lang/String;
}

public final class org/jetbrains/exposed/sql/AutoIncColumnType : org/jetbrains/exposed/sql/IColumnType {
Expand Down Expand Up @@ -1630,24 +1627,6 @@ public final class org/jetbrains/exposed/sql/ModOp : org/jetbrains/exposed/sql/E
public final class org/jetbrains/exposed/sql/ModOp$Companion {
}

public final class org/jetbrains/exposed/sql/MultiArrayColumnType : org/jetbrains/exposed/sql/ColumnType {
public fun <init> (Lorg/jetbrains/exposed/sql/ColumnType;ILjava/util/List;)V
public synthetic fun <init> (Lorg/jetbrains/exposed/sql/ColumnType;ILjava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun getDelegate ()Lorg/jetbrains/exposed/sql/ColumnType;
public final fun getDelegateType ()Ljava/lang/String;
public final fun getDimensions ()I
public final fun getMaximumCardinality ()Ljava/util/List;
public synthetic fun nonNullValueToString (Ljava/lang/Object;)Ljava/lang/String;
public fun nonNullValueToString (Ljava/util/List;)Ljava/lang/String;
public synthetic fun notNullValueToDB (Ljava/lang/Object;)Ljava/lang/Object;
public fun notNullValueToDB (Ljava/util/List;)Ljava/lang/Object;
public fun readObject (Ljava/sql/ResultSet;I)Ljava/lang/Object;
public fun setParameter (Lorg/jetbrains/exposed/sql/statements/api/PreparedStatementApi;ILjava/lang/Object;)V
public fun sqlType ()Ljava/lang/String;
public synthetic fun valueFromDB (Ljava/lang/Object;)Ljava/lang/Object;
public fun valueFromDB (Ljava/lang/Object;)Ljava/util/List;
}

public final class org/jetbrains/exposed/sql/NeqOp : org/jetbrains/exposed/sql/ComparisonOp {
public fun <init> (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;)V
}
Expand Down Expand Up @@ -2483,6 +2462,8 @@ public class org/jetbrains/exposed/sql/Table : org/jetbrains/exposed/sql/ColumnS
public synthetic fun <init> (Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun array (Ljava/lang/String;Lorg/jetbrains/exposed/sql/ColumnType;Ljava/lang/Integer;)Lorg/jetbrains/exposed/sql/Column;
public static synthetic fun array$default (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/String;Lorg/jetbrains/exposed/sql/ColumnType;Ljava/lang/Integer;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Column;
public final fun arrayN (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/String;Lorg/jetbrains/exposed/sql/ColumnType;ILjava/util/List;)Lorg/jetbrains/exposed/sql/Column;
public static synthetic fun arrayN$default (Lorg/jetbrains/exposed/sql/Table;Lorg/jetbrains/exposed/sql/Table;Ljava/lang/String;Lorg/jetbrains/exposed/sql/ColumnType;ILjava/util/List;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Column;
public final fun autoGenerate (Lorg/jetbrains/exposed/sql/Column;)Lorg/jetbrains/exposed/sql/Column;
public final fun autoIncrement (Lorg/jetbrains/exposed/sql/Column;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/Column;
public final fun autoIncrement (Lorg/jetbrains/exposed/sql/Column;Lorg/jetbrains/exposed/sql/Sequence;)Lorg/jetbrains/exposed/sql/Column;
Expand Down
101 changes: 24 additions & 77 deletions exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/ColumnType.kt
Original file line number Diff line number Diff line change
Expand Up @@ -1187,60 +1187,6 @@ class CustomEnumerationColumnType<T : Enum<T>>(

// Array columns

/**
* Array column for storing a collection of elements.
*/
class ArrayColumnType<E>(
/** Returns the base column type of this array column's individual elements. */
val delegate: ColumnType<E & Any>,
/** Returns the maximum amount of allowed elements in this array column. */
val maximumCardinality: Int? = null
) : ColumnType<List<E>>() {
override fun sqlType(): String = buildString {
append(delegate.sqlType())
when {
currentDialect is H2Dialect -> append(" ARRAY", maximumCardinality?.let { "[$it]" } ?: "")
else -> append("[", maximumCardinality?.toString() ?: "", "]")
}
}

/** The base SQL type of this array column's individual elements without extra column identifiers. */
val delegateType: String
get() = delegate.sqlType().substringBefore('(')

@Suppress("UNCHECKED_CAST")
override fun valueFromDB(value: Any): List<E> = when {
value is java.sql.Array -> (value.array as Array<*>).map { e -> e?.let { delegate.valueFromDB(it) } as E }
else -> value as? List<E> ?: error("Unexpected value $value of type ${value::class.qualifiedName}")
}

override fun notNullValueToDB(value: List<E>): Any = value.map { e -> e?.let { delegate.notNullValueToDB(it) } }.toTypedArray()

override fun valueToString(value: List<E>?): String = if (value != null) nonNullValueToString(value) else super.valueToString(null)

override fun nonNullValueToString(value: List<E>): String {
val prefix = if (currentDialect is H2Dialect) "ARRAY [" else "ARRAY["
return value.joinToString(",", prefix, "]") { delegate.valueToString(it) }
}

override fun nonNullValueAsDefaultString(value: List<E>): String {
val prefix = if (currentDialect is H2Dialect) "ARRAY [" else "ARRAY["
return value.joinToString(",", prefix, "]") { delegate.valueAsDefaultString(it) }
}

override fun readObject(rs: ResultSet, index: Int): Any? = rs.getArray(index)

override fun setParameter(stmt: PreparedStatementApi, index: Int, value: Any?) {
when {
value is Array<*> && isArrayOfByteArrays(value) ->
stmt.setArray(index, delegateType, Array(value.size) { value[it] as ByteArray })

value is Array<*> -> stmt.setArray(index, delegateType, value)
else -> super.setParameter(stmt, index, value)
}
}
}

/**
* Multi-dimensional array column type for storing a collection of nested elements.
*
Expand All @@ -1251,33 +1197,42 @@ class ArrayColumnType<E>(
* **Note:** The maximum cardinality is considered for each dimension, but it is ignored by the PostgreSQL database.
* Validation is performed on the client side.
*/
class MultiArrayColumnType<T, R : List<Any>>(
class ArrayColumnType<T, R : List<Any?>>(
val delegate: ColumnType<T & Any>,
val dimensions: Int,
val maximumCardinality: List<Int>? = null
) : ColumnType<R>() {
val delegateType: String
get() = delegate.sqlType().substringBefore('(')

override fun sqlType(): String {
override fun sqlType(): String = buildString {
if (maximumCardinality != null) {
require(maximumCardinality.size == dimensions) {
"The size of cardinality list must be equal to the amount of array dimensions. " +
"Dimensions: $dimensions, cardinality size: ${maximumCardinality.size}"
}
}
return delegate.sqlType() +
(maximumCardinality?.let { cardinality -> cardinality.joinToString("") { "[$it]" } } ?: "[]".repeat(dimensions))
append(delegate.sqlType())
when {
currentDialect is H2Dialect -> {
require(dimensions == 1) {
"H2 does not support multidimensional arrays. " +
"`dimensions` parameter for H2 database must be 1"
}
append(" ARRAY", maximumCardinality?.let { "[${it.first()}]" } ?: "")
}

else -> append(maximumCardinality?.let { cardinality -> cardinality.joinToString("") { "[$it]" } } ?: "[]".repeat(dimensions))
}
}

override fun notNullValueToDB(value: R): Any {
validateValue(value)
return recursiveNotNullValueToDB(value, dimensions)
}

private fun recursiveNotNullValueToDB(value: Any, level: Int): Array<Any> = when {
private fun recursiveNotNullValueToDB(value: Any, level: Int): Array<Any?> = when {
level > 1 -> (value as List<Any>).map { recursiveNotNullValueToDB(it, level - 1) }.toTypedArray()
else -> (value as List<T & Any>).map { delegate.notNullValueToDB(it) }.toTypedArray()
else -> (value as List<T>).map { it?.let { delegate.notNullValueToDB(it) } }.toTypedArray()
}

@Suppress("UNCHECKED_CAST")
Expand All @@ -1290,7 +1245,7 @@ class MultiArrayColumnType<T, R : List<Any>>(

private fun recursiveValueFromDB(value: Any?, level: Int): List<Any?> = when {
level > 1 -> (value as Array<Any?>).map { recursiveValueFromDB(it, level - 1) }
else -> (value as Array<Any>).map { delegate.valueFromDB(it) }
else -> (value as Array<Any?>).map { it?.let { delegate.valueFromDB(it) } }
}

override fun readObject(rs: ResultSet, index: Int): Any? {
Expand All @@ -1299,33 +1254,25 @@ class MultiArrayColumnType<T, R : List<Any>>(

override fun setParameter(stmt: PreparedStatementApi, index: Int, value: Any?) {
when {
value is Array<*> && isArrayOfByteArrays(value) ->
stmt.setArray(index, delegateType, Array(value.size) { value[it] as ByteArray })

value is Array<*> -> stmt.setArray(index, delegateType, value)
else -> super.setParameter(stmt, index, value)
}
}

override fun nonNullValueToString(value: R): String {
return "ARRAY" + recursiveNonNullValueToString(value, dimensions)
return when {
currentDialect is H2Dialect -> "ARRAY " + recursiveNonNullValueToString(value, dimensions)
else -> "ARRAY" + recursiveNonNullValueToString(value, dimensions)
}
}

private fun recursiveNonNullValueToString(value: Any?, level: Int): String = when {
level > 1 -> (value as List<Any?>).joinToString(",", "[", "]") { recursiveNonNullValueToString(it, level - 1) }
else -> (value as List<T & Any>).joinToString(",", "[", "]") { delegate.nonNullValueAsDefaultString(it) }
}

private fun validateValue(value: R) {
validateValueRecursive(value, dimensions)
}

private fun validateValueRecursive(value: R, level: Int) {
if (maximumCardinality == null) return
require(value.size <= maximumCardinality[dimensions - level]) {
"Value must have no more than ${maximumCardinality[dimensions - level]} elements, but it has ${value.size}"
}
if (level > 1) {
(value as List<R>).forEach { validateValueRecursive(it, level - 1) }
}
}
}

private fun isArrayOfByteArrays(value: Array<*>) =
Expand Down
68 changes: 27 additions & 41 deletions exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Op.kt
Original file line number Diff line number Diff line change
Expand Up @@ -687,53 +687,24 @@ fun decimalLiteral(value: BigDecimal): LiteralOp<BigDecimal> = LiteralOp(Decimal
*
* @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<T>? = null): LiteralOp<List<T>> {
@OptIn(InternalApi::class)
return LiteralOp(ArrayColumnType(delegateType ?: resolveColumnType(T::class)), value)
}

/**
* Returns the specified 3-dimensional [value] as an array literal, with elements parsed by the [delegateType] if provided.
*
* @param value The 3-dimensional list of elements to be represented as an array literal.
* @param delegateType An optional parameter which provides an explicit column type to parse the elements. If not provided,
* the column type will be resolved based on the element type [T].
* @return A `LiteralOp` representing the 3-dimensional array literal for the specified [value].
* @throws IllegalStateException If no column type mapping is found and a [delegateType] is not provided.
*/
inline fun <reified T : Any> multi3ArrayLiteral(value: List<List<List<T>>>, delegateType: ColumnType<T>? = null): LiteralOp<List<List<List<T>>>> =
multiArrayLiteral(value, dimensions = 3, delegateType)
inline fun <reified T : Any> arrayLiteral(value: List<T>, delegateType: ColumnType<T>? = null): LiteralOp<List<T>> =
arrayNLiteral(value, delegateType, dimensions = 1)

/**
* Returns the specified 2-dimensional [value] as an array literal, with elements parsed by the [delegateType] if provided.
*
* @param value The 2-dimensional list of elements to be represented as an array literal.
* @param delegateType An optional parameter which provides an explicit column type to parse the elements. If not provided,
* the column type will be resolved based on the element type [T].
* @return A `LiteralOp` representing the 2-dimensional array literal for the specified [value].
* @throws IllegalStateException If no column type mapping is found and a [delegateType] is not provided.
*/
inline fun <reified T : Any> multi2ArrayLiteral(value: List<List<T>>, delegateType: ColumnType<T>? = null): LiteralOp<List<List<T>>> =
multiArrayLiteral(value, dimensions = 2, delegateType)

/**
* Returns the specified multi-dimensional [value] as an array literal, with elements parsed by the [delegateType] if provided.
* The number of dimensions is specified by the [dimensions] parameter.
* 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
* **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].
*
* @param value The multi-dimensional list of elements to be represented as an array literal.
* @param dimensions The number of dimensions of the array. This value should be greater than 1.
* @param delegateType An optional parameter which provides an explicit column type to parse the elements. If not provided,
* the column type will be resolved based on the element type [T].
* @return A `LiteralOp` representing the multi-dimensional array literal for the specified [value].
* @throws IllegalArgumentException If [dimensions] is less than or equal to 1.
* **Note:** Because arrays can have varying dimensions, you must specify the type of elements
* and the number of dimensions when using array literals.
* For example, use `arrayNLiteral<Int, List<List<Int>>>(list, dimensions = 2)`.
*
* @throws IllegalStateException If no column type mapping is found and a [delegateType] is not provided.
*/
inline fun <reified T : Any, R : List<Any>> multiArrayLiteral(value: R, dimensions: Int, delegateType: ColumnType<T>? = null): LiteralOp<R> {
inline fun <reified T : Any, R : List<Any>> arrayNLiteral(value: R, delegateType: ColumnType<T>? = null, dimensions: Int): LiteralOp<R> {
@OptIn(InternalApi::class)
return LiteralOp(MultiArrayColumnType(delegateType ?: resolveColumnType(T::class), dimensions), value)
return LiteralOp(ArrayColumnType(delegateType ?: resolveColumnType(T::class), dimensions), value)
}

// Query Parameters
Expand Down Expand Up @@ -820,9 +791,24 @@ fun blobParam(value: ExposedBlob, useObjectIdentifier: Boolean = false): Express
*
* @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<T>? = null): Expression<List<T>> {
inline fun <reified T : Any> arrayParam(value: List<T>, delegateType: ColumnType<T>? = null): Expression<List<T>> =
arrayNParam(value, delegateType, dimensions = 1)

/**
* 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].
*
* **Note:** Because arrays can have varying dimensions, you must specify the type of elements
* and the number of dimensions when using array literals.
* For example, use `arrayNParam<Int, List<List<Int>>>(list, dimensions = 2)`.
*
* @throws IllegalStateException If no column type mapping is found and a [delegateType] is not provided.
*/
inline fun <reified T : Any, R : List<Any>> arrayNParam(value: R, delegateType: ColumnType<T>? = null, dimensions: Int): Expression<R> {
@OptIn(InternalApi::class)
return QueryParameter(value, ArrayColumnType(delegateType ?: resolveColumnType(T::class)))
return QueryParameter(value, ArrayColumnType(delegateType ?: resolveColumnType(T::class), dimensions))
}

// Misc.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ fun <E, T : List<E>?> allFrom(expression: Expression<T>): Op<E> = AllAnyFromExpr
* @sample org.jetbrains.exposed.sql.tests.shared.types.ArrayColumnTypeTests.testSelectUsingArrayGet
*/
infix operator fun <E, T : List<E>?> ExpressionWithColumnType<T>.get(index: Int): ArrayGet<E, T> =
ArrayGet(this, index, (this.columnType as ArrayColumnType<E>).delegate)
ArrayGet(this, index, (this.columnType as ArrayColumnType<E, List<E>>).delegate)

/**
* Returns a subarray of elements stored from between [lower] and [upper] bounds (inclusive),
Expand Down
Loading

0 comments on commit ef054cb

Please sign in to comment.