Exposed icon indicating copy to clipboard operation
Exposed copied to clipboard

Array column type support

Open lirispp opened this issue 6 years ago • 25 comments

Do you have any plan to support array column support?

lirispp avatar Sep 04 '17 02:09 lirispp

Very small batch of databases support array data type and provide related functions. So the only way to support it from Exposed side is through serialization/deserialization, which means that it wont be possible to use it in any "WHERE" predicates or check it with functions.

We don't see many benefits from adding such support, btw you can easily make it by your own with help of transform function:

object FooTable : IntIdTable() {
    val bar = text("bar")
}

class Foo(id: EntityID<Int>) : IntEntity(id) {
    var bar by FooTable.bar.transform(
            { a -> a.joinToString(SEPARATOR) }, 
            { str -> str.split(SEPARATOR).map { it.toIntOrNull() }.toTypedArray() }
    )
    
    companion object : IntEntityClass<Foo>(FooTable, Foo::class.java) {
        const val SEPARATOR = ":"
    }
}

fun main(args: Array<String>) {
    val new = Foo.new {
        bar = arrayOf(1, 3, 5)
    }
    
    new.bar += 5
}

Tapac avatar Sep 04 '17 06:09 Tapac

This would work for some situations. But major databases like MySQL / Postgres / Oracle support array and have very useful functions for array.

lirispp avatar Sep 07 '17 04:09 lirispp

@lirispp , I can't find info about Array Type in MySQL, also SQLite doesn't support it. But we can think how to add that type support in future.

Tapac avatar Sep 07 '17 06:09 Tapac

Sorry. MySQL doesn't have array type. But we can use json type instead of array type.

lirispp avatar Sep 07 '17 07:09 lirispp

json type only available since MySQL 5.7 version.

Tapac avatar Sep 07 '17 07:09 Tapac

java.sql.ResultSet supports Array type. I think it is reasonable to support array type.

lirispp avatar Sep 11 '17 05:09 lirispp

PostgreSQL supports array types, and they massively come in handy.

phase avatar Jan 06 '18 11:01 phase

Hey, thanks for contributing Exposed to opensource. Is there any chance for pgsql Array Types support for Exposed?

swistaczek avatar Oct 29 '18 11:10 swistaczek

We cannot enroll our own array column type because of this line https://github.com/JetBrains/Exposed/blob/master/src/main/kotlin/org/jetbrains/exposed/sql/ColumnType.kt#L38, which unpacks the array and breaks the code.

riyadparvez avatar Mar 06 '19 22:03 riyadparvez

@riyadparvez Are you creating your own class that implements IColumnType, as in @lirispp's example linked above? Could you override valueToString with your own custom Iterable<*> handling before handing off other branches to IColumnType's default implementation?

doxavore avatar Mar 06 '19 22:03 doxavore

Yes I am doing more or less similar to @lirispp example. There's no valueToString to override.

fun Table.uuidArray(name: String): Column<List<UUID>>
        = registerColumn(name, UUIDArrayColumnType())

private class UUIDArrayColumnType : ColumnType() {
    override fun sqlType() = "uuid[]"
    override fun valueFromDB(value: Any): List<UUID> {
        return if (value is PgArray) {
            val array = value.array
            if (array is Array<*>) {
                array.map {
                    it as UUID
                }
            }
            else {
                throw Exception("Values returned from database if not of type kotlin Array<*> ")
            }
        }
        else throw Exception("Values returned from database if not of type PgArray")
    }
    
    override fun notNullValueToDB(value: Any): Any {
        throw java.lang.UnsupportedOperationException("Please use the helper method to insert in db")
    }

    override fun nonNullValueToString(value: Any): String {
        return "'${notNullValueToDB(value)}'"
    }
}

By the time notNullValueToDB is called the Iterable is already unpacked by exposed and it breaks the code.

riyadparvez avatar Mar 06 '19 22:03 riyadparvez

Interesting - ColumnType is a pretty light abstract class implementing IColumnType, and I'm able to override fun valueToString(value: Any?): String on the latest released version of 0.12.2.

doxavore avatar Mar 06 '19 22:03 doxavore

My bad. Yes, @lirispp solution works.

riyadparvez avatar Mar 07 '19 14:03 riyadparvez

Kotlin please officially support array type, it's very unwiedy since very large community is using Postgres and it does have array types

wangbo223 avatar Sep 17 '19 20:09 wangbo223

Hi! I made a implementation array column type for postgres..

fun Table.arrayOfLong(name: String): Column<List<Long>> =
    registerColumn(name, LongArrayColumnType())

internal class LongArrayColumnType : ColumnType() {
    override fun sqlType() = "BIGINT[]"
    override fun valueFromDB(value: Any): List<Long> = when (value) {
        is Iterable<*> -> value.map { it.toString().toLong() }
        is PgArray -> {
            val array = value.array
            if (array is Array<*>) {
                array.map {
                    when (it) {
                        is Long -> it
                        is Number -> it.toLong()
                        null -> error("Unexpected value of type Long but value is $it")
                        else -> error("Unexpected value of type Long: $it of ${it::class.qualifiedName}")
                    }
                }
            } else {
                throw Exception("Values returned from database if not of type kotlin Array<*>")
            }
        }
        else -> throw Exception("Values returned from database if not of type PgArray")
    }

    override fun valueToString(value: Any?): String = when (value) {
        null -> {
            if (!nullable) error("NULL in non-nullable column")
            "NULL"
        }

        is Iterable<*> -> {
            "'{${value.joinToString()}}'"
        }

        else -> {
            nonNullValueToString(value)
        }
    }

    override fun setParameter(stmt: PreparedStatement, index: Int, value: Any?) {
        if (value is List<*>) {
            stmt.setArray(index, stmt.connection.createArrayOf("bigint", value.toTypedArray()))
        } else {
            super.setParameter(stmt, index, value)
        }
    }
}

vyasenenko avatar Oct 12 '19 13:10 vyasenenko

This is my version, based on @lirispp's implementation.

This version works with Exposed 0.24.1 while previous versions in this thread doesn't due to Exposed's internal changes.

This is also a "generic" version, so you don't need to create a different array column type every time you want to use a different type.

import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.statements.jdbc.JdbcConnectionImpl
import org.jetbrains.exposed.sql.transactions.TransactionManager

fun <T> Table.array(name: String, columnType: ColumnType): Column<Array<T>> = registerColumn(name, ArrayColumnType(columnType))

class ArrayColumnType(private val type: ColumnType) : ColumnType() {
	override fun sqlType(): String = buildString {
		append(type.sqlType())
		append(" ARRAY")
	}
	override fun valueToDB(value: Any?): Any? {
		if (value is Array<*>) {
			val columnType = type.sqlType().split("(")[0]
			val jdbcConnection = (TransactionManager.current().connection as JdbcConnectionImpl).connection
			return jdbcConnection.createArrayOf(columnType, value)
		} else {
			return super.valueToDB(value)
		}
	}
	override fun valueFromDB(value: Any): Any {
		if (value is java.sql.Array) {
			return value.array
		}
		if (value is Array<*>) {
			return value
		}
		error("Array does not support for this database")
	}

	override fun notNullValueToDB(value: Any): Any {
		if (value is Array<*>) {
			if (value.isEmpty())
				return "'{}'"

			val columnType = type.sqlType().split("(")[0]
			val jdbcConnection = (TransactionManager.current().connection as JdbcConnectionImpl).connection
			return jdbcConnection.createArrayOf(columnType, value) ?: error("Can't create non null array for $value")
		} else {
			return super.notNullValueToDB(value)
		}
	}
}

class AnyOp(val expr1: Expression<*>, val expr2: Expression<*>) : Op<Boolean>() {
	override fun toQueryBuilder(queryBuilder: QueryBuilder) {
		if (expr2 is OrOp) {
			queryBuilder.append("(").append(expr2).append(")")
		} else {
			queryBuilder.append(expr2)
		}
		queryBuilder.append(" = ANY (")
		if (expr1 is OrOp) {
			queryBuilder.append("(").append(expr1).append(")")
		} else {
			queryBuilder.append(expr1)
		}
		queryBuilder.append(")")
	}
}

class ContainsOp(expr1: Expression<*>, expr2: Expression<*>) : ComparisonOp(expr1, expr2, "@>")

infix fun<T, S> ExpressionWithColumnType<T>.any(t: S) : Op<Boolean> {
	if (t == null) {
		return IsNullOp(this)
	}
	return AnyOp(this, QueryParameter(t, columnType))
}

infix fun<T, S> ExpressionWithColumnType<T>.contains(arry: Array<in S>) : Op<Boolean> = ContainsOp(this, QueryParameter(arry, columnType))

Source: https://github.com/LorittaBot/Loritta/blob/development/loritta-discord/src/main/java/com/mrpowergamerbr/loritta/utils/exposed/array.kt

MrPowerGamerBR avatar May 08 '20 11:05 MrPowerGamerBR

I believe googles Room persistence library serializes arrays into a string for SQLite, then de serializes them.

For basic types such as Int, Long, String, etc

Doomsdayrs avatar May 20 '20 15:05 Doomsdayrs

Just an anecdote - I'm working on a small service that uses an existing PostgreSQL database. It's the first project in our organization using Kotlin, so will likely serve as a template of sorts for future projects.

I originally chose Exposed for my ORM, but had to switch to ktorm because we already have text[] columns in the database.

So, in my case, Exposed's lack of array and hstore types prevented me from using it.

lyndsysimon avatar Aug 11 '20 17:08 lyndsysimon

Out of curiosity, if someone were to provide a patch for Exposed that supported Array, json, jsonb, and hstore types, would you be willing to include it in the next release?

I've already had to use my nascent Kotlin skills to patch ktorm in a couple of instances, so it's not as intimidating to me now as it was a few weeks ago...

lyndsysimon avatar Aug 11 '20 18:08 lyndsysimon

@lyndsysimon , sure any PR is very welcome.

Tapac avatar Aug 11 '20 20:08 Tapac

This is my version, based on @lirispp's implementation.

This version works with Exposed 0.24.1 while previous versions in this thread doesn't due to Exposed's internal changes.

This is also a "generic" version, so you don't need to create a different array column type every time you want to use a different type.

import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.statements.jdbc.JdbcConnectionImpl
import org.jetbrains.exposed.sql.transactions.TransactionManager

fun <T> Table.array(name: String, columnType: ColumnType): Column<Array<T>> = registerColumn(name, ArrayColumnType(columnType))

class ArrayColumnType(private val type: ColumnType) : ColumnType() {
	override fun sqlType(): String = buildString {
		append(type.sqlType())
		append(" ARRAY")
	}
	override fun valueToDB(value: Any?): Any? {
		if (value is Array<*>) {
			val columnType = type.sqlType().split("(")[0]
			val jdbcConnection = (TransactionManager.current().connection as JdbcConnectionImpl).connection
			return jdbcConnection.createArrayOf(columnType, value)
		} else {
			return super.valueToDB(value)
		}
	}
	override fun valueFromDB(value: Any): Any {
		if (value is java.sql.Array) {
			return value.array
		}
		if (value is Array<*>) {
			return value
		}
		error("Array does not support for this database")
	}

	override fun notNullValueToDB(value: Any): Any {
		if (value is Array<*>) {
			if (value.isEmpty())
				return "'{}'"

			val columnType = type.sqlType().split("(")[0]
			val jdbcConnection = (TransactionManager.current().connection as JdbcConnectionImpl).connection
			return jdbcConnection.createArrayOf(columnType, value) ?: error("Can't create non null array for $value")
		} else {
			return super.notNullValueToDB(value)
		}
	}
}

class AnyOp(val expr1: Expression<*>, val expr2: Expression<*>) : Op<Boolean>() {
	override fun toQueryBuilder(queryBuilder: QueryBuilder) {
		if (expr2 is OrOp) {
			queryBuilder.append("(").append(expr2).append(")")
		} else {
			queryBuilder.append(expr2)
		}
		queryBuilder.append(" = ANY (")
		if (expr1 is OrOp) {
			queryBuilder.append("(").append(expr1).append(")")
		} else {
			queryBuilder.append(expr1)
		}
		queryBuilder.append(")")
	}
}

class ContainsOp(expr1: Expression<*>, expr2: Expression<*>) : ComparisonOp(expr1, expr2, "@>")

infix fun<T, S> ExpressionWithColumnType<T>.any(t: S) : Op<Boolean> {
	if (t == null) {
		return IsNullOp(this)
	}
	return AnyOp(this, QueryParameter(t, columnType))
}

infix fun<T, S> ExpressionWithColumnType<T>.contains(arry: Array<in S>) : Op<Boolean> = ContainsOp(this, QueryParameter(arry, columnType))

Source: https://github.com/LorittaBot/Loritta/blob/development/loritta-discord/src/main/java/com/mrpowergamerbr/loritta/utils/exposed/array.kt

Hi @MrPowerGamerBR I'm trying to implements this solution for something like: val departments = array("departement", ColumnType ) . What should be my ColumnType in this case ? Given departments is a list of Department.

sabiou avatar Nov 12 '20 03:11 sabiou

@sabiou use the above in following way, val databaseCompanyOwner = array<Int>("subOwners", IntegerColumnType() )

TheReprator avatar Nov 13 '20 21:11 TheReprator

@sabiou use the above in following way, val databaseCompanyOwner = array("subOwners", IntegerColumnType() )

Thanks @TheReprator

sabiou avatar Nov 15 '20 20:11 sabiou

Hi, Any update? Any native implementation of exposed?

This is irrelevant to the issue

Doomsdayrs avatar Apr 05 '21 15:04 Doomsdayrs

I have my working solution here: https://gist.github.com/DRSchlaubi/cb146ee2b4d94d1c89b17b358187c612

DRSchlaubi avatar Feb 05 '22 18:02 DRSchlaubi

Based on the above ideas I have something like this: https://gist.github.com/frynet/59b68faee6927bb95302a7d290e1cff8

@Tapac I have no time for PR yet. But you can consider my gist for the basis. It works fine in my case (I needed to save array Enums and use it for filtering query).

Any enthusiasts can finish the code to make it better :smiley:

frynet avatar Nov 28 '23 12:11 frynet

Thank you so much @bog-walk! When can we expect this to be included in a release?

williamboxhall avatar Feb 13 '24 03:02 williamboxhall

@williamboxhall We aim for monthly releases at the end of every month. When the release is available, it will also be announced on the Slack channel. If you run into any issues with the new column type or if you'd like to share feedback later, please consider opening a ticket on YouTrack.

bog-walk avatar Feb 13 '24 13:02 bog-walk