Skip to content

Commit addc439

Browse files
author
Ralph Küpper
committed
fix(sqlite): keep statement iterators exhausted
1 parent 504e180 commit addc439

2 files changed

Lines changed: 57 additions & 8 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix `DatabaseSync` statement iterators so they remain exhausted after a `for...of` loop. A later `.next()` on the same iterator now returns `{ done: true, value: null }` instead of restarting from the first row.

crates/perry-runtime/src/array/iter_object.rs

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -734,8 +734,9 @@ unsafe fn dispatch_array_iterator_method_inner(
734734
// Field 0: backing array pointer (NaN-boxed).
735735
let backing_field = js_object_get_field(iter_obj(), 0);
736736
let backing_f64 = f64::from_bits(backing_field.bits());
737-
// Array iterators clear their backing array at exhaustion. SQLite's
738-
// statement iterator restarts a completed execution on the next call.
737+
// Iterators clear their backing array at exhaustion. A completed
738+
// SQLite statement iterator is also permanently closed; calling
739+
// `StatementSync::iterate()` again creates a separate iterator.
739740
if JSValue::from_bits(backing_f64.to_bits()).is_undefined() {
740741
return crate::iter_result::emit_iter_result_cached(
741742
&scope,
@@ -763,12 +764,7 @@ unsafe fn dispatch_array_iterator_method_inner(
763764
};
764765

765766
if idx >= len {
766-
if kind == KIND_VALUES_NULL_DONE {
767-
// SQLite's statement iterator restarts on the next call.
768-
js_object_set_field(iter_obj(), 1, JSValue::number(0.0));
769-
} else {
770-
js_object_set_field(iter_obj(), 0, JSValue::undefined());
771-
}
767+
js_object_set_field(iter_obj(), 0, JSValue::undefined());
772768
return crate::iter_result::emit_iter_result_cached(
773769
&scope,
774770
&iter_h,
@@ -851,3 +847,55 @@ unsafe fn dispatch_array_iterator_method_inner(
851847
_ => f64::from_bits(TAG_UNDEFINED),
852848
}
853849
}
850+
851+
#[cfg(test)]
852+
mod sqlite_iterator_tests {
853+
use super::*;
854+
use std::sync::atomic::AtomicU64;
855+
856+
unsafe fn result_fields(result: f64) -> (u64, u64) {
857+
let result = js_nanbox_get_pointer(result) as *mut ObjectHeader;
858+
(
859+
js_object_get_field(result, 0).bits(),
860+
js_object_get_field(result, 1).bits(),
861+
)
862+
}
863+
864+
#[test]
865+
fn sqlite_iterator_stays_exhausted_after_fused_for_of_drain() {
866+
let _serialized = crate::array::test_serialize();
867+
let epoch = AtomicU64::new(0);
868+
let rows = crate::array::js_array_push_f64(crate::array::js_array_alloc(1), 7.0);
869+
let scope = crate::gc::RuntimeHandleScope::new();
870+
let iter_h = scope.root_nanbox_f64(array_values_iter_null_done(
871+
js_nanbox_pointer(rows as i64),
872+
&epoch,
873+
0,
874+
));
875+
let iter = || js_nanbox_get_pointer(iter_h.get_nanbox_f64()) as *mut ObjectHeader;
876+
877+
unsafe {
878+
// Model the optimized `for...of` driver: one yielded row followed
879+
// by its terminal advance.
880+
let first = dispatch_array_iterator_method_emit(iter(), "next", true, true);
881+
assert_eq!(
882+
result_fields(first),
883+
(crate::value::TAG_FALSE, 7.0f64.to_bits())
884+
);
885+
let done = dispatch_array_iterator_method_emit(iter(), "next", true, true);
886+
assert_eq!(
887+
result_fields(done),
888+
(crate::value::TAG_TRUE, crate::value::TAG_NULL)
889+
);
890+
891+
// The same iterator must remain closed when user code calls
892+
// `.next()` after the loop. Resetting its cursor used to return
893+
// the first row again here.
894+
let after = dispatch_array_iterator_method(iter(), "next");
895+
assert_eq!(
896+
result_fields(after),
897+
(crate::value::TAG_TRUE, crate::value::TAG_NULL)
898+
);
899+
}
900+
}
901+
}

0 commit comments

Comments
 (0)