From ef054cb32dab5f74988034682232ce52891aee19 Mon Sep 17 00:00:00 2001 From: Oleg Babichev Date: Wed, 25 Sep 2024 15:14:02 +0200 Subject: [PATCH] Review issues: use only one column type for both 1 dimentional and multidimentional arrays --- .../Writerside/topics/Breaking-Changes.md | 7 ++ .../Writerside/topics/Data-Types.topic | 10 +- exposed-core/api/exposed-core.api | 31 ++---- .../org/jetbrains/exposed/sql/ColumnType.kt | 101 +++++------------- .../kotlin/org/jetbrains/exposed/sql/Op.kt | 68 +++++------- .../exposed/sql/SQLExpressionBuilder.kt | 2 +- .../org/jetbrains/exposed/sql/SchemaUtils.kt | 2 +- .../kotlin/org/jetbrains/exposed/sql/Table.kt | 45 +++++--- .../jetbrains/exposed/sql/ops/AllAnyOps.kt | 4 +- .../exposed/sql/vendors/PostgreSQL.kt | 2 +- .../shared/types/MultiArrayColumnTypeTests.kt | 58 ++++++---- 11 files changed, 140 insertions(+), 190 deletions(-) diff --git a/documentation-website/Writerside/topics/Breaking-Changes.md b/documentation-website/Writerside/topics/Breaking-Changes.md index c762298453..fff2751a89 100644 --- a/documentation-website/Writerside/topics/Breaking-Changes.md +++ b/documentation-website/Writerside/topics/Breaking-Changes.md @@ -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`, now it should + be defined as `ArrayColumnType>`, for example `ArrayColumnType` -> `ArrayColumnType>` + + ## 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. diff --git a/documentation-website/Writerside/topics/Data-Types.topic b/documentation-website/Writerside/topics/Data-Types.topic index a0cd53bc88..59194e3514 100644 --- a/documentation-website/Writerside/topics/Data-Types.topic +++ b/documentation-website/Writerside/topics/Data-Types.topic @@ -334,9 +334,9 @@ object Teams : Table("teams") { - val memberIds = multi2Array("member_ids") - val memberNames = multi3Array("member_names") - val budgets = multi2Array("budgets") + val memberIds = array2("member_ids") + val memberNames = array3("member_names") + val budgets = array2("budgets") }

If more control is needed over the base content type, or if the latter is user-defined or from a non-core @@ -344,8 +344,8 @@ object Teams : Table("teams") { - val memberIds = multi2Array("member_ids") - val memberNames = multi3Array("member_names", VarCharColumnType(colLength = 32)) + val memberIds = array2("member_ids") + val memberNames = array3("member_names", VarCharColumnType(colLength = 32)) } diff --git a/exposed-core/api/exposed-core.api b/exposed-core/api/exposed-core.api index 8631c6eadd..0732ab94ef 100644 --- a/exposed-core/api/exposed-core.api +++ b/exposed-core/api/exposed-core.api @@ -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 (Lorg/jetbrains/exposed/sql/ColumnType;Ljava/lang/Integer;)V - public synthetic fun (Lorg/jetbrains/exposed/sql/ColumnType;Ljava/lang/Integer;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lorg/jetbrains/exposed/sql/ColumnType;ILjava/util/List;)V + public synthetic fun (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; @@ -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 { @@ -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 (Lorg/jetbrains/exposed/sql/ColumnType;ILjava/util/List;)V - public synthetic fun (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 (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;)V } @@ -2483,6 +2462,8 @@ public class org/jetbrains/exposed/sql/Table : org/jetbrains/exposed/sql/ColumnS public synthetic fun (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; diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/ColumnType.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/ColumnType.kt index ef3eb305b9..c0c6b5165b 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/ColumnType.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/ColumnType.kt @@ -1187,60 +1187,6 @@ class CustomEnumerationColumnType>( // Array columns -/** - * Array column for storing a collection of elements. - */ -class ArrayColumnType( - /** Returns the base column type of this array column's individual elements. */ - val delegate: ColumnType, - /** Returns the maximum amount of allowed elements in this array column. */ - val maximumCardinality: Int? = null -) : ColumnType>() { - 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 = when { - value is java.sql.Array -> (value.array as Array<*>).map { e -> e?.let { delegate.valueFromDB(it) } as E } - else -> value as? List ?: error("Unexpected value $value of type ${value::class.qualifiedName}") - } - - override fun notNullValueToDB(value: List): Any = value.map { e -> e?.let { delegate.notNullValueToDB(it) } }.toTypedArray() - - override fun valueToString(value: List?): String = if (value != null) nonNullValueToString(value) else super.valueToString(null) - - override fun nonNullValueToString(value: List): String { - val prefix = if (currentDialect is H2Dialect) "ARRAY [" else "ARRAY[" - return value.joinToString(",", prefix, "]") { delegate.valueToString(it) } - } - - override fun nonNullValueAsDefaultString(value: List): 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. * @@ -1251,7 +1197,7 @@ class ArrayColumnType( * **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>( +class ArrayColumnType>( val delegate: ColumnType, val dimensions: Int, val maximumCardinality: List? = null @@ -1259,25 +1205,34 @@ class MultiArrayColumnType>( 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 = when { + private fun recursiveNotNullValueToDB(value: Any, level: Int): Array = when { level > 1 -> (value as List).map { recursiveNotNullValueToDB(it, level - 1) }.toTypedArray() - else -> (value as List).map { delegate.notNullValueToDB(it) }.toTypedArray() + else -> (value as List).map { it?.let { delegate.notNullValueToDB(it) } }.toTypedArray() } @Suppress("UNCHECKED_CAST") @@ -1290,7 +1245,7 @@ class MultiArrayColumnType>( private fun recursiveValueFromDB(value: Any?, level: Int): List = when { level > 1 -> (value as Array).map { recursiveValueFromDB(it, level - 1) } - else -> (value as Array).map { delegate.valueFromDB(it) } + else -> (value as Array).map { it?.let { delegate.valueFromDB(it) } } } override fun readObject(rs: ResultSet, index: Int): Any? { @@ -1299,33 +1254,25 @@ class MultiArrayColumnType>( 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).joinToString(",", "[", "]") { recursiveNonNullValueToString(it, level - 1) } else -> (value as List).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).forEach { validateValueRecursive(it, level - 1) } - } - } } private fun isArrayOfByteArrays(value: Array<*>) = 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 d6f1fc29f5..5e45dfef39 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 @@ -687,53 +687,24 @@ fun decimalLiteral(value: BigDecimal): LiteralOp = LiteralOp(Decimal * * @throws IllegalStateException If no column type mapping is found and a [delegateType] is not provided. */ -inline fun arrayLiteral(value: List, delegateType: ColumnType? = null): LiteralOp> { - @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 multi3ArrayLiteral(value: List>>, delegateType: ColumnType? = null): LiteralOp>>> = - multiArrayLiteral(value, dimensions = 3, delegateType) +inline fun arrayLiteral(value: List, delegateType: ColumnType? = null): LiteralOp> = + 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 multi2ArrayLiteral(value: List>, delegateType: ColumnType? = null): LiteralOp>> = - 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>>(list, dimensions = 2)`. + * * @throws IllegalStateException If no column type mapping is found and a [delegateType] is not provided. */ -inline fun > multiArrayLiteral(value: R, dimensions: Int, delegateType: ColumnType? = null): LiteralOp { +inline fun > arrayNLiteral(value: R, delegateType: ColumnType? = null, dimensions: Int): LiteralOp { @OptIn(InternalApi::class) - return LiteralOp(MultiArrayColumnType(delegateType ?: resolveColumnType(T::class), dimensions), value) + return LiteralOp(ArrayColumnType(delegateType ?: resolveColumnType(T::class), dimensions), value) } // Query Parameters @@ -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 arrayParam(value: List, delegateType: ColumnType? = null): Expression> { +inline fun arrayParam(value: List, delegateType: ColumnType? = null): Expression> = + 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>>(list, dimensions = 2)`. + * + * @throws IllegalStateException If no column type mapping is found and a [delegateType] is not provided. + */ +inline fun > arrayNParam(value: R, delegateType: ColumnType? = null, dimensions: Int): Expression { @OptIn(InternalApi::class) - return QueryParameter(value, ArrayColumnType(delegateType ?: resolveColumnType(T::class))) + return QueryParameter(value, ArrayColumnType(delegateType ?: resolveColumnType(T::class), dimensions)) } // Misc. 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 4be37ac362..34d3cb63b1 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 @@ -199,7 +199,7 @@ fun ?> allFrom(expression: Expression): Op = AllAnyFromExpr * @sample org.jetbrains.exposed.sql.tests.shared.types.ArrayColumnTypeTests.testSelectUsingArrayGet */ infix operator fun ?> ExpressionWithColumnType.get(index: Int): ArrayGet = - ArrayGet(this, index, (this.columnType as ArrayColumnType).delegate) + ArrayGet(this, index, (this.columnType as ArrayColumnType>).delegate) /** * Returns a subarray of elements stored from between [lower] and [upper] bounds (inclusive), diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/SchemaUtils.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/SchemaUtils.kt index 17e8c8226a..378f8e6216 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/SchemaUtils.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/SchemaUtils.kt @@ -221,7 +221,7 @@ object SchemaUtils { } } - column.columnType is ArrayColumnType<*> && dialect is PostgreSQLDialect -> { + column.columnType is ArrayColumnType<*, *> && dialect is PostgreSQLDialect -> { (value as List<*>) .takeIf { it.isNotEmpty() } ?.run { diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Table.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Table.kt index 19c218bcb7..b464be6362 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Table.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Table.kt @@ -907,7 +907,7 @@ open class Table(name: String = "") : ColumnSet(), DdlAware { * when using the PostgreSQL dialect is allowed, but this value will be ignored by the database. */ fun array(name: String, columnType: ColumnType, maximumCardinality: Int? = null): Column> = - registerColumn(name, ArrayColumnType(columnType.apply { nullable = true }, maximumCardinality)) + arrayN>(name, columnType, dimensions = 1, maximumCardinality = maximumCardinality?.let { listOf(it) }) /** * Creates an array column, with the specified [name], for storing elements of a `List`. @@ -924,10 +924,8 @@ open class Table(name: String = "") : ColumnSet(), DdlAware { * 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 array(name: String, maximumCardinality: Int? = null): Column> { - @OptIn(InternalApi::class) - return array(name, resolveColumnType(E::class), maximumCardinality) - } + inline fun array(name: String, maximumCardinality: Int? = null): Column> = + arrayN>(name, dimensions = 1, maximumCardinality?.let { listOf(it) }) /** * Creates a 3-dimensional array column, with the specified [name], for storing elements of a nested `List`. @@ -943,8 +941,8 @@ open class Table(name: String = "") : ColumnSet(), DdlAware { * @return A column instance that represents a 3-dimensional list of elements of type [T]. * @throws IllegalStateException If no column type mapping is found. */ - inline fun Table.multi3Array(name: String, maximumCardinality: List? = null): Column>>> = - multiArray>>>(name, dimensions = 3, maximumCardinality) + inline fun Table.array3(name: String, maximumCardinality: List? = null): Column>>> = + arrayN>>>(name, dimensions = 3, maximumCardinality) /** * Creates a 2-dimensional array column, with the specified [name], for storing elements of a nested `List`. @@ -960,8 +958,8 @@ open class Table(name: String = "") : ColumnSet(), DdlAware { * @return A column instance that represents a 2-dimensional list of elements of type [T]. * @throws IllegalStateException If no column type mapping is found. */ - inline fun Table.multi2Array(name: String, maximumCardinality: List? = null): Column>> = - multiArray>>(name, dimensions = 2, maximumCardinality) + inline fun Table.array2(name: String, maximumCardinality: List? = null): Column>> = + arrayN>>(name, dimensions = 2, maximumCardinality) /** * Creates a multi-dimensional array column, with the specified [name], for storing elements of a nested `List`. @@ -970,7 +968,7 @@ open class Table(name: String = "") : ColumnSet(), DdlAware { * **Note:** This column type is only supported by PostgreSQL dialect. * * @param name Name of the column. - * @param dimensions The number of dimensions of the array. This value should be greater than 1. + * @param dimensions The number of dimensions of the array. * @param maximumCardinality The maximum cardinality (number of allowed elements) for each dimension in the array. * * **Note:** Providing an array size limit when using the PostgreSQL dialect is allowed, but this value will be ignored by the database. @@ -980,14 +978,31 @@ open class Table(name: String = "") : ColumnSet(), DdlAware { * @throws IllegalArgumentException If [dimensions] is less than or equal to 1. * @throws IllegalStateException If no column type mapping is found. */ - inline fun > Table.multiArray(name: String, dimensions: Int, maximumCardinality: List? = null): Column { - if (dimensions <= 1) { - error("Dimension $dimensions should be greater than 1") - } + inline fun > Table.arrayN(name: String, dimensions: Int, maximumCardinality: List? = null): Column { @OptIn(InternalApi::class) - return registerColumn(name, MultiArrayColumnType(resolveColumnType(T::class), dimensions, maximumCardinality)) + return arrayN(name, resolveColumnType(T::class), dimensions, maximumCardinality) } + /** + * Creates a multi-dimensional array column, with the specified [name], for storing elements of a nested `List`. + * The number of dimensions is specified by the [dimensions] parameter. + * + * **Note:** This column type is only supported by PostgreSQL dialect. + * + * @param name Name of the column. + * @param dimensions The number of dimensions of the array. + * @param maximumCardinality The maximum cardinality (number of allowed elements) for each dimension in the array. + * + * **Note:** Providing an array size limit when using the PostgreSQL dialect is allowed, but this value will be ignored by the database. + * The whole validation is performed on the client side. + * + * @return A column instance that represents a multi-dimensional list of elements of type [E]. + * @throws IllegalArgumentException If [dimensions] is less than or equal to 1. + * @throws IllegalStateException If no column type mapping is found. + */ + fun > Table.arrayN(name: String, columnType: ColumnType, dimensions: Int, maximumCardinality: List? = null): Column = + registerColumn(name, ArrayColumnType(columnType, dimensions, maximumCardinality)) + // Auto-generated values /** diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/ops/AllAnyOps.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/ops/AllAnyOps.kt index 0c170c9823..79ce3a7af7 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/ops/AllAnyOps.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/ops/AllAnyOps.kt @@ -41,6 +41,8 @@ class AllAnyFromSubQueryOp( * against an array of values. * * **Note** This operation is only supported by PostgreSQL and H2 dialects. + * + * **Note** This operation is supported only for 1 dimensional arrays */ class AllAnyFromArrayOp( isAny: Boolean, @@ -48,7 +50,7 @@ class AllAnyFromArrayOp( private val delegateType: ColumnType ) : AllAnyFromBaseOp>(isAny, array) { override fun QueryBuilder.registerSubSearchArgument(subSearch: List) { - registerArgument(ArrayColumnType(delegateType), subSearch) + registerArgument(ArrayColumnType>(delegateType, dimensions = 1), subSearch) } } diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/PostgreSQL.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/PostgreSQL.kt index c555633382..22b3ad0876 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/PostgreSQL.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/PostgreSQL.kt @@ -32,7 +32,7 @@ internal object PostgreSQLDataTypeProvider : DataTypeProvider() { e is LiteralOp<*> && e.columnType is BlobColumnType && e.columnType.useObjectIdentifier && (currentDialect as? H2Dialect) == null -> { "lo_from_bytea(0, ${super.processForDefaultValue(e)} :: bytea)" } - e is LiteralOp<*> && e.columnType is ArrayColumnType<*> -> { + e is LiteralOp<*> && e.columnType is ArrayColumnType<*, *> -> { val processed = super.processForDefaultValue(e) processed .takeUnless { it == "ARRAY[]" } diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/types/MultiArrayColumnTypeTests.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/types/MultiArrayColumnTypeTests.kt index 30d7f3d31a..746edca3e7 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/types/MultiArrayColumnTypeTests.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/types/MultiArrayColumnTypeTests.kt @@ -4,17 +4,11 @@ import org.jetbrains.exposed.dao.IntEntity import org.jetbrains.exposed.dao.IntEntityClass import org.jetbrains.exposed.dao.id.EntityID import org.jetbrains.exposed.dao.id.IntIdTable -import org.jetbrains.exposed.sql.insert -import org.jetbrains.exposed.sql.insertAndGetId -import org.jetbrains.exposed.sql.multi2ArrayLiteral -import org.jetbrains.exposed.sql.selectAll +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.assertEqualLists import org.jetbrains.exposed.sql.tests.shared.assertEquals -import org.jetbrains.exposed.sql.tests.shared.expectException -import org.jetbrains.exposed.sql.update -import org.jetbrains.exposed.sql.upsert import org.junit.Test import kotlin.test.assertNull @@ -25,7 +19,7 @@ class MultiArrayColumnTypeTests : DatabaseTestsBase() { @Test fun test2xMultiArray() { val tester = object : IntIdTable("test_table") { - val multiArray = multi2Array("multi_array") + val multiArray = array2("multi_array") } withTables(excludeSettings = multiArrayTypeUnsupportedDb, tester) { @@ -43,7 +37,7 @@ class MultiArrayColumnTypeTests : DatabaseTestsBase() { @Test fun test3xMultiArray() { val tester = object : IntIdTable("test_table") { - val multiArray = multi3Array("multi_array") + val multiArray = array3("multi_array") } withTables(excludeSettings = multiArrayTypeUnsupportedDb, tester) { @@ -63,7 +57,7 @@ class MultiArrayColumnTypeTests : DatabaseTestsBase() { @Test fun test5xMultiArray() { val tester = object : IntIdTable("test_table") { - val multiArray = multiArray>>>>>("multi_array", 5) + val multiArray = arrayN>>>>>("multi_array", 5) } withTables(excludeSettings = multiArrayTypeUnsupportedDb, tester) { @@ -85,12 +79,12 @@ class MultiArrayColumnTypeTests : DatabaseTestsBase() { val default = listOf(listOf(1, 2), listOf(3, 4)) val tester = object : IntIdTable("test_table") { - val multiArray = multi2Array("multi_array") + val multiArray = array2("multi_array") .default(default) } val testerDatabaseGenerated = object : IntIdTable("test_table") { - val multiArray = multi2Array("multi_array") + val multiArray = array2("multi_array") .databaseGenerated() } @@ -108,22 +102,22 @@ class MultiArrayColumnTypeTests : DatabaseTestsBase() { val list = listOf(listOf(1, 2, 3), listOf(4, 5, 6)) val tester = object : IntIdTable("test_table") { - val multiArray = multi2Array("multi_array", maximumCardinality = listOf(2, 2)) + val multiArray = array2("multi_array", maximumCardinality = listOf(2, 2)) } withTables(excludeSettings = multiArrayTypeUnsupportedDb, tester) { - expectException { - tester.insert { - it[tester.multiArray] = list - } + tester.insert { + it[tester.multiArray] = list } + + assertEqualLists(list.flatten(), tester.selectAll().first()[tester.multiArray].flatten()) } } @Test fun testMultiArrayWithNullable() { val tester = object : IntIdTable("test_table") { - val multiArray = multi2Array("multi_array") + val multiArray = array2("multi_array") .nullable() } @@ -139,14 +133,32 @@ class MultiArrayColumnTypeTests : DatabaseTestsBase() { @Test fun testMultiArrayLiteral() { val tester = object : IntIdTable("test_table") { - val multiArray = multi2Array("multi_array") + val multiArray = array2("multi_array") + } + + withTables(excludeSettings = multiArrayTypeUnsupportedDb, tester) { + val list = listOf(listOf(1, 2), listOf(3, 4)) + + tester.insert { + it[multiArray] = arrayNLiteral>>(list, dimensions = 2) + } + + val value = tester.selectAll().first()[tester.multiArray] + assertEqualLists(list.flatten(), value.flatten()) + } + } + + @Test + fun testMultiArrayParam() { + val tester = object : IntIdTable("test_table") { + val multiArray = array2("multi_array") } withTables(excludeSettings = multiArrayTypeUnsupportedDb, tester) { val list = listOf(listOf(1, 2), listOf(3, 4)) tester.insert { - it[multiArray] = multi2ArrayLiteral(list) + it[multiArray] = arrayNParam>>(list, dimensions = 2) } val value = tester.selectAll().first()[tester.multiArray] @@ -157,7 +169,7 @@ class MultiArrayColumnTypeTests : DatabaseTestsBase() { @Test fun testMultiArrayUpdate() { val tester = object : IntIdTable("test_table") { - val multiArray = multi2Array("multi_array") + val multiArray = array2("multi_array") } withTables(excludeSettings = multiArrayTypeUnsupportedDb, tester) { @@ -185,7 +197,7 @@ class MultiArrayColumnTypeTests : DatabaseTestsBase() { @Test fun testMultiArrayUpsert() { val tester = object : IntIdTable("test_table") { - val multiArray = multi2Array("multi_array") + val multiArray = array2("multi_array") } withTables(excludeSettings = multiArrayTypeUnsupportedDb, tester) { @@ -217,7 +229,7 @@ class MultiArrayColumnTypeTests : DatabaseTestsBase() { } object MultiArrayTable : IntIdTable() { - val multiArray = multi2Array("multi_array") + val multiArray = array2("multi_array") } class MultiArrayEntity(id: EntityID) : IntEntity(id) {