Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions core/mvcc/cursor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -800,6 +800,9 @@ impl<Clock: LogicalClock + 'static> MvccLazyCursor<Clock> {

impl<Clock: LogicalClock + 'static> CursorTrait for MvccLazyCursor<Clock> {
fn last(&mut self) -> Result<IOResult<()>> {
// A cursor may be NullRow'd during outer-join unmatched emission.
// Repositioning to a real row must clear that synthetic NULL state.
self.set_null_flag(false);
let state = self.state.clone();
if state.is_none() {
let _ = self.table_iterator.take();
Expand Down Expand Up @@ -1534,6 +1537,9 @@ impl<Clock: LogicalClock + 'static> CursorTrait for MvccLazyCursor<Clock> {
}

fn rewind(&mut self) -> Result<IOResult<()>> {
// A cursor may be NullRow'd during outer-join unmatched emission.
// Repositioning to a real row must clear that synthetic NULL state.
self.set_null_flag(false);
let state = self.state.clone();
if state.is_none() {
let _ = self.table_iterator.take();
Expand Down
2 changes: 2 additions & 0 deletions core/storage/btree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4957,6 +4957,7 @@ impl CursorTrait for BTreeCursor {

#[cfg_attr(debug_assertions, instrument(skip_all, level = Level::DEBUG))]
fn last(&mut self) -> Result<IOResult<()>> {
self.set_null_flag(false);
let always_seek = false;
let cursor_has_record = return_if_io!(self.move_to_rightmost(always_seek));
self.set_has_record(cursor_has_record);
Expand Down Expand Up @@ -5643,6 +5644,7 @@ impl CursorTrait for BTreeCursor {

#[cfg_attr(debug_assertions, instrument(skip_all, level = Level::DEBUG))]
fn rewind(&mut self) -> Result<IOResult<()>> {
self.set_null_flag(false);
if self.valid_state == CursorValidState::Invalid {
return Ok(IOResult::Done(()));
}
Expand Down
4 changes: 4 additions & 0 deletions core/vdbe/execute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3082,6 +3082,10 @@ pub fn op_row_id(
.get_mut(*cursor_id)
.expect("cursor_id should be valid")
{
if btree_cursor.get_null_flag() {
state.registers[*dest] = Register::Value(Value::Null);
break;
}
if let Some(ref rowid) = return_if_io!(btree_cursor.rowid()) {
state.registers[*dest].set_int(*rowid);
} else {
Expand Down
16 changes: 16 additions & 0 deletions testing/runner/turso-tests/mvcc_left_join_null_row.sqltest
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Regression test for MVCC LEFT JOIN NullRow bug.
# MvccLazyCursor::rowid() was not checking null_flag, so after NullRow
# it returned the stale rowid of the last seeked row instead of NULL.

@database :memory:

test mvcc-left-join-null-row {
CREATE TABLE t1(x INTEGER PRIMARY KEY);
CREATE TABLE t2(x INTEGER PRIMARY KEY, v TEXT);
INSERT INTO t1 VALUES (1);
INSERT INTO t2 VALUES (1, 'hello');
SELECT t1.x, t2.x FROM t1 LEFT JOIN t2 ON t1.x = t2.x AND t2.v = 'nonexistent';
}
expect {
1|
}
Loading