Summary
Engine commit c334986f (Improve handling of raw-queries (#3147) — "Better raw query support. Do not wrap top-level infixes."), first released in quill-engine 4.8.6, changes the decode contract of entity-typed top-level raw queries:
val q = quote { sql"""SELECT b.* FROM blocked_periods b WHERE ...""".as[Query[BlockedPeriod]] }
ctx.run(q) // dynamic in our case (runtime #\$ splices)
Through 4.8.5, the engine wrapped such queries: SELECT x.id, x.home_id, ... FROM (<raw sql>) AS x — the generated extractor (which decodes positionally, in case-class field order) always matched. After #3147 the raw SQL passes through unwrapped, so the result set has the table's physical column order (DDL order incl. appended ALTER columns), while the extractor still decodes in field order.
Consequences when the orders differ:
- decoders read neighboring columns' values — in our production case an enumeratum decoder got the next enum column's value:
NoSuchElementException: OCCUPIED is not a member of Enum(...);
- worse, when adjacent columns have compatible types/overlapping enum member names, the corruption is silent.
Bisection evidence
Verified single-variable on a large production codebase (Scala 3.8.4, ProtoQuill master + Postgres testcontainers suite):
| engine |
spec result |
| 4.8.5 |
green (71/71 suite) |
4.8.5 + cherry-pick c334986f only |
red — exact production failure |
| 4.8.6 / forks based on it |
red |
ProtoQuill releases pin engine ≤4.8.5, which is why this seems unreported; downstream forks of master (e.g. org.li-nk 4.8.7-RC1) inherit it.
Suggested direction
Either restore the projecting wrapper when the infix is decoded into a Product (entity/case-class) type — positional decode requires a known projection — or expand the entity's fields into the wrapper only when Quat.Product is present, keeping the unwrapped passthrough for scalar/tuple raw queries that motivated #3147. Happy to test candidate patches against our reproducer suite.
Summary
Engine commit
c334986f(Improve handling of raw-queries (#3147) — "Better raw query support. Do not wrap top-level infixes."), first released in quill-engine 4.8.6, changes the decode contract of entity-typed top-level raw queries:Through 4.8.5, the engine wrapped such queries:
SELECT x.id, x.home_id, ... FROM (<raw sql>) AS x— the generated extractor (which decodes positionally, in case-class field order) always matched. After #3147 the raw SQL passes through unwrapped, so the result set has the table's physical column order (DDL order incl. appended ALTER columns), while the extractor still decodes in field order.Consequences when the orders differ:
NoSuchElementException: OCCUPIED is not a member of Enum(...);Bisection evidence
Verified single-variable on a large production codebase (Scala 3.8.4, ProtoQuill master + Postgres testcontainers suite):
c334986fonlyProtoQuill releases pin engine ≤4.8.5, which is why this seems unreported; downstream forks of master (e.g. org.li-nk 4.8.7-RC1) inherit it.
Suggested direction
Either restore the projecting wrapper when the infix is decoded into a Product (entity/case-class) type — positional decode requires a known projection — or expand the entity's fields into the wrapper only when
Quat.Productis present, keeping the unwrapped passthrough for scalar/tuple raw queries that motivated #3147. Happy to test candidate patches against our reproducer suite.