Skip to content

Commit

Permalink
feat: EXPOSED-248 Support array column type (#1986)
Browse files Browse the repository at this point in the history
* feat: EXPOSED-248 Support array column type

- Add new ArrayColumnType that stores and retrieves elements of a List.
- Supported databases include: PostgreSQL, PostgreSQLNG, H2 (and variants).
- Maximum array size can be set (for H2).
- Currently on 1-dimensional arrays are supported.
- Stored array can be accessed using index reference as well as sliced.
- Updating a stored array using index reference is currently not supported.
- Using an arry column with ANY or ALL operatora is not currently supported.

* feat: EXPOSED-248 Support array column type

- Add more tests
- Add apiDump changes
- Edit KDocs

* feat: EXPOSED-248 Support array column type

- Replace SchemaUtils if block with when block.
- Add index reference get operator and update test.

* feat: EXPOSED-248 Support array column type

Drop elementAt() in favor of get() index reference operator.
  • Loading branch information
bog-walk authored Feb 9, 2024
1 parent 6732924 commit b909d07
Show file tree
Hide file tree
Showing 14 changed files with 525 additions and 14 deletions.
38 changes: 38 additions & 0 deletions exposed-core/api/exposed-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,20 @@ public final class org/jetbrains/exposed/sql/AndOp : org/jetbrains/exposed/sql/C
public fun <init> (Ljava/util/List;)V
}

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 final fun getDelegate ()Lorg/jetbrains/exposed/sql/ColumnType;
public final fun getDelegateType ()Ljava/lang/String;
public final fun getMaximumCardinality ()Ljava/lang/Integer;
public fun notNullValueToDB (Ljava/lang/Object;)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 fun valueFromDB (Ljava/lang/Object;)Ljava/lang/Object;
public fun valueToString (Ljava/lang/Object;)Ljava/lang/String;
}

public final class org/jetbrains/exposed/sql/AutoIncColumnType : org/jetbrains/exposed/sql/IColumnType {
public fun <init> (Lorg/jetbrains/exposed/sql/ColumnType;Ljava/lang/String;Ljava/lang/String;)V
public fun equals (Ljava/lang/Object;)Z
Expand Down Expand Up @@ -1509,6 +1523,8 @@ 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;)Lorg/jetbrains/exposed/sql/Expression;
public static final fun booleanLiteral (Z)Lorg/jetbrains/exposed/sql/LiteralOp;
public static final fun booleanParam (Z)Lorg/jetbrains/exposed/sql/Expression;
Expand Down Expand Up @@ -1786,6 +1802,7 @@ public final class org/jetbrains/exposed/sql/SQLExpressionBuilderKt {
public static final fun count (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;)Lorg/jetbrains/exposed/sql/Count;
public static final fun countDistinct (Lorg/jetbrains/exposed/sql/Column;)Lorg/jetbrains/exposed/sql/Count;
public static final fun function (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/CustomFunction;
public static final fun get (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;I)Lorg/jetbrains/exposed/sql/functions/array/ArrayGet;
public static final fun groupConcat (Lorg/jetbrains/exposed/sql/Expression;Ljava/lang/String;ZLkotlin/Pair;)Lorg/jetbrains/exposed/sql/GroupConcat;
public static final fun groupConcat (Lorg/jetbrains/exposed/sql/Expression;Ljava/lang/String;Z[Lkotlin/Pair;)Lorg/jetbrains/exposed/sql/GroupConcat;
public static synthetic fun groupConcat$default (Lorg/jetbrains/exposed/sql/Expression;Ljava/lang/String;ZLkotlin/Pair;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/GroupConcat;
Expand All @@ -1796,6 +1813,8 @@ public final class org/jetbrains/exposed/sql/SQLExpressionBuilderKt {
public static final fun min (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;)Lorg/jetbrains/exposed/sql/Min;
public static final fun nextIntVal (Lorg/jetbrains/exposed/sql/Sequence;)Lorg/jetbrains/exposed/sql/NextVal;
public static final fun nextLongVal (Lorg/jetbrains/exposed/sql/Sequence;)Lorg/jetbrains/exposed/sql/NextVal;
public static final fun slice (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Integer;Ljava/lang/Integer;)Lorg/jetbrains/exposed/sql/functions/array/ArraySlice;
public static synthetic fun slice$default (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Integer;Ljava/lang/Integer;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/functions/array/ArraySlice;
public static final fun stdDevPop (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;I)Lorg/jetbrains/exposed/sql/StdDevPop;
public static synthetic fun stdDevPop$default (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;IILjava/lang/Object;)Lorg/jetbrains/exposed/sql/StdDevPop;
public static final fun stdDevSamp (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;I)Lorg/jetbrains/exposed/sql/StdDevSamp;
Expand Down Expand Up @@ -2174,6 +2193,8 @@ public class org/jetbrains/exposed/sql/Table : org/jetbrains/exposed/sql/ColumnS
public fun <init> ()V
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 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 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 static synthetic fun autoIncrement$default (Lorg/jetbrains/exposed/sql/Table;Lorg/jetbrains/exposed/sql/Column;Ljava/lang/String;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Column;
Expand Down Expand Up @@ -2568,6 +2589,21 @@ public final class org/jetbrains/exposed/sql/XorBitOp : org/jetbrains/exposed/sq
public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V
}

public final class org/jetbrains/exposed/sql/functions/array/ArrayGet : org/jetbrains/exposed/sql/Function {
public fun <init> (Lorg/jetbrains/exposed/sql/Expression;ILorg/jetbrains/exposed/sql/IColumnType;)V
public final fun getExpression ()Lorg/jetbrains/exposed/sql/Expression;
public final fun getIndex ()I
public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V
}

public final class org/jetbrains/exposed/sql/functions/array/ArraySlice : org/jetbrains/exposed/sql/Function {
public fun <init> (Lorg/jetbrains/exposed/sql/Expression;Ljava/lang/Integer;Ljava/lang/Integer;Lorg/jetbrains/exposed/sql/IColumnType;)V
public final fun getExpression ()Lorg/jetbrains/exposed/sql/Expression;
public final fun getLower ()Ljava/lang/Integer;
public final fun getUpper ()Ljava/lang/Integer;
public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V
}

public final class org/jetbrains/exposed/sql/functions/math/ACosFunction : org/jetbrains/exposed/sql/CustomFunction {
public fun <init> (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;)V
}
Expand Down Expand Up @@ -3092,6 +3128,7 @@ public abstract interface class org/jetbrains/exposed/sql/statements/api/Prepare
public abstract fun getResultSet ()Ljava/sql/ResultSet;
public abstract fun getTimeout ()Ljava/lang/Integer;
public abstract fun set (ILjava/lang/Object;)V
public abstract fun setArray (ILjava/lang/String;[Ljava/lang/Object;)V
public abstract fun setFetchSize (Ljava/lang/Integer;)V
public abstract fun setInputStream (ILjava/io/InputStream;)V
public abstract fun setNull (ILorg/jetbrains/exposed/sql/IColumnType;)V
Expand Down Expand Up @@ -3455,6 +3492,7 @@ public abstract class org/jetbrains/exposed/sql/vendors/FunctionProvider {
protected final fun appendInsertToUpsertClause (Lorg/jetbrains/exposed/sql/QueryBuilder;Lorg/jetbrains/exposed/sql/Table;Ljava/util/List;Lorg/jetbrains/exposed/sql/Transaction;)V
protected final fun appendJoinPartForUpdateClause (Lorg/jetbrains/exposed/sql/QueryBuilder;Lorg/jetbrains/exposed/sql/Table;Lorg/jetbrains/exposed/sql/Join;Lorg/jetbrains/exposed/sql/Transaction;)V
protected final fun appendUpdateToUpsertClause (Lorg/jetbrains/exposed/sql/QueryBuilder;Lorg/jetbrains/exposed/sql/Table;Ljava/util/List;Ljava/util/List;Lorg/jetbrains/exposed/sql/Transaction;Z)V
public fun arraySlice (Lorg/jetbrains/exposed/sql/Expression;Ljava/lang/Integer;Ljava/lang/Integer;Lorg/jetbrains/exposed/sql/QueryBuilder;)V
public fun cast (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/IColumnType;Lorg/jetbrains/exposed/sql/QueryBuilder;)V
public fun charLength (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/QueryBuilder;)V
public fun concat (Ljava/lang/String;Lorg/jetbrains/exposed/sql/QueryBuilder;[Lorg/jetbrains/exposed/sql/Expression;)V
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1006,6 +1006,8 @@ class CustomEnumerationColumnType<T : Enum<T>>(
override fun nonNullValueToString(value: Any): String = super.nonNullValueToString(notNullValueToDB(value))
}

// Array columns

/**
* Array column for storing arrays of any size and type.
*
Expand All @@ -1018,6 +1020,55 @@ internal object UntypedAndUnsizedArrayColumnType : ColumnType() {
currentDialect.dataTypeProvider.untypedAndUnsizedArrayType()
}

/**
* 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('(')

override fun valueFromDB(value: Any): Any = when {
value is java.sql.Array -> (value.array as Array<*>).map { e -> e?.let { delegate.valueFromDB(it) } }
else -> value
}

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

override fun valueToString(value: Any?): String = when {
value is List<*> -> {
val prefix = if (currentDialect is H2Dialect) "ARRAY [" else "ARRAY["
value.joinToString(",", prefix, "]") { delegate.valueToString(it) }
}
else -> super.valueToString(value)
}

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

override fun setParameter(stmt: PreparedStatementApi, index: Int, value: Any?) {
when {
value is Array<*> -> stmt.setArray(index, delegateType, value)
else -> super.setParameter(stmt, index, value)
}
}
}

// Date/Time columns

/**
Expand Down
8 changes: 8 additions & 0 deletions exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Op.kt
Original file line number Diff line number Diff line change
Expand Up @@ -682,6 +682,10 @@ 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)

// Query Parameters

/**
Expand Down Expand Up @@ -742,6 +746,10 @@ fun decimalParam(value: BigDecimal): Expression<BigDecimal> = QueryParameter(val
/** Returns the specified [value] as a blob query parameter. */
fun blobParam(value: ExposedBlob): Expression<ExposedBlob> = QueryParameter(value, BlobColumnType())

/** 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))

// Misc.

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ package org.jetbrains.exposed.sql
import org.jetbrains.exposed.dao.id.EntityID
import org.jetbrains.exposed.dao.id.EntityIDFunctionProvider
import org.jetbrains.exposed.dao.id.IdTable
import org.jetbrains.exposed.sql.functions.array.ArrayGet
import org.jetbrains.exposed.sql.functions.array.ArraySlice
import org.jetbrains.exposed.sql.ops.*
import org.jetbrains.exposed.sql.vendors.FunctionProvider
import org.jetbrains.exposed.sql.vendors.currentDialect
Expand Down Expand Up @@ -129,6 +131,24 @@ fun <T> allFrom(array: Array<T>): Op<T> = AllAnyFromArrayOp(false, array)
/** 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)

/**
* Returns the array element stored at the one-based [index] position, or `null` if the stored array itself is null.
*
* @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).delegate)

/**
* Returns a subarray of elements stored from between [lower] and [upper] bounds (inclusive),
* or `null` if the stored array itself is null.
* **Note** If either bounds is left `null`, the database will use the stored array's respective lower or upper limit.
*
* @sample org.jetbrains.exposed.sql.tests.shared.types.ArrayColumnTypeTests.testSelectUsingArraySlice
*/
fun <E, T : List<E>?> ExpressionWithColumnType<T>.slice(lower: Int? = null, upper: Int? = null): ArraySlice<E, T> =
ArraySlice(this, lower, upper, ArrayColumnType((this.columnType as ArrayColumnType).delegate))

// Sequence Manipulation Functions

/** Advances this sequence and returns the new value. */
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.jetbrains.exposed.sql

import org.jetbrains.exposed.exceptions.ExposedSQLException
import org.jetbrains.exposed.sql.SqlExpressionBuilder.asLiteral
import org.jetbrains.exposed.sql.transactions.TransactionManager
import org.jetbrains.exposed.sql.vendors.*
import java.io.File
Expand Down Expand Up @@ -187,23 +188,38 @@ object SchemaUtils {
}

else -> {
if (column.columnType is JsonColumnMarker) {
val processed = processForDefaultValue(exp)
when (dialect) {
is PostgreSQLDialect -> {
if (column.columnType.usesBinaryFormat) {
processed.replace(Regex("(\"|})(:|,)(\\[|\\{|\")"), "$1$2 $3")
} else {
processed
when {
column.columnType is JsonColumnMarker -> {
val processed = processForDefaultValue(exp)
when (dialect) {
is PostgreSQLDialect -> {
if (column.columnType.usesBinaryFormat) {
processed.replace(Regex("(\"|})(:|,)(\\[|\\{|\")"), "$1$2 $3")
} else {
processed
}
}
is MariaDBDialect -> processed.trim('\'')
is MysqlDialect -> "_utf8mb4\\'${processed.trim('(', ')', '\'')}\\"
else -> processed.trim('\'')
}

is MariaDBDialect -> processed.trim('\'')
is MysqlDialect -> "_utf8mb4\\'${processed.trim('(', ')', '\'')}\\"
else -> processed.trim('\'')
}
} else {
processForDefaultValue(exp)
column.columnType is ArrayColumnType && dialect is PostgreSQLDialect -> {
(value as List<*>)
.takeIf { it.isNotEmpty() }
?.run {
val delegate = column.withColumnType(column.columnType.delegate)
val processed = map {
if (delegate.columnType is StringColumnType) {
"'$it'::text"
} else {
dbDefaultToString(delegate, delegate.asLiteral(it))
}
}
"ARRAY$processed"
} ?: processForDefaultValue(exp)
}
else -> processForDefaultValue(exp)
}
}
}
Expand Down
15 changes: 15 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 @@ -811,6 +811,21 @@ open class Table(name: String = "") : ColumnSet(), DdlAware {
toDb: (T) -> Any
): Column<T> = registerColumn(name, CustomEnumerationColumnType(name, sql, fromDb, toDb))

// Array columns

/**
* Creates an array column, with the specified [name], for storing elements of a `List` using a base [columnType].
*
* **Note** This column type is only supported by H2 and PostgreSQL dialects.
*
* @param name Name of the column.
* @param columnType Base column type for the individual elements.
* @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.
*/
fun <T> array(name: String, columnType: ColumnType, maximumCardinality: Int? = null): Column<List<T>> =
registerColumn(name, ArrayColumnType(columnType.apply { nullable = true }, maximumCardinality))

// Auto-generated values

/**
Expand Down
Loading

0 comments on commit b909d07

Please sign in to comment.