Skip to content

feat: Enhanced projection handling - new types#117

Open
luanpotter wants to merge 2 commits intomainfrom
luan.03-12-feat_enhanced_projection_handling_-_new_types
Open

feat: Enhanced projection handling - new types#117
luanpotter wants to merge 2 commits intomainfrom
luan.03-12-feat_enhanced_projection_handling_-_new_types

Conversation

@luanpotter
Copy link
Member

@luanpotter luanpotter commented Mar 13, 2026

I am redesigning our projection infra! This will be a relatively big change, so I am attempting to split this the best as possible to facilitate review (and migration). I will go over the translation plan below.

Design Goals

My design goals:

  • fully decouple projections from Hibernate
  • move handling of projection collection, parsing, deduping and indexing to Yawn core and out of projection definitions and query factories
  • support nested groupings!

Current Problem

Let me go into some detail for the later - what actually current breaks and the limitations of how we do things (other than the coupling).

Let's see how hibernate works first for context when using nested projections. As a simple example, consider this nested double projection list:

      val result = session.session.createCriteria(Book::class.java)
          .setProjection(
              Projections.projectionList()
                  .add(Projections.property("name"))
                  .add(
                      Projections.projectionList()
                          .add(Projections.property("numberOfPages"))
                          .add(Projections.property("originalLanguage")),
                  ),
          )
          .list()
          .first() as Array<*>

You might think that this will result in a [name, [pages, language]] array, but it doesn't. Since this translates to a list of sql clauses, hibernate returns [name, pages, language].

That breaks yawn's current implementation which would expect the former. The current projections are unaware of this flattening. the following test will break when trying to cast the string pages into an array of [pages, language].

    @Test
    fun `pairs will not work inside projection classes`() {
        val result = transactor.open { session ->
            session.project(BookTable) { books ->
                val authors = join(books.author)
                addEq(authors.name, "J.K. Rowling")
                val notes = addIsNotNull(books.notes)
                project(
                    YawnProjectionTest_ProjectionWithPairProjection.create(
                        name = books.name,
                        authorAndNotes = YawnProjections.pair(authors.name, notes),
                    ),
                )
            }.uniqueResult()!!
        }

        assertThat(result.name).isEqualTo("Harry Potter")
        assertThat(result.authorAndNotes.first).isEqualTo("J.K. Rowling")
        assertThat(result.authorAndNotes.second).isEqualTo("Note for The Hobbit and Harry")
    }

    @YawnProjection
    internal data class ProjectionWithPair(
        val name: String,
        val authorAndNotes: Pair<String, String>,
    )

This is not only impactful for contrived examples, but blocks us from having transformations as well. A very simple example is YawnProjections.mapping that I wanted to add; you can see how the test is incorrect due to me copy and pasting the output.

The whole reason is that yawn doesn't know the indices of where projection columns will go. We also ran into this when I added the "constant" projection. as you can see in the impl, it has to add a sentinel to the hibernate projection list that is actually added to the sql query for no reason other than appeasing the indexing management. that constant could be entirely populated in memory by yawn on the resulting object instead.

In a way, we will be taking over and re-implementing part of hibernate. we will never do nested projectionLists(), and always send the simplest possible set of flattened projections to hibernate. all nesting, mapping, and transforming is resolved by yawn.

A Bright New Future

With all that in mind, I am proposing a bold new structure; the YawnQueryProjection is broken into two parts:

  • YawnProjector is what column defs, data class projections and custom projections on YawnProjections implement. they are not project-ions, they are project-ors, that can be projected to a ProjectionNode. they do not do the actual projecting, they define how to do it. this is what you compose to make your projection node tree
  • when you call project() now it actually does something -> it will convert the projection node tree into a resolved projection. this actually does a lot of heavy lifting: it will flatten groups, collect value nodes, de-dupe, re-index and store a final list of projections to send to hibernate (or the ORM), and is able to then re-collect the result in order to generate the final object after the code is run.

That is the basic structure plus some niceties. Hopefully this first PR will make it more clear. The following will add a compatibility adapter so that we can try out the new structure within the framework of the old one on our internal repos.

Migration Path

My proposed migration path is as follows: merge PRs (1) and (2) which are entirely net-new and release a version. Test it out at small scale on our internal repos by using the "adapt" method. That goes through the whole tree parsing and the most complicated parts but still uses the v1 query factory. that will be the first test. note that the ergonomics for the adapt method are clunky (you can see on PR (2)), but that is because you have to explicitly create the projectors. the actual end user facing api will be identical in the end. and the actual definitions of specific typed projections, column def and data-class-projections will be simpler. once we have sufficiently tested v2, we can update YawnProjectionts to hook up v2 by default, and then remove the adapt method.

Copy link
Member Author

luanpotter commented Mar 13, 2026

@luanpotter luanpotter added the do-not-request-reviewers Stops PR bot from automatically requesting reviewers label Mar 13, 2026
@luanpotter luanpotter force-pushed the luan.03-12-feat_enhanced_projection_handling_-_new_types branch 2 times, most recently from d3ab85f to 1d29904 Compare March 13, 2026 01:10
@luanpotter luanpotter force-pushed the luan.03-12-feat_enhanced_projection_handling_-_new_types branch from 1d29904 to a44ad7e Compare March 13, 2026 02:03
@luanpotter luanpotter removed the do-not-request-reviewers Stops PR bot from automatically requesting reviewers label Mar 14, 2026
@luanpotter luanpotter requested a review from a team March 14, 2026 15:26
Copy link
Collaborator

@QuinnB73 QuinnB73 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is amazing! I'm so excited to see us decouple further from Hibernate

I just have a few questions mostly for my understanding

@luanpotter luanpotter force-pushed the luan.03-12-feat_enhanced_projection_handling_-_new_types branch from a44ad7e to 2e033fc Compare March 17, 2026 15:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants