Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
15 changes: 15 additions & 0 deletions yawn-api/src/main/kotlin/com/faire/yawn/project/AggregateKind.kt
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,
}
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 yawn-api/src/main/kotlin/com/faire/yawn/project/ProjectionLeaf.kt
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>(
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>
}
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.
* 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 yawn-api/src/main/kotlin/com/faire/yawn/project/ProjectionNode.kt
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))

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)
}
}
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)) }
}
}
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 yawn-api/src/main/kotlin/com/faire/yawn/project/YawnProjector.kt
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>
}
Loading
Loading