Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 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
27c6476
Add Retrofit badge and update README links
LeandroLCD Dec 9, 2025
c7f40a6
fix(SQLOperator): Correctly quote values in `Between` operator
LeandroLCD Dec 10, 2025
39c0117
Merge branch 'master' into develop
LeandroLCD Dec 10, 2025
135aed3
chore(deps): Update AGP to 8.13.2
LeandroLCD Dec 26, 2025
2eac0b0
chore(UnionQuery): Remove unused imports
LeandroLCD Dec 26, 2025
b9a220e
Merge remote-tracking branch 'origin/develop' into develop
LeandroLCD Dec 26, 2025
3b6c776
refactor(Query): Standardize LogicalOperation with a sealed interface
LeandroLCD Jan 22, 2026
d778016
refactor(Query): Rename property 'type' to 'symbol' in LogicalOperati…
LeandroLCD Jan 22, 2026
5c3ab5c
Merge branch 'master' into develop
LeandroLCD Jan 22, 2026
c2a12dc
Merge branch 'master' into develop
LeandroLCD Jan 22, 2026
d1daaee
refactor(Query): Remove unused main function and transformOperation m…
LeandroLCD Jan 22, 2026
2b1c8d7
Merge remote-tracking branch 'origin/develop' into develop
LeandroLCD Jan 22, 2026
b5a24de
fix(QuerySelect): Reset limit and orderBy in clear method
LeandroLCD Feb 9, 2026
56ac9b8
feat(build.gradle.kts): Add Maven publishing configuration for releas…
LeandroLCD Feb 9, 2026
c61f619
chore(deps): Upgrade Gradle and Android Gradle Plugin
LeandroLCD Feb 12, 2026
97f48dc
feat(deps): Update kotlin and compose dependencies
LeandroLCD Feb 12, 2026
92713cd
feat(deps): Update AndroidX and Compose dependencies
LeandroLCD Mar 17, 2026
ccb0cb4
feat(query): Add getOperators and clearOrderBy methods
LeandroLCD Mar 17, 2026
ae210e1
feat(query): Add Collation support and transform capabilities to OrderBy
LeandroLCD Mar 24, 2026
dbc8dd3
feat(query): Add limit management and refine OrderBy behavior in Unio…
LeandroLCD May 27, 2026
a580f3e
feat(query): Add toQueryDelete method to QuerySelect
LeandroLCD May 27, 2026
41ba064
Merge branch 'master' into develop
LeandroLCD Jun 9, 2026
5ea199b
refactor(query): Enhance cloning, immutability, and testing standards
LeandroLCD Jun 9, 2026
8da4ef2
refactor(query): Remove unused imports in query-related files
LeandroLCD Jun 9, 2026
2c25bcc
refactor(query): Rename and fix cloning extension methods for `Logica…
LeandroLCD Jun 18, 2026
847b6f7
ci(release): Setup automated release pipeline and bump version to 0.13.3
LeandroLCD Jun 18, 2026
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
512 changes: 512 additions & 0 deletions .github/workflows/release-pipeline.yml

Large diffs are not rendered by default.

67 changes: 67 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,73 @@ val selectQuery = QuerySelect.builder("users")
selectQuery.limit(10, 5)

val sqlString = selectQuery.asSql()

### ⬆️ ORDER BY

Para ordenar los resultados puedes utilizar el método `orderBy` con `OrderBy.Asc`, `OrderBy.Desc` o una lista mediante `OrderBy.Multiple`.

`OrderBy.Asc` y `OrderBy.Desc` aceptan tres parámetros:

| Parámetro | Tipo | Descripción |
|-------------|------------------------|-------------|
| `column` | `String` | Nombre de la columna. |
| `collation` | `Collation` | Cláusula COLLATE que se añade **al final** de la expresión (por defecto `Collation.NONE`). |
| `transform` | `(String) -> String` | Función que envuelve la columna en SQL arbitrario (REPLACE, LOWER, etc.). Se aplica **antes** del COLLATE. Por defecto es la identidad `{ it }`. |

```kotlin
// ORDER BY simple asc
val q1 = QuerySelect.builder("users")
.where(SQLOperator.Equals("status", "active"))
.build()
q1.orderBy(OrderBy.Asc("name")) // -> ORDER BY name ASC

// ORDER BY con múltiples columnas y direcciones mezcladas
q1.orderBy(OrderBy.Multiple(listOf(
OrderBy.Asc("department"),
OrderBy.Desc("created_at")
))) // -> ORDER BY department ASC, created_at DESC

// ORDER BY usando Collation (ej: NOCASE o RTRIM)
q1.orderBy(OrderBy.Asc("name", collation = Collation.NOCASE))
// -> ORDER BY name COLLATE NOCASE ASC

q1.orderBy(OrderBy.Desc("name", collation = Collation.RTRIM))
// -> ORDER BY RTRIM(name) DESC
```

#### 🔄 Transform + Collation

Usa `transform` para aplicar funciones SQL sobre la columna (como `REPLACE`) y `collation` para
el `COLLATE`. El `transform` envuelve la columna **antes** de que se añada el COLLATE,
garantizando SQL correcto como:

```sql
SELECT * FROM vehicle
ORDER BY REPLACE(identification, '-', '') COLLATE NOCASE ASC
```

> **Nota:** En SQLite, `LOWER()` no garantiza un ordenamiento case-insensitive correcto.
> Usa siempre `COLLATE NOCASE` para ignorar mayúsculas/minúsculas al ordenar.

```kotlin
// Eliminar guiones e ignorar mayúsculas/minúsculas
val strip = "-".removeCharsTransform() // extensión de CollationUtils
q1.orderBy(OrderBy.Asc("identification", collation = Collation.NOCASE, transform = strip))
// -> ORDER BY REPLACE(identification, '-', '') COLLATE NOCASE ASC

// Solo COLLATE NOCASE (sin transform)
q1.orderBy(OrderBy.Asc("name", collation = Collation.NOCASE))
// -> ORDER BY name COLLATE NOCASE ASC

// Quitar espacios, guiones y puntos + COLLATE NOCASE
val stripChars = " -.".removeCharsTransform()
q1.orderBy(OrderBy.Asc("name", collation = Collation.NOCASE, transform = stripChars))
// -> ORDER BY REPLACE(REPLACE(REPLACE(name, ' ', ''), '-', ''), '.', '') COLLATE NOCASE ASC

// Combinar RTRIM + NOCASE con transform
val stripDash = "-".removeCharsTransform()
q1.orderBy(OrderBy.Asc("code", collation = Collation.RTRIM and Collation.NOCASE, transform = stripDash))
// -> ORDER BY RTRIM(REPLACE(code, '-', '')) COLLATE NOCASE ASC
```

### ➕ INSERT
Expand Down
259 changes: 259 additions & 0 deletions TESTING_GUIDELINES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
# Testing Guidelines for Agents

## 1. Test Naming Convention

Test names must follow **snake_case** format and clearly describe:
- **What** is expected to happen (should)
- **What behavior** is being tested
- **When** that behavior occurs
- **Which method** is being tested (in_invoke, in_execute, etc.)

### Format:
```
should_{action}_whent_{condition}_in_{method}
```

### Examples:
```kotlin
@Test
fun should_remove_item_from_cart_when_quantity_is_zero_in_invoke()

@Test
fun should_throw_QuantityMustBePositive_when_quantity_is_negative_in_invoke()

@Test
fun should_update_quantity_when_quantity_is_valid_and_stock_is_sufficient_in_invoke()
```

## 2. Test Structure: Given-When-Then

All tests must be divided into **3 clearly identified sections** with comments:

```kotlin
@Test
fun should_do_something_when_condition_in_invoke() = runTest {
//GIVEN
// Preparation: variables, mocks, initial setup

//WHEN
// Execution: call to the method being tested

//THEN
// Verification: asserts and behavior verifications
}
```

### Complete example:
```kotlin
@Test
fun should_update_quantity_when_quantity_is_valid_and_stock_is_sufficient_in_invoke() = runTest {
//GIVEN
val productId = "1"
val quantity = 3
val product = productBuilder {
withId(productId)
withStock(5)
}
every { productRepository.getProductById(productId) } returns flowOf(product)

//WHEN
useCase.invoke(productId, quantity)

//THEN
coVerify { cartItemRepository.updateQuantity(productId, quantity) }
}
```

## 3. Exception Testing

To test exceptions, **use `runCatching`** to capture the result and verify the exception type:

```kotlin
@Test
fun should_throw_QuantityMustBePositive_when_quantity_is_negative_in_invoke() = runTest {
//GIVEN
val productId = "1"
val quantity = -1

//WHEN
val result = runCatching { useCase.invoke(productId, quantity) }

//THEN
assert(result.exceptionOrNull() is AppError.Validation.QuantityMustBePositive)
}
```

### Advantages of runCatching vs assertThrows:
- More idiomatic in Kotlin
- Allows easy inspection of exception properties
- Cleaner and clearer syntax

### Verifying exception properties:
```kotlin
@Test
fun should_throw_InsufficientStock_when_quantity_exceeds_product_stock_in_invoke() = runTest {
//GIVEN
val productId = "1"
val quantity = 10
val product = productBuilder {
withId(productId)
withStock(5)
}
every { productRepository.getProductById(productId) } returns flowOf(product)

//WHEN
val result = runCatching { useCase.invoke(productId, quantity) }

//THEN
val exception = result.exceptionOrNull() as? AppError.Validation.InsufficientStock
assert(exception != null)
assertEquals(5, exception?.available)
}
```

## 4. Builder Pattern for Object Creation

To create test objects, **use the Builder pattern** with default values that allow easy creation of valid objects.

### Location:
Builders must be in a package named `builder` at the root of the corresponding test directory.

Example path:
```
app/src/test/java/com/package/feature/domain/usecase/builder/
```

### Builder Structure:

```kotlin
class CartItemBuilder {
private var productId: String = "product-1"
private var quantity: Int = 2

fun withProductId(productId: String) = apply { this.productId = productId }
fun withQuantity(quantity: Int) = apply { this.quantity = quantity }

fun build(): CartItem {
return CartItem(productId, quantity)
}
}

fun cartItem(block: CartItemBuilder.() -> Unit): CartItem =
CartItemBuilder().apply(block).build()
```

### Using the Builder in Tests:

#### Case 1: Using default values
```kotlin
//GIVEN
val cartItem = cartItem { }
```

#### Case 2: Customizing some values
```kotlin
//GIVEN
val cartItem = cartItem {
withProductId("custom-id")
withQuantity(5)
}
```

#### Case 3: Customizing only one value
```kotlin
//GIVEN
val cartItem = cartItem {
withQuantity(10)
}
// productId will have the default value "product-1"
```

### Builder Pattern Advantages:
- **Default values**: Tests are more concise, you only specify what matters
- **Readability**: Code is self-documenting
- **Maintainability**: If the model changes, you only update the builder
- **Flexibility**: Easy to create different test scenarios

## 5. Complete Test Example

```kotlin
class UpdateCartItemUseCaseTest {

private lateinit var cartItemRepository: CartItemRepository
private lateinit var productRepository: ProductRepository
private lateinit var useCase: UpdateCartItemUseCase

@Before
fun setUp() {
cartItemRepository = mockk(relaxed = true)
productRepository = mockk()
useCase = UpdateCartItemUseCase(cartItemRepository, productRepository)
}

@Test
fun should_throw_QuantityMustBePositive_when_quantity_is_negative_in_invoke() = runTest {
//GIVEN
val productId = "1"
val quantity = -1

//WHEN
val result = runCatching { useCase.invoke(productId, quantity) }

//THEN
assert(result.exceptionOrNull() is AppError.Validation.QuantityMustBePositive)
}

@Test
fun should_remove_item_from_cart_when_quantity_is_zero_in_invoke() = runTest {
//GIVEN
val productId = "1"
val quantity = 0

//WHEN
useCase.invoke(productId, quantity)

//THEN
coVerify { cartItemRepository.removeFromCart(productId) }
coVerify(exactly = 0) { cartItemRepository.updateQuantity(any(), any()) }
}

@Test
fun should_update_quantity_when_quantity_is_valid_and_stock_is_sufficient_in_invoke() = runTest {
//GIVEN
val productId = "1"
val quantity = 3
val product = productBuilder {
withId(productId)
withStock(5)
}
every { productRepository.getProductById(productId) } returns flowOf(product)

//WHEN
useCase.invoke(productId, quantity)

//THEN
coVerify { cartItemRepository.updateQuantity(productId, quantity) }
}
}
```

## 6. Best Practices Summary

✅ **DO:**
- Use descriptive names in snake_case with `_in_{method}` suffix
- Divide tests into GIVEN-WHEN-THEN
- Use `runCatching` to capture exceptions
- Use builders to create test objects
- Define sensible default values in builders
- Use `runTest` for tests with coroutines

❌ **DON'T:**
- Never include comments in the code
- Use backticks in test names
- Use `assertThrows` (prefer `runCatching`)
- Create objects manually in each test
- Use `mockk(relaxed = true)` for repositories
- Mix preparation, execution, and verification logic
- Omit the method name in the test name


11 changes: 5 additions & 6 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget

plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
}

Expand Down Expand Up @@ -35,15 +34,15 @@ android {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlin{
compilerOptions {
jvmTarget = JvmTarget.JVM_17
}
}
buildFeatures {
compose = true
}
}
kotlin{
compilerOptions {
jvmTarget = JvmTarget.JVM_17
}
}

dependencies {
implementation(libs.androidx.core.ktx)
Expand Down
5 changes: 3 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.compose) apply false
alias(libs.plugins.android.library) apply false
}
}

version = "0.13.3"
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,4 @@ kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true
android.nonTransitiveRClass=true
Loading
Loading