Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
10 changes: 8 additions & 2 deletions query/src/main/java/com/blipblipcode/query/InnerJoint.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<OrderBy>()) { 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 ->
Expand All @@ -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())
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions query/src/main/java/com/blipblipcode/query/QueryDelete.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
30 changes: 22 additions & 8 deletions query/src/main/java/com/blipblipcode/query/QuerySelect.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, LogicalOperation>,
private val fields: List<String>
Expand Down Expand Up @@ -87,9 +87,10 @@ class QuerySelect private constructor(
}
}


override fun getSqlOperators(): List<SQLOperator<*>> {
return buildList {
add(where)
where?.let { add(it) }
operations.values.forEach { add(it.operator) }
}
}
Expand All @@ -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())
Expand All @@ -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
}
Expand Down Expand Up @@ -157,6 +162,10 @@ class QuerySelect private constructor(
return this
}

fun getOrderBy(): OrderBy? {
return this.orderBy
}


/**
* A builder for creating `QuerySelect` instances.
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
13 changes: 9 additions & 4 deletions query/src/main/java/com/blipblipcode/query/UnionQuery.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<OrderBy>()) { 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())
}
}
}
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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)"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"),

Expand Down
21 changes: 21 additions & 0 deletions query/src/main/java/com/blipblipcode/query/operator/OrdenBy.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -19,4 +27,17 @@ sealed interface OrderBy {
return "ORDER BY $column DESC"
}
}

data class Multiple(val orders: List<OrderBy>) : 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()}"
}
}
}
77 changes: 56 additions & 21 deletions query/src/main/java/com/blipblipcode/query/operator/SQLOperator.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ sealed interface SQLOperator<T> {
val column: String
val value: T

val caseConversion: CaseConversion

/**
* Returns a `Pair` of the column name and its value.
*/
Expand All @@ -26,74 +28,103 @@ sealed interface SQLOperator<T> {
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<T>(override val column: String, override val value: T) : SQLOperator<T> {
data class Equals<T>(
override val column: String,
override val value: T,
override val caseConversion: CaseConversion = CaseConversion.NONE
) : SQLOperator<T> {
override val symbol: String = "="
}

/** Represents a "!=" operation. */
data class NotEquals<T>(override val column: String, override val value: T) : SQLOperator<T> {
data class NotEquals<T>(
override val column: String,
override val value: T,
override val caseConversion: CaseConversion = CaseConversion.NONE) : SQLOperator<T> {
override val symbol: String = "!="
}

/** Represents a ">" operation. */
data class GreaterThan<T>(override val column: String, override val value: T) : SQLOperator<T> {
data class GreaterThan<T>(
override val column: String,
override val value: T,
override val caseConversion: CaseConversion = CaseConversion.NONE) : SQLOperator<T> {
override val symbol: String = ">"
}

/** Represents a "<" operation. */
data class LessThan<T>(override val column: String, override val value: T) : SQLOperator<T> {
data class LessThan<T>(
override val column: String,
override val value: T,
override val caseConversion: CaseConversion = CaseConversion.NONE) : SQLOperator<T> {
override val symbol: String = "<"
}

/** Represents a ">=" operation. */
data class GreaterThanOrEqual<T>(override val column: String, override val value: T) : SQLOperator<T> {
data class GreaterThanOrEqual<T>(
override val column: String,
override val value: T,
override val caseConversion: CaseConversion = CaseConversion.NONE) : SQLOperator<T> {
override val symbol: String = ">="
}

/** Represents a "<=" operation. */
data class LessThanOrEqual<T>(override val column: String, override val value: T) : SQLOperator<T> {
data class LessThanOrEqual<T>(
override val column: String,
override val value: T,
override val caseConversion: CaseConversion = CaseConversion.NONE) : SQLOperator<T> {
override val symbol: String = "<="
}

/** Represents a "LIKE" operation. */
data class Like(override val column: String, override val value: String) : SQLOperator<String> {
data class Like(
override val column: String,
override val value: String,
override val caseConversion: CaseConversion = CaseConversion.NONE) : SQLOperator<String> {
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<T>(override val column: String, override val value: List<T>) : SQLOperator<List<T>> {
data class In<T>(
override val column: String,
override val value: List<T>,
override val caseConversion: CaseConversion = CaseConversion.NONE) : SQLOperator<List<T>> {
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<T>(override val column: String, override val value: List<T>) : SQLOperator<List<T>> {
data class NotIn<T>(
override val column: String,
override val value: List<T>,
override val caseConversion: CaseConversion = CaseConversion.NONE) : SQLOperator<List<T>> {
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)"
}
}

/** Represents an "IS NULL" operation. */
data class IsNull(override val column: String) : SQLOperator<Unit> {
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"
}
Expand All @@ -102,19 +133,23 @@ sealed interface SQLOperator<T> {
data class IsNotNull(override val column: String) : SQLOperator<Unit> {
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<T>(override val column: String, val start: T, val end: T) : SQLOperator<Pair<T, T>> {
data class Between<T>(
override val column: String,
val start: T, val end: T,
override val caseConversion: CaseConversion = CaseConversion.NONE) : SQLOperator<Pair<T, T>> {
override val symbol: String = "BETWEEN"
override val value: Pair<T, T> = 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())}"
}
}
Loading