Skip to content
Closed
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
18 changes: 18 additions & 0 deletions yawn-api/src/main/kotlin/com/faire/yawn/query/YawnComparison.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.faire.yawn.query

import com.faire.yawn.YawnDef

enum class YawnComparison {
EQ, LT, LE, GT, GE;

fun <SOURCE : Any, F> compare(
column: YawnDef<SOURCE, *>.YawnColumnDef<F>,
value: F & Any,
): YawnQueryCriterion<SOURCE> = when (this) {
EQ -> YawnRestrictions.eq(column, value)
LT -> YawnRestrictions.lt(column, value)
LE -> YawnRestrictions.le(column, value)
GT -> YawnRestrictions.gt(column, value)
GE -> YawnRestrictions.ge(column, value)
}
}
121 changes: 121 additions & 0 deletions yawn-api/src/test/kotlin/com/faire/yawn/query/YawnComparisonTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package com.faire.yawn.query

import com.faire.yawn.YawnTableDef
import com.faire.yawn.YawnTableDefParent
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test

internal class YawnComparisonTest {

private class OrderEntity

/**
* Simulates a table with columns of different types — the scenario where
* you can't pass YawnRestrictions::le as a function parameter because F
* is invariant and would need to unify across Int, String, and Long.
*/
private object OrderDef : YawnTableDef<OrderEntity, OrderEntity>(YawnTableDefParent.RootTableDefParent) {
val amount: ColumnDef<Int> = ColumnDef("amount")
val createdAt: ColumnDef<String> = ColumnDef("created_at")
val itemCount: ColumnDef<Long> = ColumnDef("item_count")
}

@Test
fun `same comparison applies to columns of different types`() {
val (amountCriterion, createdAtCriterion, itemCountCriterion) = buildOrderCriteria(
comparison = YawnComparison.LE,
amount = 100,
createdAt = "2026-01-01",
itemCount = 50L,
)

assertThat(amountCriterion.yawnRestriction)
.isInstanceOf(YawnQueryRestriction.LessThanOrEqualTo::class.java)
assertThat(createdAtCriterion.yawnRestriction)
.isInstanceOf(YawnQueryRestriction.LessThanOrEqualTo::class.java)
assertThat(itemCountCriterion.yawnRestriction)
.isInstanceOf(YawnQueryRestriction.LessThanOrEqualTo::class.java)
}

@Test
fun `switching comparison changes all generated restrictions`() {
val (leAmount, leCreatedAt, leItemCount) = buildOrderCriteria(
comparison = YawnComparison.LE,
amount = 100,
createdAt = "2026-01-01",
itemCount = 50L,
)
val (gtAmount, gtCreatedAt, gtItemCount) = buildOrderCriteria(
comparison = YawnComparison.GT,
amount = 100,
createdAt = "2026-01-01",
itemCount = 50L,
)

assertThat(leAmount.yawnRestriction)
.isInstanceOf(YawnQueryRestriction.LessThanOrEqualTo::class.java)
assertThat(leCreatedAt.yawnRestriction)
.isInstanceOf(YawnQueryRestriction.LessThanOrEqualTo::class.java)
assertThat(leItemCount.yawnRestriction)
.isInstanceOf(YawnQueryRestriction.LessThanOrEqualTo::class.java)

assertThat(gtAmount.yawnRestriction)
.isInstanceOf(YawnQueryRestriction.GreaterThan::class.java)
assertThat(gtCreatedAt.yawnRestriction)
.isInstanceOf(YawnQueryRestriction.GreaterThan::class.java)
assertThat(gtItemCount.yawnRestriction)
.isInstanceOf(YawnQueryRestriction.GreaterThan::class.java)
}

@Test
fun `each variant produces the expected restriction type`() {
val expected = mapOf(
YawnComparison.EQ to YawnQueryRestriction.Equals::class.java,
YawnComparison.LT to YawnQueryRestriction.LessThan::class.java,
YawnComparison.LE to YawnQueryRestriction.LessThanOrEqualTo::class.java,
YawnComparison.GT to YawnQueryRestriction.GreaterThan::class.java,
YawnComparison.GE to YawnQueryRestriction.GreaterThanOrEqualTo::class.java,
)

for ((comparison, restrictionClass) in expected) {
val criterion = comparison.compare(OrderDef.amount, 42)
assertThat(criterion.yawnRestriction)
.`as`("YawnComparison.%s", comparison.name)
.isInstanceOf(restrictionClass)
}
}

@Test
fun `all comparison variants are present`() {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this test is all that valuable, it's subsumed by the restriction type test

assertThat(YawnComparison.entries).containsExactly(
YawnComparison.EQ,
YawnComparison.LT,
YawnComparison.LE,
YawnComparison.GT,
YawnComparison.GE,
)
}

/**
* A helper that applies the same comparison to columns of different types.
* This is the pattern YawnComparison enables: F is resolved independently
* at each compare() call site, so a single comparison value works across
* ColumnDef<Int>, ColumnDef<String>, and ColumnDef<Long>.
*
* This cannot be expressed with a Kotlin function type parameter like
* `(YawnDef<SOURCE, *>.YawnColumnDef<F>, F & Any) -> YawnQueryCriterion<SOURCE>`
* because F would have to be fixed for the entire function signature.
*/
private fun buildOrderCriteria(
comparison: YawnComparison,
amount: Int,
createdAt: String,
itemCount: Long,
): Triple<YawnQueryCriterion<OrderEntity>, YawnQueryCriterion<OrderEntity>, YawnQueryCriterion<OrderEntity>> {
return Triple(
comparison.compare(OrderDef.amount, amount),
comparison.compare(OrderDef.createdAt, createdAt),
comparison.compare(OrderDef.itemCount, itemCount),
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package com.faire.yawn.database

import com.faire.yawn.query.YawnComparison
import com.faire.yawn.setup.entities.BookTable
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test

internal class YawnComparisonDatabaseTest : BaseYawnDatabaseTest() {

@Test
fun `compare filters books by numberOfPages with LT`() {
transactor.open { session ->
val results = session.query(BookTable) { books ->
add(YawnComparison.LT.compare(books.numberOfPages, 110L))
}.list()

assertThat(results)
.extracting("name")
.containsExactlyInAnyOrder("The Little Mermaid")
}
}

@Test
fun `compare filters books by numberOfPages with LE`() {
transactor.open { session ->
val results = session.query(BookTable) { books ->
add(YawnComparison.LE.compare(books.numberOfPages, 110L))
}.list()

assertThat(results)
.extracting("name")
.containsExactlyInAnyOrder(
"The Little Mermaid",
"The Ugly Duckling",
)
}
}

@Test
fun `compare filters books by numberOfPages with GT`() {
transactor.open { session ->
val results = session.query(BookTable) { books ->
add(YawnComparison.GT.compare(books.numberOfPages, 300L))
}.list()

assertThat(results)
.extracting("name")
.containsExactlyInAnyOrder(
"Harry Potter",
"Lord of the Rings",
)
}
}

@Test
fun `compare filters books by name with EQ`() {
transactor.open { session ->
val results = session.query(BookTable) { books ->
add(YawnComparison.EQ.compare(books.name, "The Hobbit"))
}.list()

assertThat(results.single().name).isEqualTo("The Hobbit")
}
}

/**
* Demonstrates the core use case: a single YawnComparison parameter applied to
* columns of different types (Long and String) in the same query. This is impossible
* with a Kotlin function reference because F would need to unify across both column types.
*/
@Test
fun `same comparison applies to columns of different types in a single query`() {
transactor.open { session ->
val results = queryBooksFiltered(
session,
comparison = YawnComparison.GE,
pageThreshold = 300L,
nameThreshold = "The",
)

assertThat(results).containsExactlyInAnyOrder("The Hobbit")
}
}

@Test
fun `switching comparison changes query results`() {
transactor.open { session ->
val leResults = queryBooksFiltered(
session,
comparison = YawnComparison.LE,
pageThreshold = 1000L,
nameThreshold = "M",
)
val gtResults = queryBooksFiltered(
session,
comparison = YawnComparison.GT,
pageThreshold = 1000L,
nameThreshold = "M",
)

assertThat(leResults).containsExactlyInAnyOrder(
"Harry Potter",
"Lord of the Rings",
)
assertThat(gtResults).isEmpty()
}
}

/**
* Helper that applies the same comparison to columns of different types (Long and String).
* This pattern is what YawnComparison enables — F is resolved independently at each call site.
*/
private fun queryBooksFiltered(
session: com.faire.yawn.setup.hibernate.YawnTestSession,
comparison: YawnComparison,
pageThreshold: Long,
nameThreshold: String,
): List<String> {
return session.query(BookTable) { books ->
add(comparison.compare(books.numberOfPages, pageThreshold))
add(comparison.compare(books.name, nameThreshold))
}.list().map { it.name }
}
}
Loading