Repository navigation
fix(engine): type json built over a correlated subquery - #104
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Important
One
8000
MySQL path drops the statement's with from the new wrapped probe. When a table name is shadowed, that infers the wrong types. The fix is one line (inline).
Reviewed changes
I reviewed all 57 files: both engine crates, the three new oracle cases, the three re-blessed MySQL cases and the rebuilt artifacts.
- Wider probe order and
lateral. The widerfromnow lists the enclosingfroms outermost first. Derived tables below the top level becomelateral, so a correlated derived table reads the outer items it means. Any name the probe would find twice makes it fail, so this is sound. - Wrapped probe for a correlated
on. Afromwith a join whoseonreads the outer query is read insideLATERAL (…) reylo_probe(wrapped_textin both crates). On MySQL, a subquery's ownwithalso goes inside that wrapper. - Postgres FILTER and
coalesce. An aggregate's FILTER level no longer repeats itsfromin the wider probe.coalesce(x.col, '[]')andx.col::jsonover a column alateralsubquery builds now get the shape it builds. Defaults other than'[]'stay one value (build'sCoalesceExprarm). - Oracle cases. Adds
json_correlated,mysql_json_correlatedandsqlite_json_correlated. I checked the re-blessedmysql_json_shapes,mysql_subquery_repairandmysql_subquery_withlines against their setup schemas, and they are right.
Two things I checked that need no change. row_present keeps the from length, so slicing at Draft::inner cannot panic. Postgres returns Unknown for a subquery with its own with, so the MySQL issue has no Postgres counterpart.
ℹ️ No case covers an outer join inside the wrapped from
On both dialects, the wrapped probe's nullability now passes through a lateral derived table. Every new case with a correlated on (join_on_correlated) uses an inner join, so the oracle never sees a nullable column that has to survive the wrapper. A case like from comment c left join person a on a.id = c.author_id and a.id = p.author_id in json_correlated and mysql_json_correlated would cover it.
claude-opus-5-5 | 𝕏
| // hides an outer WITH inside a query that has its own. | ||
| let own = depth > 0 && self.stack[depth].with.is_some(); | ||
| let outer = self.stack[..depth].iter().any(|l| l.with.is_some()); | ||
| let wrap = (depth > 0 && !wider.is_empty() && !from.is_empty() && (from.iter().any(correlated_on) || (own && !outer))).then(|| Wrap { |
There was a problem hiding this comment.
When own and outer both hold, a correlated on still builds a Wrap. Because own is true, it gets with: String::new(), so the wider probe carries no with at all. An enclosing from that reads one of the statement's with queries then reads a real table of that name instead. I reproduced this with a throwaway unit test. For with posts as (select id, email as title from users) select (with r as (select 1 as k) select json_arrayagg(json_object('t', p.title, 'n', u.name)) from comments c join users u on u.id = c.user_id and c.post_id = p.id, r) as j from posts p, the narrow probe fails (p is not in its from). The wider probe is SELECT reylo_probe.* FROM posts p, LATERAL (WITH r AS (SELECT 1 AS k) SELECT p.title AS a0, …) AS reylo_probe. That reads p.title from the real posts.title (text, not null), but the statement reads users.email (nullable). Before this PR the flat probe failed at the on, so this case was unknown and is now wrong.
The suggestion applies the guard the comment above describes to both reasons for wrapping. With it, this case falls back to the flat wider probe, which fails at the on as before.
| let wrap = (depth > 0 && !wider.is_empty() && !from.is_empty() && (from.iter().any(correlated_on) || (own && !outer))).then(|| Wrap { | |
| let wrap = (depth > 0 && !wider.is_empty() && !from.is_empty() && !(own && outer) && (own || from.iter().any(correlated_on))).then(|| Wrap { |
There was a problem hiding this comment.
Right: wrapped, a subquery with a with of its own under a level with one lost the statement's with, which an enclosing from reads. Fixed in a81b425 with this guard: neither reason wraps that subquery now, so the case falls back to the flat probe, which fails at the on as before. The case is a unit test in a_correlated_from_is_read_after_the_queries_around_it.
|
Your Claude Pro/Max subscription has hit its usage limit. It resets at Oct 26, 8am (UTC). Re-trigger after that, or connect an Anthropic API key, which Pullfrog falls back to automatically. The provider's response
|
0618c46 to
8388078
Compare
|
No configured credential can serve this run. Your Claude Pro/Max subscription has hit its usage limit, which resets at 2026-10-26 08:00 UTC. Re-trigger after it resets, or add an API key for this provider, which Pullfrog falls back to automatically. The providers' responses
|
|
No configured credential can serve this run. Your Claude Pro/Max subscription has hit its usage limit, which resets at 2026-10-26 08:00 UTC. Re-trigger after it resets, or add an API key for this provider, which Pullfrog falls back to automatically. The providers' responses
|
a81b425 to
ed2f21d
Compare
|
No configured credential can serve this run. Your Claude Pro/Max subscription has hit its usage limit, which resets at 2026-10-26 08:00 UTC. Re-trigger after it resets, or add an API key for this provider, which Pullfrog falls back to automatically. The providers' responses
|
ed2f21d to
0865967
Compare
|
No configured credential can serve this run. Your Claude Pro/Max subscription has hit its usage limit, which resets at 2026-10-26 08:00 UTC. Re-trigger after it resets, or add an API key for this provider, which Pullfrog falls back to automatically. The providers' responses
|

JSON built over rows a subquery reads, where that subquery refers to the query around it, lost every field's type: the "latest few children per parent" pattern,
(select coalesce(json_agg(json_build_object(…)), '[]') from (select … from comment c where c.post_id = p.id order by … limit 2) c), was typed{ id: unknown?, body: unknown? }[].Cause. Each field is typed by a probe, a synthesized
selectof the field's expressions over the constructor's ownfrom. A derived table there that readsp.idcannot be described alone, so the engine falls back to a wider probe that appends every enclosingfrom— after the derived table, which, not beinglateral, could not see them either. Both probes failed and every field wasunknown.Fix (Postgres and MySQL).
froms first, outermost first, and makes every derived table inside the statement's ownfromlateral, so it reads the outer items as it read the outer query. A name the probe would find twice fails the probe rather than reading another item, so the fallback stays sound.onreads the outer query cannot see an item beside it, so thatfromis read inside alateralderived table of the probe's own, where the outer query is an outer query again.withquery that refers to the outer query goes inside that derived table too, where no enclosingfromsees it (only where no level around has awith, since MySQL hides an outer one there).coalesce(x.col, '[]')over a column alateralsubquery builds is read as the shape it builds, as the docs already state; it was plainjson.SQLite has no such gap: its probes already type every form.
Measured (new cases
oracle/cases/json_correlated,mysql_json_correlated,sqlite_json_correlated): on Postgres, 17 statements go fromunknownfields or plainjsonto typed fields, among themjson_agg/jsonb_agg,to_json/row_to_json/to_jsonbof the derived row, two nested levels, a correlated derived table inside alateraljoin, and a join to another table; on MySQL, 9. Three existing MySQL cases gain types too (mysql_json_shapes/array_agg_ordered_subquery,mysql_subquery_repair/subquery_inside_cte,mysql_subquery_with/nested_correlatedandwider_shadowed): each newly typed field is anot nullcolumn or acount(*).Checks. Whole oracle with
--strict-coverageon Postgres 18 and MySQL 8.4; the engine's tests;cargo build --workspace --all-targetswith no warning;engine/build.sh(the Postgres and MySQL artifacts change; SQLite's does not);nub run typecheckandnub run testwith both test databases, none skipped;nub run build.