Skip to content

Fix EXISTS with select-list window functions (row-index error) - #3682

Open
vishnujayvel wants to merge 1 commit into
dolthub:mainfrom
vishnujayvel:fix/gms-11421-exists-window
Open

Fix EXISTS with select-list window functions (row-index error)#3682
vishnujayvel wants to merge 1 commit into
dolthub:mainfrom
vishnujayvel:fix/gms-11421-exists-window

Conversation

@vishnujayvel

Copy link
Copy Markdown
Contributor

Fixes: dolthub/dolt#11421

Reported by @Yibo-Dong.

Problem

A correlated EXISTS whose select list is a window expression (e.g. ROW_NUMBER() OVER ()) fails with an internal error:

unable to find field with index 3 in row of 3 columns
This is a bug. Please file an issue here: https://github.com/dolthub/dolt/issues

The same query with SELECT 1 (or any non-window projection) works. MySQL accepts the window form and returns the expected existence result.

This is not crash-only. Multi-window EXISTS bodies (two or more window functions with distinct partition schemes) return silently wrong rows on main — for example, with u2(x,y) rows (1,10),(1,20),(2,30) and t ids 1,2,3:

SELECT id FROM t
WHERE EXISTS (
  SELECT ROW_NUMBER() OVER (PARTITION BY u2.y), RANK() OVER (ORDER BY u2.y)
  FROM u2 WHERE u2.x = t.a
)
ORDER BY id;
-- main (wrong): 1, 2, 3
-- correct / MySQL: 1, 2

NOT EXISTS of the same shape returns no rows on main instead of row 3. Silent wrong results are the highest-priority maintainer concern for this fix.

Separately, the uncorrelated NOT EXISTS ... OVER () shape panics the server on main (nil-pointer SIGSEGV at sql/core.go:432 via join_iters.go:433). Panic outranks internal error.

How to reproduce (crash shape)

CREATE TABLE t(id INT PRIMARY KEY, a INT);
CREATE TABLE u(x INT);
INSERT INTO t VALUES (1,1),(2,2),(3,3);
INSERT INTO u VALUES (1),(1),(2);

SELECT id FROM t
WHERE EXISTS (SELECT ROW_NUMBER() OVER () FROM u WHERE u.x = t.a)
ORDER BY id;
-- expected: 1, 2

Root cause

simplifyPartialJoinParents in sql/analyzer/unnest_exists_subqueries.go strips nodes that do not affect existence (Project, GroupBy, Sort, Distinct, TopN, Limit) from the decorrelated EXISTS body before building the semi-join right side. It did not strip *plan.Window.

The Window node shrinks the right-side schema to the window outputs, while join-filter GetField indices still refer to pre-window column ids. Evaluation then hits an out-of-bounds GetField in sql/expression/get_field.go via the exists iterator — or, with multiple windows, can produce silently incorrect existence results.

Fix

Treat *plan.Window like *plan.Project in simplifyPartialJoinParents: strip it when simplifying the EXISTS body. Window functions are 1:1 row-preserving (empty child → empty output), so existence does not depend on select-list window outputs.

Filter is not in the strip list, so the loop halts at any surviving Filter; correlated Filters are hoisted by decorrelateOuterCols and re-attached above the simplified node (unnest_exists_subqueries.go:416-417). A Filter with zero correlated conjuncts is left untouched (:314-317 returns SameTree). Having bodies are separately rejected earlier (case *plan.Having: return nil, false). Stripping is therefore limited to pure select-list window projection wrappers.

Tests

Enginetest script in JoinScriptTests (enginetest/queries/join_queries.go):

  • Control: EXISTS (SELECT 1 FROM u WHERE u.x = t.a) → rows 1, 2
  • Bug (crash shape): EXISTS (SELECT ROW_NUMBER() OVER () FROM u WHERE u.x = t.a) → rows 1, 2
  • NOT EXISTS variant with the same window select list → row 3
  • Multi-window silent-corruption shape (distinct partition schemes on u2) → EXISTS 1, 2; NOT EXISTS 3
  • Uncorrelated panic shape: NOT EXISTS (SELECT ROW_NUMBER() OVER () FROM u) → empty result (0 rows)

Test plan

export CGO_CFLAGS="-I/opt/homebrew/opt/icu4c@78/include"
export CGO_CXXFLAGS="-I/opt/homebrew/opt/icu4c@78/include"
export CGO_LDFLAGS="-L/opt/homebrew/opt/icu4c@78/lib"

go test ./sql/analyzer/... -count=1
go test ./enginetest/... -count=1
  • Full suite re-run after this patch:
$ go test ./sql/analyzer/... -count=1
ok  github.com/dolthub/go-mysql-server/sql/analyzer  0.457s

$ go test ./enginetest/... -count=1
ok  github.com/dolthub/go-mysql-server/enginetest              44.573s
ok  github.com/dolthub/go-mysql-server/enginetest/mysql_harness 1.047s
ok  github.com/dolthub/go-mysql-server/enginetest/scriptgen/setup 0.205s

JoinQueries multi-window subtests (verbatim -v):

--- PASS: TestJoinQueries/EXISTS_with_select-list_window_function (0.00s)
    --- PASS: .../SELECT_id_FROM_t_WHERE_EXISTS_(SELECT_1_FROM_u_WHERE_u.x_=_t.a)_ORDER_BY_id
    --- PASS: .../SELECT_id_FROM_t_WHERE_EXISTS_(SELECT_ROW_NUMBER()_OVER_()_FROM_u_WHERE_u.x_=_t.a)_ORDER_BY_id
    --- PASS: .../SELECT_id_FROM_t_WHERE_NOT_EXISTS_(SELECT_ROW_NUMBER()_OVER_()_FROM_u_WHERE_u.x_=_t.a)_ORDER_BY_id
    --- PASS: .../SELECT_id_FROM_t_WHERE_EXISTS_(SELECT_ROW_NUMBER()_OVER_(PARTITION_BY_u2.y),_RANK()_OVER_(ORDER_BY_u2.y)_FROM_u2_WHERE_u2.x_=_t.a)_ORDER_BY_id
    --- PASS: .../SELECT_id_FROM_t_WHERE_NOT_EXISTS_(SELECT_ROW_NUMBER()_OVER_(PARTITION_BY_u2.y),_RANK()_OVER_(ORDER_BY_u2.y)_FROM_u2_WHERE_u2.x_=_t.a)_ORDER_BY_id

Limitations / follow-ups

  • Fix is in go-mysql-server. Dolt consumers need a GMS dependency pin bump as a separate change.
  • Only pure select-list windows are stripped. Bodies where a window feeds a HAVING above the Window still refuse unnest (Having returns false); that path was already conservative and is unchanged.
  • LIMIT stripping is pre-existing (TODO at unnest_exists_subqueries.go:114-115, dolt#10493). With Window now stripped, EXISTS(...window... LIMIT 0) reaches that pre-existing hole (base hard-errors; patched returns rows where MySQL says empty) — same behavior as the existing SELECT 1 LIMIT 0 shape; nothing new introduced, noted for completeness.

Checklist

  • Minimal analyzer change (*plan.Window only)
  • Failing-before / passing-after enginetest for the issue query + NOT EXISTS + SELECT 1 control + multi-window silent-corruption shape
  • CI green
  • Dolt GMS pin bump (follow-up)

simplifyPartialJoinParents discarded Project/GroupBy/Sort/etc. when
building the right side of a decorrelated EXISTS semi-join, but left
*plan.Window in place. The Window shrinks the right-side schema to the
window outputs while join-filter GetField indices still refer to
pre-window columns, causing "unable to find field with index N in row of
M columns" (dolt#11421).

Treat *plan.Window like Project: existence does not depend on select-list
window outputs. Filter/Having remain unstripped so QUALIFY-like predicates
above a Window are preserved.

Add enginetest coverage for EXISTS/NOT EXISTS with ROW_NUMBER() OVER (),
multi-window silent-corruption shapes, uncorrelated NOT EXISTS + window
(panic on main), and the SELECT 1 control.

Fixes dolthub/dolt#11421
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Dolt raises an internal row-index error for EXISTS with a window expression.

2 participants