-- SPDX-FileCopyrightText: © 2026 Vladimir Zorin -- SPDX-License-Identifier: OWL-1.0 or later local testimony = require("testimony") local std = require("std") local mneme = require("mneme") local testify = testimony.new("== mneme: cursor iteration ==") local tmp_path = function() return "/tmp/mneme_cursor_" .. std.nanoid() .. ".mneme" end testify:that("cursor iterates all keys in sorted order", function() local path = tmp_path() local db = mneme.open(path, { sync = "none" }) local txn = db:txn_begin(false) local root = db:info().keyspace_root_pgno -- Insert 50 keys in random-ish order local keys = {} for i = 1, 50 do local k = string.format("key:%04d", (i * 37) % 50 + 1) root = txn:btree_put(root, k, "v" .. i) keys[k] = true end -- Iterate with cursor local cur = txn:cursor_open(root) local collected = {} local k = cur:first() while k do table.insert(collected, k) k = cur:next() end cur:close() -- Should be sorted testimony.assert_equal(50, #collected) for i = 2, #collected do testimony.assert_true(collected[i - 1] < collected[i]) end txn:abort() db:close() std.fs.remove(path) end) testify:that("cursor iterates backward with last/prev", function() local path = tmp_path() local db = mneme.open(path, { sync = "none" }) local txn = db:txn_begin(false) local root = db:info().keyspace_root_pgno for i = 1, 20 do root = txn:btree_put(root, string.format("r:%03d", i), "v") end local cur = txn:cursor_open(root) local collected = {} local k = cur:last() while k do table.insert(collected, k) k = cur:prev() end cur:close() testimony.assert_equal(20, #collected) -- Should be reverse sorted for i = 2, #collected do testimony.assert_true(collected[i - 1] > collected[i]) end txn:abort() db:close() std.fs.remove(path) end) testify:that("cursor seek positions at first key >= target", function() local path = tmp_path() local db = mneme.open(path, { sync = "none" }) local txn = db:txn_begin(false) local root = db:info().keyspace_root_pgno for i = 1, 10 do root = txn:btree_put(root, string.format("s:%02d", i * 2), "v") end -- Keys: s:02, s:04, s:06, s:08, s:10, s:12, s:14, s:16, s:18, s:20 local cur = txn:cursor_open(root) -- Seek to "s:05" → should land on "s:06" local k = cur:seek("s:05") testimony.assert_equal("s:06", k) -- Seek to "s:10" → exact match k = cur:seek("s:10") testimony.assert_equal("s:10", k) -- Seek to "s:21" → past all keys → nil k = cur:seek("s:21") testimony.assert_nil(k) -- Seek to "s:01" → should land on "s:02" (first key) k = cur:seek("s:01") testimony.assert_equal("s:02", k) cur:close() txn:abort() db:close() std.fs.remove(path) end) testify:that("cursor skips long runs of emptied leaves", function() local path = tmp_path() local db = mneme.open(path, { sync = "none" }) local txn = db:txn_begin(false) local root = db:info().keyspace_root_pgno local big = string.rep("v", 600) for i = 1, 1200 do root = txn:btree_put(root, string.format("e:%06d", i), big) end for i = 2, 1199 do root = txn:btree_del(root, string.format("e:%06d", i)) end local ok, err = txn:commit() testimony.assert_true(ok, err) txn = db:txn_begin(true) local cur = txn:cursor_open(root) local k = cur:first() testimony.assert_equal("e:000001", k) k = cur:next() testimony.assert_equal("e:001200", k) testimony.assert_nil(cur:next()) k = cur:last() testimony.assert_equal("e:001200", k) k = cur:prev() testimony.assert_equal("e:000001", k) testimony.assert_nil(cur:prev()) k = cur:seek("e:000500") testimony.assert_equal("e:001200", k) cur:close() txn:abort() db:close() std.fs.remove(path) end) testify:that("cursor on empty tree returns nil", function() local path = tmp_path() local db = mneme.open(path, { sync = "none" }) local txn = db:txn_begin(true) local root = db:info().keyspace_root_pgno local cur = txn:cursor_open(root) local k = cur:first() testimony.assert_nil(k) k = cur:last() testimony.assert_nil(k) cur:close() txn:abort() db:close() std.fs.remove(path) end) testify:that("cursor works across leaf splits (many keys)", function() local path = tmp_path() local db = mneme.open(path, { sync = "none" }) local txn = db:txn_begin(false) local root = db:info().keyspace_root_pgno -- Insert 300 keys to force multiple leaf pages for i = 1, 300 do root = txn:btree_put(root, string.format("m:%06d", i), "v" .. i) end local cur = txn:cursor_open(root) local count = 0 local prev_k = "" local k, v = cur:first() while k do count = count + 1 testimony.assert_true(k > prev_k) prev_k = k k, v = cur:next() end cur:close() testimony.assert_equal(300, count) txn:abort() db:close() std.fs.remove(path) end) testify:conclude()