Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add ALL and ANY operators accepting array, subquery, or table parameters #1886

Merged
merged 12 commits into from
Jan 3, 2024
Merged
30 changes: 30 additions & 0 deletions exposed-core/api/exposed-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -1467,6 +1467,7 @@ 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 arrayParam ([Ljava/lang/Object;)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 @@ -1719,6 +1720,10 @@ public final class org/jetbrains/exposed/sql/RowNumber : org/jetbrains/exposed/s
public final class org/jetbrains/exposed/sql/SQLExpressionBuilderKt {
public static final fun CustomLongFunction (Ljava/lang/String;[Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/CustomFunction;
public static final fun CustomStringFunction (Ljava/lang/String;[Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/CustomFunction;
public static final fun allFunction ([Ljava/lang/Object;)Lorg/jetbrains/exposed/sql/functions/AllFunction;
public static final fun allOp ([Ljava/lang/Object;)Lorg/jetbrains/exposed/sql/ops/AllAnyOp;
public static final fun anyFunction ([Ljava/lang/Object;)Lorg/jetbrains/exposed/sql/functions/AnyFunction;
public static final fun anyOp ([Ljava/lang/Object;)Lorg/jetbrains/exposed/sql/ops/AllAnyOp;
public static final fun avg (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;I)Lorg/jetbrains/exposed/sql/Avg;
public static synthetic fun avg$default (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;IILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Avg;
public static final fun castTo (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/IColumnType;)Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;
Expand Down Expand Up @@ -2373,6 +2378,11 @@ public final class org/jetbrains/exposed/sql/UnionAll : org/jetbrains/exposed/sq
public fun withDistinct (Z)Lorg/jetbrains/exposed/sql/SetOperation;
}

public final class org/jetbrains/exposed/sql/UntypedAndUnsizedArrayColumnType : org/jetbrains/exposed/sql/ColumnType {
public static final field INSTANCE Lorg/jetbrains/exposed/sql/UntypedAndUnsizedArrayColumnType;
public fun sqlType ()Ljava/lang/String;
}

public final class org/jetbrains/exposed/sql/UpperCase : org/jetbrains/exposed/sql/Function {
public fun <init> (Lorg/jetbrains/exposed/sql/Expression;)V
public final fun getExpr ()Lorg/jetbrains/exposed/sql/Expression;
Expand Down Expand Up @@ -2479,6 +2489,18 @@ public final class org/jetbrains/exposed/sql/XorBitOp : org/jetbrains/exposed/sq
public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V
}

public abstract class org/jetbrains/exposed/sql/functions/AllAnyFunction : org/jetbrains/exposed/sql/CustomFunction {
public fun <init> (Ljava/lang/String;[Ljava/lang/Object;)V
}

public final class org/jetbrains/exposed/sql/functions/AllFunction : org/jetbrains/exposed/sql/functions/AllAnyFunction {
public fun <init> ([Ljava/lang/Object;)V
}

public final class org/jetbrains/exposed/sql/functions/AnyFunction : org/jetbrains/exposed/sql/functions/AllAnyFunction {
public fun <init> ([Ljava/lang/Object;)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 @@ -2552,6 +2574,13 @@ public final class org/jetbrains/exposed/sql/functions/math/TanFunction : org/je
public fun <init> (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;)V
}

public final class org/jetbrains/exposed/sql/ops/AllAnyOp : org/jetbrains/exposed/sql/Op {
public fun <init> (Ljava/lang/String;[Ljava/lang/Object;)V
public final fun getArray ()[Ljava/lang/Object;
public final fun getOpName ()Ljava/lang/String;
public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V
}

public abstract class org/jetbrains/exposed/sql/ops/InListOrNotInListBaseOp : org/jetbrains/exposed/sql/Op, org/jetbrains/exposed/sql/ComplexExpression {
public fun <init> (Ljava/lang/Object;Ljava/lang/Iterable;Z)V
public synthetic fun <init> (Ljava/lang/Object;Ljava/lang/Iterable;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V
Expand Down Expand Up @@ -3098,6 +3127,7 @@ public abstract class org/jetbrains/exposed/sql/vendors/DataTypeProvider {
public fun ubyteType ()Ljava/lang/String;
public fun uintegerType ()Ljava/lang/String;
public fun ulongType ()Ljava/lang/String;
public abstract fun untypedAndUnsizedArrayType ()Ljava/lang/String;
public fun ushortType ()Ljava/lang/String;
public fun uuidToDB (Ljava/util/UUID;)Ljava/lang/Object;
public fun uuidType ()Ljava/lang/String;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -985,6 +985,11 @@ class CustomEnumerationColumnType<T : Enum<T>>(
override fun nonNullValueToString(value: Any): String = super.nonNullValueToString(notNullValueToDB(value))
}

object UntypedAndUnsizedArrayColumnType : ColumnType() {
override fun sqlType(): String =
currentDialect.dataTypeProvider.untypedAndUnsizedArrayType()
}
Comment on lines +988 to +991
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I appreciate the need for this and for the scope of this PR it's ok. It does have some problems, for example:

  • If a logger is enabled, the array object value is not processed in the output string, which isn't helpful to users wanting to see the values.
  • It won't work with edge cases like if a user wants to check if a datetime column value is in an array of LocalDateTimes.

But we need to flesh out the ArrayColumnType more thoroughly anyway, so I can work on these problems after this PR is merged.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't remove this because the implemented Ops still depend on it. As you commented this will be fleshed out, so to not keep these new definitions in the PR, one way I can think of is to replace its usage with a temporary inlined anonymous ColumnType with its sqlType being the empty string or just "ARRAY" and the tests still pass.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, that sounds like a good temporary option to me.

Copy link
Contributor Author

@ShreckYe ShreckYe Dec 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, so shall I remove it and adopt this approach in the next commit?


// Date/Time columns

/**
Expand Down
3 changes: 3 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 @@ -730,6 +730,9 @@ 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. */
fun <T> arrayParam(value: Array<T>): Expression<Array<T>> = QueryParameter(value, UntypedAndUnsizedArrayColumnType)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove this as it isn't necessary for the Op implementation. An arrayParam() will be introduced to the API when the ArrayColumnType is fully supported for storing arrays.


// Misc.

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ 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.ops.InListOrNotInListBaseOp
import org.jetbrains.exposed.sql.ops.PairInListOp
import org.jetbrains.exposed.sql.ops.SingleValueInListOp
import org.jetbrains.exposed.sql.ops.TripleInListOp
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.functions.AllFunction
import org.jetbrains.exposed.sql.functions.AnyFunction
import org.jetbrains.exposed.sql.ops.*
import org.jetbrains.exposed.sql.vendors.FunctionProvider
import org.jetbrains.exposed.sql.vendors.currentDialect
import java.math.BigDecimal
Expand Down Expand Up @@ -96,6 +96,27 @@ fun <T : Any?> ExpressionWithColumnType<T>.varPop(scale: Int = 2): VarPop<T> = V
*/
fun <T : Any?> ExpressionWithColumnType<T>.varSamp(scale: Int = 2): VarSamp<T> = VarSamp(this, scale)

// using `Op`

/** Returns this array of data wrapped in the `ALL` operator. */
fun <T> Array<T>.allOp(): Op<T> = AllAnyOp("ALL", this)

/** Returns this array of data wrapped in the `ANY` operator. The name is explicitly distinguished from [Array.any]. */
fun <T> Array<T>.anyOp(): Op<T> = AllAnyOp("ANY", this)
Copy link
Member

@bog-walk bog-walk Dec 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having these be extension functions makes it difficult to extend the functionality to, for example, a subquery. It also doesn't read like a natural SQL query: WHERE { Users.id eq arrayOf(1, 2).anyOp() }.

Please replace them with anyFrom which takes an array argument, that way we can make an override for subqueries and both will read well:

fun <T> anyFrom(array: Array<T>): AnyFromArrayOp<T> = AnyFromArrayOp(array)
fun <T> allFrom(array: Array<T>): AllFromArrayOp<T> = AllFromArrayOp(array)

// use
WHERE { Users.id eq anyFrom(arrayOf(1, 2)) }
WHERE { Users.id eq anyFrom(Users.selectAll()) }

Also, please place the functions in the subsection titled "// Array Comparisons".


// using `CustomFunction`

/** Returns this array of data wrapped in the `ALL` operator. */
fun <T> Array<T>.allFunction(): CustomFunction<T> = AllFunction(this)

/** Returns this array of data wrapped in the `ANY` operator. The name is explicitly distinguished from [Array.any]. */
fun <T> Array<T>.anyFunction(): CustomFunction<T> = AnyFunction(this)

/** Checks if this expression is equal to any element from [array].
* This is a more efficient alternative to [ISqlExpressionBuilder.inList] on PostgreSQL and H2. */
infix fun <T> ExpressionWithColumnType<T>.eqAny(array: Array<T>): Op<Boolean> =
this eq array.anyOp() // TODO or `array.anyFunction()`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This won't be necessary once the functions above are switched to take an argument instead of the more verbose extension function.


// Sequence Manipulation Functions

/** Advances this sequence and returns the new value. */
Expand Down Expand Up @@ -578,7 +599,7 @@ interface ISqlExpressionBuilder {

// Array Comparisons

/** Checks if this expression is equals to any element from [list]. */
/** Checks if this expression is equal to any element from [list]. */
infix fun <T> ExpressionWithColumnType<T>.inList(list: Iterable<T>): InListOrNotInListBaseOp<T> = SingleValueInListOp(this, list, isInList = true)

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package org.jetbrains.exposed.sql.functions

import org.jetbrains.exposed.sql.UntypedAndUnsizedArrayColumnType
import org.jetbrains.exposed.sql.CustomFunction
import org.jetbrains.exposed.sql.arrayParam

abstract class AllAnyFunction<T>(functionName: String, array: Array<T>) : CustomFunction<T>(functionName, UntypedAndUnsizedArrayColumnType, arrayParam(array))
class AllFunction<T>(array: Array<T>) : AllAnyFunction<T>("ALL", array)
class AnyFunction<T>(array: Array<T>) : AllAnyFunction<T>("ANY", array)
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.jetbrains.exposed.sql.ops

import org.jetbrains.exposed.sql.Op
import org.jetbrains.exposed.sql.QueryBuilder
import org.jetbrains.exposed.sql.UntypedAndUnsizedArrayColumnType

class AllAnyOp<T>(val opName: String, val array: Array<T>) : Op<T>() {

override fun toQueryBuilder(queryBuilder: QueryBuilder) = queryBuilder {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My suggestion would be to replace this with a sealed class, something like this:

sealed class AnyOrAllFromBaseOp<T>(
    val isAny: Boolean,
    val subSearch: Any
) : Op<T>() {
    override fun toQueryBuilder(queryBuilder: QueryBuilder): Unit = queryBuilder {
        if (isAny) {
            +"ANY ("
        } else {
            +"ALL ("
        }
        when (subSearch) {
            is Array<*> -> registerArgument(UntypedAndUnsizedArrayColumnType, subSearch)
        }
        +")"
    }
}

class AnyFromArrayOp<T>(array: Array<T>) : AnyOrAllFromBaseOp<T>(isAny = true, subSearch = array)

class AllFromArrayOp<T>(array: Array<T>) : AnyOrAllFromBaseOp<T>(isAny = false, subSearch = array)

With that, we'd be able to easily go in an extend support to subqueries, etc. by just adding a branch to the when block and some more subclasses.

Copy link
Contributor Author

@ShreckYe ShreckYe Dec 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I refactored opName into isAny as you suggested but there might still be other row and array comparison operators, for example SOME though it's only an alias of ANY. (see PostgreSQL docs and MySQL docs) If it's necessary to leave room for them (though I haven't find many cases supporting this), this class can be renamed to RowAndArrayComparisonOp as how PostgreSQL calls them and the old opName approach can be adopted.

Also I added a SubSearch type parameter instead of using Any for better type safety and implemented registerArgument in subclasses for better extensibility from my point of view. If this is unnecessary I can change them to what you suggested.

+opName
+'('
registerArgument(UntypedAndUnsizedArrayColumnType, array)
+')'
}
}

inline fun <reified T> AllAnyOp(opName: String, list: List<T>) =
AllAnyOp(opName, list.toTypedArray())
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,10 @@ abstract class DataTypeProvider {
open fun jsonBType(): String =
throw UnsupportedByDialectException("This vendor does not support binary JSON data type", currentDialect)

/** Data type for arrays with no specified size or element type, used only as types of [QueryParameter]s for PostgreSQL and H2.
* An array with no element type cannot be used for storing arrays in a column in either PostgreSQL or H2. */
abstract fun untypedAndUnsizedArrayType(): String

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's go by the majority here, since only 2 dialects are implemented. Please swap this with the following and remove all exceptions from unsupported dialect files:

open fun untypedAndUnsizedArrayType(): String =
        throw UnsupportedByDialectException("This vendor does not support array data type", currentDialect)

// Misc.

/** Returns the SQL representation of the specified expression, for it to be used as a column default value. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ internal object H2DataTypeProvider : DataTypeProvider() {
override fun timestampWithTimeZoneType(): String = "TIMESTAMP(9) WITH TIME ZONE"

override fun jsonBType(): String = "JSON"
override fun untypedAndUnsizedArrayType(): String = "ARRAY[]"

override fun hexToDb(hexString: String): String = "X'$hexString'"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ internal object MysqlDataTypeProvider : DataTypeProvider() {
}

override fun jsonBType(): String = "JSON"
override fun untypedAndUnsizedArrayType(): String =
throw UnsupportedByDialectException("This vendor does not support array data type", currentDialect)

override fun processForDefaultValue(e: Expression<*>): String = when {
e is LiteralOp<*> && e.columnType is JsonColumnMarker -> when {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.jetbrains.exposed.sql.vendors

import org.jetbrains.exposed.exceptions.UnsupportedByDialectException
import org.jetbrains.exposed.exceptions.throwUnsupportedException
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.TransactionManager
Expand Down Expand Up @@ -63,6 +64,9 @@ internal object OracleDataTypeProvider : DataTypeProvider() {

override fun jsonType(): String = "VARCHAR2(4000)"

override fun untypedAndUnsizedArrayType(): String =
throw UnsupportedByDialectException("This vendor does not support array data type", currentDialect)

override fun processForDefaultValue(e: Expression<*>): String = when {
e is LiteralOp<*> && (e.columnType as? IDateColumnType)?.hasTimePart == false -> "DATE ${super.processForDefaultValue(e)}"
e is LiteralOp<*> && e.columnType is IDateColumnType -> "TIMESTAMP ${super.processForDefaultValue(e)}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ internal object PostgreSQLDataTypeProvider : DataTypeProvider() {
override fun dateTimeType(): String = "TIMESTAMP"
override fun jsonBType(): String = "JSONB"

override fun untypedAndUnsizedArrayType(): String = "ARRAY"

override fun processForDefaultValue(e: Expression<*>): String = when {
e is LiteralOp<*> && e.columnType is JsonColumnMarker && (currentDialect as? H2Dialect) == null -> {
val cast = if (e.columnType.usesBinaryFormat) "::jsonb" else "::json"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.jetbrains.exposed.sql.vendors

import org.jetbrains.exposed.exceptions.UnsupportedByDialectException
import org.jetbrains.exposed.exceptions.throwUnsupportedException
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.TransactionManager
Expand Down Expand Up @@ -41,6 +42,8 @@ internal object SQLServerDataTypeProvider : DataTypeProvider() {
override fun mediumTextType(): String = textType()
override fun largeTextType(): String = textType()
override fun jsonType(): String = "NVARCHAR(MAX)"
override fun untypedAndUnsizedArrayType(): String =
throw UnsupportedByDialectException("This vendor does not support array data type", currentDialect)

override fun precessOrderByClause(queryBuilder: QueryBuilder, expression: Expression<*>, sortOrder: SortOrder) {
when (sortOrder) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.jetbrains.exposed.sql.vendors

import org.jetbrains.exposed.exceptions.UnsupportedByDialectException
import org.jetbrains.exposed.exceptions.throwUnsupportedException
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.TransactionManager
Expand All @@ -19,6 +20,8 @@ internal object SQLiteDataTypeProvider : DataTypeProvider() {
override fun dateType(): String = "TEXT"
override fun booleanToStatementString(bool: Boolean) = if (bool) "1" else "0"
override fun jsonType(): String = "TEXT"
override fun untypedAndUnsizedArrayType(): String =
throw UnsupportedByDialectException("This vendor does not support array data type", currentDialect)
override fun hexToDb(hexString: String): String = "X'$hexString'"
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import org.jetbrains.exposed.crypt.encryptedBinary
import org.jetbrains.exposed.crypt.encryptedVarchar
import org.jetbrains.exposed.dao.id.IntIdTable
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.tests.DatabaseTestsBase
import org.jetbrains.exposed.sql.tests.TestDB
import org.jetbrains.exposed.sql.tests.shared.assertEquals
import org.jetbrains.exposed.sql.tests.shared.entities.EntityTests
import org.junit.Test
import java.math.BigDecimal
import kotlin.test.assertNull

class SelectTests : DatabaseTestsBase() {
Expand Down Expand Up @@ -211,6 +213,55 @@ class SelectTests : DatabaseTestsBase() {
}
}

val testDBsSupportingArrays =
listOf(TestDB.POSTGRESQL, TestDB.POSTGRESQLNG, TestDB.H2, TestDB.H2_MYSQL, TestDB.H2_MARIADB, TestDB.H2_PSQL, TestDB.H2_ORACLE, TestDB.H2_SQLSERVER)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TestDB.allH2TestDB is available to replace listing all H2 modes.


// adapted from `testInList01`
// TODO all the other tests of `inList` can be adapted to test both `inList` and `eqAny` if necessary
fun testEqAny(eqOp: Column<String>.() -> Op<Boolean>) {
withDb(testDBsSupportingArrays) {
withCitiesAndUsers { _, users, _ ->
val r = users.select { users.id.eqOp() }.orderBy(users.name).toList()

assertEquals(2, r.size)
assertEquals("Alex", r[0][users.name])
assertEquals("Andrey", r[1][users.name])
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I appreciate that this helps reduce redundancy, especially since you implemented the feature using both Op and Function. After refactoring, please use this (or whatever code you're testing) directly in each unit test instead. That way it will be more clear what the DSL we're testing actually looks like when used.

The tests that would be great to see are:

  1. eq anyFrom
  2. One other comparison op: the example below is good -> greaterEq anyFrom
  3. neq anyFrom
  4. eq anyFrom(emptyArray())

}
}

fun testEqAny(anyExpression: Expression<String>) =
testEqAny { this eq anyExpression }

val userIds = arrayOf("andrey", "alex")

@Test
fun testEqAnyOp() = testEqAny(userIds.anyOp())

@Test
fun testEqAnyFunction() = testEqAny(userIds.anyFunction())

@Test
fun testEqAny() = testEqAny { this eqAny userIds }

val amounts = arrayOf(1, 10, 100, 1000).map { it.toBigDecimal() }.toTypedArray()

fun testGreaterEqAll(allExpression: Expression<BigDecimal>) {
withDb(testDBsSupportingArrays) {
withSales { _, sales ->
val r = sales.select { sales.amount greaterEq allExpression }.toList()
assertEquals(3, r.size)
r.forEach { assertEquals("coffee", it[sales.product]) }
}
}
}

@Test
fun testGreaterEqAllOp() = testGreaterEqAll(amounts.allOp())

@Test
fun testGreaterEqAllFunction() = testGreaterEqAll(amounts.allFunction())

@Test
fun testSelectDistinct() {
val tbl = DMLTestsData.Cities
Expand Down