-- 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 litls = require("litls.core") local testify = testimony.new("== mneme: statistics, compaction, and maintenance ==") local tmp_path = function() return "/tmp/mneme_maint_" .. std.nanoid() .. ".mneme" end testify:that("db:stats() on fresh database", function() local path = tmp_path() local db = mneme.open(path, { sync = "none" }) local stats = db:stats() testimony.assert_not_nil(stats) testimony.assert_true(stats.page_count >= 3) -- meta A, meta B, empty leaf testimony.assert_nil(stats.free_pages) testimony.assert_equal(stats.page_count, stats.live_pages) testimony.assert_equal(0, stats.reclaimable_pages) testimony.assert_equal(0, stats.bloat_ratio) testimony.assert_equal(0, stats.keyspaces) testimony.assert_true(stats.file_size > 0) db:close() std.fs.remove(path) end) testify:that("db:stats() reflects keyspaces and data", function() local path = tmp_path() local db = mneme.open(path, { sync = "none" }) db:keyspace("first"):set("a", "1") db:keyspace("second"):set("b", "2") db:keyspace("third"):set("c", "3") local stats = db:stats() testimony.assert_equal(3, stats.keyspaces) testimony.assert_true(stats.page_count > 3) db:close() std.fs.remove(path) end) testify:that("db:stats() reports reclaimable pages after deletion", function() local path = tmp_path() local db = mneme.open(path, { sync = "none" }) local ks = db:keyspace("data") for i = 1, 80 do ks:set(string.format("k:%04d", i), string.rep("v", 100)) end for i = 1, 60 do ks:del(string.format("k:%04d", i)) end local stats = db:stats() testimony.assert_true(stats.live_pages > 0) testimony.assert_true(stats.reclaimable_pages > 0) testimony.assert_true(stats.bloat_ratio > 0) testimony.assert_equal(stats.page_count, stats.live_pages + stats.reclaimable_pages) db:close() std.fs.remove(path) end) testify:that("ks:stats() after inserting keys", function() local path = tmp_path() local db = mneme.open(path, { sync = "none" }) local ks = db:keyspace("data") for i = 1, 100 do ks:set(string.format("k:%04d", i), "v" .. i) end local stats = ks:stats() testimony.assert_equal(100, stats.keys) testimony.assert_true(stats.depth >= 1) testimony.assert_true(stats.leaf_pages >= 1) db:close() std.fs.remove(path) end) testify:that("ks:stats() shows reduced keys after deletion", function() local path = tmp_path() local db = mneme.open(path, { sync = "none" }) local ks = db:keyspace("data") for i = 1, 50 do ks:set(string.format("k:%04d", i), "v" .. i) end for i = 1, 25 do ks:del(string.format("k:%04d", i)) end local stats = ks:stats() testimony.assert_equal(25, stats.keys) db:close() std.fs.remove(path) end) testify:that("drop_keyspace pages are reported as reclaimable", function() local path = tmp_path() local db = mneme.open(path, { sync = "none" }) local ks = db:keyspace("temporary") for i = 1, 80 do ks:set(string.format("k:%04d", i), string.rep("x", 100)) end local before = db:stats() db:drop_keyspace("temporary") local after = db:stats() testimony.assert_equal(0, after.keyspaces) testimony.assert_true(after.reclaimable_pages > before.reclaimable_pages) db:close() std.fs.remove(path) end) testify:that("deleted-key pages are reclaimable via db:compact()", function() -- Retired pages may be reused by later writers, but compaction should -- still physically shrink a fragmented file. local path = tmp_path() local compact_path = path .. ".compact" local db = mneme.open(path, { sync = "none" }) local ks = db:keyspace("data") for i = 1, 200 do ks:set(string.format("k:%04d", i), string.rep("x", 50)) end for i = 1, 150 do ks:del(string.format("k:%04d", i)) end local before_size = db:stats().file_size db:compact(compact_path) db:close() local db2 = mneme.open(compact_path) testimony.assert_true(db2:stats().file_size < before_size) db2:close() std.fs.remove(path) std.fs.remove(compact_path) end) testify:that("db:compact() produces smaller file with surviving keys", function() local path = tmp_path() local compact_path = path .. ".compact" local db = mneme.open(path, { sync = "none" }) local ks = db:keyspace("data") -- Insert then delete to create fragmentation for i = 1, 200 do ks:set(string.format("k:%04d", i), string.rep("v", 50)) end for i = 1, 150 do ks:del(string.format("k:%04d", i)) end local before_stats = db:stats() -- Compact local ok, err = db:compact(compact_path) testimony.assert_not_nil(ok, err) db:close() -- Open compacted database and verify local db2 = mneme.open(compact_path) local ks2 = db2:keyspace("data") -- Surviving keys should be accessible for i = 151, 200 do local val = ks2:get(string.format("k:%04d", i)) testimony.assert_not_nil(val) testimony.assert_equal(50, #val) end -- Deleted keys should not exist for i = 1, 5 do testimony.assert_nil(ks2:get(string.format("k:%04d", i))) end -- Compacted file should have fewer pages and minimal fragmentation local after_stats = db2:stats() testimony.assert_equal(after_stats.page_count, after_stats.live_pages + after_stats.reclaimable_pages) testimony.assert_true(after_stats.page_count <= before_stats.page_count) local ks2_stats = ks2:stats() testimony.assert_equal(50, ks2_stats.keys) db2:close() std.fs.remove(path) std.fs.remove(compact_path) end) testify:that("compact preserves multiple keyspaces", function() local path = tmp_path() local compact_path = path .. ".compact" local db = mneme.open(path, { sync = "none" }) db:keyspace("alpha"):set("a", "1") db:keyspace("beta"):set("b", "2") db:keyspace("gamma"):set("c", "3") db:compact(compact_path) db:close() local db2 = mneme.open(compact_path) testimony.assert_equal("1", db2:keyspace("alpha"):get("a")) testimony.assert_equal("2", db2:keyspace("beta"):get("b")) testimony.assert_equal("3", db2:keyspace("gamma"):get("c")) local names = db2:keyspaces() testimony.assert_equal(3, #names) db2:close() std.fs.remove(path) std.fs.remove(compact_path) end) testify:that("compact skips expired keys", function() local path = tmp_path() local compact_path = path .. ".compact" local db = mneme.open(path, { sync = "none" }) local ks = db:keyspace("data") ks:set("live", "yes") ks:set("dead", "no", { ttl = 1 }) os.execute("sleep 2") db:compact(compact_path) db:close() local db2 = mneme.open(compact_path) testimony.assert_equal("yes", db2:keyspace("data"):get("live")) testimony.assert_nil(db2:keyspace("data"):get("dead")) db2:close() std.fs.remove(path) std.fs.remove(compact_path) end) testify:that("key too long returns error", function() local path = tmp_path() local db = mneme.open(path, { sync = "none" }) local ks = db:keyspace("data") local long_key = string.rep("x", 4001) local ok, err = ks:set(long_key, "value") testimony.assert_nil(ok) testimony.assert_equal("key too long", err) db:close() std.fs.remove(path) end) testify:that("operations on closed db return error", function() local path = tmp_path() local db = mneme.open(path, { sync = "none" }) db:close() local ok, err = db:stats() testimony.assert_nil(ok) testimony.assert_not_nil(err) local ks, ks_err = db:keyspace("test") testimony.assert_nil(ks) testimony.assert_not_nil(ks_err) std.fs.remove(path) end) testify:that("stress: 10000 insert, 5000 delete, compact, verify 5000 remain", function() local path = tmp_path() local compact_path = path .. ".compact" local db = mneme.open(path, { sync = "none" }) local ks = db:keyspace("stress") for i = 1, 10000 do ks:set(string.format("s:%06d", i), "val" .. i) end testimony.assert_equal(10000, ks:stats().keys) for i = 1, 5000 do ks:del(string.format("s:%06d", i)) end testimony.assert_equal(5000, ks:stats().keys) db:compact(compact_path) db:close() local db2 = mneme.open(compact_path) local ks2 = db2:keyspace("stress") testimony.assert_equal(5000, ks2:stats().keys) -- Spot check testimony.assert_nil(ks2:get("s:000001")) testimony.assert_equal("val5001", ks2:get("s:005001")) testimony.assert_equal("val10000", ks2:get("s:010000")) db2:close() std.fs.remove(path) std.fs.remove(compact_path) end) testify:that("compact preserves encryption metadata", function() local path = tmp_path() local compact_path = path .. ".compact" local seed = litls.random_bytes(32) local pubkey = litls.ed25519_keypair(seed) local enc = { seed = seed, pubkey = pubkey } -- Create encrypted database with data local db = mneme.open(path, { sync = "none", encryption = enc }) local ks = db:keyspace("secrets", { encrypted = true }) ks:set("api-key", "sk-12345") ks:set("token", "tok-abcde") db:close() -- Re-open and compact local db2 = mneme.open(path, { encryption = enc }) local ok, err = db2:compact(compact_path) testimony.assert_not_nil(ok, err) db2:close() -- Open compacted database — must not fail with "truncated encryption metadata" local db3, oerr = mneme.open(compact_path, { encryption = enc }) testimony.assert_not_nil(db3, oerr) -- Encrypted keyspace data must survive compaction local ks3 = db3:keyspace("secrets", { encrypted = true }) testimony.assert_equal("sk-12345", ks3:get("api-key")) testimony.assert_equal("tok-abcde", ks3:get("token")) db3:close() std.fs.remove(path) std.fs.remove(compact_path) end) testify:conclude()