diff --git a/yawn-api/src/main/kotlin/com/faire/yawn/project/YawnProjections.kt b/yawn-api/src/main/kotlin/com/faire/yawn/project/YawnProjections.kt index 16c94b8..278e739 100644 --- a/yawn-api/src/main/kotlin/com/faire/yawn/project/YawnProjections.kt +++ b/yawn-api/src/main/kotlin/com/faire/yawn/project/YawnProjections.kt @@ -1,6 +1,7 @@ package com.faire.yawn.project import com.faire.yawn.YawnDef +import com.faire.yawn.project.YawnProjections.mapping import com.faire.yawn.query.YawnCompilationContext import org.hibernate.criterion.Projection import org.hibernate.criterion.Projections @@ -240,20 +241,9 @@ object YawnProjections { } internal class PairProjection( - private val firstProjection: YawnQueryProjection, - private val secondProjection: YawnQueryProjection, - ) : YawnQueryProjection> { - override fun compile(context: YawnCompilationContext): Projection { - return Projections.projectionList() - .add(firstProjection.compile(context)) - .add(secondProjection.compile(context)) - } - - override fun project(value: Any?): Pair { - val queryResult = value as Array<*> - return Pair(firstProjection.project(queryResult[0]), secondProjection.project(queryResult[1])) - } - } + firstProjection: YawnQueryProjection, + secondProjection: YawnQueryProjection, + ) : Mapping2Projection>(firstProjection, secondProjection, { a, b -> Pair(a, b) }) fun pair( firstProjection: YawnQueryProjection, @@ -263,33 +253,97 @@ object YawnProjections { } internal class TripleProjection( - private val firstProjection: YawnQueryProjection, - private val secondProjection: YawnQueryProjection, - private val thirdProjection: YawnQueryProjection, - ) : YawnQueryProjection> { + firstProjection: YawnQueryProjection, + secondProjection: YawnQueryProjection, + thirdProjection: YawnQueryProjection, + ) : Mapping3Projection>( + firstProjection, + secondProjection, + thirdProjection, + { a, b, c -> Triple(a, b, c) }, + ) + + fun triple( + firstProjection: YawnQueryProjection, + secondProjection: YawnQueryProjection, + thirdProjection: YawnQueryProjection, + ): YawnQueryProjection> { + return TripleProjection(firstProjection, secondProjection, thirdProjection) + } + + /** + * Provides an in-memory transformation over a column value to a different type. + * Use this when using more complex data classes as projections to apply minor + * type or value compliance transformations to database column values + * while keeping your projection classes type-safe, without needing to use + * intermediary representations. + * NOTE: this _does not_ change the query and is post-processed in memory. + */ + fun mapping( + column: YawnQueryProjection, + transform: (FROM) -> TO, + ): YawnQueryProjection { + return Mapping1Projection(column, transform) + } + + /** + * A 2-arity version of the [mapping] method. + */ + fun mapping( + column1: YawnQueryProjection, + column2: YawnQueryProjection, + transform: (C1, C2) -> TO, + ): YawnQueryProjection { + return Mapping2Projection(column1, column2, transform) + } + + internal open class Mapping1Projection( + private val column: YawnQueryProjection, + private val transform: (FROM) -> TO, + ) : YawnQueryProjection { + override fun compile(context: YawnCompilationContext): Projection = column.compile(context) + + override fun project(value: Any?): TO = transform(column.project(value)) + } + + internal open class Mapping2Projection( + private val column1: YawnQueryProjection, + private val column2: YawnQueryProjection, + private val transform: (C1, C2) -> TO, + ) : YawnQueryProjection { override fun compile(context: YawnCompilationContext): Projection { return Projections.projectionList() - .add(firstProjection.compile(context)) - .add(secondProjection.compile(context)) - .add(thirdProjection.compile(context)) + .add(column1.compile(context)) + .add(column2.compile(context)) } - override fun project(value: Any?): Triple { + override fun project(value: Any?): TO { val queryResult = value as Array<*> - return Triple( - firstProjection.project(queryResult[0]), - secondProjection.project(queryResult[1]), - thirdProjection.project(queryResult[2]), - ) + return transform(column1.project(queryResult[0]), column2.project(queryResult[1])) } } - fun triple( - firstProjection: YawnQueryProjection, - secondProjection: YawnQueryProjection, - thirdProjection: YawnQueryProjection, - ): YawnQueryProjection> { - return TripleProjection(firstProjection, secondProjection, thirdProjection) + internal open class Mapping3Projection( + private val column1: YawnQueryProjection, + private val column2: YawnQueryProjection, + private val column3: YawnQueryProjection, + private val transform: (C1, C2, C3) -> TO, + ) : YawnQueryProjection { + override fun compile(context: YawnCompilationContext): Projection { + return Projections.projectionList() + .add(column1.compile(context)) + .add(column2.compile(context)) + .add(column3.compile(context)) + } + + override fun project(value: Any?): TO { + val queryResult = value as Array<*> + return transform( + column1.project(queryResult[0]), + column2.project(queryResult[1]), + column3.project(queryResult[2]), + ) + } } } diff --git a/yawn-database-test/src/test/kotlin/com/faire/yawn/database/YawnProjectionMappingTest.kt b/yawn-database-test/src/test/kotlin/com/faire/yawn/database/YawnProjectionMappingTest.kt new file mode 100644 index 0000000..996bc03 --- /dev/null +++ b/yawn-database-test/src/test/kotlin/com/faire/yawn/database/YawnProjectionMappingTest.kt @@ -0,0 +1,38 @@ +package com.faire.yawn.database + +import com.faire.yawn.project.YawnProjection +import com.faire.yawn.project.YawnProjections +import com.faire.yawn.setup.entities.BookTable +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +internal class YawnProjectionMappingTest : BaseYawnDatabaseTest() { + @Test + fun `yawn query with projection`() { + transactor.open { session -> + val hobbit = session.project(BookTable) { books -> + addEq(books.name, "The Hobbit") + val authors = join(books.author) + project( + YawnProjectionMappingTest_BookNameAndNotesProjection.create( + uppercaseTitle = YawnProjections.mapping(books.name) { it.uppercase() }, + authorNotes = YawnProjections.mapping(authors.name, books.notes) { author, notes -> + "$author says: $notes" + }, + ), + ) + }.uniqueResult() + + with(hobbit!!) { + assertThat(uppercaseTitle).isEqualTo("THE HOBBIT") + assertThat(authorNotes).isEqualTo("J.R.R. Tolkien says: J.R.R. Tolkien") + } + } + } + + @YawnProjection + internal data class BookNameAndNotes( + val uppercaseTitle: String, + val authorNotes: String, + ) +}