Skip to content

Commit

Permalink
Review issues: rename functions (arrayN -> array, arrayNLiteral -> ar…
Browse files Browse the repository at this point in the history
…rayLiteral, arrayNParam -> arrayParam), fix breaking change in ArrayColumnType constructor, remove array2(), array3() functions, update documentation. Fix GetFunctions for multi-dimensional arrays, add tests for GetFunction and SliceFunction
  • Loading branch information
obabichevjb committed Oct 14, 2024
1 parent 448778f commit 7871b65
Show file tree
Hide file tree
Showing 9 changed files with 163 additions and 104 deletions.
7 changes: 3 additions & 4 deletions documentation-website/Writerside/topics/Breaking-Changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,9 @@
* In Oracle and H2 Oracle, the `uinteger()` column now maps to data type `NUMBER(10)` instead of `NUMBER(13)`.
* In Oracle and H2 Oracle, the `integer()` column now maps to data type `NUMBER(10)` and `INTEGER` respectively, instead of `NUMBER(12)`.
In Oracle and SQLite, using the integer column in a table now also creates a CHECK constraint to ensure that no out-of-range values are inserted.
* `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>>`

* `ArrayColumnType` now supports multidimensional arrays and includes an additional generic parameter.
If it was previously used for one-dimensional arrays with the parameter `T` like `ArrayColumnType<T>`,
it should now be defined as `ArrayColumnType<T, List<T>>`. For instance, `ArrayColumnType<Int>` should now be `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`.
Expand Down
91 changes: 57 additions & 34 deletions documentation-website/Writerside/topics/Data-Types.topic
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,50 @@
it[budgets] = listOf(9999.0)
}
</code-block>
<chapter title="Multi-dimensional arrays" id="how-to-use-multi-dimensional-array-types">
<p>PostgreSQL database supports the explicit ARRAY data type, which includes support for multi-dimensional arrays.</p>
<p>Exposed supports columns defined as multi-dimensional arrays, with the stored contents being any
out-of-the-box or custom data type.
If the contents are of a type with a supported <code>ColumnType</code> in the <code>exposed-core</code>
module, the column can be simply defined with that type:</p>

<code-block lang="kotlin">
object Teams : Table("teams") {
val nestedMemberIds = array&lt;UUID, List&lt;List&lt;UUID&gt;&gt;&gt;(
"nested_member_ids", dimensions = 2
)
val hierarchicalMemberNames = array&lt;String, List&lt;List&lt;List&lt;String&gt;&gt;&gt;&gt;(
"member_names", dimensions = 3
)
}
</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 nestedMemberIds = array&lt;UUID, List&lt;List&lt;UUID&gt;&gt;&gt;(
"nested_member_ids", dimensions = 2
)
val hierarchicalMemberNames = array&lt;String, List&lt;List&lt;List&lt;String&gt;&gt;&gt;&gt;(
"hierarchical_member_names",
VarCharColumnType(colLength = 32),
dimensions = 3
)
}
</code-block>

<p>A multi-dimensional array column accepts inserts and retrieves stored array contents as a Kotlin nested <code>List</code>:</p>

<code-block lang="kotlin">
Teams.insert {
it[nestedMemberIds] = List(5) { List(5) { UUID.randomUUID() } }
it[hierarchicalMemberNames] = List(3) { List(3) { List(3) {
i -> "Member ${'A' + i}"
} } }
}
</code-block>
</chapter>
<chapter title="Array Functions" id="array-functions">
<p>A single element in a stored array can be accessed using the index reference <code>get()</code> operator:
</p>
Expand All @@ -300,6 +344,14 @@
.select(firstMember)
.where { Teams.expenses[1] greater Teams.budgets[1] }
</code-block>

<p>This also applies to multidimensional arrays:</p>
<code-block lang="kotlin">
Teams
.selectAll()
.where { Teams.hierarchicalMemberNames[1][1] eq "Mr. Smith" }
</code-block>

<note>
Both PostgreSQL and H2 use a one-based indexing convention, so the first element is retrieved by using
index 1.
Expand All @@ -310,6 +362,11 @@
<code-block lang="kotlin">
Teams.select(Teams.deadlines.slice(1, 3))
</code-block>

<p>In the case of multidimensional arrays, the <code>slice()</code> calls can be nested:</p>
<code-block lang="kotlin">
Teams.select(Teams.hierarchicalMemberNames.slice(1, 2).slice(3, 4))
</code-block>
<p>Both arguments for these bounds are optional if using PostgreSQL.</p>
<p>An array column can also be used as an argument for the <code>ANY</code> and <code>ALL</code> SQL
operators, either by providing the entire column or a new array expression via <code>slice()</code>:</p>
Expand All @@ -325,40 +382,6 @@
</code-block>
</chapter>
</chapter>
<chapter title="How to use Multi-Dimensional Array types" id="how-to-use-multi-dimensional-array-types">
<p>PostgreSQL database supports the explicit ARRAY data type, which includes support for multi-dimensional arrays.</p>
<p>Exposed supports columns defined as multi-dimensional arrays, with the stored contents being any
out-of-the-box or custom data type.
If the contents are of a type with a supported <code>ColumnType</code> in the <code>exposed-core</code>
module, the column can be simply defined with that type:</p>

<code-block lang="kotlin">
object Teams : Table("teams") {
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 = array2<UUID>("member_ids")
val memberNames = array3<String>("member_names", VarCharColumnType(colLength = 32))
}
</code-block>

<p>A multi-dimensional array column accepts inserts and retrieves stored array contents as a Kotlin nested <code>List</code>:</p>

<code-block lang="kotlin">
Teams.insert {
it[memberIds] = List(5) { List(5) { UUID.randomUUID() } }
it[memberNames] = List(3) { List(3) { List(3) { i -> "Member ${'A' + i}" } } }
it[budgets] = listOf(listOf(9999.0, 8888.0))
}
</code-block>
</chapter>
<chapter title="Custom Data Types" id="custom-data-types">
<p>If a database-specific data type is not immediately supported by Exposed, any existing and open column type
class can be extended or
Expand Down
10 changes: 6 additions & 4 deletions exposed-core/api/exposed-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -203,8 +203,10 @@ 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;ILjava/util/List;)V
public synthetic fun <init> (Lorg/jetbrains/exposed/sql/ColumnType;ILjava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
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;Ljava/util/List;I)V
public synthetic fun <init> (Lorg/jetbrains/exposed/sql/ColumnType;Ljava/util/List;IILkotlin/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
Expand Down Expand Up @@ -2464,9 +2466,9 @@ public class org/jetbrains/exposed/sql/Table : org/jetbrains/exposed/sql/ColumnS
public fun <init> (Ljava/lang/String;)V
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 final fun array (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/String;Lorg/jetbrains/exposed/sql/ColumnType;Ljava/util/List;I)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 static synthetic fun array$default (Lorg/jetbrains/exposed/sql/Table;Lorg/jetbrains/exposed/sql/Table;Ljava/lang/String;Lorg/jetbrains/exposed/sql/ColumnType;Ljava/util/List;IILjava/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
Original file line number Diff line number Diff line change
Expand Up @@ -1240,13 +1240,23 @@ class CustomEnumerationColumnType<T : Enum<T>>(
* @property maximumCardinality The maximum cardinality (number of allowed elements) for each dimension of the array.
*
* **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 ArrayColumnType<T, R : List<Any?>>(
val delegate: ColumnType<T & Any>,
val dimensions: Int,
val maximumCardinality: List<Int>? = null
val maximumCardinality: List<Int>? = null,
val dimensions: Int = 1
) : ColumnType<R>() {
/**
* Constructor with maximum cardinality for a single dimension.
*
* @param delegate The base column type associated with this array column's individual elements.
* @param maximumCardinality The maximum cardinality (number of allowed elements) for the array.
*/
constructor(delegate: ColumnType<T & Any>, maximumCardinality: Int? = null) : this(delegate, maximumCardinality?.let { listOf(it) })

/**
* The SQL type definition of the delegate column type without any potential array dimensions.
*/
val delegateType: String
get() = delegate.sqlType().substringBefore('(')

Expand Down Expand Up @@ -1316,7 +1326,7 @@ class ArrayColumnType<T, R : List<Any?>>(

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) }
else -> (value as List<T & Any>).joinToString(",", "[", "]") { delegate.nonNullValueToString(it) }
}
}

Expand Down
16 changes: 8 additions & 8 deletions exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Op.kt
Original file line number Diff line number Diff line change
Expand Up @@ -688,7 +688,7 @@ 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>> =
arrayNLiteral(value, delegateType, dimensions = 1)
arrayLiteral(value, 1, delegateType)

/**
* Returns the specified [value] as an array literal, with elements parsed by the [delegateType] if provided.
Expand All @@ -698,13 +698,13 @@ inline fun <reified T : Any> arrayLiteral(value: List<T>, delegateType: ColumnTy
*
* **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)`.
* For example, use `arrayLiteral<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>> arrayNLiteral(value: R, delegateType: ColumnType<T>? = null, dimensions: Int): LiteralOp<R> {
inline fun <reified T : Any, R : List<Any>> arrayLiteral(value: R, dimensions: Int, delegateType: ColumnType<T>? = null): LiteralOp<R> {
@OptIn(InternalApi::class)
return LiteralOp(ArrayColumnType(delegateType ?: resolveColumnType(T::class), dimensions), value)
return LiteralOp(ArrayColumnType(delegateType ?: resolveColumnType(T::class), dimensions = dimensions), value)
}

// Query Parameters
Expand Down Expand Up @@ -792,7 +792,7 @@ 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>> =
arrayNParam(value, delegateType, dimensions = 1)
arrayParam(value, 1, delegateType)

/**
* Returns the specified [value] as an array query parameter, with elements parsed by the [delegateType] if provided.
Expand All @@ -802,13 +802,13 @@ inline fun <reified T : Any> arrayParam(value: List<T>, delegateType: ColumnType
*
* **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)`.
* For example, use `arrayParam<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> {
inline fun <reified T : Any, R : List<Any>> arrayParam(value: R, dimensions: Int, delegateType: ColumnType<T>? = null): Expression<R> {
@OptIn(InternalApi::class)
return QueryParameter(value, ArrayColumnType(delegateType ?: resolveColumnType(T::class), dimensions))
return QueryParameter(value, ArrayColumnType(delegateType ?: resolveColumnType(T::class), dimensions = dimensions))
}

// Misc.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,8 +198,12 @@ 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, List<E>>).delegate)
infix operator fun <E, T : List<E>?> ExpressionWithColumnType<T>.get(index: Int): ArrayGet<E, T> {
return when (this) {
is ArrayGet<*, *> -> ArrayGet(this as Expression<T>, index, this.columnType as IColumnType<E & Any>) as ArrayGet<E, T>
else -> 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 7871b65

Please sign in to comment.