8000 fix(mysql): null a merged view's column on an outer join's null side by colinhacks · Pull Request #46 · colinhacks/reylo · GitHub
[go: up one dir, main page]

Skip to content

fix(mysql): null a merged view's column on an outer join's null side - #46

Merged
colinhacks merged 2 commits into
mainfrom
mysql-merge-views
Oct 22, 2026
Merged

colinhacks merged 2 commits into
mainfrom
mysql-merge-views

Conversation

@colinhacks
Copy link
Copy Markdown
Owner

On MySQL, a view's computed column read on the null-extended side of an outer join was typed non-null, and is NULL wherever the join finds no row:

create algorithm = merge view merged as select id, user_id, id + 1 as next from posts;

select u.id, m.next from users u left join merged m on m.user_id = u.id + 1000

typed next as int8; it is now int8?.

Why

The server merges a view into the query that reads it, and a merged expression keeps its own NOT NULL flag under the outer join (measured on 8.4). #33 reads the flags from a copy of the statement prepared with SET_VAR(optimizer_switch = 'derived_merge=off'), which materializes derived tables, WITH queries and views of the default algorithm. A view created with ALGORITHM = MERGE is merged whatever that hint says, and a NO_MERGE hint does not move it either.

What changed

  • The materialized copy reads each view through a derived table of its own, (SELECT * FROM view) AS name, under the name the statement reads it by. The hint materializes that derived table, so the server flags its columns as a temporary table's on the null-extended side. A block that only selects every column of one table is left alone: nothing in it can null-extend the view, and it is the shape a view is read through.
  • The engine now asks for the session's views (information_schema.views, which it already asked for some statements) for every SELECT; the answer is per session. A statement that reads a view is read materialized.

Tests

  • oracle/cases/mysql_merged_views (new, 12 statements, none unchecked): a view with ALGORITHM = MERGE, one of the default algorithm and one with TEMPTABLE, on the null-extended side of LEFT and RIGHT joins, unaliased, inside a derived table, in a nested join, under an outer expression, with a WHERE that rejects the null row, on the preserved side, read alone, and a WITH query that shadows the view's name. Every statement runs on MySQL 8.4 over the seeded values.
  • No existing expected type changes across the mysql_* cases.
  • Unit tests in mysql_derived cover the rewrite of a view, with and without an alias, in a schema, inside a nested join and inside a derived table.
  • cargo test for the four engines, the oracle with --strict-coverage over oracle/cases/mysql_*, nub run typecheck, and nub run test against Postgres 18 and MySQL 8.4 (203 pass, none skipped) all pass.
  • packages/mysql/engine.wasm is rebuilt.

Copilot AI balanced review requested due to automatic review settings October 22, 2026 22:27
@vercel
vercel Bot commented Oct 22, 2026 •
Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
reylo Ready Ready Preview Oct 22, 2026 11:18pm UTC

Request Review

Copilot AI left a comment
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@pullfrog pullfrog Bot left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ℹ️ No critical issues — two rough edges in how the copy's rewrite interacts with the rest of prove.

Reviewed changes

Reviewed the full diff: the engine change that reads each view through a materialized derived table in the hinted copy, the new oracle case, and the unit tests.

  • View rewrite in the materialized copy — materialized now wraps each TableFactor::Table whose name is a session view as (SELECT * FROM view) AS name, skipping SELECT * FROM <one table> blocks, and a statement that reads a view always gets the copy.
  • Views asked for every SELECT — analyze now asks VIEWS_QUERY (session-scoped) for every query and threads the set through prove and through_derived.
  • Oracle case mysql_merged_views — 12 statements covering MERGE, default and TEMPTABLE views on both sides of outer joins, nested joins, derived tables and a WITH query that shadows a view's name.

tests/fingerprint.test.ts passes at b14a44b, so the committed engine.wasm matches its sources.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using claude-opus-5-5 | 𝕏

Comment thread engine/mysql/src/mysql.rs Resolved
Comment thread engine/mysql/src/mysql_derived.rs Resolved
A view created with ALGORITHM = MERGE is merged whatever the
derived_merge hint says, and its expression keeps its NOT NULL flag on
the null-extended side of an outer join. The materialized copy now
reads each view through a derived table of its own.
A view read through a derived table no longer counts as a derived table to prove, and a copy the view rewrite breaks falls back to the copy without it.
@pullfrog pullfrog Bot left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ No new issues found. Both points from the previous review are fixed.

Reviewed changes

Reviewed f7f70ae, which keeps the derived-table materialization when the view rewrite breaks the copy and stops the views' synthetic derived tables from counting as through_derived targets.

  • Fallback copy in prove: when the copy with views rewritten fails to prepare, prove now describes the copy without the rewrite, and only if that copy prints differently. The successful copy then becomes q_read for everything after it. A statement that refers to app.v.x keeps its derived tables' nullability.
  • reads_one_table targets skipped: through_derived no longer describes a target that is exactly SELECT * FROM <one table>, and such a target does not count toward MAX_DERIVED. A user-written (select * from cte) d is skipped as well, which loses nothing because the WITH query is still a target of its own.
  • Unit test: a_view_read_through_a_derived_table_neither_breaks_the_copy_nor_takes_a_target covers both changes.

tests/fingerprint.test.ts passes at f7f70ae, so the rebuilt packages/mysql/engine.wasm matches its sources.

Pullfrog  | View workflow run | Using claude-opus-5-5 | 𝕏

@colinhacks
colinhacks merged commit 06aa101 into main Oct 22, 2026
6 checks passed
@colinhacks
colinhacks deleted the mysql-merge-views branch October 22, 2026 23:26
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

0