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 73a1ac9..aab6cd6 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 @@ -87,9 +87,10 @@ class QuerySelect private constructor( } } + override fun getSqlOperators(): List> { return buildList { - add(where) + where?.let { add(it) } operations.values.forEach { add(it.operator) } } } @@ -110,7 +111,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()) @@ -129,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 } @@ -157,6 +162,10 @@ class QuerySelect private constructor( return this } + fun getOrderBy(): OrderBy? { + return this.orderBy + } + /** * A builder for creating `QuerySelect` instances. @@ -237,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 } @@ -292,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. @@ -320,9 +332,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 diff --git a/query/src/main/java/com/blipblipcode/query/UnionQuery.kt b/query/src/main/java/com/blipblipcode/query/UnionQuery.kt index 477ce39..57244dc 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()) } } } @@ -51,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 { diff --git a/query/src/main/java/com/blipblipcode/query/operator/CaseConversion.kt b/query/src/main/java/com/blipblipcode/query/operator/CaseConversion.kt new file mode 100644 index 0000000..25ca80f --- /dev/null +++ b/query/src/main/java/com/blipblipcode/query/operator/CaseConversion.kt @@ -0,0 +1,14 @@ +package com.blipblipcode.query.operator + +enum class CaseConversion { + NONE, + LOWER, + UPPER; + fun asSqlFunction(value: Any): String { + return when (this) { + NONE -> value.toString() + LOWER -> "LOWER($value)" + UPPER -> "UPPER($value)" + } + } +} \ No newline at end of file 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/main/java/com/blipblipcode/query/operator/SQLOperator.kt b/query/src/main/java/com/blipblipcode/query/operator/SQLOperator.kt index 8637140..f0654f1 100644 --- a/query/src/main/java/com/blipblipcode/query/operator/SQLOperator.kt +++ b/query/src/main/java/com/blipblipcode/query/operator/SQLOperator.kt @@ -11,6 +11,8 @@ sealed interface SQLOperator { val column: String val value: T + val caseConversion: CaseConversion + /** * Returns a `Pair` of the column name and its value. */ @@ -26,67 +28,95 @@ sealed interface SQLOperator { is String -> "'$value'" else -> value.toString() } - return "$column $symbol $valueStr" + return "${caseConversion.asSqlFunction(column)} $symbol ${caseConversion.asSqlFunction(valueStr)}" } /** * Provides a simple string representation of the operator. */ - fun asString(): String = "$column $symbol $value" + fun asString(): String = "${caseConversion.asSqlFunction(column)} $symbol ${caseConversion.asSqlFunction(value.toString())}" /** Represents an "=" operation. */ - data class Equals(override val column: String, override val value: T) : SQLOperator { + data class Equals( + override val column: String, + override val value: T, + override val caseConversion: CaseConversion = CaseConversion.NONE + ) : SQLOperator { override val symbol: String = "=" } /** Represents a "!=" operation. */ - data class NotEquals(override val column: String, override val value: T) : SQLOperator { + data class NotEquals( + override val column: String, + override val value: T, + override val caseConversion: CaseConversion = CaseConversion.NONE) : SQLOperator { override val symbol: String = "!=" } /** Represents a ">" operation. */ - data class GreaterThan(override val column: String, override val value: T) : SQLOperator { + data class GreaterThan( + override val column: String, + override val value: T, + override val caseConversion: CaseConversion = CaseConversion.NONE) : SQLOperator { override val symbol: String = ">" } /** Represents a "<" operation. */ - data class LessThan(override val column: String, override val value: T) : SQLOperator { + data class LessThan( + override val column: String, + override val value: T, + override val caseConversion: CaseConversion = CaseConversion.NONE) : SQLOperator { override val symbol: String = "<" } /** Represents a ">=" operation. */ - data class GreaterThanOrEqual(override val column: String, override val value: T) : SQLOperator { + data class GreaterThanOrEqual( + override val column: String, + override val value: T, + override val caseConversion: CaseConversion = CaseConversion.NONE) : SQLOperator { override val symbol: String = ">=" } /** Represents a "<=" operation. */ - data class LessThanOrEqual(override val column: String, override val value: T) : SQLOperator { + data class LessThanOrEqual( + override val column: String, + override val value: T, + override val caseConversion: CaseConversion = CaseConversion.NONE) : SQLOperator { override val symbol: String = "<=" } /** Represents a "LIKE" operation. */ - data class Like(override val column: String, override val value: String) : SQLOperator { + data class Like( + override val column: String, + override val value: String, + override val caseConversion: CaseConversion = CaseConversion.NONE) : SQLOperator { override val symbol: String = "LIKE" override fun toSQLString(): String { - return "$column $symbol '%$value%'" + return "${caseConversion.asSqlFunction(column)} $symbol ${caseConversion.asSqlFunction("'%$value%'")}" } } /** Represents an "IN" operation. */ - data class In(override val column: String, override val value: List) : SQLOperator> { + data class In( + override val column: String, + override val value: List, + override val caseConversion: CaseConversion = CaseConversion.NONE) : SQLOperator> { override val symbol: String = "IN" override fun toSQLString(): String { - val list = value.joinToString(", ") { if (it is String) "'$it'" else it.toString() } - return "$column $symbol ($list)" + val list = value.joinToString(", ") { caseConversion.asSqlFunction(it.toString()) } + return "${caseConversion.asSqlFunction(column)} $symbol ($list)" } } /** Represents a "NOT IN" operation. */ - data class NotIn(override val column: String, override val value: List) : SQLOperator> { + data class NotIn( + override val column: String, + override val value: List, + override val caseConversion: CaseConversion = CaseConversion.NONE) : SQLOperator> { override val symbol: String = "NOT IN" override fun toSQLString(): String { - val list = value.joinToString(", ") { if (it is String) "'$it'" else it.toString() } - return "$column $symbol ($list)" + val list = value.joinToString(", ") { caseConversion.asSqlFunction(it.toString())} + return "${caseConversion.asSqlFunction(column)} $symbol ($list)" } } @@ -94,6 +124,7 @@ sealed interface SQLOperator { data class IsNull(override val column: String) : SQLOperator { override val symbol: String = "IS NULL" override val value: Unit = Unit + override val caseConversion: CaseConversion = CaseConversion.NONE override fun toSQLString(): String = "$column $symbol" override fun asString(): String = "$column $symbol" } @@ -102,19 +133,23 @@ sealed interface SQLOperator { data class IsNotNull(override val column: String) : SQLOperator { override val symbol: String = "IS NOT NULL" override val value: Unit = Unit + override val caseConversion: CaseConversion = CaseConversion.NONE override fun toSQLString(): String = "$column $symbol" override fun asString(): String = "$column $symbol" } /** Represents a "BETWEEN" operation. */ - data class Between(override val column: String, val start: T, val end: T) : SQLOperator> { + data class Between( + override val column: String, + val start: T, val end: T, + override val caseConversion: CaseConversion = CaseConversion.NONE) : SQLOperator> { override val symbol: String = "BETWEEN" override val value: Pair = start to end override fun toSQLString(): String { - val startStr = if (start is String) "'$start'" else start.toString() - val endStr = if (end is String) "'$end'" else end.toString() - return "$column $symbol $startStr AND $endStr" + val startStr = caseConversion.asSqlFunction(start.toString()) + val endStr = caseConversion.asSqlFunction(end.toString()) + return "${caseConversion.asSqlFunction(column)} $symbol $startStr AND $endStr" } - override fun asString(): String = "$column $symbol $start AND $end" + override fun asString(): String = "${caseConversion.asSqlFunction(column)} $symbol ${caseConversion.asSqlFunction(start.toString())} AND ${caseConversion.asSqlFunction(start.toString())}" } } diff --git a/query/src/test/java/com/blipblipcode/query/QuerySelectTest.kt b/query/src/test/java/com/blipblipcode/query/QuerySelectTest.kt index cba561d..557a9b5 100644 --- a/query/src/test/java/com/blipblipcode/query/QuerySelectTest.kt +++ b/query/src/test/java/com/blipblipcode/query/QuerySelectTest.kt @@ -1,7 +1,9 @@ package com.blipblipcode.query +import com.blipblipcode.query.operator.CaseConversion 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 @@ -194,6 +196,24 @@ class QuerySelectTest { assertEquals(expectedSql, query.asSql().trim()) } + @Test + fun `asSql with a basic WHERE `() { + val query = QuerySelect.builder("users") + .build() + val expectedSql = "SELECT * FROM users" + assertEquals(expectedSql, query.asSql().trim()) + } + + @Test + fun `asSql with a WHERE clause and uppercase`() { + val query = QuerySelect.builder("users") + .where(SQLOperator.Equals("id", 1)) + .and("status", SQLOperator.Equals("status", "active", caseConversion = CaseConversion.UPPER)) + .build() + val expectedSql = "SELECT * FROM users WHERE id = 1 AND UPPER(status) = UPPER('active')" + assertEquals(expectedSql, query.asSql()) + } + @Test fun `asSql with specific fields and no logical operations`() { val query = QuerySelect.builder("users") @@ -358,4 +378,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()) + } }