diff --git a/src/tidesdb.lua b/src/tidesdb.lua index a34052a..81729d8 100644 --- a/src/tidesdb.lua +++ b/src/tidesdb.lua @@ -40,6 +40,7 @@ ffi.cdef[[ static const int TDB_ERR_LOCKED = -12; static const int TDB_ERR_READONLY = -13; static const int TDB_ERR_BUSY = -14; + static const int TDB_ERR_PRECONDITION = -15; // Structures static const int TDB_MAX_CF_NAME_LEN = 128; @@ -227,7 +228,7 @@ ffi.cdef[[ // Transaction functions int tidesdb_txn_begin(void* db, void** txn); int tidesdb_txn_begin_with_isolation(void* db, int isolation, void** txn); - int tidesdb_txn_put(void* txn, void* cf, const uint8_t* key, size_t key_len, const uint8_t* value, size_t value_len, int ttl); + int tidesdb_txn_put(void* txn, void* cf, const uint8_t* key, size_t key_len, const uint8_t* value, size_t value_len, int64_t ttl); int tidesdb_txn_get(void* txn, void* cf, const uint8_t* key, size_t key_len, uint8_t** value, size_t* value_len); int tidesdb_txn_delete(void* txn, void* cf, const uint8_t* key, size_t key_len); int tidesdb_txn_single_delete(void* txn, void* cf, const uint8_t* key, size_t key_len); @@ -310,6 +311,8 @@ ffi.cdef[[ uint64_t total_uploads; uint64_t total_upload_failures; int replica_mode; + uint64_t primary_epoch; + uint64_t seen_epoch; uint64_t uwal_bytes_written; uint64_t wal_bytes_written; uint64_t flush_bytes_written; @@ -415,6 +418,7 @@ tidesdb.TDB_ERR_UNKNOWN = -11 tidesdb.TDB_ERR_LOCKED = -12 tidesdb.TDB_ERR_READONLY = -13 tidesdb.TDB_ERR_BUSY = -14 +tidesdb.TDB_ERR_PRECONDITION = -15 -- Compression algorithms tidesdb.CompressionAlgorithm = { @@ -451,6 +455,30 @@ tidesdb.IsolationLevel = { SERIALIZABLE = 4, } +-- Built-in comparator names. These comparators are registered automatically on +-- every database at open time, so a column family can select one simply by +-- setting `comparator_name` in its config -- no register_comparator call needed. +tidesdb.Comparator = { + MEMCMP = "memcmp", + LEXICOGRAPHIC = "lexicographic", + UINT64 = "uint64", + INT64 = "int64", + REVERSE = "reverse", + CASE_INSENSITIVE = "case_insensitive", +} + +-- Built-in comparator C function pointers. Exposed for callers that want to +-- register a built-in implementation under a custom name via +-- TidesDB:register_comparator, or invoke it directly. +tidesdb.builtin_comparators = { + memcmp = lib.tidesdb_comparator_memcmp, + lexicographic = lib.tidesdb_comparator_lexicographic, + uint64 = lib.tidesdb_comparator_uint64, + int64 = lib.tidesdb_comparator_int64, + reverse_memcmp = lib.tidesdb_comparator_reverse_memcmp, + case_insensitive = lib.tidesdb_comparator_case_insensitive, +} + -- Error messages local error_messages = { [tidesdb.TDB_ERR_MEMORY] = "memory allocation failed", @@ -467,6 +495,7 @@ local error_messages = { [tidesdb.TDB_ERR_LOCKED] = "database is locked", [tidesdb.TDB_ERR_READONLY] = "database is read-only", [tidesdb.TDB_ERR_BUSY] = "resource busy", + [tidesdb.TDB_ERR_PRECONDITION] = "precondition failed", } -- TidesDBError class @@ -973,6 +1002,8 @@ function ColumnFamily:get_stats() tombstone_density_trigger = c_cfg.tombstone_density_trigger, tombstone_density_min_entries = tonumber(c_cfg.tombstone_density_min_entries), use_btree = c_cfg.use_btree ~= 0, + object_lazy_compaction = c_cfg.object_lazy_compaction ~= 0, + object_prefetch_compaction = c_cfg.object_prefetch_compaction ~= 0, } end @@ -1068,7 +1099,11 @@ function Transaction:get(cf, key) check_result(result, "failed to get value") local value = ffi.string(value_ptr[0], value_size[0]) - ffi.C.free(value_ptr[0]) + -- The value buffer is allocated by TidesDB's allocator (tdb_malloc), so it + -- must be released through tidesdb_free, not libc free. They coincide for the + -- default allocator but differ once a custom allocator is installed via + -- tidesdb.init (and may bind to different CRT heaps on Windows). + lib.tidesdb_free(value_ptr[0]) return value end @@ -1375,15 +1410,18 @@ function TidesDB:list_column_families() return {} end + -- Both the array and each name string are allocated by TidesDB's allocator + -- (malloc/tdb_strdup), so release them through tidesdb_free rather than libc + -- free so a custom allocator installed via tidesdb.init frees on its own heap. local names = {} for i = 0, count[0] - 1 do local str_ptr = names_ptr[0][i] if str_ptr ~= nil then table.insert(names, ffi.string(str_ptr)) - ffi.C.free(str_ptr) + lib.tidesdb_free(str_ptr) end end - ffi.C.free(names_ptr[0]) + lib.tidesdb_free(names_ptr[0]) return names end @@ -1491,6 +1529,8 @@ function TidesDB:get_db_stats() total_uploads = tonumber(c_stats.total_uploads), total_upload_failures = tonumber(c_stats.total_upload_failures), replica_mode = c_stats.replica_mode ~= 0, + primary_epoch = tonumber(c_stats.primary_epoch), + seen_epoch = tonumber(c_stats.seen_epoch), uwal_bytes_written = tonumber(c_stats.uwal_bytes_written), wal_bytes_written = tonumber(c_stats.wal_bytes_written), flush_bytes_written = tonumber(c_stats.flush_bytes_written), @@ -1595,6 +1635,6 @@ function tidesdb.save_config_to_ini(ini_file, section_name, config) end -- Version -tidesdb._VERSION = "0.7.1" +tidesdb._VERSION = "0.7.2" return tidesdb diff --git a/tests/test_tidesdb.lua b/tests/test_tidesdb.lua index 8e25fc7..328e953 100644 --- a/tests/test_tidesdb.lua +++ b/tests/test_tidesdb.lua @@ -1623,30 +1623,34 @@ function tests.test_cancel_background_work() print("PASS: test_cancel_background_work") end -function tests.test_objstore_s3_unavailable() - -- The bundled library is built without TIDESDB_WITH_S3, so the S3 connector - -- factories should raise a clear error rather than crash. (When built with S3 - -- support these would instead attempt a real connection.) - assert_error(function() - tidesdb.objstore_s3_create({ - endpoint = "localhost:9000", - bucket = "test", - access_key = "minioadmin", - secret_key = "minioadmin", - use_path_style = true, - }) - end, "objstore_s3_create should error when S3 support is unavailable") - - assert_error(function() - tidesdb.objstore_s3_create_config({ - endpoint = "localhost:9000", - bucket = "test", - access_key = "minioadmin", - secret_key = "minioadmin", - use_path_style = true, - }) - end, "objstore_s3_create_config should error when S3 support is unavailable") - print("PASS: test_objstore_s3_unavailable") +function tests.test_objstore_s3_factories() + -- The S3 connector factories must behave well regardless of how libtidesdb was + -- compiled: when built WITHOUT TIDESDB_WITH_S3 they must raise a clear error + -- rather than crash; when built WITH S3 they return a connector handle (lazily, + -- without contacting the endpoint). Either outcome is acceptable here -- what we + -- guard against is a crash or a silent nil. This keeps the suite green against + -- both build flavours. + local function check(factory, args, label) + local ok, result = pcall(factory, args) + if ok then + -- S3 built in: a non-nil connector handle must come back. + assert_true(result ~= nil, label .. " returned nil without erroring") + else + -- S3 not built in: the wrapper raises a TidesDBError, not a raw crash. + assert_true(result ~= nil, label .. " raised an empty error") + end + end + + local opts = { + endpoint = "localhost:9000", + bucket = "test", + access_key = "minioadmin", + secret_key = "minioadmin", + use_path_style = true, + } + check(tidesdb.objstore_s3_create, opts, "objstore_s3_create") + check(tidesdb.objstore_s3_create_config, opts, "objstore_s3_create_config") + print("PASS: test_objstore_s3_factories") end function tests.test_cf_stats_wa_fields() @@ -1774,6 +1778,97 @@ print("CHILD_OK") print("PASS: test_init_finalize") end +function tests.test_error_precondition_constant() + assert_eq(tidesdb.TDB_ERR_PRECONDITION, -15, "TDB_ERR_PRECONDITION value") + -- The code must map to a human-readable message, not the "unknown error" fallback. + local err = tidesdb.TidesDBError.from_code(tidesdb.TDB_ERR_PRECONDITION, "ctx") + assert_eq(err.code, -15, "error code preserved") + assert_true(err.message:find("precondition", 1, true) ~= nil, + "precondition error message, got: " .. tostring(err.message)) + print("PASS: test_error_precondition_constant") +end + +function tests.test_builtin_comparators() + -- Name constants are exposed. + assert_eq(tidesdb.Comparator.MEMCMP, "memcmp", "MEMCMP name") + assert_eq(tidesdb.Comparator.UINT64, "uint64", "UINT64 name") + assert_eq(tidesdb.Comparator.CASE_INSENSITIVE, "case_insensitive", "CASE_INSENSITIVE name") + -- Built-in C function pointers are reachable. + assert_true(tidesdb.builtin_comparators.memcmp ~= nil, "memcmp fn pointer") + assert_true(tidesdb.builtin_comparators.uint64 ~= nil, "uint64 fn pointer") + + -- A column family can select a built-in comparator by name; it orders keys + -- per that comparator without any register_comparator call. + local path = "./test_db_builtin_cmp" + cleanup_db(path) + local db = tidesdb.TidesDB.open(path, { log_level = tidesdb.LogLevel.LOG_WARN }) + local cfg = tidesdb.default_column_family_config() + cfg.comparator_name = tidesdb.Comparator.UINT64 + db:create_column_family("nums", cfg) + local cf = db:get_column_family("nums") + + -- The uint64 comparator memcpy's 8 key bytes into a uint64 (native byte + -- order, little-endian on x86-64), so encode keys little-endian. + local function u64(n) + local b = {} + for i = 1, 8 do + b[i] = string.char(n % 256) + n = math.floor(n / 256) + end + return table.concat(b) + end + local txn = db:begin_txn() + txn:put(cf, u64(300), "three-hundred") + txn:put(cf, u64(20), "twenty") + txn:put(cf, u64(1), "one") + txn:commit() + txn:free() + + local rtxn = db:begin_txn() + local iter = rtxn:new_iterator(cf) + iter:seek_to_first() + local order = {} + while iter:valid() do + table.insert(order, iter:value()) + iter:next() + end + iter:free() + rtxn:free() + assert_eq(order[1], "one", "uint64 order first") + assert_eq(order[2], "twenty", "uint64 order second") + assert_eq(order[3], "three-hundred", "uint64 order third") + + db:close() + cleanup_db(path) + print("PASS: test_builtin_comparators") +end + +function tests.test_large_ttl_not_truncated() + -- TTL is an absolute expiry epoch (time_t). A value above 2^32 must survive + -- as a 64-bit argument; truncating it to a 32-bit int would wrap it down to a + -- small "already expired" timestamp and silently drop the key on read. + local path = "./test_db_large_ttl" + cleanup_db(path) + local db = tidesdb.TidesDB.open(path, { log_level = tidesdb.LogLevel.LOG_WARN }) + db:create_column_family("c") + local cf = db:get_column_family("c") + + local far_future = 4294968296 -- 2^32 + 1000, ~year 2106; low 32 bits = 1000 (in the past) + local txn = db:begin_txn() + txn:put(cf, "k", "v", far_future) + txn:commit() + txn:free() + + local rtxn = db:begin_txn() + local val = rtxn:get(cf, "k") + rtxn:free() + assert_eq(val, "v", "key with far-future TTL must not be treated as expired") + + db:close() + cleanup_db(path) + print("PASS: test_large_ttl_not_truncated") +end + -- Run all tests local function run_tests() print("Running TidesDB Lua tests...") diff --git a/tidesdb-0.7.1-1.rockspec b/tidesdb-0.7.2-1.rockspec similarity index 95% rename from tidesdb-0.7.1-1.rockspec rename to tidesdb-0.7.2-1.rockspec index 4ab4ae5..77f28dc 100644 --- a/tidesdb-0.7.1-1.rockspec +++ b/tidesdb-0.7.2-1.rockspec @@ -1,8 +1,8 @@ package = "tidesdb" -version = "0.7.1-1" +version = "0.7.2-1" source = { url = "git://github.com/tidesdb/tidesdb-lua.git", - tag = "v0.7.1" + tag = "v0.7.2" } description = { summary = "Official Lua bindings for TidesDB - A high-performance embedded key-value storage engine",