- Zig 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| 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();
var 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();
var 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,
no_sub_dir: bool = false, // MDB_NOSUBDIR
no_sync: bool = false, // MDB_NOSYNC
no_meta_sync: bool = false, // MDB_NOMETASYNC
map_async: bool = false, // MDB_MAPASYNC
no_mem_init: bool = false, // MDB_NOMEMINIT
no_read_ahead: bool = false, // MDB_NORDAHEAD
prev_snapshot: bool = false, // MDB_PREVSNAPSHOT
mode: u16 = 0o664,
};
pub const Info = struct {
map_size: usize,
last_pgno: usize,
last_txnid: 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
};
no_sync and no_meta_sync trade durability for commit throughput: with the defaults every commit fsyncs, and that dominates single-write transactions (see benchmarks/). no_sync can lose the last transactions in a system crash, no_meta_sync at most the last one; neither can corrupt the database. no_mem_init and no_read_ahead are smaller wins for write-heavy and larger-than-RAM workloads respectively.
info().last_pgno lets you grow the map before a write fails: (last_pgno + 1) * stat().psize is the number of bytes in use. resize may only be called with no transactions open in this process.
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: Transaction) !Cursor
pub fn database(self: Transaction, name: ?[*:0]const u8, options: Database.Options) !Database
};
commit and abort clear the handle, so aborting after a commit (successful or not) is a no-op. This matters because LMDB ends the transaction itself when a commit fails, for instance with error.MDB_MAP_FULL, and ending it a second time frees the environment's preallocated write transaction. Both errdefer txn.abort() before try txn.commit() and a plain defer txn.abort() are safe.
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.
setCurrentValue costs two calls, because MDB_CURRENT still needs the real key. If you already hold it, cursor.put(entry.key, value, .{ .current = true }) does the same in one. The key passed with current must be the one at the cursor's position; LMDB does not check.
⚠️ 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));