diff --git a/yawn-api/src/main/kotlin/com/faire/yawn/YawnDef.kt b/yawn-api/src/main/kotlin/com/faire/yawn/YawnDef.kt index fac946e..23a745c 100644 --- a/yawn-api/src/main/kotlin/com/faire/yawn/YawnDef.kt +++ b/yawn-api/src/main/kotlin/com/faire/yawn/YawnDef.kt @@ -1,9 +1,8 @@ package com.faire.yawn -import com.faire.yawn.project.YawnQueryProjection +import com.faire.yawn.project.ProjectionNode +import com.faire.yawn.project.YawnValueProjector import com.faire.yawn.query.YawnCompilationContext -import org.hibernate.criterion.Projection -import org.hibernate.criterion.Projections /** * A Yawn definition that can be queried, i.e. either a [YawnTableDef] or a [com.faire.yawn.project.YawnProjectionDef]. @@ -17,22 +16,18 @@ abstract class YawnDef { * Base class for all Yawn Column-like definitions. * This can be either a column from a table or a projection. * + * Implements [YawnValueProjector] so that columns can be used directly as projections + * in both the v2 [ProjectionNode] tree and as arguments to [com.faire.yawn.project.YawnProjections] methods. + * * @param F the type of the column. */ - abstract inner class YawnColumnDef : YawnQueryProjection { + abstract inner class YawnColumnDef : YawnValueProjector { abstract fun generatePath(context: YawnCompilationContext): String open fun adaptValue(value: F): Any? { return value } - override fun compile(context: YawnCompilationContext): Projection { - return Projections.property(generatePath(context)) - } - - override fun project(value: Any?): F { - @Suppress("UNCHECKED_CAST") - return value as F - } + override fun projection(): ProjectionNode.Value = ProjectionNode.property(this) } } diff --git a/yawn-api/src/main/kotlin/com/faire/yawn/criteria/query/ProjectedTypeSafeCriteriaQuery.kt b/yawn-api/src/main/kotlin/com/faire/yawn/criteria/query/ProjectedTypeSafeCriteriaQuery.kt index 8671f64..ae8aaac 100644 --- a/yawn-api/src/main/kotlin/com/faire/yawn/criteria/query/ProjectedTypeSafeCriteriaQuery.kt +++ b/yawn-api/src/main/kotlin/com/faire/yawn/criteria/query/ProjectedTypeSafeCriteriaQuery.kt @@ -1,6 +1,9 @@ package com.faire.yawn.criteria.query import com.faire.yawn.YawnTableDef +import com.faire.yawn.project.ProjectorResolver +import com.faire.yawn.project.ResolvedProjectionAdapter +import com.faire.yawn.project.YawnProjector import com.faire.yawn.project.YawnQueryProjection import com.faire.yawn.query.YawnQuery @@ -36,6 +39,10 @@ private constructor( projection: YawnQueryProjection, ): YawnQueryProjection { ensureUniqueProjection() + if (projection is YawnProjector) { + val resolved = ProjectorResolver().resolve(projection) + return ResolvedProjectionAdapter(resolved) + } return projection } diff --git a/yawn-api/src/main/kotlin/com/faire/yawn/project/YawnCompositeQueryProjection.kt b/yawn-api/src/main/kotlin/com/faire/yawn/project/YawnCompositeQueryProjection.kt deleted file mode 100644 index 83db49f..0000000 --- a/yawn-api/src/main/kotlin/com/faire/yawn/project/YawnCompositeQueryProjection.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.faire.yawn.project - -import com.faire.yawn.query.YawnCompilationContext -import org.hibernate.criterion.Projection -import org.hibernate.criterion.Projections - -/** - * A composite implementation of [YawnQueryProjection] that allows you to specify both a list of projections - * and the mapper function. - */ -class YawnCompositeQueryProjection( - private val projections: List>, - private val mapper: (Any?) -> TO, -) : YawnQueryProjection { - - constructor(vararg projections: YawnQueryProjection, mapper: (Any?) -> TO) : this( - projections = projections.toList(), - mapper = mapper, - ) - - override fun compile(context: YawnCompilationContext): Projection = Projections.projectionList().apply { - for (projection in projections) { - add(projection.compile(context)) - } - } - - override fun project(value: Any?): TO { - return mapper(value) - } -} 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 a222cde..e30f76d 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,295 +1,138 @@ package com.faire.yawn.project import com.faire.yawn.YawnDef -import com.faire.yawn.query.YawnCompilationContext -import org.hibernate.criterion.Projection -import org.hibernate.criterion.Projections -import org.hibernate.type.StandardBasicTypes +import com.faire.yawn.project.AggregateKind.AVG +import com.faire.yawn.project.AggregateKind.COUNT +import com.faire.yawn.project.AggregateKind.COUNT_DISTINCT +import com.faire.yawn.project.AggregateKind.GROUP_BY +import com.faire.yawn.project.AggregateKind.MAX +import com.faire.yawn.project.AggregateKind.MIN +import com.faire.yawn.project.AggregateKind.SUM /** - * Yawn equivalent of Hibernate [Projections]. - * A utility object to create type-safe [YawnQueryProjection]. + * Yawn's projection factory object. This is the type-safe equivalent of Hibernate's `Projections`. + * + * All methods produce [YawnProjector] instances that describe projections as [ProjectionNode] trees. + * The [ProjectorResolver] flattens these trees into [ResolvedProjection]s at query build time, + * and [ResolvedProjectionAdapter] bridges them to Hibernate for execution. */ object YawnProjections { - internal class Distinct( - private val projection: YawnQueryProjection, - ) : YawnQueryProjection { - override fun compile( - context: YawnCompilationContext, - ): Projection = Projections.distinct(projection.compile(context)) - - override fun project(value: Any?): TO = projection.project(value) - } - - fun distinct( - projection: YawnQueryProjection, - ): YawnQueryProjection { - return Distinct(projection) - } - - internal class Count( - private val columnDef: YawnDef.YawnColumnDef, - ) : YawnQueryProjection { - override fun compile( - context: YawnCompilationContext, - ): Projection = Projections.count(columnDef.generatePath(context)) - - override fun project(value: Any?): Long = value as Long - } - fun count( columnDef: YawnDef.YawnColumnDef, - ): YawnQueryProjection { - return Count(columnDef) - } - - internal class CountDistinct( - private val columnDef: YawnDef.YawnColumnDef, - ) : YawnQueryProjection { - override fun compile( - context: YawnCompilationContext, - ): Projection = Projections.countDistinct(columnDef.generatePath(context)) - - override fun project(value: Any?): Long = value as Long + ): YawnProjector { + return YawnValueProjector { ProjectionNode.aggregateAs(COUNT, columnDef) } } fun countDistinct( columnDef: YawnDef.YawnColumnDef, - ): YawnQueryProjection { - return CountDistinct(columnDef) - } - - internal class SumNullable( - private val columnDef: YawnDef.YawnColumnDef, - ) : YawnQueryProjection { - override fun compile( - context: YawnCompilationContext, - ): Projection = Projections.sum(columnDef.generatePath(context)) - - override fun project(value: Any?): Long? = value as Long? + ): YawnProjector { + return YawnValueProjector { ProjectionNode.aggregateAs(COUNT_DISTINCT, columnDef) } } @JvmName("sumNullable") fun sum( columnDef: YawnDef.YawnColumnDef, - ): YawnQueryProjection { - return SumNullable(columnDef) - } - - internal class Sum( - private val columnDef: YawnDef.YawnColumnDef, - ) : YawnQueryProjection { - override fun compile( - context: YawnCompilationContext, - ): Projection = Projections.sum(columnDef.generatePath(context)) - - override fun project(value: Any?): Long = value as Long + ): YawnProjector { + return YawnValueProjector { ProjectionNode.aggregateAs(SUM, columnDef) } } fun sum( columnDef: YawnDef.YawnColumnDef, - ): YawnQueryProjection { - return Sum(columnDef) - } - - internal class AvgNullable( - private val columnDef: YawnDef.YawnColumnDef, - ) : YawnQueryProjection { - override fun compile( - context: YawnCompilationContext, - ): Projection = Projections.avg(columnDef.generatePath(context)) - - override fun project(value: Any?): Double? = value as Double? + ): YawnProjector { + return YawnValueProjector { ProjectionNode.aggregateAs(SUM, columnDef) } } @JvmName("avgNullable") fun avg( columnDef: YawnDef.YawnColumnDef, - ): YawnQueryProjection { - return AvgNullable(columnDef) - } - - internal class Avg( - private val columnDef: YawnDef.YawnColumnDef, - ) : YawnQueryProjection { - override fun compile( - context: YawnCompilationContext, - ): Projection = Projections.avg(columnDef.generatePath(context)) - - override fun project(value: Any?): Double = value as Double + ): YawnProjector { + return YawnValueProjector { ProjectionNode.aggregateAs(AVG, columnDef) } } fun avg( columnDef: YawnDef.YawnColumnDef, - ): YawnQueryProjection { - return Avg(columnDef) - } - - internal class Max?>( - private val columnDef: YawnDef.YawnColumnDef, - ) : YawnQueryProjection { - override fun compile( - context: YawnCompilationContext, - ): Projection = Projections.max(columnDef.generatePath(context)) - - @Suppress("UNCHECKED_CAST") - override fun project(value: Any?): FROM = value as FROM + ): YawnProjector { + return YawnValueProjector { ProjectionNode.aggregateAs(AVG, columnDef) } } fun ?> max( columnDef: YawnDef.YawnColumnDef, - ): YawnQueryProjection { - return Max(columnDef) - } - - internal class Min?>( - private val columnDef: YawnDef.YawnColumnDef, - ) : YawnQueryProjection { - override fun compile( - context: YawnCompilationContext, - ): Projection = Projections.min(columnDef.generatePath(context)) - - @Suppress("UNCHECKED_CAST") - override fun project(value: Any?): FROM = value as FROM + ): YawnProjector { + return YawnValueProjector { ProjectionNode.aggregate(MAX, columnDef) } } fun ?> min( columnDef: YawnDef.YawnColumnDef, - ): YawnQueryProjection { - return Min(columnDef) - } - - internal class GroupBy( - private val columnDef: YawnDef.YawnColumnDef, - ) : YawnQueryProjection { - override fun compile( - context: YawnCompilationContext, - ): Projection = Projections.groupProperty(columnDef.generatePath(context)) - - @Suppress("UNCHECKED_CAST") - override fun project(value: Any?): FROM = value as FROM + ): YawnProjector { + return YawnValueProjector { ProjectionNode.aggregate(MIN, columnDef) } } fun groupBy( columnDef: YawnDef.YawnColumnDef, - ): YawnQueryProjection { - return GroupBy(columnDef) - } - - internal class RowCount : YawnQueryProjection { - override fun compile(context: YawnCompilationContext): Projection = Projections.rowCount() - - override fun project(value: Any?): Long = value as Long + ): YawnProjector { + return YawnValueProjector { ProjectionNode.aggregate(GROUP_BY, columnDef) } } - fun rowCount(): YawnQueryProjection { - return RowCount() + fun rowCount(): YawnProjector { + return YawnValueProjector { ProjectionNode.rowCount() } } - internal class SelectConstant( - private val constant: String, - ) : YawnQueryProjection { - override fun compile(context: YawnCompilationContext): Projection { - return Projections.sqlProjection( - "'$constant' as $CONSTANT_ALIAS", - arrayOf(CONSTANT_ALIAS), - arrayOf(StandardBasicTypes.STRING), + fun selectConstant(constant: String): YawnProjector { + return YawnValueProjector { + ProjectionNode.sql( + sqlExpression = "'$constant' as $CONSTANT_ALIAS", + aliases = listOf(CONSTANT_ALIAS), + resultTypes = listOf(String::class), ) } - - override fun project(value: Any?): String = value as String - } - - fun selectConstant(constant: String): YawnQueryProjection { - return SelectConstant(constant) - } - - internal class Coalesce( - private val projection: YawnQueryProjection, - private val defaultValue: FROM, - ) : YawnQueryProjection { - override fun compile( - context: YawnCompilationContext, - ): Projection = projection.compile(context) - - @Suppress("UNCHECKED_CAST") - override fun project(value: Any?): FROM = value as FROM? ?: defaultValue - } - - fun coalesce( - projection: YawnQueryProjection, - defaultValue: FROM, - ): YawnQueryProjection { - return Coalesce(projection, defaultValue) } - internal class Null : YawnQueryProjection { - override fun compile(context: YawnCompilationContext): Projection { - return Projections.sqlProjection( - "null as $CONSTANT_ALIAS", - arrayOf(CONSTANT_ALIAS), - arrayOf(StandardBasicTypes.STRING), + fun `null`(): YawnProjector { + return YawnValueProjector { + ProjectionNode.sql( + sqlExpression = "null as $CONSTANT_ALIAS", + aliases = listOf(CONSTANT_ALIAS), + resultTypes = listOf(String::class), ) } - - override fun project(value: Any?): FROM? = null - } - - fun `null`(): YawnQueryProjection { - return Null() } - 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])) + fun distinct( + projection: YawnValueProjector, + ): YawnValueProjector { + return YawnValueProjector { + ProjectionNode.Value( + ProjectionLeaf.Modifier(ModifierKind.DISTINCT, projection.projection().leaf), + ) } } - fun pair( - firstProjection: YawnQueryProjection, - secondProjection: YawnQueryProjection, - ): YawnQueryProjection> { - return PairProjection(firstProjection, secondProjection) + fun coalesce( + projection: YawnProjector, + defaultValue: FROM, + ): YawnProjector { + return YawnProjector { ProjectionNode.mapped(projection) { it ?: defaultValue } } } - internal class TripleProjection( - private val firstProjection: YawnQueryProjection, - private val secondProjection: YawnQueryProjection, - private val thirdProjection: YawnQueryProjection, - ) : YawnQueryProjection> { - override fun compile(context: YawnCompilationContext): Projection { - return Projections.projectionList() - .add(firstProjection.compile(context)) - .add(secondProjection.compile(context)) - .add(thirdProjection.compile(context)) - } - - override fun project(value: Any?): Triple { - val queryResult = value as Array<*> - return Triple( - firstProjection.project(queryResult[0]), - secondProjection.project(queryResult[1]), - thirdProjection.project(queryResult[2]), - ) + fun pair( + firstProjection: YawnProjector, + secondProjection: YawnProjector, + ): YawnProjector> { + return YawnProjector { + ProjectionNode.composite(firstProjection, secondProjection) { a, b -> a to b } } } fun triple( - firstProjection: YawnQueryProjection, - secondProjection: YawnQueryProjection, - thirdProjection: YawnQueryProjection, - ): YawnQueryProjection> { - return TripleProjection(firstProjection, secondProjection, thirdProjection) + firstProjection: YawnProjector, + secondProjection: YawnProjector, + thirdProjection: YawnProjector, + ): YawnProjector> { + return YawnProjector { + ProjectionNode.composite(firstProjection, secondProjection, thirdProjection) { a, b, c -> + Triple(a, b, c) + } + } } } diff --git a/yawn-api/src/main/kotlin/com/faire/yawn/project/YawnProjector.kt b/yawn-api/src/main/kotlin/com/faire/yawn/project/YawnProjector.kt index d9709f4..b477b3d 100644 --- a/yawn-api/src/main/kotlin/com/faire/yawn/project/YawnProjector.kt +++ b/yawn-api/src/main/kotlin/com/faire/yawn/project/YawnProjector.kt @@ -1,5 +1,8 @@ package com.faire.yawn.project +import com.faire.yawn.query.YawnCompilationContext +import org.hibernate.criterion.Projection + /** * A type-safe projection descriptor that can be resolved into a flat list of * [ProjectionNode.Value] nodes for query compilation. @@ -9,20 +12,65 @@ package com.faire.yawn.project * flattening composites, eliminating constants and mapped transforms, deduplicating * identical leaves, and producing a [ResolvedProjection] that the query factory can compile. * + * Extends [YawnQueryProjection] so that projectors can be used anywhere a legacy projection is + * expected. The default [compile] and [project] implementations resolve through the v2 pipeline. + * In the query pipeline, [ProjectedTypeSafeCriteriaQuery.project] detects projectors and wraps + * them in a [ResolvedProjectionAdapter] once, avoiding repeated resolution. + * + * Note: this is a `fun interface` for ergonomic lambda syntax, but JVM SAM lambdas do NOT + * inherit default method implementations from super-interfaces. Production code that needs + * `compile()` or `project()` to work must use the [YawnValueProjector] or [YawnProjector] + * factory functions instead of SAM conversion. + * * @param SOURCE the type of the entity being queried. * @param TO the result type of this projection. */ -fun interface YawnProjector { +fun interface YawnProjector : YawnQueryProjection { fun projection(): ProjectionNode + + override fun compile(context: YawnCompilationContext): Projection { + return resolve().compile(context) + } + + override fun project(value: Any?): TO { + return resolve().project(value) + } + + private fun resolve(): ResolvedProjectionAdapter { + return ResolvedProjectionAdapter(ProjectorResolver().resolve(this)) + } } /** * A [YawnProjector] that is guaranteed to produce a [ProjectionNode.Value]. * - * This subtype exists mostly so that modifiers like distinct can enforce at compile time - * that they only wrap single-value projections, not composites. But it can be used for other - * contexts as needed. + * This subtype exists so that modifiers like distinct can enforce at compile time + * that they only wrap single-value projections, not composites. */ fun interface YawnValueProjector : YawnProjector { override fun projection(): ProjectionNode.Value } + +/** + * Factory function for [YawnProjector] that creates a concrete anonymous object + * instead of a SAM lambda. Avoids the JVM SAM default-method inheritance limitation. + */ +fun YawnProjector( + projection: () -> ProjectionNode, +): YawnProjector { + return object : YawnProjector { + override fun projection() = projection() + } +} + +/** + * Factory function for [YawnValueProjector] that creates a concrete anonymous object + * instead of a SAM lambda. Avoids the JVM SAM default-method inheritance limitation. + */ +fun YawnValueProjector( + projection: () -> ProjectionNode.Value, +): YawnValueProjector { + return object : YawnValueProjector { + override fun projection() = projection() + } +} diff --git a/yawn-database-test/src/test/kotlin/com/faire/yawn/database/ResolvedProjectionAdapterTest.kt b/yawn-database-test/src/test/kotlin/com/faire/yawn/database/ResolvedProjectionAdapterTest.kt index c41cc85..2e03525 100644 --- a/yawn-database-test/src/test/kotlin/com/faire/yawn/database/ResolvedProjectionAdapterTest.kt +++ b/yawn-database-test/src/test/kotlin/com/faire/yawn/database/ResolvedProjectionAdapterTest.kt @@ -10,8 +10,6 @@ import com.faire.yawn.project.AggregateKind.SUM import com.faire.yawn.project.ModifierKind.DISTINCT import com.faire.yawn.project.ProjectionLeaf import com.faire.yawn.project.ProjectionNode -import com.faire.yawn.project.ProjectorResolver -import com.faire.yawn.project.ResolvedProjectionAdapter import com.faire.yawn.project.YawnProjector import com.faire.yawn.project.YawnValueProjector import com.faire.yawn.query.YawnQueryOrder @@ -23,8 +21,12 @@ import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test /** - * Integration tests for [ResolvedProjectionAdapter], verifying that the new projection system - * produces correct SQL and result mapping when bridged into the existing Hibernate pipeline. + * Integration tests for the v2 projection pipeline, verifying that [YawnProjector] instances + * produce correct SQL and result mapping when resolved through [com.faire.yawn.project.ProjectorResolver] + * and bridged to Hibernate via [com.faire.yawn.project.ResolvedProjectionAdapter]. + * + * These tests exercise v2-specific features like [ProjectionNode.Composite], [ProjectionNode.Mapped], + * [ProjectionNode.Constant], and leaf deduplication. */ internal class ResolvedProjectionAdapterTest : BaseYawnDatabaseTest() { @Test @@ -32,7 +34,7 @@ internal class ResolvedProjectionAdapterTest : BaseYawnDatabaseTest() { transactor.open { session -> val languages = session.project(BookTable) { books -> addEq(books.name, "The Hobbit") - project(adapt(YawnValueProjector { ProjectionNode.property(books.originalLanguage) })) + project(YawnValueProjector { ProjectionNode.property(books.originalLanguage) }) }.list() assertThat(languages).containsOnly(ENGLISH) @@ -45,7 +47,7 @@ internal class ResolvedProjectionAdapterTest : BaseYawnDatabaseTest() { val sum = session.project(BookTable) { books -> val authors = join(books.author) addEq(authors.name, "J.R.R. Tolkien") - project(adapt(YawnValueProjector { ProjectionNode.aggregate(SUM, books.numberOfPages) })) + project(YawnValueProjector { ProjectionNode.aggregate(SUM, books.numberOfPages) }) }.uniqueResult()!! assertThat(sum).isEqualTo(1_300L) @@ -58,7 +60,7 @@ internal class ResolvedProjectionAdapterTest : BaseYawnDatabaseTest() { val count = session.project(BookTable) { books -> val authors = join(books.author) addEq(authors.name, "Hans Christian Andersen") - project(adapt(YawnValueProjector { ProjectionNode.aggregateAs(COUNT, books.id) })) + project(YawnValueProjector { ProjectionNode.aggregateAs(COUNT, books.id) }) }.uniqueResult()!! assertThat(count).isEqualTo(3L) @@ -72,7 +74,7 @@ internal class ResolvedProjectionAdapterTest : BaseYawnDatabaseTest() { val authors = join(books.author) addEq(authors.name, "Hans Christian Andersen") project( - adapt(YawnValueProjector { ProjectionNode.aggregateAs(COUNT_DISTINCT, authors.name) }), + YawnValueProjector { ProjectionNode.aggregateAs(COUNT_DISTINCT, authors.name) }, ) }.uniqueResult()!! @@ -87,7 +89,7 @@ internal class ResolvedProjectionAdapterTest : BaseYawnDatabaseTest() { val authors = join(books.author) addEq(authors.name, "J.R.R. Tolkien") project( - adapt(YawnValueProjector { ProjectionNode.aggregateAs(AVG, books.numberOfPages) }), + YawnValueProjector { ProjectionNode.aggregateAs(AVG, books.numberOfPages) }, ) }.uniqueResult()!! @@ -99,12 +101,12 @@ internal class ResolvedProjectionAdapterTest : BaseYawnDatabaseTest() { fun `aggregate - min and max`() { transactor.open { session -> val min = session.project(BookTable) { books -> - project(adapt(YawnValueProjector { ProjectionNode.aggregate(MIN, books.numberOfPages) })) + project(YawnValueProjector { ProjectionNode.aggregate(MIN, books.numberOfPages) }) }.uniqueResult()!! assertThat(min).isEqualTo(100L) val max = session.project(BookTable) { books -> - project(adapt(YawnValueProjector { ProjectionNode.aggregate(MAX, books.numberOfPages) })) + project(YawnValueProjector { ProjectionNode.aggregate(MAX, books.numberOfPages) }) }.uniqueResult()!! assertThat(max).isEqualTo(1_000L) } @@ -114,7 +116,7 @@ internal class ResolvedProjectionAdapterTest : BaseYawnDatabaseTest() { fun `row count`() { transactor.open { session -> val count = session.project(BookTable) { - project(adapt(YawnValueProjector { ProjectionNode.rowCount() })) + project(YawnValueProjector { ProjectionNode.rowCount() }) }.uniqueResult()!! assertThat(count).isEqualTo(6L) @@ -127,13 +129,11 @@ internal class ResolvedProjectionAdapterTest : BaseYawnDatabaseTest() { val authors = session.project(BookTable) { books -> val authors = join(books.author) project( - adapt( - YawnValueProjector { - ProjectionNode.Value( - ProjectionLeaf.Modifier(DISTINCT, ProjectionLeaf.Property(authors.name)), - ) - }, - ), + YawnValueProjector { + ProjectionNode.Value( + ProjectionLeaf.Modifier(DISTINCT, ProjectionLeaf.Property(authors.name)), + ) + }, ) }.list() @@ -152,7 +152,7 @@ internal class ResolvedProjectionAdapterTest : BaseYawnDatabaseTest() { val authors = join(books.author) addEq(authors.name, "J.K. Rowling") project( - adapt { + YawnProjector { ProjectionNode.composite( YawnValueProjector { ProjectionNode.property(authors.name) }, YawnValueProjector { ProjectionNode.aggregate(SUM, books.numberOfPages) }, @@ -171,7 +171,7 @@ internal class ResolvedProjectionAdapterTest : BaseYawnDatabaseTest() { val results = session.project(BookTable) { books -> val authors = join(books.author) project( - adapt { + YawnProjector { ProjectionNode.composite( YawnValueProjector { ProjectionNode.aggregate(GROUP_BY, authors.name) }, YawnValueProjector { ProjectionNode.aggregateAs(COUNT, books.name) }, @@ -194,7 +194,7 @@ internal class ResolvedProjectionAdapterTest : BaseYawnDatabaseTest() { val results = session.project(BookTable) { books -> addEq(books.name, "The Hobbit") project( - adapt { + YawnProjector { ProjectionNode.composite( YawnValueProjector { ProjectionNode.property(books.name) }, { ProjectionNode.constant("hardcoded") }, @@ -219,7 +219,7 @@ internal class ResolvedProjectionAdapterTest : BaseYawnDatabaseTest() { val authors = join(books.author) addEq(authors.name, "J.R.R. Tolkien") project( - adapt { + YawnProjector { ProjectionNode.composite( YawnValueProjector { ProjectionNode.aggregate(SUM, books.numberOfPages) }, YawnValueProjector { ProjectionNode.aggregate(SUM, books.numberOfPages) }, @@ -237,15 +237,13 @@ internal class ResolvedProjectionAdapterTest : BaseYawnDatabaseTest() { transactor.open { session -> val results = session.project(BookTable) { project( - adapt( - YawnValueProjector { - ProjectionNode.sql( - sqlExpression = "COUNT(*) AS total", - aliases = listOf("total"), - resultTypes = listOf(Long::class), - ) - }, - ), + YawnValueProjector { + ProjectionNode.sql( + sqlExpression = "COUNT(*) AS total", + aliases = listOf("total"), + resultTypes = listOf(Long::class), + ) + }, ) }.uniqueResult()!! @@ -260,7 +258,7 @@ internal class ResolvedProjectionAdapterTest : BaseYawnDatabaseTest() { val authors = join(books.author) addEq(authors.name, "J.K. Rowling") project( - adapt { + YawnProjector { ProjectionNode.composite( YawnValueProjector { ProjectionNode.property(books.name) }, YawnValueProjector { ProjectionNode.property(authors.name) }, @@ -280,14 +278,14 @@ internal class ResolvedProjectionAdapterTest : BaseYawnDatabaseTest() { val results1 = session.project(BookTable) { books -> val authors = join(books.author) addEq(authors.name, "J.R.R. Tolkien") - project(adapt(YawnValueProjector { ProjectionNode.property(authors.name) })) + project(YawnValueProjector { ProjectionNode.property(authors.name) }) }.set() assertThat(results1).containsExactlyInAnyOrder("J.R.R. Tolkien") val results2 = session.project(BookTable) { books -> addLike(books.name, "The %") val authors = join(books.author) - project(adapt(YawnValueProjector { ProjectionNode.property(authors.name) })) + project(YawnValueProjector { ProjectionNode.property(authors.name) }) }.set() assertThat(results2).containsExactlyInAnyOrder("J.R.R. Tolkien", "Hans Christian Andersen") } @@ -298,7 +296,7 @@ internal class ResolvedProjectionAdapterTest : BaseYawnDatabaseTest() { transactor.open { session -> val resultsAsc = session.project(BookTable) { books -> orderAsc(books.name) - project(adapt(YawnValueProjector { ProjectionNode.property(books.name) })) + project(YawnValueProjector { ProjectionNode.property(books.name) }) }.list() assertThat(resultsAsc).containsExactly( @@ -312,7 +310,7 @@ internal class ResolvedProjectionAdapterTest : BaseYawnDatabaseTest() { val resultsDesc = session.project(BookTable) { books -> orderDesc(books.name) - project(adapt(YawnValueProjector { ProjectionNode.property(books.name) })) + project(YawnValueProjector { ProjectionNode.property(books.name) }) }.list() assertThat(resultsDesc).containsExactly( @@ -328,7 +326,7 @@ internal class ResolvedProjectionAdapterTest : BaseYawnDatabaseTest() { val authors = join(books.author) order(YawnQueryOrder.asc(authors.name), YawnQueryOrder.desc(books.name)) project( - adapt { + YawnProjector { ProjectionNode.composite( YawnValueProjector { ProjectionNode.property(authors.name) }, YawnValueProjector { ProjectionNode.property(books.name) }, @@ -353,7 +351,7 @@ internal class ResolvedProjectionAdapterTest : BaseYawnDatabaseTest() { transactor.open { session -> val publisherIdMap = session.project(PublisherTable) { publishers -> project( - adapt { + YawnProjector { ProjectionNode.composite( YawnValueProjector { ProjectionNode.property(publishers.name) }, YawnValueProjector { ProjectionNode.property(publishers.id) }, @@ -365,7 +363,7 @@ internal class ResolvedProjectionAdapterTest : BaseYawnDatabaseTest() { val publisherWithThe = session.project(BookTable) { books -> addLike(books.name, "The %") addIsNotNull(books.publisher.foreignKey) - project(adapt(YawnValueProjector { ProjectionNode.property(books.publisher.foreignKey) })) + project(YawnValueProjector { ProjectionNode.property(books.publisher.foreignKey) }) }.set() assertThat(publisherWithThe) .containsExactlyInAnyOrder( @@ -383,7 +381,7 @@ internal class ResolvedProjectionAdapterTest : BaseYawnDatabaseTest() { val authors = join(books.author) addIn(authors.name, *authorNames) project( - adapt { + YawnProjector { ProjectionNode.composite( YawnValueProjector { ProjectionNode.aggregate(GROUP_BY, books.originalLanguage) }, YawnValueProjector { ProjectionNode.aggregateAs(SUM, books.rating) }, @@ -417,7 +415,7 @@ internal class ResolvedProjectionAdapterTest : BaseYawnDatabaseTest() { val authors = join(books.author) addIn(authors.name, *authorNames) project( - adapt { + YawnProjector { ProjectionNode.composite( YawnValueProjector { ProjectionNode.aggregate(GROUP_BY, books.originalLanguage) }, YawnValueProjector { ProjectionNode.aggregateAs(AVG, books.rating) }, @@ -450,7 +448,7 @@ internal class ResolvedProjectionAdapterTest : BaseYawnDatabaseTest() { addIn(books.name, setOf("The Hobbit", "The Little Mermaid")) val authors = join(books.author) project( - adapt { + YawnProjector { ProjectionNode.composite( YawnValueProjector { ProjectionNode.property(books.name) }, { ProjectionNode.constant(null) }, @@ -474,7 +472,7 @@ internal class ResolvedProjectionAdapterTest : BaseYawnDatabaseTest() { addIn(books.name, setOf("The Hobbit", "The Little Mermaid")) orderAsc(books.name) project( - adapt { + YawnProjector { ProjectionNode.composite( YawnValueProjector { ProjectionNode.property(books.name) }, { @@ -500,7 +498,7 @@ internal class ResolvedProjectionAdapterTest : BaseYawnDatabaseTest() { val tolkienStats = session.query(BookTable) .applyProjection { books -> project( - adapt { + YawnProjector { ProjectionNode.composite( YawnValueProjector { ProjectionNode.aggregateAs(COUNT, books.id) }, YawnValueProjector { ProjectionNode.aggregate(SUM, books.numberOfPages) }, @@ -520,7 +518,7 @@ internal class ResolvedProjectionAdapterTest : BaseYawnDatabaseTest() { val multipleAuthorsStats = session.query(BookTable) .applyProjection { books -> project( - adapt { + YawnProjector { ProjectionNode.composite( YawnValueProjector { ProjectionNode.aggregateAs(COUNT, books.id) }, YawnValueProjector { ProjectionNode.aggregate(SUM, books.numberOfPages) }, @@ -545,7 +543,7 @@ internal class ResolvedProjectionAdapterTest : BaseYawnDatabaseTest() { val stats = session.query(BookTable) .applyProjection { books -> project( - adapt { + YawnProjector { ProjectionNode.composite( YawnValueProjector { ProjectionNode.aggregateAs(COUNT, books.id) }, YawnValueProjector { ProjectionNode.aggregate(SUM, books.numberOfPages) }, @@ -573,7 +571,7 @@ internal class ResolvedProjectionAdapterTest : BaseYawnDatabaseTest() { val results = session.project(BookTable) { books -> val authors = join(books.author) project( - adapt { + YawnProjector { ProjectionNode.composite( // outer level: author name (group by) YawnValueProjector { ProjectionNode.aggregate(GROUP_BY, authors.name) }, @@ -603,7 +601,7 @@ internal class ResolvedProjectionAdapterTest : BaseYawnDatabaseTest() { val results = session.project(BookTable) { books -> val authors = join(books.author) project( - adapt { + YawnProjector { ProjectionNode.composite( YawnValueProjector { ProjectionNode.aggregate(GROUP_BY, authors.name) }, { @@ -641,11 +639,4 @@ internal class ResolvedProjectionAdapterTest : BaseYawnDatabaseTest() { ) } } - - private fun adapt( - projector: YawnProjector, - ): ResolvedProjectionAdapter { - val resolved = ProjectorResolver().resolve(projector) - return ResolvedProjectionAdapter(resolved) - } } diff --git a/yawn-database-test/src/test/kotlin/com/faire/yawn/database/YawnProjectionTest.kt b/yawn-database-test/src/test/kotlin/com/faire/yawn/database/YawnProjectionTest.kt index cb99e73..d0e630f 100644 --- a/yawn-database-test/src/test/kotlin/com/faire/yawn/database/YawnProjectionTest.kt +++ b/yawn-database-test/src/test/kotlin/com/faire/yawn/database/YawnProjectionTest.kt @@ -818,6 +818,39 @@ internal class YawnProjectionTest : BaseYawnDatabaseTest() { } } + @YawnProjection + internal data class AuthorWithPageStats( + val author: String, + val pageStats: Pair, + ) + + @Test + fun `nested projection with pair inside generated projection`() { + transactor.open { session -> + val results = session.project(BookTable) { books -> + val authors = join(books.author) + project( + YawnProjections.pair( + YawnProjectionTest_AuthorWithPageStatsProjection.create( + author = YawnProjections.groupBy(authors.name), + pageStats = YawnProjections.pair( + YawnProjections.min(books.numberOfPages), + YawnProjections.max(books.numberOfPages), + ), + ), + YawnProjections.count(books.name), + ), + ) + }.list() + + assertThat(results).containsExactlyInAnyOrder( + AuthorWithPageStats("J.R.R. Tolkien", 300L to 1_000L) to 2L, + AuthorWithPageStats("J.K. Rowling", 500L to 500L) to 1L, + AuthorWithPageStats("Hans Christian Andersen", 100L to 120L) to 3L, + ) + } + } + @YawnProjection internal data class BookStatistics( val totalBooks: Long, diff --git a/yawn-processor/src/main/kotlin/com/faire/yawn/generators/objects/YawnProjectionRefObjectGenerator.kt b/yawn-processor/src/main/kotlin/com/faire/yawn/generators/objects/YawnProjectionRefObjectGenerator.kt index a70731b..2e6af74 100644 --- a/yawn-processor/src/main/kotlin/com/faire/yawn/generators/objects/YawnProjectionRefObjectGenerator.kt +++ b/yawn-processor/src/main/kotlin/com/faire/yawn/generators/objects/YawnProjectionRefObjectGenerator.kt @@ -3,9 +3,9 @@ package com.faire.yawn.generators.objects import com.faire.ksp.getAllPropertiesWithAllAnnotations import com.faire.ksp.getEffectiveVisibility import com.faire.yawn.generators.addGeneratedAnnotation -import com.faire.yawn.project.YawnCompositeQueryProjection +import com.faire.yawn.project.ProjectionNode import com.faire.yawn.project.YawnProjectionRef -import com.faire.yawn.project.YawnQueryProjection +import com.faire.yawn.project.YawnProjector import com.faire.yawn.util.YawnContext import com.faire.yawn.util.YawnNamesGenerator.generateProjectionObjectName import com.faire.yawn.util.isConstructorProperty @@ -20,8 +20,8 @@ import com.squareup.kotlinpoet.asTypeName import com.squareup.kotlinpoet.ksp.toClassName import com.squareup.kotlinpoet.ksp.toTypeName -private val yawnQueryProjection = YawnQueryProjection::class.asClassName() -private val typedProjectionImpl = YawnCompositeQueryProjection::class.asClassName() +private val yawnProjector = YawnProjector::class.asClassName() +private val projectionNodeClass = ProjectionNode::class.asClassName() internal object YawnProjectionRefObjectGenerator : YawnReferenceObjectGenerator { /** @@ -31,7 +31,7 @@ internal object YawnProjectionRefObjectGenerator : YawnReferenceObjectGenerator * The output code will look like: * object SimpleBookProjection: YawnProjectionRef>() { * // see the definition of the create function below - * fun create(...): TypedProjection { ... } + * fun create(...): YawnProjector { ... } * } */ override fun generate( @@ -62,7 +62,7 @@ internal object YawnProjectionRefObjectGenerator : YawnReferenceObjectGenerator val create = FunSpec.builder("create") .addTypeVariable(source) - .returns(yawnQueryProjection.parameterizedBy(source, f)) + .returns(yawnProjector.parameterizedBy(source, f)) data class Property( val index: Int, @@ -83,8 +83,8 @@ internal object YawnProjectionRefObjectGenerator : YawnReferenceObjectGenerator var extraTypeParametersIdx = 0 for (property in properties) { // if the type is nullable, we want to accept both nullable and non-nullable projections - // so we add an extra type parameter `Tx : Type?`, so that Projection can be either - // Projection or Projection. + // so we add an extra type parameter `Tx : Type?`, so that YawnProjector can be either + // YawnProjector or YawnProjector. val projectionType = if (property.type.isNullable) { val typeVariable = TypeVariableName("T$extraTypeParametersIdx", property.type) extraTypeParametersIdx++ @@ -94,27 +94,30 @@ internal object YawnProjectionRefObjectGenerator : YawnReferenceObjectGenerator } else { property.type } - create.addParameter(property.name, yawnQueryProjection.parameterizedBy(source, projectionType)) + create.addParameter(property.name, yawnProjector.parameterizedBy(source, projectionType)) } - val propertyProjections = properties.joinToString(separator = ",\n") { it.name } + val propertyProjections = properties.joinToString(separator = ", ") { it.name } val propertyParameters = properties.joinToString(separator = ",\n") { - "${it.name} = ${it.name}.project(queryResult[${it.index}])" + "${it.name} = values[${it.index}] as ${it.type}" } create.addStatement( """ + @Suppress("UNCHECKED_CAST") return ( - %T( - $propertyProjections - ) { row -> - val queryResult = row as Array<*> - %T( - $propertyParameters - ) + %T { + %T.composite( + listOf($propertyProjections) + ) { values -> + %T( + $propertyParameters + ) + } } ) """.trimIndent(), - typedProjectionImpl.parameterizedBy(source, f), + yawnProjector.parameterizedBy(source, f), + projectionNodeClass, yawnContext.classDeclaration.toClassName(), )