From 412a98fd06f54e67d6e73ed1e737424bd6dbb292 Mon Sep 17 00:00:00 2001 From: bog-walk <82039410+bog-walk@users.noreply.github.com> Date: Tue, 9 Jan 2024 09:26:28 -0500 Subject: [PATCH] fix: EXPOSED-252 Json contains() throws with iterable as argument (#1963) If a json column is declared as storing a list/set/array of values and one of these iterables is used with the function contains(), a syntax error exception is thrown. The function now properly wraps the serialized json list with quotes when the SQL is generated. --- .../exposed/sql/json/JsonConditions.kt | 7 ++-- .../exposed/sql/json/JsonColumnTests.kt | 36 +++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/exposed-json/src/main/kotlin/org/jetbrains/exposed/sql/json/JsonConditions.kt b/exposed-json/src/main/kotlin/org/jetbrains/exposed/sql/json/JsonConditions.kt index 1a9f8a8ec7..0003b0590f 100644 --- a/exposed-json/src/main/kotlin/org/jetbrains/exposed/sql/json/JsonConditions.kt +++ b/exposed-json/src/main/kotlin/org/jetbrains/exposed/sql/json/JsonConditions.kt @@ -7,6 +7,7 @@ import org.jetbrains.exposed.sql.IColumnType import org.jetbrains.exposed.sql.Op import org.jetbrains.exposed.sql.QueryBuilder import org.jetbrains.exposed.sql.SqlExpressionBuilder.asLiteral +import org.jetbrains.exposed.sql.stringLiteral import org.jetbrains.exposed.sql.vendors.currentDialect // Operator Classes @@ -66,8 +67,10 @@ fun ExpressionWithColumnType<*>.contains(candidate: Expression<*>, path: String? * **Note:** Optional [path] argument is not supported by all vendors; please check the documentation. * @sample org.jetbrains.exposed.sql.json.JsonColumnTests.testJsonContains */ -fun ExpressionWithColumnType<*>.contains(candidate: T, path: String? = null): Contains = - Contains(this, asLiteral(candidate), path, columnType) +fun ExpressionWithColumnType<*>.contains(candidate: T, path: String? = null): Contains = when (candidate) { + is Iterable<*>, is Array<*> -> Contains(this, stringLiteral(asLiteral(candidate).toString()), path, columnType) + else -> Contains(this, asLiteral(candidate), path, columnType) +} /** * Checks whether data exists within [this] JSON expression at the specified [path]. diff --git a/exposed-json/src/test/kotlin/org/jetbrains/exposed/sql/json/JsonColumnTests.kt b/exposed-json/src/test/kotlin/org/jetbrains/exposed/sql/json/JsonColumnTests.kt index 5702734104..b46b0295d6 100644 --- a/exposed-json/src/test/kotlin/org/jetbrains/exposed/sql/json/JsonColumnTests.kt +++ b/exposed-json/src/test/kotlin/org/jetbrains/exposed/sql/json/JsonColumnTests.kt @@ -6,6 +6,8 @@ import kotlinx.serialization.builtins.ArraySerializer import kotlinx.serialization.builtins.ListSerializer import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json +import org.jetbrains.exposed.dao.id.EntityID +import org.jetbrains.exposed.dao.id.IntIdTable import org.jetbrains.exposed.exceptions.UnsupportedByDialectException import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq @@ -14,6 +16,7 @@ import org.jetbrains.exposed.sql.tests.DatabaseTestsBase import org.jetbrains.exposed.sql.tests.TestDB import org.jetbrains.exposed.sql.tests.currentDialectTest import org.jetbrains.exposed.sql.tests.shared.assertEqualCollections +import org.jetbrains.exposed.sql.tests.shared.assertEqualLists import org.jetbrains.exposed.sql.tests.shared.assertEquals import org.jetbrains.exposed.sql.tests.shared.assertTrue import org.jetbrains.exposed.sql.tests.shared.expectException @@ -267,6 +270,39 @@ class JsonColumnTests : DatabaseTestsBase() { } } + @Test + fun testJsonContainsWithIterables() { + val iterables = object : IntIdTable("iterables") { + val userList = json>("user_list", Json.Default) + val userSet = json>("user_set", Json.Default) + val userArray = json>("user_array", Json.Default) + } + + fun selectIdWhere(condition: SqlExpressionBuilder.() -> Op): List> { + val query = iterables.select(iterables.id).where(SqlExpressionBuilder.condition()) + return query.map { it[iterables.id] } + } + + withTables(excludeSettings = jsonContainsNotSupported, iterables) { + val user1 = User("A", "Team A") + val user2 = User("B", "Team B") + val id1 = iterables.insertAndGetId { + it[userList] = listOf(user1, user2) + it[userSet] = setOf(user1) + it[userArray] = arrayOf(user1, user2) + } + val id2 = iterables.insertAndGetId { + it[userList] = listOf(user2) + it[userSet] = setOf(user2) + it[userArray] = arrayOf(user1, user2) + } + + assertEqualLists(listOf(id1), selectIdWhere { iterables.userList.contains(listOf(user1)) }) + assertEqualLists(listOf(id2), selectIdWhere { iterables.userSet.contains(setOf(user2)) }) + assertEqualLists(listOf(id1, id2), selectIdWhere { iterables.userArray.contains(arrayOf(user1, user2)) }) + } + } + @Test fun testJsonWithDefaults() { val defaultUser = User("UNKNOWN", "UNASSIGNED")