Repository navigation
fix(mysql): type a json aggregate over a derived table as non-null - #21
Merged
Merged
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
I reviewed how Builder::owns decides that a scalar subquery's aggregate is its own when the subquery reads from a derived table, a JSON_TABLE, or a WITH query. I also checked the oracle expectations this changes. cargo test -p reylo-engine-mysql --lib mysql_shape passes locally (9 tests).
- Derived-table and
JSON_TABLEaliases count as qualifiers.ownsnow pushes these aliases intoitems. It also flattens unaliasedNestedJoins, so a qualified read such asp.idthrough them proves ownership. A qualifier always resolves to the innermost FROM first, so this is sound. - Bare columns of derived tables and WITH queries. The new
output_columnsreads the alias column list if there is one. Otherwise it reads the first select's items that are a plain column or have anASalias. It deliberately leaves out wildcards and items named by their expression's text. That under-approximates the column set, so the worst case is lost typing, never a wrong non-null. Using the left branch of a set operation matches how MySQL names aUNION's columns. - WITH-query lookup keeps the old case rule. A reference only gets the WITH query's columns when it names the query in exactly the same case. A name in another case still falls back to
Source::Unknown, as the oldctes.contains(lowercase)check did. - Truth-value splitting is unaffected. The other caller,
keeps_its_aggregates, compares againstaggregates_in(q.to_string()). That count also includes aggregates inside derived tables, so it stays conservative there. - Oracle. The new case
mysql_derived_aggregatescovers the positive and negative forms. Three existing expectations tighten, and I checked each against its SQL:bare_own_derived,bare_own_withandarray_agg_ordered_subquery. In eac 8000 h one the aggregate reads only the subquery's own derived-table or WITH-query columns.
claude-opus-5-5 | 𝕏
A scalar subquery is one row when its aggregate reads columns of its own FROM. A derived table's columns, by its alias or by the names its select list gives, and a WITH query's bare columns now count, so a guarded json_arrayagg over a derived table is non-null as over a WITH query.
colinhacks
force-pushed
the
mysql-derived-null
branch
from
October 22, 2026 19:34
a014c9b to
7cd5a9b
Compare
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.

On MySQL, a guarded JSON aggregate in a scalar subquery over a derived table was typed as possibly null, while the same query over a WITH query was not:
recent_postswas{ id: number; title: string }[] | null; it is now{ id: number; title: string }[].A scalar subquery is exactly one row when it is an ungrouped aggregate of its own, and an aggregate is the subquery's own when it reads a column of the subquery's FROM. That check only recognized plain tables, so a column read through a derived table's alias proved nothing and the subquery fell back to nullable. The LIMIT inside the derived table played no part.
Now the check also counts:
An aggregate over only the outer query's columns still leaves the subquery nullable, and so does an unguarded aggregate.
Tests:
mysql_shapeunit tests cover the derived-table form, the WITH form, no LIMIT,ifnull, a derived table in the outer FROM, bare columns, column lists, and the negative cases.oracle/cases/mysql_derived_aggregatescovers the same forms. Each statement runs on MySQL 8.4 over seeded values, and none is unchecked.mysql_aggregate_levels/bare_own_derivedandbare_own_withbecomeint8(acountover the subquery's own columns), andmysql_json_shapes/array_agg_ordered_subqueryloses its outer?.packages/mysql/engine.wasmis rebuilt.