From 7ce951bfbc77cc63e97b9353c39cf29ffafa31a9 Mon Sep 17 00:00:00 2001 From: Chantal Loncle <82039410+bog-walk@users.noreply.github.com> Date: Mon, 11 Dec 2023 09:55:16 -0500 Subject: [PATCH 1/4] docs: Add missing KDocs for exposed-core queries API --- .../kotlin/org/jetbrains/exposed/sql/Alias.kt | 37 +++++++- .../org/jetbrains/exposed/sql/Function.kt | 31 ++++++- .../org/jetbrains/exposed/sql/IterableEx.kt | 21 +++++ .../org/jetbrains/exposed/sql/Queries.kt | 92 ++++++++++++++----- .../org/jetbrains/exposed/sql/ResultRow.kt | 8 ++ .../exposed/sql/SQLExpressionBuilder.kt | 26 ++++++ .../sql/tests/shared/dml/InsertTests.kt | 2 +- 7 files changed, 191 insertions(+), 26 deletions(-) diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Alias.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Alias.kt index 94d646dbd7..0be6f2e172 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Alias.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Alias.kt @@ -40,9 +40,11 @@ class Alias(val delegate: T, val alias: String) : Table() { ?: error("Column not found in original table") } +/** Represents a temporary SQL identifier, [alias], for a [delegate] expression. */ class ExpressionAlias(val delegate: Expression, val alias: String) : Expression() { override fun toQueryBuilder(queryBuilder: QueryBuilder) = queryBuilder { append(delegate).append(" $alias") } + /** Returns an expression containing only the string representation of this [alias]. */ fun aliasOnlyExpression(): Expression { return if (delegate is ExpressionWithColumnType) { object : Function(delegate.columnType) { @@ -56,6 +58,7 @@ class ExpressionAlias(val delegate: Expression, val alias: String) : Expre } } +/** Represents a temporary SQL identifier, [alias], for a [query]. */ class QueryAlias(val query: AbstractQuery<*>, val alias: String) : ColumnSet() { override fun describe(s: Transaction, queryBuilder: QueryBuilder) = queryBuilder { @@ -106,19 +109,49 @@ class QueryAlias(val query: AbstractQuery<*>, val alias: String) : ColumnSet() { } fun T.alias(alias: String) = Alias(this, alias) + +/** + * Creates a temporary identifier, [alias], for [this] query. + * + * @sample org.jetbrains.exposed.sql.tests.shared.AliasesTests.testJoinSubQuery01 + */ fun > T.alias(alias: String) = QueryAlias(this, alias) + +/** + * Creates a temporary identifier, [alias], for [this] expression. + * + * @sample org.jetbrains.exposed.sql.tests.shared.AliasesTests.testJoinSubQuery01 + */ fun Expression.alias(alias: String) = ExpressionAlias(this, alias) +/** + * Creates a join relation with a query specified in [joinPart]. + * + * @sample org.jetbrains.exposed.sql.tests.shared.AliasesTests.testJoinSubQuery02 + */ fun Join.joinQuery(on: (SqlExpressionBuilder.(QueryAlias) -> Op), joinType: JoinType = JoinType.INNER, joinPart: () -> AbstractQuery<*>): Join { val qAlias = joinPart().alias("q${joinParts.count { it.joinPart is QueryAlias }}") return join(qAlias, joinType, additionalConstraint = { on(qAlias) }) } +/** Creates a join relation between [this] table and a query specified in [joinPart]. */ fun Table.joinQuery(on: (SqlExpressionBuilder.(QueryAlias) -> Op), joinType: JoinType = JoinType.INNER, joinPart: () -> AbstractQuery<*>) = Join(this).joinQuery(on, joinType, joinPart) -val Join.lastQueryAlias: QueryAlias? get() = joinParts.map { it.joinPart as? QueryAlias }.firstOrNull() - +/** + * Returns the most recent [QueryAlias] instance used to create this join relation, or `null` if a query was not joined. + * + * @sample org.jetbrains.exposed.sql.tests.shared.AliasesTests.testJoinSubQuery02 + */ +val Join.lastQueryAlias: QueryAlias? + get() = joinParts.map { it.joinPart as? QueryAlias }.firstOrNull() + +/** + * Wraps a [query] as an [Expression] so that it can be used as part of an SQL statement or in another query clause. + * + * @sample org.jetbrains.exposed.sql.tests.shared.dml.OrderByTests.testOrderByExpressions + * @sample org.jetbrains.exposed.sql.tests.shared.dml.InsertTests.testInsertWithColumnExpression + */ fun wrapAsExpression(query: AbstractQuery<*>) = object : Expression() { override fun toQueryBuilder(queryBuilder: QueryBuilder) = queryBuilder { append("(") diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Function.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Function.kt index f86e96c371..d80a9bf09a 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Function.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Function.kt @@ -363,26 +363,53 @@ sealed class NextVal( } // Conditional Expressions + +/** + * Represents an SQL function that allows the comparison of [value] to chained conditional clauses. + * + * If a [value] is not provided, each chained conditional will be evaluated independently. + */ @Suppress("FunctionNaming") -class Case(val value: Expression<*>? = null) { +class Case( + /** The value that is compared against every conditional expression. */ + val value: Expression<*>? = null +) { + /** Adds a conditional expression with a [result] if the expression evaluates to `true`. */ fun When(cond: Expression, result: Expression): CaseWhen = CaseWhen(value).When(cond, result) } +/** + * Represents an SQL function that allows the comparison of [value] to chained conditional clauses. + * + * If a [value] is not provided, each chained conditional will be evaluated independently. + */ @Suppress("FunctionNaming") -class CaseWhen(val value: Expression<*>?) { +class CaseWhen( + /** The value that is compared against every conditional expression. */ + val value: Expression<*>? +) { + /** The boolean conditions to check and their resulting expressions if the condition is met. */ val cases: MutableList, Expression>> = mutableListOf() + /** Adds a conditional expression with a [result] if the expression evaluates to `true`. */ @Suppress("UNCHECKED_CAST") fun When(cond: Expression, result: Expression): CaseWhen { cases.add(cond to result) return this as CaseWhen } + /** Adds an expression that will be used as the function result if all [cases] evaluate to `false`. */ fun Else(e: Expression): ExpressionWithColumnType = CaseWhenElse(this, e) } +/** + * Represents an SQL function that steps through conditions, and either returns a value when the first condition is met + * or returns [elseResult] if all conditions are `false`. + */ class CaseWhenElse( + /** The conditions to check and their results if met. */ val caseWhen: CaseWhen, + /** The result if none of the conditions checked are found to be `true`. */ val elseResult: Expression ) : ExpressionWithColumnType(), ComplexExpression { diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/IterableEx.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/IterableEx.kt index 913d8599ee..47b27a8b94 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/IterableEx.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/IterableEx.kt @@ -2,18 +2,34 @@ package org.jetbrains.exposed.sql import org.jetbrains.exposed.sql.vendors.ForUpdateOption +/** Represents iterable elements of a database result that can be manipulated using SQL clauses. */ interface SizedIterable : Iterable { + /** Returns the specified amount of elements, [n], starting after the specified [offset]. */ fun limit(n: Int, offset: Long = 0): SizedIterable + + /** Returns the number of elements that match a specified criterion. */ fun count(): Long + + /** Whether there are no elements. */ fun empty(): Boolean + + /** Adds a locking read for the elements against concurrent updates according to the rules specified by [option]. */ fun forUpdate(option: ForUpdateOption = ForUpdateOption.ForUpdate): SizedIterable = this + + /** Removes any locking read for the elements. */ fun notForUpdate(): SizedIterable = this + + /** Copies the elements. */ fun copy(): SizedIterable + + /** Sorts the elements according to the specified expression [order]. */ fun orderBy(vararg order: Pair, SortOrder>): SizedIterable } +/** Returns an [EmptySizedIterable]. */ fun emptySized(): SizedIterable = EmptySizedIterable() +/** Represents an empty set of elements that cannot be iterated over. */ @Suppress("IteratorNotThrowingNoSuchElementException") class EmptySizedIterable : SizedIterable, Iterator { override fun count(): Long = 0 @@ -35,6 +51,7 @@ class EmptySizedIterable : SizedIterable, Iterator { override fun orderBy(vararg order: Pair, SortOrder>): SizedIterable = this } +/** Represents a collection of elements that defers to the specified [delegate]. */ class SizedCollection(val delegate: Collection) : SizedIterable { constructor(vararg values: T) : this(values.toList()) override fun limit(n: Int, offset: Long): SizedIterable { @@ -52,6 +69,7 @@ class SizedCollection(val delegate: Collection) : SizedIterable { override fun orderBy(vararg order: Pair, SortOrder>): SizedIterable = this } +/** Represents a lazily loaded collection of elements. */ class LazySizedCollection(_delegate: SizedIterable) : SizedIterable { private var delegate: SizedIterable = _delegate @@ -59,6 +77,7 @@ class LazySizedCollection(_delegate: SizedIterable) : SizedIterable private var _size: Long? = null private var _empty: Boolean? = null + /** A list wrapping data loaded lazily loaded. */ val wrapper: List get() { if (_wrapper == null) { _wrapper = delegate.toList() @@ -117,9 +136,11 @@ class LazySizedCollection(_delegate: SizedIterable) : SizedIterable return this } + /** Whether the collection already has data loaded by its delegate. */ fun isLoaded(): Boolean = _wrapper != null } +/** */ infix fun SizedIterable.mapLazy(f: (T) -> R): SizedIterable { val source = this return object : SizedIterable { diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Queries.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Queries.kt index e478181a14..c75b2dc771 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Queries.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Queries.kt @@ -100,30 +100,52 @@ fun Query.selectAllBatched( } /** - * @sample org.jetbrains.exposed.sql.tests.shared.DMLTests.testDelete01 + * Represents the SQL statement that deletes only rows in a table that match the provided [op]. + * + * @param limit Maximum number of rows to delete. + * @param offset The number of rows to skip. + * @param op Condition that determines which rows to delete. + * @sample org.jetbrains.exposed.sql.tests.shared.dml.DeleteTests.testDelete01 */ fun T.deleteWhere(limit: Int? = null, offset: Long? = null, op: T.(ISqlExpressionBuilder) -> Op) = DeleteStatement.where(TransactionManager.current(), this@deleteWhere, op(SqlExpressionBuilder), false, limit, offset) +/** + * Represents the SQL statement that deletes only rows in a table that match the provided [op], while ignoring any + * possible errors that occur during the process. + * + * **Note:** `DELETE IGNORE` is not supported by all vendors. Please check the documentation. + * + * @param limit Maximum number of rows to delete. + * @param offset The number of rows to skip. + * @param op Condition that determines which rows to delete. + */ fun T.deleteIgnoreWhere(limit: Int? = null, offset: Long? = null, op: T.(ISqlExpressionBuilder) -> Op) = DeleteStatement.where(TransactionManager.current(), this@deleteIgnoreWhere, op(SqlExpressionBuilder), true, limit, offset) /** - * @sample org.jetbrains.exposed.sql.tests.shared.DMLTests.testDelete01 + * Represents the SQL statement that deletes all rows in a table. + * + * @sample org.jetbrains.exposed.sql.tests.shared.dml.DeleteTests.testDelete01 */ fun Table.deleteAll() = DeleteStatement.all(TransactionManager.current(), this@deleteAll) /** - * @sample org.jetbrains.exposed.sql.tests.shared.DMLTests.testInsert01 + * Represents the SQL statement that inserts a new row into a table. + * + * @sample org.jetbrains.exposed.sql.tests.h2.H2Tests.insertInH2 */ -fun T.insert(body: T.(InsertStatement) -> Unit): InsertStatement = InsertStatement(this).apply { - body(this) - execute(TransactionManager.current()) -} +fun T.insert(body: T.(InsertStatement) -> Unit): InsertStatement = + InsertStatement(this).apply { + body(this) + execute(TransactionManager.current()) + } /** - * @sample org.jetbrains.exposed.sql.tests.shared.DMLTests.testGeneratedKey03 + * Represents the SQL statement that inserts a new row into a table and returns the generated ID for the new row. + * + * @sample org.jetbrains.exposed.sql.tests.shared.dml.InsertTests.testGeneratedKey04 */ fun , T : IdTable> T.insertAndGetId(body: T.(InsertStatement>) -> Unit) = InsertStatement>(this, false).run { @@ -133,7 +155,15 @@ fun , T : IdTable> T.insertAndGetId(body: T.(InsertSt } /** - * @sample org.jetbrains.exposed.sql.tests.shared.DMLTests.testBatchInsert01 + * Represents the SQL statement that batch inserts new rows into a table. + * + * @param data Collection of values to use in the batch insert. + * @param ignore Whether to ignore errors or not. + * **Note** [ignore] is not supported by all vendors. Please check the documentation. + * @param shouldReturnGeneratedValues Specifies whether newly generated values (for example, auto-incremented IDs) + * should be returned. See [Batch Insert](https://github.com/JetBrains/Exposed/wiki/DSL#batch-insert) for more details. + * @return A list of [ResultRow] representing data from each newly inserted row. + * @sample org.jetbrains.exposed.sql.tests.shared.dml.InsertTests.testBatchInsert01 */ fun T.batchInsert( data: Iterable, @@ -142,6 +172,17 @@ fun T.batchInsert( body: BatchInsertStatement.(E) -> Unit ): List = batchInsert(data.iterator(), ignoreErrors = ignore, shouldReturnGeneratedValues, body) +/** + * Represents the SQL statement that batch inserts new rows into a table. + * + * @param data Sequence of values to use in the batch insert. + * @param ignore Whether to ignore errors or not. + * **Note** [ignore] is not supported by all vendors. Please check the documentation. + * @param shouldReturnGeneratedValues Specifies whether newly generated values (for example, auto-incremented IDs) + * should be returned. See [Batch Insert](https://github.com/JetBrains/Exposed/wiki/DSL#batch-insert) for more details. + * @return A list of [ResultRow] representing data from each newly inserted row. + * @sample org.jetbrains.exposed.sql.tests.shared.dml.InsertTests.testBatchInsertWithSequence + */ fun T.batchInsert( data: Sequence, ignore: Boolean = false, @@ -163,7 +204,7 @@ private fun T.batchInsert( } /** - * Represents the SQL command that either batch inserts new rows into a table, or, if insertions violate unique constraints, + * Represents the SQL statement that either batch inserts new rows into a table, or, if insertions violate unique constraints, * first deletes the existing rows before inserting new rows. * * **Note:** This operation is not supported by all vendors, please check the documentation. @@ -180,7 +221,7 @@ fun T.batchReplace( ): List = batchReplace(data.iterator(), shouldReturnGeneratedValues, body) /** - * Represents the SQL command that either batch inserts new rows into a table, or, if insertions violate unique constraints, + * Represents the SQL statement that either batch inserts new rows into a table, or, if insertions violate unique constraints, * first deletes the existing rows before inserting new rows. * * **Note:** This operation is not supported by all vendors, please check the documentation. @@ -197,7 +238,7 @@ fun T.batchReplace( ): List = batchReplace(data.iterator(), shouldReturnGeneratedValues, body) /** - * Represents the SQL command that either batch inserts new rows into a table, or, if insertions violate unique constraints, + * Represents the SQL statement that either batch inserts new rows into a table, or, if insertions violate unique constraints, * first deletes the existing rows before inserting new rows. * * **Note:** This operation is not supported by all vendors, please check the documentation. @@ -261,10 +302,19 @@ private fun executeBatch( return result } -fun T.insertIgnore(body: T.(UpdateBuilder<*>) -> Unit): InsertStatement = InsertStatement(this, isIgnore = true).apply { - body(this) - execute(TransactionManager.current()) -} +/** + * Represents the SQL statement that inserts a new row into a table, while ignoring any possible errors that occur + * during the process (for example, if the new row would violate a unique constraint, it's insertion would be ignored). + * + * **Note:** `INSERT IGNORE` is not supported by all vendors. Please check the documentation. + * + * @sample + */ +fun T.insertIgnore(body: T.(UpdateBuilder<*>) -> Unit): InsertStatement = + InsertStatement(this, isIgnore = true).apply { + body(this) + execute(TransactionManager.current()) + } fun , T : IdTable> T.insertIgnoreAndGetId(body: T.(UpdateBuilder<*>) -> Unit) = InsertStatement>(this, isIgnore = true).run { @@ -276,7 +326,7 @@ fun , T : IdTable> T.insertIgnoreAndGetId(body: T.(Up } /** - * Represents the SQL command that either inserts a new row into a table, or, if insertion would violate a unique constraint, + * Represents the SQL statement that either inserts a new row into a table, or, if insertion would violate a unique constraint, * first deletes the existing row before inserting a new row. * * **Note:** This operation is not supported by all vendors, please check the documentation. @@ -317,7 +367,7 @@ fun Join.update(where: (SqlExpressionBuilder.() -> Op)? = null, limit: } /** - * Represents the SQL command that either inserts a new row into a table, or updates the existing row if insertion would violate a unique constraint. + * Represents the SQL statement that either inserts a new row into a table, or updates the existing row if insertion would violate a unique constraint. * * **Note:** Vendors that do not support this operation directly implement the standard MERGE USING command. * @@ -338,7 +388,7 @@ fun T.upsert( } /** - * Represents the SQL command that either batch inserts new rows into a table, or updates the existing rows if insertions violate unique constraints. + * Represents the SQL statement that either batch inserts new rows into a table, or updates the existing rows if insertions violate unique constraints. * * **Note**: Unlike `upsert`, `batchUpsert` does not include a `where` parameter. Please log a feature request on * [YouTrack](https://youtrack.jetbrains.com/newIssue?project=EXPOSED&c=Type%20Feature&draftId=25-4449790) if a use-case requires inclusion of a `where` clause. @@ -363,7 +413,7 @@ fun T.batchUpsert( } /** - * Represents the SQL command that either batch inserts new rows into a table, or updates the existing rows if insertions violate unique constraints. + * Represents the SQL statement that either batch inserts new rows into a table, or updates the existing rows if insertions violate unique constraints. * * **Note**: Unlike `upsert`, `batchUpsert` does not include a `where` parameter. Please log a feature request on * [YouTrack](https://youtrack.jetbrains.com/newIssue?project=EXPOSED&c=Type%20Feature&draftId=25-4449790) if a use-case requires inclusion of a `where` clause. @@ -388,7 +438,7 @@ fun T.batchUpsert( } /** - * Represents the SQL command that either batch inserts new rows into a table, or updates the existing rows if insertions violate unique constraints. + * Represents the SQL statement that either batch inserts new rows into a table, or updates the existing rows if insertions violate unique constraints. * * **Note**: Unlike `upsert`, `batchUpsert` does not include a `where` parameter. Please log a feature request on * [YouTrack](https://youtrack.jetbrains.com/newIssue?project=EXPOSED&c=Type%20Feature&draftId=25-4449790) if a use-case requires inclusion of a `where` clause. diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/ResultRow.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/ResultRow.kt index 3bada266c2..5e09d8fde2 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/ResultRow.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/ResultRow.kt @@ -4,7 +4,11 @@ import org.jetbrains.exposed.sql.transactions.TransactionManager import org.jetbrains.exposed.sql.vendors.withDialect import java.sql.ResultSet +/** + * A row of data representing a single record retrieved from a database result set. + */ class ResultRow( + /** Mapping of the expressions stored on this row to their index positions. */ val fieldIndex: Map, Int>, private val data: Array = arrayOfNulls(fieldIndex.size) ) { @@ -37,6 +41,7 @@ class ResultRow( data[index] = value } + /** Whether the given [expression] has been initialized with a value on this row. */ fun hasValue(expression: Expression): Boolean = fieldIndex[expression]?.let { data[it] != NotInitializedValue } ?: false /** @@ -119,6 +124,7 @@ class ResultRow( companion object { + /** Creates a [ResultRow] storing all expressions in [fieldsIndex] with their values retrieved from a [ResultSet]. */ fun create(rs: ResultSet, fieldsIndex: Map, Int>): ResultRow { return ResultRow(fieldsIndex).apply { fieldsIndex.forEach { (field, index) -> @@ -133,6 +139,7 @@ class ResultRow( } } + /** Creates a [ResultRow] using the expressions and values provided by [data]. */ fun createAndFillValues(data: Map, Any?>): ResultRow { val fieldIndex = HashMap, Int>(data.size) val values = arrayOfNulls(data.size) @@ -144,6 +151,7 @@ class ResultRow( return ResultRow(fieldIndex, values) } + /** Creates a [ResultRow] storing [columns] with their default or nullable values. */ fun createAndFillDefaults(columns: List>): ResultRow = ResultRow(columns.withIndex().associate { it.value to it.index }).apply { columns.forEach { diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/SQLExpressionBuilder.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/SQLExpressionBuilder.kt index 2a3a9663a2..31877c7f22 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/SQLExpressionBuilder.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/SQLExpressionBuilder.kt @@ -25,12 +25,28 @@ fun Expression.lowerCase(): LowerCase = LowerCase(this) /** Converts this string expression to upper case. */ fun Expression.upperCase(): UpperCase = UpperCase(this) +/** + * Concatenates all non-null input values of each group from this string expression, separated by [separator]. + * + * When [distinct] is set to `true`, duplicate values will be eliminated. + * [orderBy] can be used to sort values in the concatenated string. + * + * @sample org.jetbrains.exposed.sql.tests.shared.dml.GroupByTests.testGroupConcat + */ fun Expression.groupConcat( separator: String? = null, distinct: Boolean = false, orderBy: Pair, SortOrder> ): GroupConcat = GroupConcat(this, separator, distinct, orderBy) +/** + * Concatenates all non-null input values of each group from this string expression, separated by [separator]. + * + * When [distinct] is set to `true`, duplicate values will be eliminated. + * [orderBy] can be used to sort values in the concatenated string by one or more expressions. + * + * @sample org.jetbrains.exposed.sql.tests.shared.dml.GroupByTests.testGroupConcat + */ fun Expression.groupConcat( separator: String? = null, distinct: Boolean = false, @@ -134,8 +150,11 @@ fun CustomLongFunction( vararg params: Expression<*> ): CustomFunction = CustomFunction(functionName, LongColumnType(), *params) +/** Represents a pattern used for comparisons of string expressions. */ data class LikePattern( + /** The string representation of a pattern to match. */ val pattern: String, + /** The special character to use as the escape character. */ val escapeChar: Char? = null ) { @@ -149,6 +168,7 @@ data class LikePattern( } companion object { + /** Creates a [LikePattern] from the provided [text], with any special characters escaped using [escapeChar]. */ fun ofLiteral(text: String, escapeChar: Char = '\\'): LikePattern { val likePatternSpecialChars = currentDialect.likePatternSpecialChars val nextExpectedPatternQueue = arrayListOf() @@ -182,6 +202,7 @@ data class LikePattern( } } +/** Represents all the operators available when building SQL expressions. */ @Suppress("INAPPLICABLE_JVM_NAME", "TooManyFunctions") interface ISqlExpressionBuilder { @@ -560,6 +581,11 @@ interface ISqlExpressionBuilder { vararg others: A ): Coalesce = Coalesce(expr, alternate, others = others) + /** + * Compares [value] against any chained conditional expressions. + * + * If [value] is `null`, chained conditionals will be evaluated separately until the first is evaluated as `true`. + */ fun case(value: Expression<*>? = null): Case = Case(value) // Subquery Expressions diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/InsertTests.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/InsertTests.kt index 1f94e38d36..ea8269fc9d 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/InsertTests.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/InsertTests.kt @@ -186,7 +186,7 @@ class InsertTests : DatabaseTestsBase() { } @Test - fun `batchInserting using a sequence should work`() { + fun testBatchInsertWithSequence() { val cities = DMLTestsData.Cities withTables(cities) { val names = List(25) { UUID.randomUUID().toString() }.asSequence() From 8d69fcdad0bf6e040ebd0a39fbd92d98241aafe8 Mon Sep 17 00:00:00 2001 From: Chantal Loncle <82039410+bog-walk@users.noreply.github.com> Date: Mon, 11 Dec 2023 13:48:29 -0500 Subject: [PATCH 2/4] docs: Add missing KDocs for exposed-core queries API Add KDocs to `exposed-core` files relating to Queries: batch 2 --- .../jetbrains/exposed/sql/AbstractQuery.kt | 18 +++++- .../kotlin/org/jetbrains/exposed/sql/Op.kt | 1 + .../org/jetbrains/exposed/sql/Queries.kt | 52 ++++++++++++++++-- .../kotlin/org/jetbrains/exposed/sql/Query.kt | 42 ++++++++++++-- .../jetbrains/exposed/sql/SetOperations.kt | 55 +++++++++++++++++-- .../sql/tests/shared/dml/UnionTests.kt | 8 +-- 6 files changed, 159 insertions(+), 17 deletions(-) diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/AbstractQuery.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/AbstractQuery.kt index d8c773d5f6..e6c12c95b1 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/AbstractQuery.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/AbstractQuery.kt @@ -5,19 +5,29 @@ import org.jetbrains.exposed.sql.statements.StatementType import org.jetbrains.exposed.sql.transactions.TransactionManager import java.sql.ResultSet -abstract class AbstractQuery>(targets: List) : SizedIterable, Statement(StatementType.SELECT, targets) { +/** Base class representing an SQL query that returns a [ResultSet] when executed. */ +abstract class AbstractQuery>( + targets: List
+) : SizedIterable, Statement(StatementType.SELECT, targets) { protected val transaction get() = TransactionManager.current() + /** The stored list of columns and their [SortOrder] for an `ORDER BY` clause in this query. */ var orderByExpressions: List, SortOrder>> = mutableListOf() private set + /** The stored value for a `LIMIT` clause in this query. */ var limit: Int? = null protected set + + /** The stored value for a `OFFSET` clause in this query. */ var offset: Long = 0 private set + + /** The number of results that should be fetched when generated by an SQL query in this query. */ var fetchSize: Int? = null private set + /** The set of columns on which a query should be executed, contained by a [ColumnSet]. */ abstract val set: FieldSet protected fun copyTo(other: AbstractQuery) { @@ -29,6 +39,7 @@ abstract class AbstractQuery>(targets: List
) : Sized override fun prepareSQL(transaction: Transaction, prepared: Boolean) = prepareSQL(QueryBuilder(prepared)) + /** Returns the string representation of a query, generated by appending SQL expressions to a [QueryBuilder]. **/ abstract fun prepareSQL(builder: QueryBuilder): String override fun arguments() = QueryBuilder(true).let { @@ -36,19 +47,24 @@ abstract class AbstractQuery>(targets: List
) : Sized if (it.args.isNotEmpty()) listOf(it.args) else emptyList() } + /** Modifies an SQL query to retrieve only distinct results if [value] is set to `true`. */ abstract fun withDistinct(value: Boolean = true): T + /** Modifies an SQL query to return only [n] results, starting after the specified [offset]. **/ override fun limit(n: Int, offset: Long): T = apply { limit = n this.offset = offset } as T + /** Modifies an SQL query to sort results by the specified [column], according to the provided [order]. **/ fun orderBy(column: Expression<*>, order: SortOrder = SortOrder.ASC): T = orderBy(column to order) + /** Modifies an SQL query to sort results according to the provided [order] of expressions. **/ override fun orderBy(vararg order: Pair, SortOrder>): T = apply { (orderByExpressions as MutableList).addAll(order) } as T + /** Modifies the number of results that should be fetched when generated by an SQL query. */ fun fetchSize(n: Int): T = apply { fetchSize = n } as T diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Op.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Op.kt index 642a1170e0..73187c5f30 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Op.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Op.kt @@ -584,6 +584,7 @@ class NotExists( /** Returns an SQL operator that checks if [query] doesn't returns any row. */ fun notExists(query: AbstractQuery<*>) = NotExists(query) +/** Represents an SQL operator that compares [expr] to any row returned from [query]. */ sealed class SubQueryOp( val operator: String, /** Returns the expression compared to each row of the query result. */ diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Queries.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Queries.kt index c75b2dc771..5f41e3abb1 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Queries.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Queries.kt @@ -304,11 +304,12 @@ private fun executeBatch( /** * Represents the SQL statement that inserts a new row into a table, while ignoring any possible errors that occur - * during the process (for example, if the new row would violate a unique constraint, it's insertion would be ignored). + * during the process. * + * For example, if the new row would violate a unique constraint, its insertion would be ignored. * **Note:** `INSERT IGNORE` is not supported by all vendors. Please check the documentation. * - * @sample + * @sample org.jetbrains.exposed.sql.tests.shared.dml.InsertTests.testInsertIgnoreAndGetIdWithPredefinedId */ fun T.insertIgnore(body: T.(UpdateBuilder<*>) -> Unit): InsertStatement = InsertStatement(this, isIgnore = true).apply { @@ -316,6 +317,16 @@ fun T.insertIgnore(body: T.(UpdateBuilder<*>) -> Unit): InsertStatem execute(TransactionManager.current()) } +/** + * Represents the SQL statement that inserts a new row into a table, while ignoring any possible errors that occur + * during the process. + * + * For example, if the new row would violate a unique constraint, its insertion would be ignored. + * **Note:** `INSERT IGNORE` is not supported by all vendors. Please check the documentation. + * + * @return The generated ID for the new row, or `null` if none was retrieved after statement execution. + * @sample org.jetbrains.exposed.sql.tests.shared.dml.InsertTests.testInsertIgnoreAndGetId01 + */ fun , T : IdTable> T.insertIgnoreAndGetId(body: T.(UpdateBuilder<*>) -> Unit) = InsertStatement>(this, isIgnore = true).run { body(this) @@ -339,20 +350,42 @@ fun T.replace(body: T.(UpdateBuilder<*>) -> Unit): ReplaceStatement< } /** - * @sample org.jetbrains.exposed.sql.tests.shared.DMLTests.testInsertSelect01 + * Represents the SQL statement that uses data retrieved from a [selectQuery] to insert new rows into a table. + * + * @param selectQuery Source `SELECT` query that provides the values to insert. + * @param columns Columns to insert the values into. This defaults to all columns in the table that are not + * auto-increment columns without a valid sequence to generate new values. + * @return The number of inserted rows, or `null` if nothing was retrieved after statement execution. + * @sample org.jetbrains.exposed.sql.tests.shared.dml.InsertSelectTests.testInsertSelect04 */ fun T.insert( selectQuery: AbstractQuery<*>, columns: List> = this.columns.filter { !it.columnType.isAutoInc || it.autoIncColumnType?.nextValExpression != null } ) = InsertSelectStatement(columns, selectQuery).execute(TransactionManager.current()) +/** + * Represents the SQL statement that uses data retrieved from a [selectQuery] to insert new rows into a table, + * while ignoring any possible errors that occur during the process. + * + * **Note:** `INSERT IGNORE` is not supported by all vendors. Please check the documentation. + * + * @param selectQuery Source `SELECT` query that provides the values to insert. + * @param columns Columns to insert the values into. This defaults to all columns in the table that are not + * auto-increment columns without a valid sequence to generate new values. + * @return The number of inserted rows, or `null` if nothing was retrieved after statement execution. + */ fun T.insertIgnore( selectQuery: AbstractQuery<*>, columns: List> = this.columns.filter { !it.columnType.isAutoInc || it.autoIncColumnType?.nextValExpression != null } ) = InsertSelectStatement(columns, selectQuery, true).execute(TransactionManager.current()) /** - * @sample org.jetbrains.exposed.sql.tests.shared.DMLTests.testUpdate01 + * Represents the SQL statement that updates rows of a table. + * + * @param where Condition that determines which rows to update. + * @param limit Maximum number of rows to update. + * @return The number of updated rows. + * @sample org.jetbrains.exposed.sql.tests.shared.dml.UpdateTests.testUpdate01 */ fun T.update(where: (SqlExpressionBuilder.() -> Op)? = null, limit: Int? = null, body: T.(UpdateStatement) -> Unit): Int { val query = UpdateStatement(this, limit, where?.let { SqlExpressionBuilder.it() }) @@ -360,6 +393,14 @@ fun T.update(where: (SqlExpressionBuilder.() -> Op)? = null return query.execute(TransactionManager.current())!! } +/** + * Represents the SQL statement that updates rows of a join relation. + * + * @param where Condition that determines which rows to update. + * @param limit Maximum number of rows to update. + * @return The number of updated rows. + * @sample org.jetbrains.exposed.sql.tests.shared.dml.UpdateTests.testUpdateWithJoin01 + */ fun Join.update(where: (SqlExpressionBuilder.() -> Op)? = null, limit: Int? = null, body: (UpdateStatement) -> Unit): Int { val query = UpdateStatement(this, limit, where?.let { SqlExpressionBuilder.it() }) body(query) @@ -376,6 +417,7 @@ fun Join.update(where: (SqlExpressionBuilder.() -> Op)? = null, limit: * @param onUpdate List of pairs of specific columns to update and the expressions to update them with. * If left null, all columns will be updated with the values provided for the insert. * @param where Condition that determines which rows to update, if a unique violation is found. + * @sample org.jetbrains.exposed.sql.tests.shared.dml.UpsertTests.testUpsertWithUniqueIndexConflict */ fun T.upsert( vararg keys: Column<*>, @@ -463,6 +505,8 @@ private fun T.batchUpsert( } /** + * Returns whether [this] table exists in the database. + * * @sample org.jetbrains.exposed.sql.tests.shared.DDLTests.tableExists02 */ fun Table.exists(): Boolean = currentDialect.tableExists(this) diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Query.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Query.kt index 137691a8e6..d83ce3ea05 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Query.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Query.kt @@ -18,13 +18,17 @@ enum class SortOrder(val code: String) { DESC_NULLS_LAST(code = "DESC NULLS LAST") } +/** Class representing an SQL `SELECT` statement on which query clauses can be built. */ open class Query(override var set: FieldSet, where: Op?) : AbstractQuery(set.source.targetTables()) { + /** Whether only distinct results should be retrieved by this `SELECT` query. */ var distinct: Boolean = false protected set + /** The stored list of columns for a `GROUP BY` clause in this `SELECT` query. */ var groupedByColumns: List> = mutableListOf() private set + /** The stored condition for a `HAVING` clause in this `SELECT` query. */ var having: Op? = null private set @@ -43,6 +47,7 @@ open class Query(override var set: FieldSet, where: Op?) : AbstractQuer } } + /** Creates a new [Query] instance using all stored properties of this `SELECT` query. */ override fun copy(): Query = Query(set, where).also { copy -> copyTo(copy) copy.distinct = distinct @@ -94,20 +99,29 @@ open class Query(override var set: FieldSet, where: Op?) : AbstractQuer } /** - * Changes [where] field of a Query. - * @param body new WHERE condition builder, previous value used as a receiver + * Changes the [where] field of this query. + * + * @param body Builder for the new `WHERE` condition, with the previous value used as the receiver. * @sample org.jetbrains.exposed.sql.tests.shared.dml.AdjustQueryTests.testAdjustQueryWhere */ fun adjustWhere(body: Op?.() -> Op): Query = apply { where = where.body() } /** - * Changes [having] field of a Query. - * @param body new HAVING condition builder, previous value used as a receiver + * Changes the [having] field of this query. + * + * @param body Builder for the new `HAVING` condition, with the previous value used as the receiver. * @sample org.jetbrains.exposed.sql.tests.shared.dml.AdjustQueryTests.testAdjustQueryHaving */ fun adjustHaving(body: Op?.() -> Op): Query = apply { having = having.body() } + /** Whether this `SELECT` query already has a stored value option for performing locking reads. */ fun hasCustomForUpdateState() = forUpdate != null + + /** + * Whether this `SELECT` query will perform a locking read. + * + * **Note:** `SELECT FOR UPDATE` is not supported by all vendors. Please check the documentation. + */ fun isForUpdate() = (forUpdate?.let { it != ForUpdateOption.NoForUpdateOption } ?: false) && currentDialect.supportsSelectForUpdate() override fun PreparedStatementApi.executeInternal(transaction: Transaction): ResultSet? { @@ -177,6 +191,11 @@ open class Query(override var set: FieldSet, where: Op?) : AbstractQuer return builder.toString() } + /** + * Appends a `GROUP BY` clause with the specified [columns] to this `SELECT` query. + * + * @sample org.jetbrains.exposed.sql.tests.shared.dml.GroupByTests.testGroupBy02 + */ fun groupBy(vararg columns: Expression<*>): Query { for (column in columns) { (groupedByColumns as MutableList).add(column) @@ -184,6 +203,11 @@ open class Query(override var set: FieldSet, where: Op?) : AbstractQuer return this } + /** + * Appends a `HAVING` clause with the specified [op] condition to this `SELECT` query. + * + * @sample org.jetbrains.exposed.sql.tests.shared.dml.GroupByTests.testGroupBy02 + */ fun having(op: SqlExpressionBuilder.() -> Op): Query { val oop = SqlExpressionBuilder.op() if (having != null) { @@ -269,6 +293,11 @@ open class Query(override var set: FieldSet, where: Op?) : AbstractQuer } } + /** + * Returns the number of results retrieved after query execution. + * + * @sample org.jetbrains.exposed.sql.tests.shared.dml.InsertSelectTests.testInsertSelect02 + */ override fun count(): Long { return if (distinct || groupedByColumns.isNotEmpty() || limit != null) { fun Column<*>.makeAlias() = @@ -302,6 +331,11 @@ open class Query(override var set: FieldSet, where: Op?) : AbstractQuer } } + /** + * Returns whether any results were retrieved by query execution. + * + * @sample org.jetbrains.exposed.sql.tests.shared.dml.SelectTests.testSizedIterable + */ override fun empty(): Boolean { val oldLimit = limit try { diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/SetOperations.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/SetOperations.kt index 0aa914abba..19e944185f 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/SetOperations.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/SetOperations.kt @@ -11,11 +11,17 @@ import org.jetbrains.exposed.sql.vendors.currentDialect import org.jetbrains.exposed.sql.vendors.h2Mode import java.sql.ResultSet +/** + * Represents an SQL operation that combine the results of multiple queries into a single result. + * + * @param secondStatement The SQL statement on the right-hand side of the set operator. + */ sealed class SetOperation( operationName: String, _firstStatement: AbstractQuery<*>, val secondStatement: AbstractQuery<*> ) : AbstractQuery((_firstStatement.targets + secondStatement.targets).distinct()) { + /** The SQL statement on the left-hand side of the set operator. */ val firstStatement: AbstractQuery<*> = when (_firstStatement) { is Query -> { val newSlice = _firstStatement.set.fields.mapIndexed { index, expression -> @@ -47,8 +53,10 @@ sealed class SetOperation( override val queryToExecute: Statement get() = this + /** The SQL keyword representing the set operation. */ open val operationName = operationName + /** Returns the number of results retrieved after query execution. */ override fun count(): Long { try { count = true @@ -63,6 +71,7 @@ sealed class SetOperation( } } + /** Returns whether any results were retrieved by query execution. */ override fun empty(): Boolean { val oldLimit = limit try { @@ -116,7 +125,11 @@ sealed class SetOperation( } } -class Union(firstStatement: AbstractQuery<*>, secondStatement: AbstractQuery<*>) : SetOperation("UNION", firstStatement, secondStatement) { +/** Represents an SQL operation that combines all results from two queries, without any duplicates. */ +class Union( + firstStatement: AbstractQuery<*>, + secondStatement: AbstractQuery<*> +) : SetOperation("UNION", firstStatement, secondStatement) { override fun withDistinct(value: Boolean): SetOperation { return if (!value) { UnionAll(firstStatement, secondStatement).also { @@ -132,7 +145,11 @@ class Union(firstStatement: AbstractQuery<*>, secondStatement: AbstractQuery<*>) } } -class UnionAll(firstStatement: AbstractQuery<*>, secondStatement: AbstractQuery<*>) : SetOperation("UNION ALL", firstStatement, secondStatement) { +/** Represents an SQL operation that combines all results from two queries, with duplicates included. */ +class UnionAll( + firstStatement: AbstractQuery<*>, + secondStatement: AbstractQuery<*> +) : SetOperation("UNION ALL", firstStatement, secondStatement) { override fun withDistinct(value: Boolean): SetOperation { return if (value) { @@ -147,7 +164,11 @@ class UnionAll(firstStatement: AbstractQuery<*>, secondStatement: AbstractQuery< } } -class Intersect(firstStatement: AbstractQuery<*>, secondStatement: AbstractQuery<*>) : SetOperation("INTERSECT", firstStatement, secondStatement) { +/** Represents an SQL operation that returns only the common rows from two query results, without any duplicates. */ +class Intersect( + firstStatement: AbstractQuery<*>, + secondStatement: AbstractQuery<*> +) : SetOperation("INTERSECT", firstStatement, secondStatement) { override fun copy() = Intersect(firstStatement, secondStatement).also { copyTo(it) } @@ -163,7 +184,13 @@ class Intersect(firstStatement: AbstractQuery<*>, secondStatement: AbstractQuery } } -class Except(firstStatement: AbstractQuery<*>, secondStatement: AbstractQuery<*>) : SetOperation("EXCEPT", firstStatement, secondStatement) { +/** + * Represents an SQL operation that returns the distinct results of [firstStatement] that are not common to [secondStatement]. + */ +class Except( + firstStatement: AbstractQuery<*>, + secondStatement: AbstractQuery<*> +) : SetOperation("EXCEPT", firstStatement, secondStatement) { override val operationName: String get() = when { currentDialect is OracleDialect || currentDialect.h2Mode == H2Dialect.H2CompatibilityMode.Oracle -> "MINUS" @@ -185,10 +212,30 @@ class Except(firstStatement: AbstractQuery<*>, secondStatement: AbstractQuery<*> } } +/** + * Combines all results from [this] query with the results of [other], WITHOUT including duplicates. + * + * @sample org.jetbrains.exposed.sql.tests.shared.dml.UnionTests.testUnionWithLimit + */ fun AbstractQuery<*>.union(other: Query): Union = Union(this, other) +/** + * Combines all results from [this] query with the results of [other], WITH duplicates included. + * + * @sample org.jetbrains.exposed.sql.tests.shared.dml.UnionTests.testUnionWithAllResults + */ fun AbstractQuery<*>.unionAll(other: Query): UnionAll = UnionAll(this, other) +/** + * Returns only results from [this] query that are common to the results of [other], WITHOUT including any duplicates. + * + * @sample org.jetbrains.exposed.sql.tests.shared.dml.UnionTests.testIntersectWithThreeQueries + */ fun AbstractQuery<*>.intersect(other: Query): Intersect = Intersect(this, other) +/** + * Returns only distinct results from [this] query that are NOT common to the results of [other]. + * + * @sample org.jetbrains.exposed.sql.tests.shared.dml.UnionTests.testExceptWithTwoQueries + */ fun AbstractQuery<*>.except(other: Query): Except = Except(this, other) diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/UnionTests.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/UnionTests.kt index 06fa20cb68..d1414ed8ea 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/UnionTests.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/UnionTests.kt @@ -17,7 +17,7 @@ import kotlin.test.assertTrue class UnionTests : DatabaseTestsBase() { @Test - fun `test limit`() { + fun testUnionWithLimit() { withCitiesAndUsers(exclude = listOf(TestDB.SQLSERVER)) { _, users, _ -> val andreyQuery = users.selectAll().where { users.id eq "andrey" } val sergeyQuery = users.selectAll().where { users.id.eq("sergey") } @@ -79,7 +79,7 @@ class UnionTests : DatabaseTestsBase() { } @Test - fun `test intersection of three queries`() { + fun testIntersectWithThreeQueries() { withCitiesAndUsers(listOf(TestDB.MYSQL)) { _, users, _ -> val usersQuery = users.selectAll() val sergeyQuery = users.selectAll().where { users.id eq "sergey" } @@ -102,7 +102,7 @@ class UnionTests : DatabaseTestsBase() { } @Test - fun `test except of two queries`() { + fun testExceptWithTwoQueries() { withCitiesAndUsers(listOf(TestDB.MYSQL)) { _, users, _ -> val usersQuery = users.selectAll() val expectedUsers = usersQuery.map { it[users.id] } - "sergey" @@ -221,7 +221,7 @@ class UnionTests : DatabaseTestsBase() { } @Test - fun `test union with all results`() { + fun testUnionWithAllResults() { withCitiesAndUsers { _, users, _ -> val andreyQuery = users.selectAll().where { users.id eq "andrey" } andreyQuery.unionAll(andreyQuery).map { it[users.id] }.apply { From 01d51e0aa2b583f5b53ea4efb614677115b95122 Mon Sep 17 00:00:00 2001 From: Chantal Loncle <82039410+bog-walk@users.noreply.github.com> Date: Tue, 12 Dec 2023 14:42:46 -0500 Subject: [PATCH 3/4] docs: Add missing KDocs for exposed-core queries API --- .../test/kotlin/org/jetbrains/exposed/JodaTimeDefaultsTest.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/exposed-jodatime/src/test/kotlin/org/jetbrains/exposed/JodaTimeDefaultsTest.kt b/exposed-jodatime/src/test/kotlin/org/jetbrains/exposed/JodaTimeDefaultsTest.kt index 89c6fd9d8d..2aede9804c 100644 --- a/exposed-jodatime/src/test/kotlin/org/jetbrains/exposed/JodaTimeDefaultsTest.kt +++ b/exposed-jodatime/src/test/kotlin/org/jetbrains/exposed/JodaTimeDefaultsTest.kt @@ -302,6 +302,10 @@ class JodaTimeDefaultsTest : JodaTimeBaseTest() { val sortedEntries = testDate.selectAll().map { it[testDate.time] }.sorted() + println("EXPOSED TEST DEBUG: ${sortedEntries[1].millis - sortedEntries[0].millis} >= 2000") + println("EXPOSED TEST DEBUG: ${sortedEntries[2].millis - sortedEntries[0].millis} >= 6000") + println("EXPOSED TEST DEBUG: ${sortedEntries[3].millis - sortedEntries[0].millis} >= 8000") + assertTrue(sortedEntries[1].millis - sortedEntries[0].millis >= 2000) assertTrue(sortedEntries[2].millis - sortedEntries[0].millis >= 6000) assertTrue(sortedEntries[3].millis - sortedEntries[0].millis >= 8000) From f093154c9c15a4277f43c4fd0727f48b41724abc Mon Sep 17 00:00:00 2001 From: Chantal Loncle <82039410+bog-walk@users.noreply.github.com> Date: Tue, 12 Dec 2023 17:30:35 -0500 Subject: [PATCH 4/4] docs: Add missing KDocs for exposed-core queries API Minor edits --- .../jetbrains/exposed/sql/AbstractQuery.kt | 19 ++++----- .../kotlin/org/jetbrains/exposed/sql/Alias.kt | 15 +++++-- .../org/jetbrains/exposed/sql/Function.kt | 4 +- .../org/jetbrains/exposed/sql/IterableEx.kt | 39 ++++++++++--------- .../kotlin/org/jetbrains/exposed/sql/Op.kt | 1 + .../org/jetbrains/exposed/sql/Queries.kt | 38 +++++++++--------- .../org/jetbrains/exposed/sql/ResultRow.kt | 5 +-- .../exposed/sql/SQLExpressionBuilder.kt | 8 ++-- .../jetbrains/exposed/sql/SetOperations.kt | 2 +- .../jetbrains/exposed/JodaTimeDefaultsTest.kt | 4 -- 10 files changed, 73 insertions(+), 62 deletions(-) diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/AbstractQuery.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/AbstractQuery.kt index e6c12c95b1..5a423d46d8 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/AbstractQuery.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/AbstractQuery.kt @@ -9,7 +9,8 @@ import java.sql.ResultSet abstract class AbstractQuery>( targets: List
) : SizedIterable, Statement(StatementType.SELECT, targets) { - protected val transaction get() = TransactionManager.current() + protected val transaction + get() = TransactionManager.current() /** The stored list of columns and their [SortOrder] for an `ORDER BY` clause in this query. */ var orderByExpressions: List, SortOrder>> = mutableListOf() @@ -19,11 +20,11 @@ abstract class AbstractQuery>( var limit: Int? = null protected set - /** The stored value for a `OFFSET` clause in this query. */ + /** The stored value for an `OFFSET` clause in this query. */ var offset: Long = 0 private set - /** The number of results that should be fetched when generated by an SQL query in this query. */ + /** The number of results that should be fetched when this query is executed. */ var fetchSize: Int? = null private set @@ -39,7 +40,7 @@ abstract class AbstractQuery>( override fun prepareSQL(transaction: Transaction, prepared: Boolean) = prepareSQL(QueryBuilder(prepared)) - /** Returns the string representation of a query, generated by appending SQL expressions to a [QueryBuilder]. **/ + /** Returns the string representation of an SQL query, generated by appending SQL expressions to a [QueryBuilder]. **/ abstract fun prepareSQL(builder: QueryBuilder): String override fun arguments() = QueryBuilder(true).let { @@ -47,24 +48,24 @@ abstract class AbstractQuery>( if (it.args.isNotEmpty()) listOf(it.args) else emptyList() } - /** Modifies an SQL query to retrieve only distinct results if [value] is set to `true`. */ + /** Modifies this query to retrieve only distinct results if [value] is set to `true`. */ abstract fun withDistinct(value: Boolean = true): T - /** Modifies an SQL query to return only [n] results, starting after the specified [offset]. **/ + /** Modifies this query to return only [n] results, starting after the specified [offset]. **/ override fun limit(n: Int, offset: Long): T = apply { limit = n this.offset = offset } as T - /** Modifies an SQL query to sort results by the specified [column], according to the provided [order]. **/ + /** Modifies this query to sort results by the specified [column], according to the provided [order]. **/ fun orderBy(column: Expression<*>, order: SortOrder = SortOrder.ASC): T = orderBy(column to order) - /** Modifies an SQL query to sort results according to the provided [order] of expressions. **/ + /** Modifies this query to sort results according to the provided [order] of expressions. **/ override fun orderBy(vararg order: Pair, SortOrder>): T = apply { (orderByExpressions as MutableList).addAll(order) } as T - /** Modifies the number of results that should be fetched when generated by an SQL query. */ + /** Modifies the number of results that should be fetched when this query is executed. */ fun fetchSize(n: Int): T = apply { fetchSize = n } as T diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Alias.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Alias.kt index 0be6f2e172..8359cecda6 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Alias.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Alias.kt @@ -44,7 +44,7 @@ class Alias(val delegate: T, val alias: String) : Table() { class ExpressionAlias(val delegate: Expression, val alias: String) : Expression() { override fun toQueryBuilder(queryBuilder: QueryBuilder) = queryBuilder { append(delegate).append(" $alias") } - /** Returns an expression containing only the string representation of this [alias]. */ + /** Returns an [Expression] containing only the string representation of this [alias]. */ fun aliasOnlyExpression(): Expression { return if (delegate is ExpressionWithColumnType) { object : Function(delegate.columnType) { @@ -125,8 +125,11 @@ fun > T.alias(alias: String) = QueryAlias(this, alias) fun Expression.alias(alias: String) = ExpressionAlias(this, alias) /** - * Creates a join relation with a query specified in [joinPart]. + * Creates a join relation with a query. * + * @param on The condition to join that will be placed in the `ON` clause. + * @param joinType The `JOIN` clause type used to combine rows. Defaults to [JoinType.INNER]. + * @param joinPart The query to join with. * @sample org.jetbrains.exposed.sql.tests.shared.AliasesTests.testJoinSubQuery02 */ fun Join.joinQuery(on: (SqlExpressionBuilder.(QueryAlias) -> Op), joinType: JoinType = JoinType.INNER, joinPart: () -> AbstractQuery<*>): Join { @@ -134,7 +137,13 @@ fun Join.joinQuery(on: (SqlExpressionBuilder.(QueryAlias) -> Op), joinT return join(qAlias, joinType, additionalConstraint = { on(qAlias) }) } -/** Creates a join relation between [this] table and a query specified in [joinPart]. */ +/** + * Creates a join relation between [this] table and a query. + * + * @param on The condition to join that will be placed in the `ON` clause. + * @param joinType The `JOIN` clause type used to combine rows. Defaults to [JoinType.INNER]. + * @param joinPart The query to join with. + */ fun Table.joinQuery(on: (SqlExpressionBuilder.(QueryAlias) -> Op), joinType: JoinType = JoinType.INNER, joinPart: () -> AbstractQuery<*>) = Join(this).joinQuery(on, joinType, joinPart) diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Function.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Function.kt index d80a9bf09a..97faac13f0 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Function.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Function.kt @@ -367,7 +367,7 @@ sealed class NextVal( /** * Represents an SQL function that allows the comparison of [value] to chained conditional clauses. * - * If a [value] is not provided, each chained conditional will be evaluated independently. + * If [value] is not provided, each chained conditional will be evaluated independently. */ @Suppress("FunctionNaming") class Case( @@ -381,7 +381,7 @@ class Case( /** * Represents an SQL function that allows the comparison of [value] to chained conditional clauses. * - * If a [value] is not provided, each chained conditional will be evaluated independently. + * If [value] is not provided, each chained conditional will be evaluated independently. */ @Suppress("FunctionNaming") class CaseWhen( diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/IterableEx.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/IterableEx.kt index 47b27a8b94..cbb3c970fa 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/IterableEx.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/IterableEx.kt @@ -2,34 +2,34 @@ package org.jetbrains.exposed.sql import org.jetbrains.exposed.sql.vendors.ForUpdateOption -/** Represents iterable elements of a database result that can be manipulated using SQL clauses. */ +/** Represents the iterable elements of a database result. */ interface SizedIterable : Iterable { - /** Returns the specified amount of elements, [n], starting after the specified [offset]. */ + /** Returns a new [SizedIterable] containing only [n] elements, starting from the specified [offset]. */ fun limit(n: Int, offset: Long = 0): SizedIterable - /** Returns the number of elements that match a specified criterion. */ + /** Returns the number of elements stored. */ fun count(): Long - /** Whether there are no elements. */ + /** Whether there are no elements stored. */ fun empty(): Boolean - /** Adds a locking read for the elements against concurrent updates according to the rules specified by [option]. */ + /** Returns a new [SizedIterable] with a locking read for the elements according to the rules specified by [option]. */ fun forUpdate(option: ForUpdateOption = ForUpdateOption.ForUpdate): SizedIterable = this - /** Removes any locking read for the elements. */ + /** Returns a new [SizedIterable] without any locking read for the elements. */ fun notForUpdate(): SizedIterable = this - /** Copies the elements. */ + /** Returns a new [SizedIterable] that is a copy of the original. */ fun copy(): SizedIterable - /** Sorts the elements according to the specified expression [order]. */ + /** Returns a new [SizedIterable] with the elements sorted according to the specified expression [order]. */ fun orderBy(vararg order: Pair, SortOrder>): SizedIterable } /** Returns an [EmptySizedIterable]. */ fun emptySized(): SizedIterable = EmptySizedIterable() -/** Represents an empty set of elements that cannot be iterated over. */ +/** Represents a [SizedIterable] that is empty and cannot be iterated over. */ @Suppress("IteratorNotThrowingNoSuchElementException") class EmptySizedIterable : SizedIterable, Iterator { override fun count(): Long = 0 @@ -51,7 +51,7 @@ class EmptySizedIterable : SizedIterable, Iterator { override fun orderBy(vararg order: Pair, SortOrder>): SizedIterable = this } -/** Represents a collection of elements that defers to the specified [delegate]. */ +/** Represents a [SizedIterable] that defers to the specified [delegate] collection. */ class SizedCollection(val delegate: Collection) : SizedIterable { constructor(vararg values: T) : this(values.toList()) override fun limit(n: Int, offset: Long): SizedIterable { @@ -69,7 +69,7 @@ class SizedCollection(val delegate: Collection) : SizedIterable { override fun orderBy(vararg order: Pair, SortOrder>): SizedIterable = this } -/** Represents a lazily loaded collection of elements. */ +/** Represents a [SizedIterable] whose elements are only loaded on first access. */ class LazySizedCollection(_delegate: SizedIterable) : SizedIterable { private var delegate: SizedIterable = _delegate @@ -77,13 +77,14 @@ class LazySizedCollection(_delegate: SizedIterable) : SizedIterable private var _size: Long? = null private var _empty: Boolean? = null - /** A list wrapping data loaded lazily loaded. */ - val wrapper: List get() { - if (_wrapper == null) { - _wrapper = delegate.toList() + /** A list wrapping lazily loaded data. */ + val wrapper: List + get() { + if (_wrapper == null) { + _wrapper = delegate.toList() + } + return _wrapper!! } - return _wrapper!! - } override fun limit(n: Int, offset: Long): SizedIterable = LazySizedCollection(delegate.limit(n, offset)) override operator fun iterator() = wrapper.iterator() @@ -140,7 +141,9 @@ class LazySizedCollection(_delegate: SizedIterable) : SizedIterable fun isLoaded(): Boolean = _wrapper != null } -/** */ +/** + * Returns a [SizedIterable] containing the lazily evaluated results of applying the function [f] to each original element. + */ infix fun SizedIterable.mapLazy(f: (T) -> R): SizedIterable { val source = this return object : SizedIterable { diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Op.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Op.kt index 73187c5f30..1b97f65ee7 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Op.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Op.kt @@ -586,6 +586,7 @@ fun notExists(query: AbstractQuery<*>) = NotExists(query) /** Represents an SQL operator that compares [expr] to any row returned from [query]. */ sealed class SubQueryOp( + /** Returns the string representation of the operator to use in the comparison. */ val operator: String, /** Returns the expression compared to each row of the query result. */ val expr: Expression, diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Queries.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Queries.kt index 5f41e3abb1..cf37cc4c98 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Queries.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Queries.kt @@ -105,9 +105,10 @@ fun Query.selectAllBatched( * @param limit Maximum number of rows to delete. * @param offset The number of rows to skip. * @param op Condition that determines which rows to delete. + * @return Count of deleted rows. * @sample org.jetbrains.exposed.sql.tests.shared.dml.DeleteTests.testDelete01 */ -fun T.deleteWhere(limit: Int? = null, offset: Long? = null, op: T.(ISqlExpressionBuilder) -> Op) = +fun T.deleteWhere(limit: Int? = null, offset: Long? = null, op: T.(ISqlExpressionBuilder) -> Op): Int = DeleteStatement.where(TransactionManager.current(), this@deleteWhere, op(SqlExpressionBuilder), false, limit, offset) /** @@ -119,16 +120,18 @@ fun T.deleteWhere(limit: Int? = null, offset: Long? = null, op: T.(I * @param limit Maximum number of rows to delete. * @param offset The number of rows to skip. * @param op Condition that determines which rows to delete. + * @return Count of deleted rows. */ -fun T.deleteIgnoreWhere(limit: Int? = null, offset: Long? = null, op: T.(ISqlExpressionBuilder) -> Op) = +fun T.deleteIgnoreWhere(limit: Int? = null, offset: Long? = null, op: T.(ISqlExpressionBuilder) -> Op): Int = DeleteStatement.where(TransactionManager.current(), this@deleteIgnoreWhere, op(SqlExpressionBuilder), true, limit, offset) /** * Represents the SQL statement that deletes all rows in a table. * + * @return Count of deleted rows. * @sample org.jetbrains.exposed.sql.tests.shared.dml.DeleteTests.testDelete01 */ -fun Table.deleteAll() = +fun Table.deleteAll(): Int = DeleteStatement.all(TransactionManager.current(), this@deleteAll) /** @@ -136,18 +139,18 @@ fun Table.deleteAll() = * * @sample org.jetbrains.exposed.sql.tests.h2.H2Tests.insertInH2 */ -fun T.insert(body: T.(InsertStatement) -> Unit): InsertStatement = - InsertStatement(this).apply { - body(this) - execute(TransactionManager.current()) - } +fun T.insert(body: T.(InsertStatement) -> Unit): InsertStatement = InsertStatement(this).apply { + body(this) + execute(TransactionManager.current()) +} /** - * Represents the SQL statement that inserts a new row into a table and returns the generated ID for the new row. + * Represents the SQL statement that inserts a new row into a table. * + * @return The generated ID for the new row. * @sample org.jetbrains.exposed.sql.tests.shared.dml.InsertTests.testGeneratedKey04 */ -fun , T : IdTable> T.insertAndGetId(body: T.(InsertStatement>) -> Unit) = +fun , T : IdTable> T.insertAndGetId(body: T.(InsertStatement>) -> Unit): EntityID = InsertStatement>(this, false).run { body(this) execute(TransactionManager.current()) @@ -311,11 +314,10 @@ private fun executeBatch( * * @sample org.jetbrains.exposed.sql.tests.shared.dml.InsertTests.testInsertIgnoreAndGetIdWithPredefinedId */ -fun T.insertIgnore(body: T.(UpdateBuilder<*>) -> Unit): InsertStatement = - InsertStatement(this, isIgnore = true).apply { - body(this) - execute(TransactionManager.current()) - } +fun T.insertIgnore(body: T.(UpdateBuilder<*>) -> Unit): InsertStatement = InsertStatement(this, isIgnore = true).apply { + body(this) + execute(TransactionManager.current()) +} /** * Represents the SQL statement that inserts a new row into a table, while ignoring any possible errors that occur @@ -327,7 +329,7 @@ fun T.insertIgnore(body: T.(UpdateBuilder<*>) -> Unit): InsertStatem * @return The generated ID for the new row, or `null` if none was retrieved after statement execution. * @sample org.jetbrains.exposed.sql.tests.shared.dml.InsertTests.testInsertIgnoreAndGetId01 */ -fun , T : IdTable> T.insertIgnoreAndGetId(body: T.(UpdateBuilder<*>) -> Unit) = +fun , T : IdTable> T.insertIgnoreAndGetId(body: T.(UpdateBuilder<*>) -> Unit): EntityID? = InsertStatement>(this, isIgnore = true).run { body(this) when (execute(TransactionManager.current())) { @@ -361,7 +363,7 @@ fun T.replace(body: T.(UpdateBuilder<*>) -> Unit): ReplaceStatement< fun T.insert( selectQuery: AbstractQuery<*>, columns: List> = this.columns.filter { !it.columnType.isAutoInc || it.autoIncColumnType?.nextValExpression != null } -) = InsertSelectStatement(columns, selectQuery).execute(TransactionManager.current()) +): Int? = InsertSelectStatement(columns, selectQuery).execute(TransactionManager.current()) /** * Represents the SQL statement that uses data retrieved from a [selectQuery] to insert new rows into a table, @@ -377,7 +379,7 @@ fun T.insert( fun T.insertIgnore( selectQuery: AbstractQuery<*>, columns: List> = this.columns.filter { !it.columnType.isAutoInc || it.autoIncColumnType?.nextValExpression != null } -) = InsertSelectStatement(columns, selectQuery, true).execute(TransactionManager.current()) +): Int? = InsertSelectStatement(columns, selectQuery, true).execute(TransactionManager.current()) /** * Represents the SQL statement that updates rows of a table. diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/ResultRow.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/ResultRow.kt index 5e09d8fde2..29a4bfb79f 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/ResultRow.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/ResultRow.kt @@ -4,9 +4,7 @@ import org.jetbrains.exposed.sql.transactions.TransactionManager import org.jetbrains.exposed.sql.vendors.withDialect import java.sql.ResultSet -/** - * A row of data representing a single record retrieved from a database result set. - */ +/** A row of data representing a single record retrieved from a database result set. */ class ResultRow( /** Mapping of the expressions stored on this row to their index positions. */ val fieldIndex: Map, Int>, @@ -123,7 +121,6 @@ class ResultRow( internal object NotInitializedValue companion object { - /** Creates a [ResultRow] storing all expressions in [fieldsIndex] with their values retrieved from a [ResultSet]. */ fun create(rs: ResultSet, fieldsIndex: Map, Int>): ResultRow { return ResultRow(fieldsIndex).apply { diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/SQLExpressionBuilder.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/SQLExpressionBuilder.kt index 31877c7f22..951b750bf4 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/SQLExpressionBuilder.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/SQLExpressionBuilder.kt @@ -26,7 +26,7 @@ fun Expression.lowerCase(): LowerCase = LowerCase(this) fun Expression.upperCase(): UpperCase = UpperCase(this) /** - * Concatenates all non-null input values of each group from this string expression, separated by [separator]. + * Concatenates all non-null input values of each group from [this] string expression, separated by [separator]. * * When [distinct] is set to `true`, duplicate values will be eliminated. * [orderBy] can be used to sort values in the concatenated string. @@ -40,7 +40,7 @@ fun Expression.groupConcat( ): GroupConcat = GroupConcat(this, separator, distinct, orderBy) /** - * Concatenates all non-null input values of each group from this string expression, separated by [separator]. + * Concatenates all non-null input values of each group from [this] string expression, separated by [separator]. * * When [distinct] is set to `true`, duplicate values will be eliminated. * [orderBy] can be used to sort values in the concatenated string by one or more expressions. @@ -150,7 +150,7 @@ fun CustomLongFunction( vararg params: Expression<*> ): CustomFunction = CustomFunction(functionName, LongColumnType(), *params) -/** Represents a pattern used for comparisons of string expressions. */ +/** Represents a pattern used for the comparison of string expressions. */ data class LikePattern( /** The string representation of a pattern to match. */ val pattern: String, @@ -585,6 +585,8 @@ interface ISqlExpressionBuilder { * Compares [value] against any chained conditional expressions. * * If [value] is `null`, chained conditionals will be evaluated separately until the first is evaluated as `true`. + * + * @sample org.jetbrains.exposed.sql.tests.shared.dml.ConditionsTests.nullOpInCaseTest */ fun case(value: Expression<*>? = null): Case = Case(value) diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/SetOperations.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/SetOperations.kt index 19e944185f..ed8b436434 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/SetOperations.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/SetOperations.kt @@ -12,7 +12,7 @@ import org.jetbrains.exposed.sql.vendors.h2Mode import java.sql.ResultSet /** - * Represents an SQL operation that combine the results of multiple queries into a single result. + * Represents an SQL operation that combines the results of multiple queries into a single result. * * @param secondStatement The SQL statement on the right-hand side of the set operator. */ diff --git a/exposed-jodatime/src/test/kotlin/org/jetbrains/exposed/JodaTimeDefaultsTest.kt b/exposed-jodatime/src/test/kotlin/org/jetbrains/exposed/JodaTimeDefaultsTest.kt index 2aede9804c..89c6fd9d8d 100644 --- a/exposed-jodatime/src/test/kotlin/org/jetbrains/exposed/JodaTimeDefaultsTest.kt +++ b/exposed-jodatime/src/test/kotlin/org/jetbrains/exposed/JodaTimeDefaultsTest.kt @@ -302,10 +302,6 @@ class JodaTimeDefaultsTest : JodaTimeBaseTest() { val sortedEntries = testDate.selectAll().map { it[testDate.time] }.sorted() - println("EXPOSED TEST DEBUG: ${sortedEntries[1].millis - sortedEntries[0].millis} >= 2000") - println("EXPOSED TEST DEBUG: ${sortedEntries[2].millis - sortedEntries[0].millis} >= 6000") - println("EXPOSED TEST DEBUG: ${sortedEntries[3].millis - sortedEntries[0].millis} >= 8000") - assertTrue(sortedEntries[1].millis - sortedEntries[0].millis >= 2000) assertTrue(sortedEntries[2].millis - sortedEntries[0].millis >= 6000) assertTrue(sortedEntries[3].millis - sortedEntries[0].millis >= 8000)