Wrong results: LEFT JOIN LATERAL (...) ON TRUE drops outer rows whose lateral body yields zero rows
- Dominant language
- Go
- Stars
- 24.4k
- Forks
- 873
- Avg merge
- 1d 8h
- Merged PRs (30d)
- 120
Description
### What happens
`LEFT JOIN LATERAL () ON TRUE` must **preserve every outer row**: when the lateral body
produces zero rows for a given outer row, that outer row must still appear once, with the body's
columns filled with `NULL`. Dolt instead **drops** such outer rows entirely — the `LEFT JOIN LATERAL`
degenerates into `CROSS`/`INNER JOIN LATERAL`. This is silent and diverges from MySQL 8.0.
In the repro, outer row `id=3` has no match in the lateral body, so the body yields zero rows. MySQL
keeps `(3, NULL)`; Dolt omits row `3` altogether — result-set membership is silently wrong.
Two boundaries scope the trigger tightly to "`LEFT JOIN LATERAL` × a body that can yield zero rows":
an **aggregate** lateral body (which always yields exactly one row, so there is nothing to drop) is
correct, and an **ordinary non-LATERAL** `LEFT JOIN` is correct.
### To Reproduce
```sql
CREATE DATABASE IF NOT EXISTS dolt_bug3;
USE dolt_bug3;
DROP TABLE IF EXISTS t0;
DROP TABLE IF EXISTS t1;
CREATE TABLE t0(id INT PRIMARY KEY, c0 INT);
CREATE TABLE t1(id INT PRIMARY KEY, c0 INT);
INSERT INTO t0 VALUES (1,10),(2,20),(3,30); -- id=3 has no match in t1
INSERT INTO t1 VALUES (1,10),(2,20);
-- LEFT JOIN LATERAL ... ON TRUE : outer row id=3 is DROPPED
SELECT t0.id, l.x
FROM t0
LEFT JOIN LATERAL (SELECT 1 AS x FROM t1 WHERE t1.c0 = t0.c0) l ON TRUE
ORDER BY t0.id;
```
### Actual (Dolt 2.2.3) vs Expected (MySQL 8.0)
| outer `id` | lateral body rows | Dolt 2.2.3 (actual) | MySQL 8.0 (expected) |
|---|---|---|---|
| 1 | 1 (matches `c0=10`) | `(1, 1)` | `(1, 1)` |
| 2 | 1 (matches `c0=20`) | `(2, 1)` | `(2, 1)` |
| 3 | 0 (no match) | **row dropped** | `(3, NULL)` |
Dolt returns `{(1,1),(2,1)}`; MySQL 8.0 returns `{(1,1),(2,1),(3,NULL)}`. Row `id=3` is silently
swallowed.
### Isolating boundary experiments
```sql
-- (Boundary 1) aggregate lateral body always yields one row -> nothing to drop -> Dolt CORRECT.
SELECT t0.id, l.n
FROM t0
LEFT JOIN LATERAL (SELECT COUNT(*) AS n FROM t1 WHERE t1.c0 = t0.c0) l ON TRUE
ORDER BY t0.id;
-- (Boundary 2) ordinary (non-LATERAL) LEFT JOIN preserves the outer row -> Dolt CORRECT.
SELECT t0.id, l.x
FROM t0
LEFT JOIN (SELECT c0, 1 AS x FROM t1) l ON l.c0 = t0.c0
ORDER BY t0.id;
```
| Boundary | Dolt 2.2.3 | MySQL 8.0 | verdict |
|---|---|---|---|
| aggregate lateral body (always 1 row) | `(1,1),(2,1),(3,0)` | `(1,1),(2,1),(3,0)` | both correct |
| ordinary non-LATERAL `LEFT JOIN` | `(1,1),(2,1),(3,NULL)` | `(1,1),(2,1),(3,NULL)` | both correct |
The aggregate-body boundary is the sharpest: it is byte-for-byte the same join shape as the failing
query except the body is `SELECT COUNT(*)` (one row) instead of `SELECT 1 … WHERE …` (zero-or-more
rows), and Dolt preserves `id=3` there. So the trigger is exactly "the lateral body can produce zero
rows".
### Related issues (please read before deduping)
This is a **still-open, distinct** manifestation in the LATERAL feature area, **not** a rediscovery of
either closed LATERAL issue:
* **#9820** (`CROSS JOIN LATERAL` + non-equi `JOIN`) — **closed**, and **verified fixed** on Dolt
2.2.3 (its repro now returns correct results here). Different join type (`CROSS`, not `LEFT`) and a
different trigger (non-equi join), so this report is not a reopening of it.
* **#6899** (comma-syntax lateral aggregation) — **closed**. Different syntax (comma lateral) and
concerns aggregation, not `LEFT JOIN LATERAL` row preservation.
The present defect is `LEFT JOIN LATERAL … ON TRUE` failing LEFT-side row preservation when the body
yields zero rows — the same feature area as #9820/#6899 but a separate, currently-reproducing bug.
### Version
Dolt 2.2.3, go-mysql-server. `SELECT dolt_version()` → `2.2.3`; `SELECT version()` → `8.0.31`.
Default configuration, macOS arm64.
---
*Re-verified on Dolt 2.2.3 (`dolt sql-server`); every Actual/Expected value above was cross-checked the same day against a live MySQL 8.0 server (Dolt's stated compatibility target) on identical data, and reproduces deterministically.*
Contributor guide
No contributing guide indexed for this repository
Research direction
Start the supplied SQL reproduction with dolt sql-server on Dolt 2.2.3, then trace the LEFT JOIN LATERAL handling in go-mysql-server. Add regression coverage for a lateral body that returns zero rows, while checking the aggregate and ordinary non-LATERAL boundary cases. Done means the outer row remains with NULL lateral columns and the existing boundary results stay correct.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go, sql
- Domain
- databases
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100