CubDB Documentation

repository·master·Indexed 20 days ago

https://github.com/lucaong/cubdb

An embedded, ACID-compliant key-value database for Elixir. CubDB uses an append-only, immutable B-tree structure to ensure data integrity and support MVCC. It features a native Elixir API with no native dependencies, making it suitable for embedded systems like Nerves. Key capabilities include atomic transactions via CubDB.Tx, read-only snapshots for consistent reads, range selection via lazy streams, and configurable background compaction.

Tokens
5.3K
Snippets
19
Records
27
Agent score
71%

What's inside CubDB

  1. Why use CubDB for Elixir applications

    master

    CubDB is an embedded, persistent database designed for Elixir applications. It functions like an Elixir collection (e.g., a Map or List) but is stored on disk and survives application restarts.

    Key benefits include:

    • Native Elixir Experience: Simple, idiomatic API that integrates with Elixir supervision trees.
    • No Native Dependencies: Runs wherever Elixir runs without the need for cross-compiling native code (unlike SQLite or LevelDB), making it ideal for embedded systems like Nerves.
    • Robustness: Designed to be efficient and robust against power loss, ensuring data is not corrupted and atomicity is maintained.

    Typical Use Cases:

    • Persisting configuration and user preferences.
    • Data logging and storing metrics or time-series data.
    • Serving as a local database for application data in single-instance applications.
  2. Compare CubDB with other storage options

    master

    When choosing a storage mechanism, consider how CubDB compares to these common alternatives:

    AlternativeComparison with CubDB
    ETSStores arbitrary Elixir/Erlang terms like CubDB, but is memory-only; data is lost on restart.
    DETSPersists values on disk like CubDB, but lacks support for sorted collections and has a more convoluted API.
    MnesiaA distributed database with schema enforcement; significantly more complex to use for simple embedded use cases.
    SQLite / LevelDB / LMDBPowerful general-purpose databases, but require non-native libraries. These can be harder to cross-compile for embedded hardware and may crash the Erlang VM if the native component fails.
    Plain FilesSimple to use, but lack efficient key/value access or sorted collections, and are more susceptible to corruption during power loss.
  3. Understand compaction in CubDB

    master

    CubDB uses an append-only B-tree data structure. Instead of modifying data in-place, every change is appended to the end of the file. This ensures high write performance and protects against data corruption during power failures.

    The Trade-off: Because changes are appended, the data file grows over time even if you update or delete values (stale data remains in the file).

    Compaction is the process of cleaning up this stale data to reclaim space. During compaction:

    1. CubDB creates a new file.
    2. It transfers only the current (reachable) entries to the new file.
    3. Once complete, it switches to the new file and removes the old one.

    Compaction runs in the background and does not block read or write operations. If interrupted by a crash, no data is lost; the system simply resumes or restarts the process on the next attempt.

  4. Use read-only snapshots for consistent reads

    master

    When you need to perform multiple reads or selects while ensuring isolation from concurrent writes (without blocking those writes), use CubDB.with_snapshot/2. Inside the callback, use the CubDB.Snapshot module to access data. This implements Multi-Version Concurrency Control (MVCC).

    # Ensuring consistency by getting both entries from the same snapshot
    {x, y} = CubDB.with_snapshot(db, fn snap ->
      x = CubDB.Snapshot.get(snap, :x)
      y = CubDB.Snapshot.get(snap, x)
    
      {x, y}
    end)
  5. Understand file sync and durability trade-offs

    master

    File sync refers to telling the operating system to flush its write buffers to the physical disk. In CubDB, you can choose between automatic or manual syncing to balance performance and durability.

    The Trade-off

    • Automatic File Sync (High Durability): Each write operation is synced to disk before completing. This ensures that once a write is successful, it is safe even in a power failure. However, this makes write operations slower.
    • Manual/No Auto Sync (High Performance): Writes are buffered by the OS. This is much faster and ideal for high-volume logging where losing a few recent entries during a sudden power loss is acceptable.

    Note: Even without auto file sync, CubDB is designed so that power failures will not corrupt the database or break atomicity. The choice is strictly a trade-off between write speed and the guarantee that data is physically on the disk immediately upon write completion.

  6. Backup and restore a CubDB database

    master

    You can create a snapshot of your database state and restore it later using the following workflow:

    1. Create a backup: Use CubDB.back_up/2 to copy the current database state to a target directory.
    2. Restore/Open backup: Start a new CubDB process by pointing CubDB.start_link/1 to the directory where the backup was created.

    This is useful for migrations, snapshots, or creating isolated environments for testing.

    # Backup the current state of the database
    :ok = CubDB.back_up(db, "some/target/path")
    
    # Open the backup as another CubDB process
    {:ok, copy} = CubDB.start_link(data_dir: "some/target/path")
  7. Update return values for get_and_update, get_and_update_multi, and select

    master
    In v2, the functions get_and_update/3, get_and_update_multi/3, and select/2 no longer return a {:ok, result} tuple. They now return the result directly. You should remove the pattern matching for {:ok, ...} when calling these functions.
  8. Update get_and_update_multi options in v2

    master
    The get_and_update_multi function no longer accepts a fourth option argument. Specifically, the :timeout option has been removed. If you need to enforce a timeout for the update function in v2, you must wrap the call in a Task and use Task.yield/2.
  9. Start a CubDB instance

    master

    Initialize CubDB by calling CubDB.start_link/1 and providing a data_dir. If the directory does not exist, it will be created.

    Warning: Avoid starting multiple CubDB processes on the same data directory. Only one CubDB process should use a specific data directory at any time.

    {:ok, db} = CubDB.start_link(data_dir: "my/data/directory")
  10. Migrate from CubDB v1 to v2

    master
    Upgrading from v1 to v2 is straightforward because the database format is completely backward compatible; v2 can load v1 databases and vice-versa. However, several function signatures have changed. You must update your code to handle new return values and different option sets for get_and_update, get_and_update_multi, and select.
  11. Migrate select options to Elixir Streams in v2

    master

    In v2, the select function no longer accepts :pipe, :reduce, or :timeout options. Instead, select returns a lazy stream. To perform transformations or reductions, use the Stream and Enum modules on the returned value.

    # This v1 code:
    {:ok, product} =
      CubDB.select(db, [
        min_key: :foo,
        max_key: :bar,
        pipe: [
          map: fn {_, val} -> val end,
          filter: fn val -> val > 0 end
        ],
        reduce: fn val, acc -> val * acc end
      ])
    
    # Can be rewritten to this code in v2:
    product =
      CubDB.select(db, min_key: :foo, max_key: :bar)
      |> Stream.map(fn {_, val} -> val end)
      |> Stream.filter(fn val -> val > 0 end)
      |> Enum.reduce(fn val, acc -> val * acc end)