Skip to content

theseyan/lmdbx-zig

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

38 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

lmdbx-zig

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.

Table of Contents

Installation

# replace {VERSION} with the latest release eg: v0.2.3
zig fetch https://github.com/theseyan/lmdbx-zig/archive/refs/tags/{VERSION}.tar.gz

Copy 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}",
        },
    },
}

Targets

lmdbx-zig officially supports cross-compiling to the following target triples:

  • x86_64-linux-gnu, x86_64-macos, x86_64-windows-gnu
  • aarch64-linux-gnu, aarch64-macos, aarch64-windows-gnu

Successful compilation on other targets is not guaranteed (but might work).

Usage

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();
}

API

Environment

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
};

BatchedDB

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).
    };
};

Transaction

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
};

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
};

Cursor

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.

Low-level bindings

lmdbx.c exposes raw libMDBX bindings (constants, types, and functions) as imported C symbols:

const lmdbx = @import("lmdbx");
const c = lmdbx.c;

Benchmarks

Run the benchmarks:

zig build bench