Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a595b16
feat(Query): Automatically use the first logical operation as the WHE…
LeandroLCD Nov 17, 2025
20e71ac
test(QueryDelete): Fix test for building without a WHERE clause
LeandroLCD Nov 17, 2025
6edf9d8
Update query/src/main/java/com/blipblipcode/query/QueryDelete.kt
LeandroLCD Nov 17, 2025
cd823c1
Update query/src/main/java/com/blipblipcode/query/QuerySelect.kt
LeandroLCD Nov 18, 2025
0ef87ec
feat(QuerySelect): Allow queries without a WHERE clause
LeandroLCD Nov 18, 2025
5899c58
Merge remote-tracking branch 'origin/master' into develop
LeandroLCD Nov 18, 2025
8cb9e02
Merge branch 'master' into develop
LeandroLCD Nov 18, 2025
5200a40
Merge remote-tracking branch 'origin/develop' into develop
LeandroLCD Nov 20, 2025
ac5b7fb
chore(deps): Update AGP to 8.13.1
LeandroLCD Nov 20, 2025
c0b92b6
docs(UnionQuery): Fix KDoc for orderBy parameter
LeandroLCD Nov 20, 2025
664d910
feat(Query): Add case-insensitive comparison support
LeandroLCD Nov 26, 2025
8bb0d5a
chore(deps): Update various dependencies
LeandroLCD Dec 8, 2025
c4c208f
chore(README): Update Kotlin version badge
LeandroLCD Dec 8, 2025
8b3be0d
refactor(Field): Implement SQLOperator for consistency
LeandroLCD Dec 8, 2025
8a9c3e0
feat(Queryable): Add filtered `asSql` method
LeandroLCD Dec 8, 2025
8f41579
feat(Queryable): Add predicate-based SQL generation and clear functio…
LeandroLCD Dec 8, 2025
4d6156b
refactor(SQLOperator): Standardize operator implementations
LeandroLCD Dec 8, 2025
e667492
feat(retrofit): Add RepeatedQueryParameters for Retrofit @QueryMap
LeandroLCD Dec 8, 2025
d797d6d
feat(Queryable): Add `asQueryRepeatedQueryParameters` extension
LeandroLCD Dec 8, 2025
a5d00b2
feat(QuerySelect): Include orderBy and limit in getSqlOperators
LeandroLCD Dec 8, 2025
ed4e44c
feat(Retrofit): Add `RepeatedQueryParameters` for Retrofit integration
LeandroLCD Dec 8, 2025
c1cd2e1
feat(Retrofit): Add `RepeatedQueryParameters` for Retrofit integration
LeandroLCD Dec 8, 2025
880424b
Merge branch 'master' into develop
LeandroLCD Dec 9, 2025
97bbc8b
docs(QuerySelect): Improve KDoc for getOrderBy function
LeandroLCD Dec 9, 2025
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
65 changes: 64 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

### Una librería de Kotlin ligera y fluida para construir consultas SQL de forma programática

[![Kotlin](https://img.shields.io/badge/Kotlin-1.9+-purple.svg?style=flat&logo=kotlin)](https://kotlinlang.org)
[![Kotlin](https://img.shields.io/badge/Kotlin-2.2.21+-purple.svg?style=flat&logo=kotlin)](https://kotlinlang.org)
[![Android](https://img.shields.io/badge/Android-API%2021+-green.svg?style=flat&logo=android)](https://developer.android.com)
[![Room](https://img.shields.io/badge/Room-Compatible-blue.svg?style=flat)](https://developer.android.com/training/data-storage/room)
![GitHub release (latest by date)](https://img.shields.io/github/v/release/LeandroLCD/android-sql-query)
Expand All @@ -17,6 +17,7 @@ Diseñada para integrarse perfectamente con la base de datos de Android y Room a
[Instalación](#-instalación) •
[Uso](#-uso) •
[Integración con Room](#-integración-con-room) •
[RepeatedQueryParameters](#-repeatedqueryparameters) •
[Contribuir](#-contribuir)

</div>
Expand All @@ -34,6 +35,7 @@ Diseñada para integrarse perfectamente con la base de datos de Android y Room a
- [DELETE](#-delete)
- [INNER JOIN](#-inner-join)
- [🔗 Integración con Room](#-integración-con-room)
- [📡 RepeatedQueryParameters](#-repeatedqueryparameters)
- [📝 Ejemplos Avanzados](#-ejemplos-avanzados)
- [🤝 Contribuir](#-contribuir)

Expand Down Expand Up @@ -209,6 +211,67 @@ class UserRepository(private val userDao: UserDao) {

---

## 📡 RepeatedQueryParameters

`RepeatedQueryParameters` es una clase utilitaria destinada a facilitar el uso de parámetros de consulta repetidos cuando se integra con Retrofit y otras librerías que consumen `Map`/`QueryMap` de parámetros.

Descripción breve:
- Permite pasar listas como valores en un `@QueryMap` y que Retrofit las expanda como múltiples pares clave=valor en la URL (ej: `?tag=a&tag=b`).
- Mantiene el orden de inserción (hereda de `LinkedHashMap`) para reproducibilidad en pruebas y cachés.
- Omite elementos `null` dentro de listas y lanza excepción si se intenta usar una clave o valor `null`.

API y métodos principales:
- `RepeatedQueryParameters.create(vararg pairs: Pair<String, Any>): RepeatedQueryParameters` — Crea la instancia a partir de pares clave/valor.
- `RepeatedQueryParameters.fromMap(map: MutableMap<String, Any>): RepeatedQueryParameters` — Convierte un `Map` existente.
- `RepeatedQueryParameters.empty(): RepeatedQueryParameters` — Instancia vacía.
- `addParameter(key: String, value: Any)` — Agrega o reemplaza un parámetro simple.
- `addRepeatedParameter(key: String, values: List<*>)` — Agrega una lista que será expandida.

Compatibilidad con la extensión `Queryable.asQueryRepeatedQueryParameters`:

La librería expone una extensión `Queryable.asQueryRepeatedQueryParameters()` que convierte los operadores SQL (devueltos por `getSqlOperators()`) en una instancia de `RepeatedQueryParameters`. Esta extensión:
- Filtra los operadores usando un `predicate: (Pair<String, Any?>) -> Boolean` opcional.
- Expande automáticamente listas en `RepeatedQueryParameters` mediante `addRepeatedParameter`.

Ejemplo usando la extensión `asQueryRepeatedQueryParameters`:

```kotlin
// Supongamos que `query` es un QuerySelect u otro Queryable con operadores que incluyen listas
val params = query.asQueryRepeatedQueryParameters()
// ahora `params` puede ser pasado directamente a Retrofit como @QueryMap
```

Ejemplo de uso con Retrofit:

```kotlin
interface ProductApi {
@GET("api/v2/product/list")
suspend fun getProductList(
@QueryMap options: RepeatedQueryParameters
): ResponsePaginListDto<ProductItemDto>
}

// Construcción de parámetros
val options = RepeatedQueryParameters.create(
"limit" to 50,
"offset" to 0,
"status" to listOf("active", "pending"),
"brand" to "michelin",
"sort" to "date"
)

// Llamada al API
val response = api.getProductList(options = options)
// Resultado en URL: ?limit=50&offset=0&status=active&status=pending&brand=michelin&sort=date
```

Notas de uso y buenas prácticas:
- Evita pasar valores `null` como clave o valor (la clase lanza excepción).
- Para agregar dinámicamente parámetros desde un `Queryable`, usa `asQueryRepeatedQueryParameters()` y opcionalmente provee un `predicate` para incluir/excluir pares.
- Mantén simples los valores no list (String, Int, Boolean); las listas son las que generan repetición en la URL.

---

## 📝 Ejemplos Avanzados

### Consultas Complejas con Múltiples Condiciones
Expand Down
10 changes: 5 additions & 5 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
[versions]
agp = "8.13.1"
kotlin = "2.0.21"
kotlin = "2.2.21"
coreKtx = "1.17.0"
junit = "4.13.2"
junitVersion = "1.3.0"
espressoCore = "3.7.0"
lifecycleRuntimeKtx = "2.9.4"
activityCompose = "1.11.0"
composeBom = "2024.09.00"
lifecycleRuntimeKtx = "2.10.0"
activityCompose = "1.12.1"
composeBom = "2025.12.00"
appcompat = "1.7.1"
material = "1.13.0"
sqliteKtx = "2.6.1"
sqliteKtx = "2.6.2"

[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
Expand Down
37 changes: 37 additions & 0 deletions query/src/main/java/com/blipblipcode/query/InnerJoint.kt
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,36 @@ class InnerJoint private constructor(
}
}

/**
* Generates the SQL string for the INNER JOIN statement.
* @param predicate The predicate to filter the operators.
* @return The complete INNER JOIN SQL query as a string.
* @throws IllegalArgumentException
*/
override fun asSql(predicate: (SQLOperator<*>) -> Boolean): 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 ->
"\nINNER JOIN \n${query.asSql(predicate)} \nON $onClause"
}

return buildString {
append("${baseQuery.asSql()} ${joins.joinToString(" ")}")
if (orders.isNotEmpty()) {
appendLine()
append(OrderBy.Multiple(orders).asString())
}
}
}

/**
* 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.
Expand All @@ -70,6 +100,13 @@ class InnerJoint private constructor(
orderBy = operator
return this
}
/**
* Retrieves the current ORDER BY clause for the INNER JOIN query.
* @return The `OrderBy` object if set, otherwise null.
*/
fun getOrderBy(): OrderBy? {
return orderBy
}

/**
* A builder for creating `InnerJoint` instances.
Expand Down
36 changes: 33 additions & 3 deletions query/src/main/java/com/blipblipcode/query/QueryDelete.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import com.blipblipcode.query.operator.SQLOperator
* @property operations A map of logical operations (AND, OR, etc.) to be appended to the WHERE clause.
*/
class QueryDelete private constructor(
private var where: SQLOperator<*>,
private var where: SQLOperator<*>?,
private val table: String,
private val operations: LinkedHashMap<String, LogicalOperation>,
):Queryable {
Expand Down Expand Up @@ -42,6 +42,12 @@ class QueryDelete private constructor(
return this
}

fun clear() : QueryDelete {
operations.clear()
where = null
return this
}

/**
* Sets or replaces the main WHERE clause of the query.
* @param operator The new SQL operator for the WHERE clause.
Expand All @@ -65,7 +71,8 @@ class QueryDelete private constructor(

override fun getSqlOperators(): List<SQLOperator<*>> {
return buildList {
add(where)
require(where != null) { "A WHERE clause must be specified." }
add(where!!)
operations.values.forEach { add(it.operator) }
}
}
Expand All @@ -81,9 +88,22 @@ class QueryDelete private constructor(
/**
* Generates the SQL string for the DELETE statement.
* @return The complete DELETE SQL query as a string.
* @throws IllegalArgumentException if the WHERE clause is not set.
*/
override fun asSql(): String {
return "DELETE FROM $table WHERE ${where.toSQLString()} ${operations.values.joinToString(" ") { it.asString() }}".trim()
require(where != null) { "A WHERE clause must be specified." }
return "DELETE FROM $table WHERE ${where!!.toSQLString()} ${operations.values.joinToString(" ") { it.asString() }}".trim()
}

/**
* Generates the SQL string for the DELETE statement.
* @param predicate The predicate to filter the operators.
* @return The complete DELETE SQL query as a string.
* @throws IllegalArgumentException if the WHERE clause is not set.
*/
override fun asSql(predicate: (SQLOperator<*>) -> Boolean): String {
require(where != null) { "A WHERE clause must be specified." }
return "DELETE FROM $table WHERE ${where!!.toSQLString()} ${operations.values.filter { predicate(it.operator) }.joinToString(" ") { it.asString() }}".trim()
}

/**
Expand All @@ -95,6 +115,16 @@ class QueryDelete private constructor(
) {
private var where: SQLOperator<*>? = null

/**
* Clears all conditions and resets the builder.
* @return The `QueryBuilder` instance for chaining.
*/
fun clear() : QueryBuilder {
operations.clear()
where = null
return this
}

/**
* Adds an AND condition to the WHERE clause.
* @param key A unique key for this condition.
Expand Down
13 changes: 13 additions & 0 deletions query/src/main/java/com/blipblipcode/query/QueryInsert.kt
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,19 @@ class QueryInsert private constructor(
return "INSERT INTO $table ($columns) VALUES ($values)"
}

/**
* Generates the SQL string for the INSERT statement.
* @param predicate The predicate to filter the operators.
* @return The complete INSERT SQL query as a string.
* @throws IllegalArgumentException if no fields are provided for insertion.
*/
override fun asSql(predicate: (SQLOperator<*>) -> Boolean): String {
require(fields.isNotEmpty()) { "At least one field must be provided for insertion." }
val columns = fields.values.filter { predicate(it) }.joinToString(", ") { it.name }
val values = fields.values.filter { predicate(it) }.joinToString(", ") { it.valueString() }
return "INSERT INTO $table ($columns) VALUES ($values)"
}

/**
* A builder for creating `QueryInsert` instances.
* This class provides a fluent API to construct an INSERT query.
Expand Down
50 changes: 50 additions & 0 deletions query/src/main/java/com/blipblipcode/query/QuerySelect.kt
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,16 @@ class QuerySelect private constructor(
return this
}

/**
* Clears all logical operations and the main WHERE clause from the query.
* @return The current `QuerySelect` instance for chaining.
*/
fun clear(): QuerySelect {
operations.clear()
where = null
return this
}

/**
* Sets or replaces the main WHERE clause of the query.
* @param operator The new SQL operator for the WHERE clause.
Expand Down Expand Up @@ -92,6 +102,8 @@ class QuerySelect private constructor(
return buildList {
where?.let { add(it) }
operations.values.forEach { add(it.operator) }
orderBy?.let { add(it) }
limit?.let { add(it) }
}
}

Expand Down Expand Up @@ -126,6 +138,30 @@ class QuerySelect private constructor(
}
}
}
/**
* Generates the SQL string for the SELECT statement.
* @param predicate The predicate to filter the operators.
* @return The complete SELECT SQL query as a string.
*/
override fun asSql(predicate: ( SQLOperator<*>) -> Boolean): String {
val fieldStr = if (fields.isEmpty()) "*" else fields.joinToString(", ")
val operationsStr = if (operations.isNotEmpty()) operations.values.filter { predicate(it.operator) }.joinToString(" ") { it.asString() } else ""
return buildString {
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())
}
if (limit != null) {
append(" ")
append(limit!!.asString())
}
}
}

/**
* Appends an ORDER BY clause to the entire UNION query.
Expand Down Expand Up @@ -180,6 +216,16 @@ class QuerySelect private constructor(
private var orderBy: OrderBy? = null
private var limit: Limit? = null

/**
* Clears all logical operations and the main WHERE clause from the query.
* @return The current `QuerySelect` instance for chaining.
*/
fun clear(): QueryBuilder {
operations.clear()
where = null
return this
}

/**
* Adds an AND condition to the WHERE clause.
* @param key A unique key for this condition.
Expand Down Expand Up @@ -301,6 +347,10 @@ class QuerySelect private constructor(
this.orderBy = orderBy
return this
}
/**
* Returns the ORDER BY clause for the query.
* @return The OrderBy object, or null if not set.
*/
fun getOrderBy(): OrderBy? {
return this.orderBy
}
Expand Down
10 changes: 10 additions & 0 deletions query/src/main/java/com/blipblipcode/query/QueryUpdate.kt
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,16 @@ class QueryUpdate private constructor(
return "UPDATE $table SET $setClause WHERE ${where.toSQLString()}".trim()
}

/**
* Generates the SQL string for the UPDATE statement.
* @param predicate The predicate to filter the operators.
* @return The complete UPDATE SQL query as a string.
*/
override fun asSql(predicate: (SQLOperator<*>) -> Boolean): String {
val setClause = fields.values.filter { predicate(it) }.joinToString(", ") { it.asString() }
return "UPDATE $table SET $setClause WHERE ${where.toSQLString()}".trim()
}

/**
* A builder for creating `QueryUpdate` instances.
* This class provides a fluent API to construct an UPDATE query.
Expand Down
7 changes: 7 additions & 0 deletions query/src/main/java/com/blipblipcode/query/Queryable.kt
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,11 @@ interface Queryable {
* @return The SQL query string.
*/
fun asSql(): String

/**
* Returns the SQL query string representation of the object.
* @param predicate The predicate to filter the operators.
* @return The SQL query string.
*/
fun asSql(predicate: ( SQLOperator<*>) -> Boolean): String
}
Loading