From a595b160dbc402200d906c67509404620e2206d4 Mon Sep 17 00:00:00 2001 From: Leandro Colmenarez <89696212+LeandroLCD@users.noreply.github.com> Date: Mon, 17 Nov 2025 20:37:42 -0300 Subject: [PATCH 1/7] feat(Query): Automatically use the first logical operation as the WHERE clause This commit introduces a convenience feature where the first logical operation (`AND` or `OR`) is automatically used as the `WHERE` clause if one is not explicitly set in the query builder. This simplifies query construction by removing the requirement to call `where()` first. The changes include: - Modifying the `build()` method in `QuerySelect.Builder` and `QueryDelete.Builder` to automatically identify and set the `WHERE` clause from the existing logical operations. - Updating `getSqlOperators()` in `QuerySelect`, `QueryDelete`, and `QueryUpdate` to correctly include the `where` operator in the returned list, improving query introspection. --- .../java/com/blipblipcode/query/QueryDelete.kt | 15 ++++++++++++--- .../java/com/blipblipcode/query/QuerySelect.kt | 14 +++++++++++--- .../java/com/blipblipcode/query/QueryUpdate.kt | 2 +- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/query/src/main/java/com/blipblipcode/query/QueryDelete.kt b/query/src/main/java/com/blipblipcode/query/QueryDelete.kt index 75cb19c..9de5d0d 100644 --- a/query/src/main/java/com/blipblipcode/query/QueryDelete.kt +++ b/query/src/main/java/com/blipblipcode/query/QueryDelete.kt @@ -64,8 +64,9 @@ class QueryDelete private constructor( } override fun getSqlOperators(): List> { - return operations.values.map { - it.operator + return buildList { + add(where) + operations.values.forEach { add(it.operator) } } } @@ -203,7 +204,15 @@ class QueryDelete private constructor( * @throws IllegalArgumentException if the WHERE clause is not set. */ fun build(): QueryDelete { - require(where != null) { "A WHERE clause must be specified." } + if(where == null){ + val w = operations.firstNotNullOfOrNull{it}.let { + it ?: throw IllegalArgumentException("A WHERE clause must be specified.") + } + operations.remove(w.key) + + where = w.value.operator + } + return QueryDelete( where = where!!, table = table, operations = operations ) diff --git a/query/src/main/java/com/blipblipcode/query/QuerySelect.kt b/query/src/main/java/com/blipblipcode/query/QuerySelect.kt index 29423d6..3ef0e0d 100644 --- a/query/src/main/java/com/blipblipcode/query/QuerySelect.kt +++ b/query/src/main/java/com/blipblipcode/query/QuerySelect.kt @@ -88,8 +88,9 @@ class QuerySelect private constructor( } override fun getSqlOperators(): List> { - return operations.values.map { - it.operator + return buildList { + add(where) + operations.values.forEach { add(it.operator) } } } @@ -319,7 +320,14 @@ class QuerySelect private constructor( * @throws IllegalArgumentException if the WHERE clause is not set. */ fun build(): QuerySelect { - require(where != null) { "A WHERE clause must be specified." } + if(where == null){ + val w = operations.firstNotNullOfOrNull{it}.let { + it ?: throw IllegalArgumentException("WHERE clause is required for QuerySelect") + } + operations.remove(w.key) + + where = w.value.operator + } return QuerySelect( where = where!!, table = table, diff --git a/query/src/main/java/com/blipblipcode/query/QueryUpdate.kt b/query/src/main/java/com/blipblipcode/query/QueryUpdate.kt index a5b85d2..c43398d 100644 --- a/query/src/main/java/com/blipblipcode/query/QueryUpdate.kt +++ b/query/src/main/java/com/blipblipcode/query/QueryUpdate.kt @@ -75,7 +75,7 @@ class QueryUpdate private constructor( } override fun getSqlOperators(): List> { - return emptyList() + return listOf(where) } override fun getTableName(): String { From 20e71ace6078aba2bd7589f61b069f8b2e479aeb Mon Sep 17 00:00:00 2001 From: Leandro Colmenarez <89696212+LeandroLCD@users.noreply.github.com> Date: Mon, 17 Nov 2025 20:46:22 -0300 Subject: [PATCH 2/7] test(QueryDelete): Fix test for building without a WHERE clause This commit corrects a test case for `QueryDelete`. The test `build without where clause throws exception` was incorrectly including a WHERE condition, which prevented it from accurately verifying that building a `QueryDelete` without any conditions throws an `IllegalArgumentException`. The extraneous condition has been removed. --- query/src/test/java/com/blipblipcode/query/QueryDeleteTest.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/query/src/test/java/com/blipblipcode/query/QueryDeleteTest.kt b/query/src/test/java/com/blipblipcode/query/QueryDeleteTest.kt index d54a8d0..fb0d7bf 100644 --- a/query/src/test/java/com/blipblipcode/query/QueryDeleteTest.kt +++ b/query/src/test/java/com/blipblipcode/query/QueryDeleteTest.kt @@ -43,7 +43,6 @@ class QueryDeleteTest { @Test fun `build without where clause throws exception`() { val builder = QueryDelete.builder("users") - .and("status", SQLOperator.Equals("status", "active")) assertThrows(IllegalArgumentException::class.java) { builder.build() From 6edf9d84cb0ec2b75ba01582a345590a54821c53 Mon Sep 17 00:00:00 2001 From: Leandro Colmenarez <89696212+LeandroLCD@users.noreply.github.com> Date: Mon, 17 Nov 2025 20:59:50 -0300 Subject: [PATCH 3/7] Update query/src/main/java/com/blipblipcode/query/QueryDelete.kt Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../main/java/com/blipblipcode/query/QueryDelete.kt | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/query/src/main/java/com/blipblipcode/query/QueryDelete.kt b/query/src/main/java/com/blipblipcode/query/QueryDelete.kt index 9de5d0d..239cec6 100644 --- a/query/src/main/java/com/blipblipcode/query/QueryDelete.kt +++ b/query/src/main/java/com/blipblipcode/query/QueryDelete.kt @@ -204,15 +204,7 @@ class QueryDelete private constructor( * @throws IllegalArgumentException if the WHERE clause is not set. */ fun build(): QueryDelete { - if(where == null){ - val w = operations.firstNotNullOfOrNull{it}.let { - it ?: throw IllegalArgumentException("A WHERE clause must be specified.") - } - operations.remove(w.key) - - where = w.value.operator - } - + require(where != null) { "A WHERE clause must be specified." } return QueryDelete( where = where!!, table = table, operations = operations ) From cd823c11c5a3a9e56a7758718e8646f58ad8f7c9 Mon Sep 17 00:00:00 2001 From: Leandro Colmenarez <89696212+LeandroLCD@users.noreply.github.com> Date: Mon, 17 Nov 2025 21:00:07 -0300 Subject: [PATCH 4/7] Update query/src/main/java/com/blipblipcode/query/QuerySelect.kt Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../src/main/java/com/blipblipcode/query/QuerySelect.kt | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/query/src/main/java/com/blipblipcode/query/QuerySelect.kt b/query/src/main/java/com/blipblipcode/query/QuerySelect.kt index 3ef0e0d..73a1ac9 100644 --- a/query/src/main/java/com/blipblipcode/query/QuerySelect.kt +++ b/query/src/main/java/com/blipblipcode/query/QuerySelect.kt @@ -320,14 +320,7 @@ class QuerySelect private constructor( * @throws IllegalArgumentException if the WHERE clause is not set. */ fun build(): QuerySelect { - if(where == null){ - val w = operations.firstNotNullOfOrNull{it}.let { - it ?: throw IllegalArgumentException("WHERE clause is required for QuerySelect") - } - operations.remove(w.key) - - where = w.value.operator - } + require(where != null) { "WHERE clause is required for QuerySelect" } return QuerySelect( where = where!!, table = table, From 0ef87ec3d2132ab40ae2aca170c49ca250cfac39 Mon Sep 17 00:00:00 2001 From: Leandro Colmenarez <89696212+LeandroLCD@users.noreply.github.com> Date: Tue, 18 Nov 2025 11:58:10 -0300 Subject: [PATCH 5/7] feat(QuerySelect): Allow queries without a WHERE clause This commit modifies `QuerySelect` to support SQL queries without a `WHERE` clause. Previously, the `WHERE` clause was mandatory. This restriction has been lifted, but the requirement is maintained if other logical operations (like `AND` or `OR`) are present. The main changes include: - The `where` property in `QuerySelect` is now nullable (`SQLOperator<*>?`). - The `build()` method in the `QuerySelect.Builder` now only requires a `WHERE` clause if subsequent logical operations are added. - The SQL generation in `asSql()` has been updated to correctly build the `SELECT` statement when no `WHERE` clause is provided. - `getSqlOperators()` is updated to handle the nullable `where` property. --- .../java/com/blipblipcode/query/QuerySelect.kt | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/query/src/main/java/com/blipblipcode/query/QuerySelect.kt b/query/src/main/java/com/blipblipcode/query/QuerySelect.kt index 73a1ac9..8d4d770 100644 --- a/query/src/main/java/com/blipblipcode/query/QuerySelect.kt +++ b/query/src/main/java/com/blipblipcode/query/QuerySelect.kt @@ -17,7 +17,7 @@ import com.blipblipcode.query.operator.SQLOperator * @property fields The list of columns to be returned in the result set. Defaults to "*". */ class QuerySelect private constructor( - private var where: SQLOperator<*>, + private var where: SQLOperator<*>?, private val table: String, private val operations: LinkedHashMap, private val fields: List @@ -89,7 +89,7 @@ class QuerySelect private constructor( override fun getSqlOperators(): List> { return buildList { - add(where) + where?.let { add(it) } operations.values.forEach { add(it.operator) } } } @@ -110,7 +110,11 @@ class QuerySelect private constructor( val fieldStr = if (fields.isEmpty()) "*" else fields.joinToString(", ") val operationsStr = if (operations.isNotEmpty()) operations.values.joinToString(" ") { it.asString() } else "" return buildString { - append("SELECT $fieldStr FROM $table WHERE ${where.toSQLString()} $operationsStr".trim()) + if (where == null) { + append("SELECT $fieldStr FROM $table") + }else{ + append("SELECT $fieldStr FROM $table WHERE ${where?.toSQLString()} $operationsStr".trim()) + } if (orderBy != null) { append(" ") append(orderBy!!.asString()) @@ -320,9 +324,11 @@ class QuerySelect private constructor( * @throws IllegalArgumentException if the WHERE clause is not set. */ fun build(): QuerySelect { - require(where != null) { "WHERE clause is required for QuerySelect" } + if(operations.isNotEmpty()){ + require(where != null) { "WHERE clause is required for QuerySelect" } + } return QuerySelect( - where = where!!, + where = where, table = table, operations = LinkedHashMap(operations), fields = fields From ac5b7fb118309a57967a1680fe5c767deb5abf4e Mon Sep 17 00:00:00 2001 From: Leandro Colmenarez <89696212+LeandroLCD@users.noreply.github.com> Date: Thu, 20 Nov 2025 11:50:57 -0300 Subject: [PATCH 6/7] chore(deps): Update AGP to 8.13.1 Updates the Android Gradle Plugin version from 8.13.0 to 8.13.1. feat(Query): Enhance `orderBy` and fix `like` operator This commit introduces several improvements to the query builder, focusing on `ORDER BY` clause handling and correcting the behavior of the `like` operator. Key changes include: * **Fix `like` Operator Logic**: The `like()` method in `QuerySelect` and `QueryDelete` builders was incorrectly using a `LIKE` logical type. This has been corrected to use the `AND` logical type, ensuring it chains correctly with other conditions. The method signature is now restricted to `SQLOperator.Like`. * **Enhanced `OrderBy` in Union and Join Queries**: `UnionQuery` and `InnerJoint` now correctly handle `ORDER BY` clauses. They aggregate `OrderBy` operators from all subqueries, clear them from the individual queries, and apply a combined `OrderBy` to the final result set. This prevents SQL errors and ensures predictable sorting. `QuerySelect` has been updated with `getOrderBy()` and nullable `orderBy()` methods to support this. * **Improved `OrderBy` Implementation**: * A new `OrderBy.Multiple` sealed class has been added to handle sorting by multiple columns with different sort directions. * The `asSqlClause()` method was introduced for more flexible SQL string generation. * **Extensive Testing**: Added comprehensive unit tests for the `orderBy` functionality in `QuerySelectTest`, covering various scenarios like single and multiple columns, mixed sort directions, aliases, and combinations with `LIMIT`. --- gradle/libs.versions.toml | 2 +- .../java/com/blipblipcode/query/InnerJoint.kt | 10 +- .../com/blipblipcode/query/QueryDelete.kt | 4 +- .../com/blipblipcode/query/QuerySelect.kt | 14 +- .../java/com/blipblipcode/query/UnionQuery.kt | 11 +- .../query/operator/LogicalType.kt | 2 - .../blipblipcode/query/operator/OrdenBy.kt | 21 + .../com/blipblipcode/query/QuerySelectTest.kt | 370 ++++++++++++++++++ 8 files changed, 421 insertions(+), 13 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 219338e..e768210 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -agp = "8.13.0" +agp = "8.13.1" kotlin = "2.0.21" coreKtx = "1.17.0" junit = "4.13.2" diff --git a/query/src/main/java/com/blipblipcode/query/InnerJoint.kt b/query/src/main/java/com/blipblipcode/query/InnerJoint.kt index 2964e96..0739c76 100644 --- a/query/src/main/java/com/blipblipcode/query/InnerJoint.kt +++ b/query/src/main/java/com/blipblipcode/query/InnerJoint.kt @@ -38,6 +38,12 @@ class InnerJoint private constructor( override fun asSql(): String { require(queries.isNotEmpty()) { "At least one query is required for an INNER JOIN" } require(queries.size == onClauses.size) { "The number of queries must be equal to the number of ON clauses (including a placeholder for the base query)" } + val orders = queries.fold(mutableListOf()) { acc, query -> + query.getOrderBy()?.let { acc.add(it) } + query.orderBy(null) + acc + } + orders.addAll(orderBy?.let { listOf(it) } ?: emptyList()) val baseQuery = queries.first() val joins = queries.drop(1).zip(onClauses.drop(1)) { query, onClause -> @@ -46,9 +52,9 @@ class InnerJoint private constructor( return buildString { append("${baseQuery.asSql()} ${joins.joinToString(" ")}") - if (orderBy != null) { + if (orders.isNotEmpty()) { appendLine() - append(orderBy!!.asString()) + append(OrderBy.Multiple(orders).asString()) } } } diff --git a/query/src/main/java/com/blipblipcode/query/QueryDelete.kt b/query/src/main/java/com/blipblipcode/query/QueryDelete.kt index 239cec6..aa51715 100644 --- a/query/src/main/java/com/blipblipcode/query/QueryDelete.kt +++ b/query/src/main/java/com/blipblipcode/query/QueryDelete.kt @@ -162,8 +162,8 @@ class QueryDelete private constructor( * @param operator The SQL operator for this condition. * @return The `QueryBuilder` instance for chaining. */ - fun like(key: String, operator: SQLOperator<*>): QueryBuilder { - operations[key] = LogicalOperation(LogicalType.LIKE, operator) + fun like(key: String, operator: SQLOperator.Like): QueryBuilder { + operations[key] = LogicalOperation(LogicalType.AND, operator) return this } diff --git a/query/src/main/java/com/blipblipcode/query/QuerySelect.kt b/query/src/main/java/com/blipblipcode/query/QuerySelect.kt index 8d4d770..aab6cd6 100644 --- a/query/src/main/java/com/blipblipcode/query/QuerySelect.kt +++ b/query/src/main/java/com/blipblipcode/query/QuerySelect.kt @@ -87,6 +87,7 @@ class QuerySelect private constructor( } } + override fun getSqlOperators(): List> { return buildList { where?.let { add(it) } @@ -133,7 +134,7 @@ class QuerySelect private constructor( * @param operator A vararg of `[OrderBy]` objects specifying the columns and direction for sorting. * @return A new `QuerySelect` instance representing the UNION query with the added ORDER BY clause. */ - fun orderBy(operator: OrderBy): Queryable { + fun orderBy(operator: OrderBy?): Queryable { orderBy = operator return this } @@ -161,6 +162,10 @@ class QuerySelect private constructor( return this } + fun getOrderBy(): OrderBy? { + return this.orderBy + } + /** * A builder for creating `QuerySelect` instances. @@ -241,8 +246,8 @@ class QuerySelect private constructor( * @param operator The SQL operator for this condition. * @return The `QueryBuilder` instance for chaining. */ - fun like(key: String, operator: SQLOperator<*>): QueryBuilder { - operations[key] = LogicalOperation(LogicalType.LIKE, operator) + fun like(key: String, operator: SQLOperator.Like): QueryBuilder { + operations[key] = LogicalOperation(LogicalType.AND, operator) return this } @@ -296,6 +301,9 @@ class QuerySelect private constructor( this.orderBy = orderBy return this } + fun getOrderBy(): OrderBy? { + return this.orderBy + } /** * Sets a LIMIT clause for the query to limit the number of rows returned. diff --git a/query/src/main/java/com/blipblipcode/query/UnionQuery.kt b/query/src/main/java/com/blipblipcode/query/UnionQuery.kt index 477ce39..973c2b3 100644 --- a/query/src/main/java/com/blipblipcode/query/UnionQuery.kt +++ b/query/src/main/java/com/blipblipcode/query/UnionQuery.kt @@ -35,14 +35,19 @@ class UnionQuery private constructor( */ override fun asSql(): String { require(queries.size >= 2) { "At least two queries are required for a UNION" } - + val orders = queries.fold(mutableListOf()) { acc, query -> + query.getOrderBy()?.let { acc.add(it) } + query.orderBy(null) + acc + } + orders.addAll(orderBy?.let { listOf(it) } ?: emptyList()) val unionKeyword = if (useUnionAll) "UNION ALL" else "UNION" return buildString { append(queries.joinToString("\n$unionKeyword\n") { it.asSql() }) - if (orderBy != null) { + if (orders.isNotEmpty()) { appendLine() - append(orderBy!!.asString()) + append(OrderBy.Multiple(orders).asString()) } } } diff --git a/query/src/main/java/com/blipblipcode/query/operator/LogicalType.kt b/query/src/main/java/com/blipblipcode/query/operator/LogicalType.kt index 06f0903..8ad523e 100644 --- a/query/src/main/java/com/blipblipcode/query/operator/LogicalType.kt +++ b/query/src/main/java/com/blipblipcode/query/operator/LogicalType.kt @@ -8,8 +8,6 @@ enum class LogicalType(val sql: String) { AND("AND"), /** Represents a logical OR operation. */ OR("OR"), - /** Represents a SQL LIKE operation. */ - LIKE("LIKE"), /** Represents a SQL ALL operation. */ ALL("ALL"), diff --git a/query/src/main/java/com/blipblipcode/query/operator/OrdenBy.kt b/query/src/main/java/com/blipblipcode/query/operator/OrdenBy.kt index beea5a6..efc7f91 100644 --- a/query/src/main/java/com/blipblipcode/query/operator/OrdenBy.kt +++ b/query/src/main/java/com/blipblipcode/query/operator/OrdenBy.kt @@ -3,6 +3,14 @@ package com.blipblipcode.query.operator sealed interface OrderBy { val column: String fun asString(): String + + fun asSqlClause(): String { + return when (this) { + is Asc -> "$column ASC" + is Desc -> "$column DESC" + is Multiple -> orders.joinToString(", ") { it.asSqlClause() } + } + } data class Asc(override val column: String) : OrderBy{ override fun asString(): String { return "ORDER BY $column ASC" @@ -19,4 +27,17 @@ sealed interface OrderBy { return "ORDER BY $column DESC" } } + + data class Multiple(val orders: List) : OrderBy{ + override val column: String + get() = orders.joinToString(", ") { it.column } + + override fun asString(): String { + return "ORDER BY ${asSqlClause()}" + } + + override fun toString(): String { + return "ORDER BY ${asSqlClause()}" + } + } } \ No newline at end of file diff --git a/query/src/test/java/com/blipblipcode/query/QuerySelectTest.kt b/query/src/test/java/com/blipblipcode/query/QuerySelectTest.kt index cba561d..4477095 100644 --- a/query/src/test/java/com/blipblipcode/query/QuerySelectTest.kt +++ b/query/src/test/java/com/blipblipcode/query/QuerySelectTest.kt @@ -2,6 +2,7 @@ package com.blipblipcode.query import com.blipblipcode.query.operator.LogicalOperation import com.blipblipcode.query.operator.LogicalType +import com.blipblipcode.query.operator.OrderBy import com.blipblipcode.query.operator.SQLOperator import com.blipblipcode.query.utils.asSQLiteQuery import org.junit.Assert.assertEquals @@ -358,4 +359,373 @@ class QuerySelectTest { val expectedSql = "SELECT * FROM users WHERE status = 'active' LIMIT 10 OFFSET -5" assertEquals(expectedSql, query.asSql().trim()) } + + @Test + fun `orderBy with ascending order`() { + val query = QuerySelect.builder("users") + .where(SQLOperator.Equals("status", "active")) + .build() + query.orderBy(OrderBy.Asc("name")) + val expectedSql = "SELECT * FROM users WHERE status = 'active' ORDER BY name ASC" + assertEquals(expectedSql, query.asSql().trim()) + } + + @Test + fun `orderBy with descending order`() { + val query = QuerySelect.builder("users") + .where(SQLOperator.Equals("status", "active")) + .build() + query.orderBy(OrderBy.Desc("created_at")) + val expectedSql = "SELECT * FROM users WHERE status = 'active' ORDER BY created_at DESC" + assertEquals(expectedSql, query.asSql().trim()) + } + + @Test + fun `orderBy with multiple columns ascending`() { + val query = QuerySelect.builder("users") + .where(SQLOperator.Equals("status", "active")) + .build() + query.orderBy(OrderBy.Multiple(listOf( + OrderBy.Asc("name"), + OrderBy.Asc("age") + ))) + val expectedSql = "SELECT * FROM users WHERE status = 'active' ORDER BY name ASC, age ASC" + assertEquals(expectedSql, query.asSql().trim()) + } + + @Test + fun `orderBy with multiple columns mixed directions`() { + val query = QuerySelect.builder("users") + .where(SQLOperator.Equals("status", "active")) + .build() + query.orderBy(OrderBy.Multiple(listOf( + OrderBy.Asc("status"), + OrderBy.Desc("created_at") + ))) + val expectedSql = "SELECT * FROM users WHERE status = 'active' ORDER BY status ASC, created_at DESC" + assertEquals(expectedSql, query.asSql().trim()) + } + + @Test + fun `orderBy with descending then ascending`() { + val query = QuerySelect.builder("users") + .where(SQLOperator.Equals("status", "active")) + .build() + query.orderBy(OrderBy.Multiple(listOf( + OrderBy.Desc("priority"), + OrderBy.Asc("name") + ))) + val expectedSql = "SELECT * FROM users WHERE status = 'active' ORDER BY priority DESC, name ASC" + assertEquals(expectedSql, query.asSql().trim()) + } + + @Test + fun `orderBy with ascending and descending columns`() { + val query = QuerySelect.builder("users") + .where(SQLOperator.Equals("status", "active")) + .build() + query.orderBy(OrderBy.Multiple(listOf( + OrderBy.Asc("department"), + OrderBy.Desc("salary") + ))) + val expectedSql = "SELECT * FROM users WHERE status = 'active' ORDER BY department ASC, salary DESC" + assertEquals(expectedSql, query.asSql().trim()) + } + + @Test + fun `orderBy with limit and ascending order`() { + val query = QuerySelect.builder("users") + .where(SQLOperator.Equals("status", "active")) + .limit(10) + .build() + query.orderBy(OrderBy.Asc("name")) + val expectedSql = "SELECT * FROM users WHERE status = 'active' ORDER BY name ASC LIMIT 10" + assertEquals(expectedSql, query.asSql().trim()) + } + + @Test + fun `orderBy with limit and descending order`() { + val query = QuerySelect.builder("users") + .where(SQLOperator.Equals("status", "active")) + .limit(5, 10) + .build() + query.orderBy(OrderBy.Desc("created_at")) + val expectedSql = "SELECT * FROM users WHERE status = 'active' ORDER BY created_at DESC LIMIT 5 OFFSET 10" + assertEquals(expectedSql, query.asSql().trim()) + } + + @Test + fun `orderBy chaining call`() { + val query = QuerySelect.builder("users") + .where(SQLOperator.Equals("status", "active")) + .build() + val instance = query.orderBy(OrderBy.Asc("name")) + assertEquals(query, instance) + } + + @Test + fun `orderBy replacing previous order`() { + val query = QuerySelect.builder("users") + .where(SQLOperator.Equals("status", "active")) + .build() + query.orderBy(OrderBy.Desc("created_at")) + query.orderBy(OrderBy.Asc("name")) + val expectedSql = "SELECT * FROM users WHERE status = 'active' ORDER BY name ASC" + assertEquals(expectedSql, query.asSql().trim()) + } + + @Test + fun `orderBy with null value`() { + val query = QuerySelect.builder("users") + .where(SQLOperator.Equals("status", "active")) + .build() + query.orderBy(null) + val expectedSql = "SELECT * FROM users WHERE status = 'active'" + assertEquals(expectedSql, query.asSql().trim()) + } + + @Test + fun `orderBy with three columns mixed directions`() { + val query = QuerySelect.builder("users") + .where(SQLOperator.Equals("status", "active")) + .build() + query.orderBy(OrderBy.Multiple(listOf( + OrderBy.Asc("department"), + OrderBy.Desc("created_at"), + OrderBy.Asc("name") + ))) + val expectedSql = "SELECT * FROM users WHERE status = 'active' ORDER BY department ASC, created_at DESC, name ASC" + assertEquals(expectedSql, query.asSql().trim()) + } + + @Test + fun `orderBy with special characters in column names`() { + val query = QuerySelect.builder("users") + .where(SQLOperator.Equals("status", "active")) + .build() + query.orderBy(OrderBy.Asc("`first name`")) + val expectedSql = "SELECT * FROM users WHERE status = 'active' ORDER BY `first name` ASC" + assertEquals(expectedSql, query.asSql().trim()) + } + + @Test + fun `orderBy with multiple columns and special characters`() { + val query = QuerySelect.builder("`user table`") + .where(SQLOperator.Equals("`user id`", 1)) + .build() + query.orderBy(OrderBy.Multiple(listOf( + OrderBy.Asc("`first name`"), + OrderBy.Desc("`last name`") + ))) + val expectedSql = "SELECT * FROM `user table` WHERE `user id` = 1 ORDER BY `first name` ASC, `last name` DESC" + assertEquals(expectedSql, query.asSql().trim()) + } + + @Test + fun `orderBy ascending with multiple logical operations`() { + val query = QuerySelect.builder("users") + .where(SQLOperator.Equals("id", 1)) + .and("status", SQLOperator.Equals("status", "active")) + .or("role", SQLOperator.Equals("role", "admin")) + .build() + query.orderBy(OrderBy.Asc("created_at")) + val expectedSql = "SELECT * FROM users WHERE id = 1 AND status = 'active' OR role = 'admin' ORDER BY created_at ASC" + assertEquals(expectedSql, query.asSql()) + } + + @Test + fun `orderBy descending with multiple logical operations`() { + val query = QuerySelect.builder("users") + .where(SQLOperator.Equals("id", 1)) + .and("status", SQLOperator.Equals("status", "active")) + .or("role", SQLOperator.Equals("role", "admin")) + .build() + query.orderBy(OrderBy.Desc("created_at")) + val expectedSql = "SELECT * FROM users WHERE id = 1 AND status = 'active' OR role = 'admin' ORDER BY created_at DESC" + assertEquals(expectedSql, query.asSql()) + } + + @Test + fun `orderBy multiple with specific fields`() { + val query = QuerySelect.builder("users") + .where(SQLOperator.Equals("status", "active")) + .setFields("name", "email", "created_at") + .build() + query.orderBy(OrderBy.Multiple(listOf( + OrderBy.Asc("name"), + OrderBy.Desc("created_at") + ))) + val expectedSql = "SELECT name, email, created_at FROM users WHERE status = 'active' ORDER BY name ASC, created_at DESC" + assertEquals(expectedSql, query.asSql().trim()) + } + + @Test + fun `setFields with single field alias`() { + val query = QuerySelect.builder("users") + .where(SQLOperator.Equals("id", 1)) + .setFields("name AS full_name") + .build() + val expectedSql = "SELECT name AS full_name FROM users WHERE id = 1" + assertEquals(expectedSql, query.asSql().trim()) + } + + @Test + fun `setFields with multiple fields with aliases`() { + val query = QuerySelect.builder("users") + .where(SQLOperator.Equals("status", "active")) + .setFields("name AS full_name", "email AS user_email", "created_at AS registration_date") + .build() + val expectedSql = "SELECT name AS full_name, email AS user_email, created_at AS registration_date FROM users WHERE status = 'active'" + assertEquals(expectedSql, query.asSql().trim()) + } + + @Test + fun `setFields with mixed fields and aliases`() { + val query = QuerySelect.builder("users") + .where(SQLOperator.Equals("status", "active")) + .setFields("id", "name AS full_name", "email") + .build() + val expectedSql = "SELECT id, name AS full_name, email FROM users WHERE status = 'active'" + assertEquals(expectedSql, query.asSql().trim()) + } + + @Test + fun `setFields with function and alias`() { + val query = QuerySelect.builder("users") + .where(SQLOperator.Equals("status", "active")) + .setFields("COUNT(*) AS total_users", "name AS user_name") + .build() + val expectedSql = "SELECT COUNT(*) AS total_users, name AS user_name FROM users WHERE status = 'active'" + assertEquals(expectedSql, query.asSql().trim()) + } + + @Test + fun `setFields with uppercase alias`() { + val query = QuerySelect.builder("users") + .where(SQLOperator.Equals("id", 1)) + .setFields("name AS NAME", "email AS EMAIL") + .build() + val expectedSql = "SELECT name AS NAME, email AS EMAIL FROM users WHERE id = 1" + assertEquals(expectedSql, query.asSql().trim()) + } + + @Test + fun `setFields with backtick quoted alias`() { + val query = QuerySelect.builder("users") + .where(SQLOperator.Equals("id", 1)) + .setFields("`name` AS `full name`", "`email` AS `user email`") + .build() + val expectedSql = "SELECT `name` AS `full name`, `email` AS `user email` FROM users WHERE id = 1" + assertEquals(expectedSql, query.asSql().trim()) + } + + @Test + fun `setFields with table prefix and alias`() { + val query = QuerySelect.builder("users") + .where(SQLOperator.Equals("users.id", 1)) + .setFields("users.name AS full_name", "users.email AS user_email") + .build() + val expectedSql = "SELECT users.name AS full_name, users.email AS user_email FROM users WHERE users.id = 1" + assertEquals(expectedSql, query.asSql().trim()) + } + + @Test + fun `setFields with aliases and orderBy`() { + val query = QuerySelect.builder("users") + .where(SQLOperator.Equals("status", "active")) + .setFields("name AS full_name", "created_at AS registration_date") + .build() + query.orderBy(OrderBy.Asc("full_name")) + val expectedSql = "SELECT name AS full_name, created_at AS registration_date FROM users WHERE status = 'active' ORDER BY full_name ASC" + assertEquals(expectedSql, query.asSql().trim()) + } + + @Test + fun `setFields with aliases and limit`() { + val query = QuerySelect.builder("users") + .where(SQLOperator.Equals("status", "active")) + .setFields("name AS full_name", "email AS user_email") + .limit(10) + .build() + val expectedSql = "SELECT name AS full_name, email AS user_email FROM users WHERE status = 'active' LIMIT 10" + assertEquals(expectedSql, query.asSql().trim()) + } + + @Test + fun `setFields with aliases orderBy and limit combined`() { + val query = QuerySelect.builder("users") + .where(SQLOperator.Equals("status", "active")) + .setFields("name AS full_name", "created_at AS registration_date", "email AS user_email") + .limit(5, 10) + .build() + query.orderBy(OrderBy.Multiple(listOf( + OrderBy.Asc("registration_date"), + OrderBy.Desc("full_name") + ))) + val expectedSql = "SELECT name AS full_name, created_at AS registration_date, email AS user_email FROM users WHERE status = 'active' ORDER BY registration_date ASC, full_name DESC LIMIT 5 OFFSET 10" + assertEquals(expectedSql, query.asSql().trim()) + } + + @Test + fun `setFields with CASE statement and alias`() { + val query = QuerySelect.builder("users") + .where(SQLOperator.Equals("id", 1)) + .setFields("CASE WHEN status = 'active' THEN 'Active User' ELSE 'Inactive' END AS user_status") + .build() + val expectedSql = "SELECT CASE WHEN status = 'active' THEN 'Active User' ELSE 'Inactive' END AS user_status FROM users WHERE id = 1" + assertEquals(expectedSql, query.asSql().trim()) + } + + @Test + fun `setFields with aggregate functions and aliases`() { + val query = QuerySelect.builder("users") + .where(SQLOperator.Equals("status", "active")) + .setFields("COUNT(*) AS total", "SUM(salary) AS total_salary", "AVG(salary) AS average_salary") + .build() + val expectedSql = "SELECT COUNT(*) AS total, SUM(salary) AS total_salary, AVG(salary) AS average_salary FROM users WHERE status = 'active'" + assertEquals(expectedSql, query.asSql().trim()) + } + + @Test + fun `setFields with mathematical expression and alias`() { + val query = QuerySelect.builder("products") + .where(SQLOperator.Equals("category", "electronics")) + .setFields("name", "price", "price * 0.1 AS discount_amount") + .build() + val expectedSql = "SELECT name, price, price * 0.1 AS discount_amount FROM products WHERE category = 'electronics'" + assertEquals(expectedSql, query.asSql().trim()) + } + + @Test + fun `setFields with alias immutability`() { + val originalQuery = QuerySelect.builder("users") + .where(SQLOperator.Equals("id", 1)) + .build() + val originalSql = originalQuery.asSql() + + originalQuery.setFields("name AS full_name", "email AS user_email") + + assertEquals(originalSql, originalQuery.asSql()) + } + + @Test + fun `setFields replacing previous fields with aliases`() { + val query = QuerySelect.builder("users") + .where(SQLOperator.Equals("status", "active")) + .setFields("id", "name") + .build() + val newQuery = query.setFields("name AS full_name", "email AS user_email", "created_at AS registration_date") + val expectedSql = "SELECT name AS full_name, email AS user_email, created_at AS registration_date FROM users WHERE status = 'active'" + assertEquals(expectedSql, newQuery.asSql().trim()) + } + + @Test + fun `setFields with multiple aliases using builder`() { + val query = QuerySelect.builder("employees") + .where(SQLOperator.Equals("department", "sales")) + .setFields("employee_id AS id", "first_name AS fname", "last_name AS lname", "salary AS monthly_salary") + .build() + val expectedSql = "SELECT employee_id AS id, first_name AS fname, last_name AS lname, salary AS monthly_salary FROM employees WHERE department = 'sales'" + assertEquals(expectedSql, query.asSql().trim()) + } } From c0b92b62ff46da799da3ad2efa7c6efcf9d6d704 Mon Sep 17 00:00:00 2001 From: Leandro Colmenarez <89696212+LeandroLCD@users.noreply.github.com> Date: Thu, 20 Nov 2025 11:52:17 -0300 Subject: [PATCH 7/7] docs(UnionQuery): Fix KDoc for orderBy parameter This commit corrects the KDoc for the `orderBy` function in `UnionQuery`. The `@param` name has been updated from `columns` to `operator` to accurately match the function's parameter name. --- query/src/main/java/com/blipblipcode/query/UnionQuery.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/query/src/main/java/com/blipblipcode/query/UnionQuery.kt b/query/src/main/java/com/blipblipcode/query/UnionQuery.kt index 973c2b3..57244dc 100644 --- a/query/src/main/java/com/blipblipcode/query/UnionQuery.kt +++ b/query/src/main/java/com/blipblipcode/query/UnionQuery.kt @@ -56,7 +56,7 @@ class UnionQuery private constructor( * Appends an ORDER BY clause to the entire UNION query. * Note that in most SQL dialects, an ORDER BY clause can only be applied to the final result of a UNION, not to individual `SELECT` statements within it. * - * @param columns A vararg of `OrderExpression` objects specifying the columns and direction for sorting. + * @param operator A vararg of `OrderExpression` objects specifying the columns and direction for sorting. * @return A new `QuerySelect` instance representing the UNION query with the added ORDER BY clause. */ fun orderBy(operator: OrderBy): Queryable {