-
Notifications
You must be signed in to change notification settings - Fork 0
feat: Enhanced projection handling - new types #117
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
luanpotter
merged 3 commits into
main
from
luan.03-12-feat_enhanced_projection_handling_-_new_types
Mar 23, 2026
Merged
Changes from 1 commit
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
15 changes: 15 additions & 0 deletions
15
yawn-api/src/main/kotlin/com/faire/yawn/project/AggregateKind.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| package com.faire.yawn.project | ||
|
|
||
| /** | ||
| * The kind of aggregate or grouping projection to apply. | ||
| * Each kind maps to a specific SQL aggregation or clause. | ||
| */ | ||
| enum class AggregateKind { | ||
| COUNT, | ||
| COUNT_DISTINCT, | ||
| SUM, | ||
| AVG, | ||
| MIN, | ||
| MAX, | ||
| GROUP_BY, | ||
| } |
8 changes: 8 additions & 0 deletions
8
yawn-api/src/main/kotlin/com/faire/yawn/project/ModifierKind.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| package com.faire.yawn.project | ||
|
|
||
| /** | ||
| * The kind of modifier to wrap around a [ProjectionLeaf]. | ||
| */ | ||
| enum class ModifierKind { | ||
| DISTINCT, | ||
| } |
60 changes: 60 additions & 0 deletions
60
yawn-api/src/main/kotlin/com/faire/yawn/project/ProjectionLeaf.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| package com.faire.yawn.project | ||
|
|
||
| import com.faire.yawn.YawnDef | ||
| import kotlin.reflect.KClass | ||
|
|
||
| /** | ||
| * An ORM-agnostic descriptor of a single atomic projection. | ||
| * | ||
| * Leaves are the terminal elements that produce actual SQL in the compiled query projection tree. | ||
| * The Query Factory is responsible for converting each leaf to the underlying ORM's implementation projection. | ||
| * | ||
| * Leaf deduplication uses data class equality: two [Property] leaves with the same [YawnDef.YawnColumnDef] | ||
| * reference, or two [Aggregate] leaves with the same [AggregateKind] and column, are considered identical | ||
| * and will share an index in the re-packed result list. | ||
| */ | ||
| sealed interface ProjectionLeaf<SOURCE : Any> { | ||
| /** | ||
| * A simple column property access (SQL: `alias.column`). | ||
| */ | ||
| data class Property<SOURCE : Any>( | ||
| val column: YawnDef<SOURCE, *>.YawnColumnDef<*>, | ||
| ) : ProjectionLeaf<SOURCE> | ||
|
|
||
| /** | ||
| * An aggregate or grouping projection on a column (SQL: `SUM(alias.column)`, `GROUP BY alias.column`, etc.). | ||
| */ | ||
| data class Aggregate<SOURCE : Any>( | ||
| val kind: AggregateKind, | ||
| val column: YawnDef<SOURCE, *>.YawnColumnDef<*>, | ||
| ) : ProjectionLeaf<SOURCE> | ||
|
|
||
| /** | ||
| * A row count projection (SQL: `COUNT(*)`). | ||
| */ | ||
| class RowCount<SOURCE : Any> : ProjectionLeaf<SOURCE> { | ||
| override fun equals(other: Any?): Boolean = other is RowCount<*> | ||
| override fun hashCode(): Int = RowCount::class.hashCode() | ||
| } | ||
|
|
||
| /** | ||
| * A raw SQL projection for custom expressions. | ||
| * | ||
| * The [sqlExpression] may use `{alias}` placeholders for table alias substitution. | ||
| * [resultTypes] are used by the query factory to map SQL results to Kotlin types. | ||
| * It is up to the user to guarantee type-safety when using raw SQL projections! | ||
| */ | ||
| data class Sql<SOURCE : Any>( | ||
QuinnB73 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| val sqlExpression: String, | ||
| val aliases: List<String>, | ||
| val resultTypes: List<KClass<*>>, | ||
| ) : ProjectionLeaf<SOURCE> | ||
|
|
||
| /** | ||
| * Wraps another leaf with a SQL modifier (e.g. DISTINCT). | ||
| */ | ||
| data class Modifier<SOURCE : Any>( | ||
| val kind: ModifierKind, | ||
| val inner: ProjectionLeaf<SOURCE>, | ||
| ) : ProjectionLeaf<SOURCE> | ||
| } | ||
13 changes: 13 additions & 0 deletions
13
yawn-api/src/main/kotlin/com/faire/yawn/project/ProjectionMapper.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| package com.faire.yawn.project | ||
|
|
||
| /** | ||
| * Maps a raw result row (as a list of values corresponding to resolved projection leaves) | ||
| * into the desired projection type. | ||
| * | ||
| * This is an internal type produced by the [ProjectorResolver] resolution engine. | ||
luanpotter marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| * You should never need to implement this directly; use the simple lambdas pointing to [ProjectionNode.Value], | ||
| * [ProjectionNode.Composite], or [ProjectionNode.Mapped]. | ||
| */ | ||
| fun interface ProjectionMapper<TO> { | ||
| fun map(results: List<Any?>): TO | ||
| } | ||
112 changes: 112 additions & 0 deletions
112
yawn-api/src/main/kotlin/com/faire/yawn/project/ProjectionNode.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,112 @@ | ||
| package com.faire.yawn.project | ||
|
|
||
| import com.faire.yawn.YawnDef | ||
|
|
||
| /** | ||
| * A declarative description of a projection's structure. | ||
| * | ||
| * The [ProjectorResolver] resolution engine walks this tree to produce a flat [ResolvedProjection]. | ||
| * There are four variants: | ||
| * * [Value]: a single leaf projection (column, aggregate, SQL, etc.). Only Values will survive resolution. | ||
| * * [Composite]: a grouping of multiple children with a mapper. Flattened during resolution. | ||
| * * [Constant]: a fixed value, with no corresponding SQL. Eliminated during resolution. | ||
| * * [Mapped]: wraps another projection with an in-memory transformation. Eliminated during resolution. | ||
| */ | ||
| sealed interface ProjectionNode<SOURCE : Any, TO> { | ||
| /** | ||
| * A single leaf projection that maps to one slot in the SQL result. | ||
| * | ||
| * @param leaf the ORM-agnostic projection descriptor. | ||
| * @param mapper transforms the raw SQL value to the desired type. Defaults to a simple unchecked cast. | ||
| */ | ||
| data class Value<SOURCE : Any, TO>( | ||
| val leaf: ProjectionLeaf<SOURCE>, | ||
| val mapper: (Any?) -> TO = { | ||
| @Suppress("UNCHECKED_CAST") | ||
| it as TO | ||
| }, | ||
| ) : ProjectionNode<SOURCE, TO> | ||
|
|
||
| /** | ||
| * Groups multiple child projections and combines their results with a mapper. | ||
| * | ||
| * During resolution, composites are recursively flattened: their children's leaves are added to | ||
| * the parent's flat list, and the mapper is composed to reconstruct the grouped result. | ||
| */ | ||
| data class Composite<SOURCE : Any, TO>( | ||
| val children: List<YawnProjector<SOURCE, *>>, | ||
| val mapper: (List<Any?>) -> TO, | ||
| ) : ProjectionNode<SOURCE, TO> | ||
|
|
||
| /** | ||
| * A constant value that requires no SQL. Eliminated during resolution. | ||
| */ | ||
| data class Constant<SOURCE : Any, TO>( | ||
| val value: TO, | ||
| ) : ProjectionNode<SOURCE, TO> | ||
|
|
||
| /** | ||
| * Wraps another projection with a post-query transform. Eliminated during resolution; | ||
| * the transform is folded into the composed mapper. | ||
| */ | ||
| data class Mapped<SOURCE : Any, FROM, TO>( | ||
| val from: YawnProjector<SOURCE, FROM>, | ||
| val transform: (FROM) -> TO, | ||
| ) : ProjectionNode<SOURCE, TO> | ||
|
|
||
| companion object { | ||
| fun <SOURCE : Any, TO> property( | ||
| column: YawnDef<SOURCE, *>.YawnColumnDef<TO>, | ||
| ): Value<SOURCE, TO> = Value(ProjectionLeaf.Property(column)) | ||
|
|
||
| fun <SOURCE : Any, TO> aggregate( | ||
| kind: AggregateKind, | ||
| column: YawnDef<SOURCE, *>.YawnColumnDef<TO>, | ||
| ): Value<SOURCE, TO> = Value(ProjectionLeaf.Aggregate(kind, column)) | ||
|
|
||
| fun <SOURCE : Any, TO> aggregateAs( | ||
| kind: AggregateKind, | ||
| column: YawnDef<SOURCE, *>.YawnColumnDef<*>, | ||
| ): Value<SOURCE, TO> = Value(ProjectionLeaf.Aggregate(kind, column)) | ||
luanpotter marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| fun <SOURCE : Any> rowCount(): Value<SOURCE, Long> = | ||
| Value(ProjectionLeaf.RowCount()) | ||
|
|
||
| fun <SOURCE : Any, TO> sql( | ||
| sqlExpression: String, | ||
| aliases: List<String>, | ||
| resultTypes: List<kotlin.reflect.KClass<*>>, | ||
| ): Value<SOURCE, TO> = Value(ProjectionLeaf.Sql(sqlExpression, aliases, resultTypes)) | ||
|
|
||
| fun <SOURCE : Any, TO> constant(value: TO): Constant<SOURCE, TO> = Constant(value) | ||
|
|
||
| fun <SOURCE : Any, FROM, TO> mapped( | ||
| from: YawnProjector<SOURCE, FROM>, | ||
| transform: (FROM) -> TO, | ||
| ): Mapped<SOURCE, FROM, TO> = Mapped(from, transform) | ||
|
|
||
| fun <SOURCE : Any, A, B, R> composite( | ||
| a: YawnProjector<SOURCE, A>, | ||
| b: YawnProjector<SOURCE, B>, | ||
| mapper: (A, B) -> R, | ||
| ): Composite<SOURCE, R> = Composite(listOf(a, b)) { values -> | ||
| @Suppress("UNCHECKED_CAST") | ||
| mapper(values[0] as A, values[1] as B) | ||
| } | ||
|
|
||
| fun <SOURCE : Any, A, B, C, R> composite( | ||
| a: YawnProjector<SOURCE, A>, | ||
| b: YawnProjector<SOURCE, B>, | ||
| c: YawnProjector<SOURCE, C>, | ||
| mapper: (A, B, C) -> R, | ||
| ): Composite<SOURCE, R> = Composite(listOf(a, b, c)) { values -> | ||
| @Suppress("UNCHECKED_CAST") | ||
| mapper(values[0] as A, values[1] as B, values[2] as C) | ||
| } | ||
|
|
||
| fun <SOURCE : Any, R> composite( | ||
| children: List<YawnProjector<SOURCE, *>>, | ||
| mapper: (List<Any?>) -> R, | ||
| ): Composite<SOURCE, R> = Composite(children, mapper) | ||
| } | ||
| } | ||
53 changes: 53 additions & 0 deletions
53
yawn-api/src/main/kotlin/com/faire/yawn/project/ProjectorResolver.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| package com.faire.yawn.project | ||
|
|
||
| /** | ||
| * Yawn's projection resolution engine, which allows for complex nested projection arrangements that are then | ||
| * flattened into an ORM-friendly node structure. | ||
| * | ||
| * Walks a [ProjectionNode] tree and produces a [ResolvedProjection] by: | ||
| * - Flattening [ProjectionNode.Composite] nodes (their children become top-level) | ||
| * - Eliminating [ProjectionNode.Constant] nodes (no SQL, value folded into mapper) | ||
| * - Eliminating [ProjectionNode.Mapped] nodes (transform folded into mapper) | ||
| * - Keeping [ProjectionNode.Value] nodes in a flat list | ||
| * - Deduplicating identical [ProjectionLeaf] instances (same leaf shares an index) | ||
| * - Composing a [ProjectionMapper] that reconstructs the full result from the flat list | ||
| */ | ||
| class ProjectorResolver<SOURCE : Any> { | ||
| private val nodes = mutableListOf<ProjectionNode.Value<SOURCE, *>>() | ||
| private val dedupedLeafsToIndices = mutableMapOf<ProjectionLeaf<SOURCE>, Int>() | ||
|
|
||
| /** | ||
| * Resolves a [YawnProjector] into a [ResolvedProjection]. | ||
| */ | ||
| fun <TO> resolve(projector: YawnProjector<SOURCE, TO>): ResolvedProjection<SOURCE, TO> { | ||
| val mapper = resolveNode(projector.projection()) | ||
| return DefaultResolvedProjection(nodes.toList(), mapper) | ||
| } | ||
|
|
||
| private fun <TO> resolveNode(node: ProjectionNode<SOURCE, TO>): ProjectionMapper<TO> { | ||
| return when (node) { | ||
| is ProjectionNode.Value -> resolveValue(node) | ||
| is ProjectionNode.Composite -> resolveComposite(node) | ||
| is ProjectionNode.Constant -> ProjectionMapper { node.value } | ||
| is ProjectionNode.Mapped<SOURCE, *, TO> -> resolveMapped(node) | ||
| } | ||
| } | ||
|
|
||
| private fun <TO> resolveValue(node: ProjectionNode.Value<SOURCE, TO>): ProjectionMapper<TO> { | ||
| val idx = dedupedLeafsToIndices.getOrPut(node.leaf) { | ||
| nodes.add(node) | ||
| nodes.size - 1 | ||
| } | ||
| return ProjectionMapper { results -> node.mapper(results[idx]) } | ||
| } | ||
|
|
||
| private fun <TO> resolveComposite(node: ProjectionNode.Composite<SOURCE, TO>): ProjectionMapper<TO> { | ||
| val childMappers = node.children.map { resolveNode(it.projection()) } | ||
| return ProjectionMapper { results -> node.mapper(childMappers.map { it.map(results) }) } | ||
| } | ||
|
|
||
| private fun <FROM, TO> resolveMapped(node: ProjectionNode.Mapped<SOURCE, FROM, TO>): ProjectionMapper<TO> { | ||
| val innerMapper = resolveNode(node.from.projection()) | ||
| return ProjectionMapper { results -> node.transform(innerMapper.map(results)) } | ||
| } | ||
| } |
35 changes: 35 additions & 0 deletions
35
yawn-api/src/main/kotlin/com/faire/yawn/project/ResolvedProjection.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| package com.faire.yawn.project | ||
|
|
||
| /** | ||
| * The result of resolving a [YawnProjector] through the [ProjectorResolver] engine. | ||
| * | ||
| * Contains a flat list of [ProjectionNode.Value] nodes (all composites, constants, and mapped | ||
| * transforms have been eliminated) and a function to map a result row back to the desired type. | ||
| * | ||
| * This interface is ORM-agnostic. The Query Factory is the one responsible for: | ||
| * - Compiling each node's [ProjectionLeaf] to the underlying implementation projection type; | ||
| * - Normalizing the ORM result row into a `List<Any?>` matching the order of [nodes]; | ||
| * - Calling [mapRow] to reconstruct the projected type. | ||
| */ | ||
| interface ResolvedProjection<SOURCE : Any, TO> { | ||
| /** | ||
| * The flat, deduped, re-ordered list of value nodes to compile into the query. | ||
| * The query factory must compile them _in this order_, and result values | ||
| * must be provided at matching indices when calling [mapRow]. | ||
| */ | ||
| val nodes: List<ProjectionNode.Value<SOURCE, *>> | ||
|
|
||
| /** | ||
| * Maps a raw result row to the projected type. | ||
| * | ||
| * @param values raw results in the same order as [nodes]. | ||
| */ | ||
| fun mapRow(values: List<Any?>): TO | ||
| } | ||
|
|
||
| internal class DefaultResolvedProjection<SOURCE : Any, TO>( | ||
| override val nodes: List<ProjectionNode.Value<SOURCE, *>>, | ||
| private val mapper: ProjectionMapper<TO>, | ||
| ) : ResolvedProjection<SOURCE, TO> { | ||
| override fun mapRow(values: List<Any?>): TO = mapper.map(values) | ||
| } |
28 changes: 28 additions & 0 deletions
28
yawn-api/src/main/kotlin/com/faire/yawn/project/YawnProjector.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| package com.faire.yawn.project | ||
|
|
||
| /** | ||
| * A type-safe projection descriptor that can be resolved into a flat list of | ||
| * [ProjectionNode.Value] nodes for query compilation. | ||
| * | ||
| * Implementations describe the shape of a projection by returning a [ProjectionNode] | ||
| * from [projection]. The [ProjectorResolver] resolution engine then walks this tree, | ||
| * flattening composites, eliminating constants and mapped transforms, deduplicating | ||
| * identical leaves, and producing a [ResolvedProjection] that the query factory can compile. | ||
| * | ||
| * @param SOURCE the type of the entity being queried. | ||
| * @param TO the result type of this projection. | ||
| */ | ||
| fun interface YawnProjector<SOURCE : Any, TO> { | ||
| fun projection(): ProjectionNode<SOURCE, TO> | ||
| } | ||
|
|
||
| /** | ||
| * 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. | ||
| */ | ||
| fun interface YawnValueProjector<SOURCE : Any, TO> : YawnProjector<SOURCE, TO> { | ||
| override fun projection(): ProjectionNode.Value<SOURCE, TO> | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.