No description
Find a file
2026-08-03 14:18:35 -04:00
benchmarks entry returning cursor ops and put flags 2026-08-03 14:18:35 -04:00
src entry returning cursor ops and put flags 2026-08-03 14:18:35 -04:00
test entry returning cursor ops and put flags 2026-08-03 14:18:35 -04:00
.gitignore add zig-pkg/ to .gitignore 2026-05-31 16:02:46 +01:00
build.zig organize build.zig 2026-06-01 11:35:43 +01:00
build.zig.zon entry returning cursor ops and put flags 2026-08-03 14:18:35 -04:00
example.zig add example.zig 2025-05-31 19:39:10 -04:00
LICENSE Update LICENSE 2025-09-23 23:26:02 -04:00
README.md entry returning cursor ops and put flags 2026-08-03 14:18:35 -04:00

zig-lmdb

Zig bindings for LMDB.

A fork of nDimensional/zig-lmdb with three additions:

  • Entry-returning cursor operations. goToFirstEntry, goToNextEntry, goToPreviousEntry, goToLastEntry, goToKeyEntry, and seekEntry return ?Entry, so a scan that reads values costs one mdb_cursor_get call per element instead of two.
  • Put flags. Database.put/Cursor.put take MDB_NOOVERWRITE and MDB_APPEND (plus MDB_CURRENT on cursors), and reserve exposes MDB_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));