Skip to content

Commit 5e8d42e

Browse files
feat: Return non-nullable column def in addIsNotNull (#111)
1 parent 7e073c2 commit 5e8d42e

File tree

4 files changed

+112
-3
lines changed

4 files changed

+112
-3
lines changed

docs/nullability.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,3 +128,27 @@ But for now you can do:
128128
// [in a projection]
129129
aNullableString = TypedProjections.`null`(),
130130
```
131+
132+
## `addIsNotNull` returns a non-nullable column
133+
134+
The `addIsNotNull` method not only adds an `IS NOT NULL` criterion to the query, but also returns a non-nullable version of the column. This is useful when
135+
you want to use the column in a projection after filtering out nulls — the returned column has the nullability stripped from its type parameter.
136+
137+
```kotlin
138+
session.project(BookTable) { books ->
139+
// books.notes is ColumnDef<String?>, but after addIsNotNull it becomes ColumnDef<String>
140+
val notes = addIsNotNull(books.notes)
141+
project(YawnProjections.pair(books.name, notes))
142+
}
143+
```
144+
145+
This works with both regular columns and foreign key join columns:
146+
147+
```kotlin
148+
session.project(BookTable) { books ->
149+
// books.publisher is JoinColumnDefWithForeignKey<*, *, Publisher?>
150+
// publisherFk is ColumnDef<Long> (non-nullable FK)
151+
val publisherFk = addIsNotNull(books.publisher)
152+
project(YawnProjections.pair(books.name, publisherFk))
153+
}
154+
```

yawn-api/src/main/kotlin/com/faire/yawn/criteria/query/TypeSafeCriteriaWithWhere.kt

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -293,14 +293,16 @@ sealed interface TypeSafeCriteriaWithWhere<SOURCE : Any, T : Any> {
293293

294294
fun <F> addIsNotNull(
295295
column: YawnTableDef<SOURCE, *>.JoinColumnDefWithForeignKey<*, *, F>,
296-
) {
297-
addIsNotNull(column.foreignKey)
296+
): YawnDef<SOURCE, *>.YawnColumnDef<F & Any> {
297+
return addIsNotNull(column.foreignKey)
298298
}
299299

300300
fun <F> addIsNotNull(
301301
column: YawnDef<SOURCE, *>.YawnColumnDef<F>,
302-
) {
302+
): YawnDef<SOURCE, *>.YawnColumnDef<F & Any> {
303303
add(YawnRestrictions.isNotNull(column))
304+
@Suppress("UNCHECKED_CAST")
305+
return column as YawnDef<SOURCE, *>.YawnColumnDef<F & Any>
304306
}
305307

306308
fun <F> addIsNull(

yawn-database-test/src/test/kotlin/com/faire/yawn/database/YawnCriterionTest.kt

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.faire.yawn.database
22

3+
import com.faire.yawn.project.YawnProjections
34
import com.faire.yawn.query.YawnRestrictions.and
45
import com.faire.yawn.query.YawnRestrictions.between
56
import com.faire.yawn.query.YawnRestrictions.eq
@@ -645,6 +646,38 @@ internal class YawnCriterionTest : BaseYawnDatabaseTest() {
645646
}
646647
}
647648

649+
@Test
650+
fun `addIsNotNull filters to non-null rows`() {
651+
transactor.open { session ->
652+
val results = session.project(BookTable) { books ->
653+
val notes = addIsNotNull(books.notes)
654+
addLike(notes, "Note for The Hobbit and Harry Potter")
655+
orderAsc(books.name)
656+
project(books.name)
657+
}.list()
658+
659+
assertThat(results).containsExactly("Harry Potter", "The Hobbit")
660+
}
661+
}
662+
663+
@Test
664+
fun `addIsNotNull with FK column filters to non-null rows`() {
665+
transactor.open { session ->
666+
val publisherIdMap = session.project(PublisherTable) { publishers ->
667+
project(YawnProjections.pair(publishers.name, publishers.id))
668+
}.set().toMap()
669+
670+
val results = session.project(BookTable) { books ->
671+
val publisherFk = addIsNotNull(books.publisher)
672+
addEq(publisherFk, publisherIdMap.getValue("Penguin"))
673+
orderAsc(books.name)
674+
project(books.name)
675+
}.list()
676+
677+
assertThat(results).containsExactly("Harry Potter", "The Emperor's New Clothes")
678+
}
679+
}
680+
648681
@Test
649682
fun `addAndOfNotNull filters out null criteria`() {
650683
transactor.open { session ->

yawn-database-test/src/test/kotlin/com/faire/yawn/database/YawnProjectionTest.kt

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -773,9 +773,59 @@ internal class YawnProjectionTest : BaseYawnDatabaseTest() {
773773
}
774774
}
775775

776+
@Test
777+
fun `project non-null column with addIsNotNull`() {
778+
transactor.open { session ->
779+
// books.notes is String? — addIsNotNull filters to non-null rows and returns ColumnDef<String>
780+
val results = session.project(BookTable) { books ->
781+
val notes = addIsNotNull(books.notes)
782+
orderAsc(books.name)
783+
project(YawnProjectionTest_BookNameAndNotesProjection.create(books.name, notes))
784+
}.list()
785+
786+
// Only books with non-null notes: Harry Potter, Lord of the Rings, The Hobbit
787+
assertThat(results).containsExactly(
788+
BookNameAndNotes(name = "Harry Potter", notes = "Note for The Hobbit and Harry Potter"),
789+
BookNameAndNotes(name = "Lord of the Rings", notes = "Note for Lord of the Rings"),
790+
BookNameAndNotes(name = "The Hobbit", notes = "Note for The Hobbit and Harry Potter"),
791+
)
792+
}
793+
}
794+
795+
@Test
796+
fun `project non-null FK column with addIsNotNull`() {
797+
transactor.open { session ->
798+
val publisherIdMap = session.project(PublisherTable) { publishers ->
799+
project(YawnProjections.pair(publishers.name, publishers.id))
800+
}.set().toMap()
801+
802+
// books.publisher is Publisher? — addIsNotNull on FK should filter to non-null rows and return non-null
803+
// FK column
804+
val results = session.project(BookTable) { books ->
805+
val publisherFk = addIsNotNull(books.publisher)
806+
orderAsc(books.name)
807+
project(YawnProjections.pair(books.name, publisherFk))
808+
}.list()
809+
810+
// 4 books have publishers: Harry Potter, Lord of the Rings, The Emperor's New Clothes, The Hobbit
811+
assertThat(results).containsExactly(
812+
"Harry Potter" to publisherIdMap.getValue("Penguin"),
813+
"Lord of the Rings" to publisherIdMap.getValue("HarperCollins"),
814+
"The Emperor's New Clothes" to publisherIdMap.getValue("Penguin"),
815+
"The Hobbit" to publisherIdMap.getValue("Random House"),
816+
)
817+
}
818+
}
819+
776820
@YawnProjection
777821
internal data class BookStatistics(
778822
val totalBooks: Long,
779823
val totalPages: Long,
780824
)
825+
826+
@YawnProjection
827+
internal data class BookNameAndNotes(
828+
val name: String,
829+
val notes: String,
830+
)
781831
}

0 commit comments

Comments
 (0)