Zig bindings for libMDBX (a fork of LMDB).
Built and tested with Zig version 0.15.2.
libmdbx is an extremely fast, compact, powerful, embedded, transactional key-value database with a specific set of properties and capabilities, focused on creating unique lightweight solutions. libmdbx is superior to legendary LMDB in terms of features and reliability, not inferior in performance. In comparison to LMDB, libmdbx make things "just work" perfectly and out-of-the-box, not silently and catastrophically break down.
# replace {VERSION} with the latest release eg: v0.2.3
zig fetch https://github.com/theseyan/lmdbx-zig/archive/refs/tags/{VERSION}.tar.gzCopy the hash generated and add lmdbx-zig to build.zig.zon:
.{
.dependencies = .{
.lmdbx = .{
.url = "https://github.com/theseyan/lmdbx-zig/archive/refs/tags/{VERSION}.tar.gz",
.hash = "{HASH}",
},
},
}lmdbx-zig officially supports cross-compiling to the following target triples:
x86_64-linux-gnu,x86_64-macos,x86_64-windows-gnuaarch64-linux-gnu,aarch64-macos,aarch64-windows-gnu
Successful compilation on other targets is not guaranteed (but might work).
A libMDBX environment can either have multiple named databases, or a single unnamed database.
To use a single unnamed database, open a transaction and use the txn.get, txn.set, txn.delete, and txn.cursor methods directly.
const lmdbx = @import("lmdbx");
pub fn main() !void {
const env = try lmdbx.Environment.init("path/to/db", .{});
defer env.deinit();
const txn = try env.transaction(.{ .mode = .ReadWrite });
errdefer txn.abort();
try txn.set("aaa", "foo", .Create);
try txn.set("bbb", "bar", .Upsert);
try txn.commit();
}To use named databases, open the environment with a non-zero max_dbs value. Then open each named database using Transaction.database, which returns a Database struct with db.get/db.set/db.delete/db.cursor methods. You don't have to close databases, but they're only valid during the lifetime of the transaction.
const lmdbx = @import("lmdbx");
pub fn main() !void {
const env = try lmdbx.Environment.init("path/to/db", .{ .max_dbs = 2 });
defer env.deinit();
const txn = try env.transaction(.{ .mode = .ReadWrite });
errdefer txn.abort();
const widgets = try txn.database("widgets", .{ .create = true });
try widgets.set("aaa", "foo", .Create);
const gadgets = try txn.database("gadgets", .{ .create = true });
try gadgets.set("aaa", "bar", .Create);
try txn.commit();
}Nested transactions are supported through Transaction.nested(...) (or Options.parent).
const lmdbx = @import("lmdbx");
pub fn main() !void {
const env = try lmdbx.Environment.init("path/to/db", .{});
defer env.deinit();
const parent = try env.transaction(.{ .mode = .ReadWrite });
errdefer parent.abort();
const child = try parent.nested(.{ .mode = .ReadWrite });
errdefer child.abort();
try child.set("k", "v", .Upsert);
try child.commit();
try parent.commit();
}BatchedDB is a fully ACID transaction layer on top of libMDBX's SAFE_NOSYNC flag combined with configurable disk syncs by interval/max-bytes threshold, enabling callback-based async transaction commits while massively increasing write throughput by amortizing disk I/O costs. On the flip side, write transactions show a considerable increase in latency which may or may not be acceptable based on your requirements.
const lmdbx = @import("lmdbx");
const std = @import("std");
pub fn main() !void {
const env = try lmdbx.Environment.init("path/to/db", .{});
defer env.deinit();
var batched: lmdbx.BatchedDB = undefined;
try batched.init(env, std.heap.page_allocator, .{
.sync_interval_ms = 2,
.sync_bytes = 0,
.callback_capacity = 64,
});
defer batched.deinit();
const txn = try batched.transaction(.{ .mode = .ReadWrite });
errdefer txn.abort();
try txn.set("aaa", "foo", .Upsert);
try txn.commit();
}pub const Environment = struct {
pub const Options = struct {
geometry: ?DatabaseGeometry = null,
max_dbs: u32 = 0,
max_readers: u32 = 126,
read_only: bool = false,
write_map: bool = false,
no_sticky_threads: bool = false,
exclusive: bool = false,
no_read_ahead: bool = false,
no_mem_init: bool = false,
lifo_reclaim: bool = false,
no_meta_sync: bool = false,
safe_nosync: bool = false,
unsafe_nosync: bool = false,
sync_durable: bool = true,
/// Autosync period in milliseconds. 0 disables periodic autosync.
sync_period_ms: u32 = 0,
/// Autosync threshold in bytes. 0 disables threshold-based autosync.
sync_bytes: usize = 0,
mode: u16 = 0o664
};
pub const FlagsInfo = struct {
raw: u32,
no_meta_sync: bool,
safe_nosync: bool,
unsafe_nosync: bool,
sync_durable: bool,
write_map: bool,
exclusive: bool,
};
pub const Info = struct {
map_size: usize,
max_readers: u32,
num_readers: u32,
autosync_period: u32,
autosync_threshold: u64,
db_pagesize: u32,
mode: u32,
sys_pagesize: u32,
unsync_volume: u64
};
pub const DatabaseGeometry = struct {
lower_size: isize = -1,
upper_size: isize = -1,
size_now: isize = -1,
growth_step: isize = -1,
shrink_threshold: isize = -1,
pagesize: isize = -1
};
pub fn init(path: [*:0]const u8, options: Options) !Environment
pub fn deinit(self: Environment) !void
pub fn transaction(self: Environment, options: Transaction.Options) !Transaction
pub fn sync(self: Environment, force: bool, nonblock: bool) !bool
pub fn stat(self: Environment) !Stat
pub fn info(self: Environment) !Info
pub fn flagsInfo(self: Environment) !FlagsInfo
pub fn syncBytes(self: Environment) !usize
pub fn syncPeriod(self: Environment) !u32
pub fn setGeometry(self: Environment, options: DatabaseGeometry) !void
};pub const BatchedDB = struct {
pub const Options = struct {
/// Sync interval in milliseconds.
sync_interval_ms: u32 = 2,
/// If non-zero, trigger early sync when unsynced volume reaches this threshold.
sync_bytes: u64 = 0,
/// Max pending callbacks. 0 disables callback queue.
callback_capacity: usize = 0,
};
pub const CommitCallback = *const fn (ctx: ?*anyopaque, success: bool) void;
pub fn init(self: *BatchedDB, env: Environment, allocator: std.mem.Allocator, options: Options) !void
pub fn deinit(self: *BatchedDB) void
pub fn transaction(self: *BatchedDB, options: Transaction.Options) !Transaction
pub const Transaction = struct {
pub fn abort(self: Transaction) !void
pub fn commit(self: Transaction) !void
pub fn commitAsync(self: Transaction, cb: CommitCallback, ctx: ?*anyopaque) !void
/// Returns the underlying lmdbx.Transaction.
pub fn inner(self: Transaction) Transaction
/// All other Transaction APIs are forwarded (get/set/delete, cursor, database, nested, etc).
};
};pub const Transaction = struct {
pub const Mode = enum { ReadOnly, ReadWrite };
pub const Options = struct {
mode: Mode = .ReadWrite,
parent: ?Transaction = null,
txn_try: bool = false
};
pub fn init(env: Environment, options: Options) !Transaction
pub fn nested(self: Transaction, options: Options) !Transaction
pub fn abort(self: Transaction) !void
pub fn commit(self: Transaction) !void
pub fn reset(self: Transaction) !void
pub fn renew(self: Transaction) !void
pub fn park(self: Transaction, autounpark: bool) !void
pub fn unpark(self: Transaction, restart_if_ousted: bool) !void
pub fn setUserctx(self: Transaction, ctx: ?*anyopaque) !void
pub fn getUserctx(self: Transaction) ?*anyopaque
pub fn replace(self: Transaction, key: []const u8, new_value: ?[]const u8, old_value: ?[]const u8, flag: Database.ReplaceFlag) !?[]const u8
pub fn estimateRange(self: Transaction, db: Database, begin_key: ?[]const u8, begin_data: ?[]const u8, end_key: ?[]const u8, end_data: ?[]const u8) !isize
pub fn canaryPut(self: Transaction, canary: ?*const Canary) !void
pub fn canaryGet(self: Transaction) !Canary
pub fn releaseAllCursors(self: Transaction, unbind: bool) !void
pub fn getEqualOrGreat(self: Transaction, key: []const u8) !?Cursor.Entry
pub fn get(self: Transaction, key: []const u8) !?[]const u8
pub fn set(self: Transaction, key: []const u8, value: []const u8, flag: Database.SetFlag) !void
pub fn delete(self: Transaction, key: []const u8) !void
pub fn cursor(self: Transaction) !Cursor
pub fn database(self: Transaction, name: ?[*:0]const u8, options: Database.Options) !Database
};pub const Database = struct {
pub const DBI = c.MDBX_dbi;
pub const Options = struct {
reverse_key: bool = false,
integer_key: bool = false,
create: bool = false,
};
pub const Stat = struct {
psize: u32,
depth: u32,
branch_pages: usize,
leaf_pages: usize,
overflow_pages: usize,
entries: usize,
};
pub const SetFlag = enum {
Create, Update, Upsert, Append, AppendDup
};
pub const ReplaceFlag = enum {
Upsert,
Create,
Update,
Append,
AppendDup,
CurrentNoOverwrite,
};
pub fn open(txn: Transaction, name: ?[*:0]const u8, options: Options) !Database
pub fn get(self: Database, key: []const u8) !?[]const u8
pub fn set(self: Database, key: []const u8, value: []const u8, flag: SetFlag) !void
pub fn delete(self: Database, key: []const u8) !void
pub fn replace(self: Database, key: []const u8, new_value: ?[]const u8, old_value: ?[]const u8, flag: ReplaceFlag) !?[]const u8
pub fn drop(self: Database, delete_dbi: bool) !void
pub fn cursor(self: Database) !Cursor
pub fn getEqualOrGreat(self: Database, key: []const u8) !?Cursor.Entry
pub fn stat(self: Database) !Stat
pub fn rename(self: Database, name: [*:0]const u8) !void
pub fn sequence(self: Database, increment: u64) !u64
};pub const Cursor = struct {
pub const Entry = struct { key: []const u8, value: []const u8 };
pub fn init(db: Database) !Cursor
pub fn deinit(self: Cursor) void
pub fn setUserctx(self: Cursor, ctx: ?*anyopaque) !void
pub fn getUserctx(self: Cursor) ?*anyopaque
pub fn renew(self: Cursor, txn: Transaction) !void
pub fn reset(self: Cursor) !void
pub fn getCurrentEntry(self: Cursor) !Entry
pub fn getCurrentKey(self: Cursor) ![]const u8
pub fn getCurrentValue(self: Cursor) ![]const u8
pub fn set(self: Cursor, key: []const u8, value: []const u8) !void
pub fn setCurrentValue(self: Cursor, value: []const u8) !void
pub fn deleteCurrentKey(self: Cursor) !void
pub fn goToNext(self: Cursor) !?[]const u8
pub fn goToPrevious(self: Cursor) !?[]const u8
pub fn goToLast(self: Cursor) !?[]const u8
pub fn goToFirst(self: Cursor) !?[]const u8
pub fn goToKey(self: Cursor, key: []const u8) !void
pub fn seek(self: Cursor, key: []const u8) !?[]const u8
};
⚠️ Always close cursors before committing or aborting the transaction.
lmdbx.c exposes raw libMDBX bindings (constants, types, and functions) as imported C symbols:
const lmdbx = @import("lmdbx");
const c = lmdbx.c;Run the benchmarks:
zig build bench