Skip to content

14.0.1: a join condition means the same thing whichever way round it is written - #226

Merged
dmitrat merged 2 commits into
mainfrom
engine/14.0.1
Aug 19, 2026
Merged

dmitrat merged 2 commits into
mainfrom
engine/14.0.1

Conversation

@dmitrat

@dmitrat dmitrat commented Aug 19, 2026

Copy link
Copy Markdown
Owner

Two defects a client found by being used, both in the planner, neither reachable from the SQL the suite happens to write. Known issues 25 and 26 are marked fixed by this.

25. The join condition

SELECT c.Country, o.Total FROM Customers c JOIN Orders o ON c.Id = o.CustomerId   -- rows
SELECT c.Country, o.Total FROM Customers c JOIN Orders o ON o.CustomerId = c.Id   -- Column 'CustomerId' not found

The equi-join key pair was built as LeftKey = binary.Left, RightKey = binary.Right - the order the condition was written in, never where the columns come from. The extractor checked that the two references named different tables and stopped there, so the hash join looked for the right table's column in rows of the left one. In a chain the trap is easier to fall into, because "the left input" is everything joined so far.

It now reads the left input's schema - a required parameter of OptimizerJoinCondition.Analyze, so the compiler asks the question at every call site - and orients each pair.

One branch is deliberately unchanged: where the engine does not know which side a column is on, the written order still decides. An INFORMATION_SCHEMA source reports no table name; such a join resolves a qualified name by column name anyway, and reading it as "neither is from the left" turned Studio's primary-key query into a cross product - five Studio cases went red before that branch was written. That is what running the provider suites before a release is for.

26. EXPLAIN

Each child's subtree was re-based by a constant instead of by where that subtree starts, so everything below the second child of any node was attributed into the first child's subtree. A join has two children, and so does every set operation:

5   2  ALIAS o
6   3  SCAN TABLE Orders     <- said 3, which is ALIAS c

Anything drawing the plan as a tree drew the wrong tree faithfully.

The guards assert the surface, not the case

  • JoinKeysBelongToTheirOwnSideTests - the two orders agree row for row across inner, left, chained, aliased, unaliased, composite-key and residual-beside-key shapes, plus the condition moved to WHERE. Controls: the plan actually says HASH (a nested loop has never cared which way round the condition is), and the joins match rows. Seven of ten go red with the orientation removed. Two of its cases passed for the wrong reason until the two sides were given different column names - a mis-oriented key still resolved by name.
  • APlanNodeNamesItsRealParentTests - each ALIAS stands over the scan of the table it aliases; both arms of a UNION keep their own children; one root and every parent a real earlier line. Three of four go red with the offset reverted. The purely structural case passes on both versions, which is why it is not the assertion.

Verified

  • Engine 2563 green, AdoNet 1054 green, Studio 1030 green.
  • Seven packages packed and every nuspec read: all at 14.0.1, every internal OutWit.Database* dependency at 14.0.1.

🤖 Generated with Claude Code

dmitrat and others added 2 commits August 19, 2026 15:32
…is written

Two defects a client found by being used, both in the planner, neither reachable from
the SQL the suite happens to write.

ON c.Id = o.CustomerId returned rows; ON o.CustomerId = c.Id - the same condition,
the same two tables - failed with Column 'CustomerId' not found. The equi-join key
pair was built from the ORDER THE CONDITION WAS WRITTEN IN, never from where the
columns come from, so the hash join looked for the right table's column in rows of
the left one. It now reads the left input's schema, which is a required parameter of
Analyze so the question is asked at every call site, and orients each pair.

Where the engine does not know which side a column is on the written order still
decides, and that branch is deliberate: an INFORMATION_SCHEMA source reports no table
name, such a join resolves a qualified name by column name anyway, and treating it as
"neither is from the left" turned Studio's primary-key query into a cross product.
Five Studio cases went red before that branch existed - which is the whole reason the
provider suites are run before a release rather than after it.

EXPLAIN gave the right input's child the wrong parent: each child's subtree was
re-based by a constant instead of by where that subtree starts, so everything below
the SECOND child of any node was attributed into the first child's subtree. A join has
two children, and so does every set operation. Anything drawing the plan as a tree
drew the wrong tree faithfully.

Both guards assert the surface rather than the case: the two orders agree ROW FOR ROW
across inner, left, chained, aliased, unaliased, composite-key and residual-beside-key
shapes, with a control that the hash join is the path being taken; and each ALIAS
stands over the scan of the table it aliases, with both arms of a UNION keeping their
own children. Seven and three cases go red respectively when the fixes are reverted.

Engine 2563, AdoNet 1054, Studio 1030 - green. Seven packages packed and read: all at
14.0.1, every internal dependency at 14.0.1. Known issues 25 and 26 are marked fixed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Eight snippets across five READMEs still said 14.0.0. Caught by
ShippedReadmesTests, which exists because five READMEs once installed 12.8.0 while
the packages were at 13.x - a README goes inside the package, so it reaches people
who never open the repository.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@dmitrat
dmitrat merged commit 4a1c981 into main Aug 19, 2026
1 check passed
@dmitrat
dmitrat deleted the engine/14.0.1 branch August 19, 2026 12:51
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.

1 participant