Merge 'Fix crash in Next opcode if cursor stack has no pages' from Jussi Saurio

Closes #2924
Unsure if this fix is that great, but it does fix the issue described in
#2924 -- added minimal regression test to illustrate the behavior
This crash requires a pretty specific set of circumstances:
- 3-way join with two innermost being left joins
- nullable seek key on the innermost table:
    * middle table gets nulled out because no matches with the outermost
table
    * hence when we seek the innermost table using middle table values,
the seek key is null, so `Insn::IsNull` entirely skips the innermost
table
Perhaps a bytecode plan illustrates this better:
```sql
turso> explain select a.x, b.x, c.x from a left join b on a.y=b.x left join c on b.y=c.x;
addr  opcode             p1    p2    p3    p4             p5  comment
----  -----------------  ----  ----  ----  -------------  --  -------
0     Init               0     34    0                    0   Start at 34
1     OpenRead           0     2     0                    0   table=a, root=2, iDb=0
2     OpenRead           1     4     0                    0   table=b, root=4, iDb=0
3     OpenRead           2     5     0                    0   index=sqlite_autoindex_b_1, root=5, iDb=0
4     OpenRead           3     7     0                    0   index=sqlite_autoindex_c_1, root=7, iDb=0
5     Rewind             0     33    0                    0   Rewind table a
6       Integer          0     4     0                    0   r[4]=0
7       Column           0     1     6                    0   r[6]=a.y
8       IsNull           6     28    0                    0   if (r[6]==NULL) goto 28
9       SeekGE           2     28    6                    0   key=[6..6]
10        IdxGT          2     28    6                    0   key=[6..6]
11        DeferredSeek   2     1     0                    0   
12        Integer        1     4     0                    0   r[4]=1
13        Integer        0     5     0                    0   r[5]=0
14        Column         1     1     7                    0   r[7]=b.y
-- if b.y is NULL, we skip the entire table loop between insns 16-23
-- except when we call NullRow and then Goto to re-enter that loop in order to
-- return NULL values for the table
15        IsNull         7     24    0                    0   if (r[7]==NULL) goto 24
16        SeekGE         3     24    7                    0   key=[7..7]
17          IdxGT        3     24    7                    0   key=[7..7]
18          Integer      1     5     0                    0   r[5]=1
19          Column       0     0     1                    0   r[1]=a.x
20          Column       1     0     2                    0   r[2]=b.x
21          Column       3     0     3                    0   r[3]=sqlite_autoindex_c_1.x
22          ResultRow    1     3     0                    0   output=r[1..3]
23        Next           3     17    0                    0   
24        IfPos          5     27    0                    0   r[5]>0 -> r[5]-=0, goto 27
25        NullRow        3     0     0                    0   Set cursor 3 to a (pseudo) NULL row
26        Goto           0     18    0                    0   
27      Next             2     10    0                    0   
28      IfPos            4     32    0                    0   r[4]>0 -> r[4]-=0, goto 32
29      NullRow          1     0     0                    0   Set cursor 1 to a (pseudo) NULL row
30      NullRow          2     0     0                    0   Set cursor 2 to a (pseudo) NULL row
31      Goto             0     12    0                    0   
32    Next               0     6     0                    0   
33    Halt               0     0     0                    0   
34    Transaction        0     0     3                    0   iDb=0 write=false
35    Goto               0     1     0                    0
```

Reviewed-by: Preston Thorpe <preston@turso.tech>

Closes #2967
This commit is contained in:
Pekka Enberg
2025-09-08 17:45:29 +03:00
committed by GitHub
2 changed files with 15 additions and 2 deletions

View File

@@ -1213,6 +1213,10 @@ impl BTreeCursor {
}
None => return Ok(IOResult::Done(false)),
}
} else if self.stack.current_page == -1 {
// This can happen in nested left joins. See:
// https://github.com/tursodatabase/turso/issues/2924
return Ok(IOResult::Done(false));
}
loop {
let mem_page = self.stack.top_ref();
@@ -4274,7 +4278,6 @@ impl BTreeCursor {
if self.valid_state == CursorValidState::Invalid {
return Ok(IOResult::Done(false));
}
loop {
match self.advance_state {
AdvanceState::Start => {

View File

@@ -318,4 +318,14 @@ do_execsql_test_on_specific_db {:memory:} left-join-seek-key-regression-test {
CREATE TABLE u (x INTEGER PRIMARY KEY);
INSERT INTO t VALUES (1);
SELECT * FROM t LEFT JOIN u ON false WHERE u.x = 1;
} {}
} {}
# regression test for issue 2924: calling Next on a cursor that hasn't moved yet
do_execsql_test_on_specific_db {:memory:} next-crash {
create table a(x int primary key,y);
create table b(x int primary key,y);
create table c(x int primary key,y);
insert into a values (1,1),(2,2);
select a.x, b.x, c.x from a left join b on a.y=b.x left join c on b.y=c.x;
} {1||
2||}