- Zig 100%
| benchmarks | ||
| src | ||
| test | ||
| .gitignore | ||
| build.zig | ||
| build.zig.zon | ||
| example.zig | ||
| LICENSE | ||
| README.md | ||
zig-lmdb
Zig bindings for LMDB.
A fork of nDimensional/zig-lmdb with three additions:
- Entry-returning cursor operations.
goToFirstEntry,goToNextEntry,goToPreviousEntry,goToLastEntry,goToKeyEntry, andseekEntryreturn?Entry, so a scan that reads values costs onemdb_cursor_getcall per element instead of two. - Put flags.
Database.put/Cursor.puttakeMDB_NOOVERWRITEandMDB_APPEND(plusMDB_CURRENTon cursors), andreserveexposesMDB_RESERVE, which returns a writable slice inside the database page instead of copying a value in. - The translated C API is exported as
lmdb.c, so calls this wrapper doesn't cover are reachable without patching the build.
Everything else is upstream, unchanged.
Table of Contents
Installation
Built and tested with Zig version 0.16.0.
zig fetch --save=lmdb \
https://forgejo.gllghr.net/d/zig-lmdb/archive/${COMMIT_HASH}.tar.gz
Usage
An LMDB 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 lmdb = @import("lmdb");
pub fn main() !void {
const env = try lmdb.Environment.init("path/to/db", .{});
defer env.deinit();
const txn = try lmdb.Transaction.init(env, .{ .mode = .ReadWrite });
errdefer txn.abort();
try txn.set("aaa", "foo");
try txn.set("bbb", "bar");
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 lmdb = @import("lmdb");
pub fn main() !void {
const env = try lmdb.Environment.init("path/to/db", .{ .max_dbs = 2 });
defer env.deinit();
const txn = try lmdb.Transaction.init(env, .{ .mode = .ReadWrite });
errdefer txn.abort();
const widgets = try txn.database("widgets", .{ .create = true });
try widgets.set("aaa", "foo");
const gadgets = try txn.database("gadgets", .{ .create = true });
try gadgets.set("aaa", "bar");
try txn.commit();
}
API
Environment
pub const Environment = struct {
pub const Options = struct {
map_size: usize = 10 * 1024 * 1024,
max_dbs: u32 = 0,
max_readers: u32 = 126,
read_only: bool = false,
write_map: bool = false,
no_tls: bool = false,
no_lock: bool = false,
mode: u16 = 0o664,
};
pub const Info = struct {
map_size: usize,
max_readers: u32,
num_readers: u32,
};
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) !void
pub fn info(self: Environment) !Info
pub fn stat(self: Environment) !Stat
pub fn resize(self: Environment, size: usize) !void // mdb_env_set_mapsize
};
Transaction
pub const Transaction = struct {
pub const Mode = enum { ReadOnly, ReadWrite };
pub const Options = struct {
mode: Mode,
parent: ?Transaction = null,
};
pub fn init(env: Environment, options: Options) !Transaction
pub fn abort(self: Transaction) void
pub fn commit(self: Transaction) !void
pub fn get(self: Transaction, key: []const u8) !?[]const u8
pub fn set(self: Transaction, key: []const u8, value: []const u8) !void
pub fn delete(self: Transaction, key: []const u8) !void
pub fn cursor(self: Database) !Cursor
pub fn database(self: Transaction, name: ?[*:0]const u8, options: Database.Options) !Database
};
Database
pub const Database = struct {
pub const Options = struct {
reverse_key: bool = false,
integer_key: bool = false,
create: bool = false,
};
pub const PutFlags = struct {
no_overwrite: bool = false, // MDB_NOOVERWRITE
append: bool = false, // MDB_APPEND
};
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) !void
pub fn put(self: Database, key: []const u8, value: []const u8, flags: PutFlags) !void
pub fn reserve(self: Database, key: []const u8, size: usize, flags: PutFlags) ![]u8
pub fn delete(self: Database, key: []const u8) !void
pub fn cursor(self: Database) !Cursor
pub fn stat(self: Database) !Stat
};
set(key, value) is put(key, value, .{}). Both no_overwrite and append report a rejected write as error.MDB_KEYEXIST — for append, that means the key did not sort after every key already in the database.
reserve allocates size bytes inside the database page and returns them for the caller to fill in, avoiding a copy of the value. The returned slice is only valid until the next write in the same transaction.
const value = try db.reserve("key", 8, .{});
std.mem.writeInt(u64, value[0..8], 42, .big);
Cursor
pub const Cursor = struct {
pub const Entry = struct { key: []const u8, value: []const u8 };
pub const PutFlags = struct {
no_overwrite: bool = false, // MDB_NOOVERWRITE
append: bool = false, // MDB_APPEND
current: bool = false, // MDB_CURRENT
};
pub fn init(db: Database) !Cursor
pub fn deinit(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 setCurrentValue(self: Cursor, value: []const u8) !void
pub fn deleteCurrentKey(self: Cursor) !void
pub fn put(self: Cursor, key: []const u8, value: []const u8, flags: PutFlags) !void
pub fn reserve(self: Cursor, key: []const u8, size: usize, flags: PutFlags) ![]u8
pub fn goToNextEntry(self: Cursor) !?Entry
pub fn goToPreviousEntry(self: Cursor) !?Entry
pub fn goToLastEntry(self: Cursor) !?Entry
pub fn goToFirstEntry(self: Cursor) !?Entry
pub fn goToKeyEntry(self: Cursor, key: []const u8) !?Entry
pub fn seekEntry(self: Cursor, key: []const u8) !?Entry
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
};
The *Entry methods return the key and the value from a single mdb_cursor_get call, which is what you want for any scan that reads values:
var next = try cursor.seekEntry(prefix);
while (next) |entry| : (next = try cursor.goToNextEntry()) {
if (!std.mem.startsWith(u8, entry.key, prefix)) break;
// entry.value is already here — no second call
}
The key-only methods are unchanged, and goToKeyEntry differs from goToKey in reporting a missing key as null rather than error.MDB_NOTFOUND.
⚠️ Always close cursors before committing or aborting the transaction.
Stat
pub const Stat = struct {
psize: u32,
depth: u32,
branch_pages: usize,
leaf_pages: usize,
overflow_pages: usize,
entries: usize,
};
c
lmdb.c is the translated LMDB header, exported as an escape hatch for calls this wrapper does not wrap, alongside lmdb.throw, which maps a return code to lmdb.Error. Handles are plain fields (env.ptr, txn.ptr, db.dbi, cursor.ptr), so dropping to the C API does not mean abandoning the wrapper:
try lmdb.throw(lmdb.c.mdb_drop(txn.ptr, db.dbi, 0));