Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 45 additions & 5 deletions src/tidesdb.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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",
Expand All @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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
143 changes: 119 additions & 24 deletions tests/test_tidesdb.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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...")
Expand Down
4 changes: 2 additions & 2 deletions tidesdb-0.7.1-1.rockspec → tidesdb-0.7.2-1.rockspec
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
Loading