-- 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: database lifecycle ==") local tmp_path = function() return "/tmp/mneme_test_" .. std.nanoid() .. ".mneme" end testify:that("open creates a new database file", function() local path = tmp_path() local db, err = mneme.open(path) testimony.assert_not_nil(db, err) local info = db:info() testimony.assert_not_nil(info) testimony.assert_true(info.page_count >= 3) testimony.assert_equal(0, info.txn_id) testimony.assert_nil(info.free_list_pgno) testimony.assert_equal(2, info.keyspace_root_pgno) db:close() std.fs.remove(path) end) testify:that("close and reopen preserves meta", function() local path = tmp_path() local db, err = mneme.open(path) testimony.assert_not_nil(db, err) local info1 = db:info() db:close() local db2, err2 = mneme.open(path) testimony.assert_not_nil(db2, err2) local info2 = db2:info() testimony.assert_equal(info1.page_count, info2.page_count) testimony.assert_equal(info1.txn_id, info2.txn_id) testimony.assert_equal(info1.keyspace_root_pgno, info2.keyspace_root_pgno) db2:close() std.fs.remove(path) end) testify:that("open with readonly works", function() local path = tmp_path() -- Create the file first local db, err = mneme.open(path) testimony.assert_not_nil(db, err) db:close() -- Reopen readonly local db2, err2 = mneme.open(path, { readonly = true }) testimony.assert_not_nil(db2, err2) local info = db2:info() testimony.assert_not_nil(info) db2:close() std.fs.remove(path) end) testify:that("open non-existent with create=false returns error", function() local path = tmp_path() local db, err = mneme.open(path, { create = false }) testimony.assert_nil(db) testimony.assert_not_nil(err) end) testify:that("double close does not crash", function() local path = tmp_path() local db, err = mneme.open(path) testimony.assert_not_nil(db, err) db:close() -- Second close returns error but doesn't crash local ok, err2 = db:close() testimony.assert_nil(ok) std.fs.remove(path) end) testify:that("operations on closed db return error", function() local path = tmp_path() local db, err = mneme.open(path) testimony.assert_not_nil(db, err) db:close() local info, err2 = db:info() testimony.assert_nil(info) testimony.assert_not_nil(err2) std.fs.remove(path) end) testify:that("page allocation increases page count", function() local path = tmp_path() local db, err = mneme.open(path) testimony.assert_not_nil(db, err) local info_before = db:info() local initial_pages = info_before.page_count -- Begin write txn, allocate pages, commit local txn, txn_err = db:txn_begin(false) testimony.assert_not_nil(txn, txn_err) local pgnos = {} for i = 1, 5 do local pgno, alloc_err = txn:page_alloc() testimony.assert_not_nil(pgno, alloc_err) table.insert(pgnos, pgno) end local ok, commit_err = txn:commit() testimony.assert_not_nil(ok, commit_err) local info_after = db:info() testimony.assert_equal(initial_pages + 5, info_after.page_count) db:close() std.fs.remove(path) end) testify:that("freed pages can be reused by subsequent alloc", function() local path = tmp_path() local db, err = mneme.open(path) testimony.assert_not_nil(db, err) -- Allocate 3 pages. local txn1 = db:txn_begin(false) local p1 = txn1:page_alloc() local p2 = txn1:page_alloc() local p3 = txn1:page_alloc() txn1:commit() local count_after_alloc = db:info().page_count -- Free 2 of them. In v3 these pages are retired, then later writers may -- harvest and reuse them once no reader can still observe the old snapshot. local txn2 = db:txn_begin(false) txn2:page_free(p1) txn2:page_free(p2) txn2:commit() local count_after_free = db:info().page_count testimony.assert_true(count_after_free >= count_after_alloc) -- A subsequent alloc may reuse freed pages instead of extending the file. local txn3 = db:txn_begin(false) local r1 = txn3:page_alloc() local r2 = txn3:page_alloc() txn3:commit() local count_after_reuse = db:info().page_count testimony.assert_true((r1 == p1 or r1 == p2) or (r2 == p1 or r2 == p2)) testimony.assert_true(count_after_reuse <= count_after_free + 1) db:close() std.fs.remove(path) end) testify:that("meta page fallback on corruption", function() local path = tmp_path() local db, err = mneme.open(path) testimony.assert_not_nil(db, err) -- Force a commit so meta page B gets a valid entry local txn = db:txn_begin(false) txn:page_alloc() txn:commit() local info = db:info() testimony.assert_equal(1, info.txn_id) db:close() -- Corrupt meta page A (page 0) by writing garbage local f = io.open(path, "r+b") testimony.assert_not_nil(f) f:seek("set", 0) f:write(string.rep("\xff", 64)) f:close() -- Reopen -- should fall back to meta page B local db2, err2 = mneme.open(path) testimony.assert_not_nil(db2, err2) local info2 = db2:info() testimony.assert_equal(1, info2.txn_id) db2:close() std.fs.remove(path) end) testify:that("read-only txn does not require write lock", function() local path = tmp_path() local db, err = mneme.open(path) testimony.assert_not_nil(db, err) local txn, txn_err = db:txn_begin(true) testimony.assert_not_nil(txn, txn_err) txn:abort() db:close() std.fs.remove(path) end) testify:that("txn meta reflects in-flight changes", function() local path = tmp_path() local db, err = mneme.open(path) testimony.assert_not_nil(db, err) local txn = db:txn_begin(false) local meta_before = txn:meta() local initial_pages = meta_before.page_count txn:page_alloc() txn:page_alloc() local meta_after = txn:meta() testimony.assert_equal(initial_pages + 2, meta_after.page_count) txn:abort() -- After abort, db info should still show original page count local info = db:info() testimony.assert_equal(initial_pages, info.page_count) db:close() std.fs.remove(path) end) testify:conclude()