lucaong

lucaong

CubDB is an embedded database written in pure Elixir, designed for robustness and minimal use of resources. It strives to be as developer-friendly as possible.

Some of you, especially in the Nerves community, already know and use CubDB since a while, and might have read the original thread about CubDB on the Nerves Forum.

Version v2.0.0 is now published on Hex, with big improvements and exciting new features, so I thought it is a good time for a proper library post here.

Since this is a long-ish post, here’s a Table of Content:

Why CubDB?

CubDB is an embedded database written in Elixir. It runs inside your application, as opposed to on a separate server, and saves its data in a local file. In this respect, it is similar to SQLite, but offers an idiomatic Elixir API.

It is NOT a replacement for Postgres for multi-instances web applications, nor a distributed database, but rather a solution for cases when a lightweight but robust local data store is needed.

Typical use cases are applications running on embedded devices (CubDB runs well on Nerves), desktop applications, or applications running locally. CubDB is often used to persistently store data and configuration, as a data logging or time series store, or to persist state of an application.

Some of the features of CubDB are:

  • Basic key/value access, and selection of sorted ranges of entries.
  • Both keys and values can be any Elixir (or Erlang) term.
  • ACID transactions to perform atomic changes.
  • Multi version concurrency control (MVCC), allowing concurrent reads that do not block nor are blocked by writes.
  • Unexpected shutdowns or crashes won’t corrupt the database, nor break atomicity of transactions.
  • Manual or automatic compaction to reclaim disk space.

How does it compare to ETS, DETS, Mnesia, SQLite, etc.?

The FAQ section in the documentation has a chapter about this.

What’s new in v2.0.0?

Head to the CHANGELOG for more information, but in short:

This major version comes with some backward incompatible changes, so refer to the upgrade guide on how to upgrade from v1.1.0 to v2.0.0. The data format is completely compatible across these major versions though, so you can upgrade and downgrade your code without needing to migrate data.

How does it look in code?

Start a CubDB database process by providing a directory to store its data:

{:ok, db} = CubDB.start_link(data_dir: "some/data/directory")

Basic key/value access

Key/value access works as you probably expect:

CubDB.put(db, :some_key, "some value")
#=> :ok

CubDB.get(db, :some_key)
#=> "some value"

CubDB.delete(db, :some_key)
#=> :ok

Both keys and values can be arbitrary Elixir (or Erlang) terms, such as scalar, tuples, maps, structs, and really anything:

CubDB.put(db, {:users, 123}, %User{id: 123, name: "Andrea"})
#=> :ok

CubDB.get(db, {:users, 123})
#=> %User{id: 123, name: "Andrea"}

Selection of sorted ranges

Selection of sorted ranges is done with CubDB.select, and returns a lazy stream that can be passed to functions in Stream and Enum. Data is fetched lazily, only when the stream is iterated or otherwise run:

# Put several entries atomically
CubDB.put_multi(db, [a: 1, b: 2, c: 3, d: 4, e: 5, f: 6, g: 7, h: 8])

# Get the sum of even entries between :b and :g
CubDB.select(db, min_key: :b, max_key: :g) # select entries in reverse order
|> Stream.map(fn {_key, value} -> value end) # discard the key and keep only the value
|> Stream.filter(fn value -> is_integer(value) && Integer.is_even(value) end) # filter only even integers
|> Enum.sum() # sum the values

Thanks to the fact that all Elixir terms have a well defined order, CubDB can be used to store and select multiple collections in the same database, akin to SQL tables.

Atomic transactions

Multiple operations can be performed atomically using the CubDB.transaction function and functions in the CubDB.Tx module:

# Swapping `:a` and `:b` atomically:
CubDB.transaction(db, fn tx ->
  a = CubDB.Tx.get(tx, :a)
  b = CubDB.Tx.get(tx, :b)

  tx = CubDB.Tx.put(tx, :a, b)
  tx = CubDB.Tx.put(tx, :b, a)

  {:commit, tx, :ok}
end)
#=> :ok

Alternatively, all the ..._multi functions perform their operations atomically.

Zero-cost immutable snapshots

If you need to ensure consistency when reading multiple values, but do not need to perform any write, there is a better alternative to transactions that won’t block writes: zero-cost immutable snapshots. Using CubDB.with_snapshot one can perform several read/select operations isolated from concurrent writes, but without blocking them. Think about this like immutability in Elixir data structures, but in a database:

# the key of y depends on the value of x, so we ensure consistency by getting
# both entries from the same snapshot, isolating from the effects of concurrent
# writes
{x, y} = CubDB.with_snapshot(db, fn snap ->
  x = CubDB.Snapshot.get(snap, :x)
  y = CubDB.Snapshot.get(snap, x)

  {x, y}
end)

Head to the API documentation for more information.

I hope you enjoy CubDB as much as I do :slight_smile:

Showing Posts 14 to 5

johnsseruwagi

johnsseruwagi

Is CubDB still maintained? As there has not been update to the repository since 2023

lucaong

lucaong OP

Hi @fabioticconi , thanks for the kind words :slight_smile:

The repostory includes performance benchmarks, but not specifically against Mnesia. My assumption would be that, as you say, for concurrent writes Mnesia would be faster. CubDB does not offer row-level locks, so while it is generally quite fast, write transactions are serialized and will executeone one after the other.

fabioticconi

fabioticconi

This is an awesome library. I just have one question @lucaong - do you have any benchmarks of cubdb vs non-distributed mnesia? Especially with regard to concurrent writes. I think mnesia’s row-level locking offers an advantage here, but it probably isn’t possible with cubdb?

I’m not sure this is going to ever be an issue, but if there’s hundreds of concurrent accesses it would be nice to have some idea of the performance loss compared to alternative implementations.

Gilou06

Gilou06

Hi @lucaong,
Thanks for the details.
I had a remote knowledge of start_link but never bridged that to what happens in application.ex with the children.
I saw magic there, when in fact it is just the application of start_link on a list.

It all make sense now.
I’m grateful.
Jean-yves :sunglasses:

zachallaun

zachallaun

Maybe it’s worth updating the Usage section to demonstrate starting it with your application supervisor? And then a note that says something like: You can also start a DB directly using start_link(), which we’ll use for the examples below.

lucaong

lucaong OP

Hi @Gilou06 ,
@zachallaun already answered correctly to your (very common) doubt.

I just want to add that CubDB.start_link already starts a GenServer, which is why you can start it as a child of a Supervisor and/or give it a name.

If you give it a name, you can then use that name instead of the db variable:

{:ok, db} = CubDB.start_link(data_dir: "tmp/foo", name: :my_db)

# These two are now equivalent:
CubDB.get(db, "some-key")
CubDB.get(:my_db, "some-key")

As pointed out by @zachallaun , instead of manually calling start_link, you would probably add it to the list of children of your application supervisor. Then, the supervisor will call start_link for you, and you will still be able to refer to the CubDB process by name.

Using a name instead of the pid is usually better when running a process under supervision, because the supervisor might restart the process in case of a crash: then, the old pid won’t be valid anymore, but the name will still be valid (and refer to the new process).

These things are not obvious when starting with Elixir, and take some time and some thinking to get used to. Unfortunately, we often give them for granted in docs, where we simply say {:ok, pid} = MyProcess.start_link(...) - which works well in the console, but is not the way processes are usually started in a real application - and assume the reader knows what to do. I will try to improve CubDB docs on this aspect.

Gilou06

Gilou06

Thank you, it worked. And I know remember that I kind of knew that 2 years ago, but my brain garbages collect too often…
Jean-yves :slight_smile:

zachallaun

zachallaun

Completely reasonable question! If you aren’t used to this pattern, it can be hard to know what to do here.

  1. Pass a :name argument to start_link — this will be forwarded to the underlying GenServer and accessible throughout your application.

  2. You probably don’t want to be calling start_link directly, but letting your application supervisor do so. So here’s the pattern you’re probably going to want to use:

children = [
  {CubDB, data_dir: “some/dir”, name: :my_db}
]

Supervisor.start_link(children, strategy: :one_for_one)

You’d then use :my_db wherever you need to pass the db into the CubDB API.

Gilou06

Gilou06

Hello,
This is probably a beginer question, but here it is.
Once I have started the CubDB with the start_link (probably in application.ex)

how do I retreive the db variable in the rest of my code. Do I need to wrap the start_link in a Genserver in order to store that db variable ?
Thank you
Jean-yves

Kabie

Kabie

Sounds like a great replacement for dets. Would definitely try.

Where Next? Top

Trending in Announcing Top

woylie
Flop is an Elixir library that applies filtering, ordering and pagination parameters to your Ecto queries. offset-based pagination with...
New
MRdotB
I needed to reuse React components from my Chrome extension in my Phoenix/LiveView backend. I noticed that for Svelte/Vue, there are live...
New
woylie
I released Doggo, a collection of unstyled Phoenix components. https://github.com/woylie/doggo Features Unstyled Phoenix components....
New
JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
anuaralfetahe
Hello Published a new library - ProcessHub! ProcessHub is a library designed to manage process distribution within the Elixir cluster. ...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New

Other Trending Topics Top

mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
mhanberg
Hi everyone! The first release candidate for the Expert language server project is now available! We’ve published a press release detai...
New
webofbits
With AI doing more of the implementation work, I’ve been wondering how much coding I should deliberately keep doing myself. My main conc...
#ai
New
sergio
It’s not that it’s vocabulary is too advanced. It’s something worse. I get lost trying to follow even a paragraph written by Claude. It’...
New
AstonJ
This showed up on my feed.. anyone heard of it? Just hype? Ox Alpha is a reasoning model designed for coding, sustained ag...
New
bartblast
Hey folks, I just published a post about Hologram’s funding and where the project goes next - the short version: Curiosum as Main Spons...
New

Latest on Elixir Forum

Elixir Forum

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews