LimDB

    Dark Mode
Search:
Group by:

Fast, in-process key-value store with a table-like interface persisted to disk using lmdb.

Why?

Memory-mapped files are one of the fastest ways to store data but are not safe to access concurrently. Lmdb is a proven and mature to solution to that problem, offering full compliance to ACID3, a common standard for database reliability, while keeping most of the speed.

Leveraging the excellent nim-lmdb interface, LimDB makes a larg-ish sub-set of lmdb features available in an interface familiar to Nim users who have experience with a table.

While programming with LimDB feels like using a table, it is still very much lmdb. Some common boilerplate is automated and LimDB is clever about bundling lmdb's moving parts, but there are no bolted-on bits or obscuring of lmdb's behavior.

Simple Usage

Provide LimDB with a local storage directory and then use it like you would use a table. After inserting the element, it's on disk an can be accessed even after the program is restarted, or concurrently by different threads or processes.

import limdb
let db = initDatabase("myDirectory")
db["foo"] = "bar"  # that's it, foo -> bar is now on disk
echo db["foo"]     # prints bar

Now if you comment out the write, you can run the program again and read the value off disk

import limdb
let db = initDatabase("myDirectory")
# db["foo"] = "bar"
echo db["foo"]  # also prints "bar"

That's it. If you just need to quickly save some data, you can stop reading here and start programming.

Transactions

Sometimes you need to read or write to the database in several related ways that are best done as a group. This is a common database concept and most databases support them. Reads and writes grouped in this way are gauranteed not to be affected by other writes happening at the same time. Also, all writes are completed at once after the transaction, and if there is an error, no writes happen at all. This goes a long way towards not messing up your data.

A transaction is started using initTransaction, and stopped with either reset or commit. Use reset if you only read data, or you want to throw away all writes. Use commit to actually perform all the writes.

import limdb
let db = initDatabase("myDirectory")
let t = db.initTransaction
t["foo"] = "bar"
t["fuz"] = "buz"
t.commit()

let t = db.initTransaction
t["foo"] = "another bar"
t["fuz"] = "another buz"
t.reset()  # foo and bar remain unchanged

let t = db.initTransaction
echo t["foo"]
echo t["bar"]
t.reset()  # read-only transactions are always reset
Caution: Make sure to reset transactions when exceptions are thrown. If you use a database object directly without calling initTransaction, LimDB handles this for you.

Iterators

While you can access any data using the keys, you might want all of the data or not know the keys. You can use the usual keys, values and pairs iterators with a LimDB. They can be used standalone on a database or as part of a transaciton.

You can also use mvalues and mpairs to modify values on the go.

import limdb
let db = initDatabase("myDirectory")
let t = db.initTransaction
t["foo"] = "bar"
t["fuz"] = "buz"
t.commit()

for key in db.keys:
  echo key
# prints:
# foo
# fuz

let t = db.initTransaction()
for value in t.values:
  echo value
t.reset()
# prints:
# bar
# buz

for key, value in db:
  echo "$# -> $#" % (key, value)

# prints:
# foo -> bar
# fuz -> buz

for value in db.mvalues:
  if value == "fuz":
    value = "buzz"

t.initTransaction
for key, value in t.mpairs:
  if key == "foo":
    value = "barz"
t.commit()

for key, value in db:
  echo "$# -> $#" % (key, value)

# prints:
# foo -> barz
# fuz -> buzz

Named Databases

More than one database can be placed in the same storage location. No keys or values are shared between databases, so the key foo will remain empty in database B if it is set in database A.

To access more than one database in the same Nim program, create an additional database from an existing one. The connection and storage location will be shared.

The default database, the one used in the examples above, also has a name, an empty string "".

import limdb
let db = initDatabase("myDirectory")

let db2 = db.initDatabase("myName")

db["foo"] = "bar"
db2["foo"] = "another bar

Database objects created from other database objects do not differ from ones created directly from a filename.

Only one database may be initialized from the same storage location, additional ones can be created from it.

Caution: If you use named databases, their names will appear as keys in the default database, The one named empty string "". In this case it is usually best not to use the default database for anything else, and iterate over the default databases' keys to get a list of named databases.

Limitations

Only strings are supported as data types, for now. In order to save other data types, they can be serialized to strings.

Improvement Areas Of Interest

  • Use generics to support any data type that a toBlob and fromBlob can be written for. Possibly keep string versions as a shortcut.
  • Use Nim views to provide an alternative interface allowing safe zero-copy data access in with Nim data types (lmdb itself does not copy data when accessing)
  • Useful iterators: keysFrom, keysBetween, other common usage of lmdb cursors
  • Map lmdb multipe values per key feature to something Nimish, perhaps iterators or seqs

Why is it called LimDB?

LimDB was originally named LimrodDB after the ancient king Nimrod's younger sibling, Limrod, who didn't make it into the history books because he was short. It was later renamed LimDB for marketing reasons.

By a wild coincidence, it also sounds a little like a vaguely pleasing jumble of Nim and LMDB.

Types

Blob = Val
A variable-length collection of bytes that can be used as either a key or value. This is LMDB's native storage type- a block of memory. string types are converted automatically, and conversion for other data types can be added by adding fromBlob and toBlob for a type.
Database = object
  env*: LMDBEnv
  dbi*: Dbi
A key-value database in a memory-mapped on-disk storage location.
Transaction = object
  txn*: LMDBTxn
  dbi*: Dbi
A transaction may be created and reads or writes performed on it instead of directly on a database object. That way, reads or writes are not affected by other writes happening at the same time, and changes happen all at once at the end or not at all.

Procs

proc `[]=`(d: Database; key, value: string) {....raises: [Exception, Exception],
    tags: [].}
Set a value in the database
Note: This inits and commits a transaction under the hood
proc `[]=`(t: Transaction; key, value: string) {....raises: [KeyError, Exception],
    tags: [].}
proc `[]`(db: Database; key: string): string {.
    ...raises: [Exception, KeyError, Exception], tags: [].}
Fetch a value in the database
Note: This inits and resets a transaction under the hood
proc `[]`(t: Transaction; key: string): string {....raises: [KeyError, Exception],
    tags: [].}
proc clear(db: Database) {....raises: [Exception], tags: [].}
Remove all key-values pairs from the database, emptying it.
Note: This creates and commits a transaction under the hood
proc copy(db: Database; filename: string) {....raises: [Exception], tags: [].}
Copy a database to a different directory. This also performs routine database maintenance so the resulting file with usually be smaller. This is best performed when no one is writing to the database directory.
proc del(db: Database; key, value: string) {....raises: [Exception, Exception],
    tags: [].}
Delete a key-value pair in the database
Note: This inits and commits a transaction under the hood
proc del(db: Database; key: string) {....raises: [Exception, Exception], tags: [].}
Deletes a value in the database
Note: This inits and commits a transaction under the hood
Note: LMDB requires you to delete by key and value. This proc fetches the value for you, giving you the more familiar interface.
proc del(t: Transaction; key, value: string) {....raises: [KeyError, Exception],
    tags: [].}
Delete a key-value pair
proc fromBlob(b: Blob): string {....raises: [], tags: [].}
Convert a chunk of data, key or value, to a string
Note: If you want other data types than a string, implement this for the data type
proc getOrDefault(d: Database; key: string): string {.
    ...raises: [Exception, Exception], tags: [].}
Fetch a value in the database and return the provided default value if it does not exist
proc getOrDefault(t: Transaction; key: string): string {....raises: [Exception],
    tags: [].}
Read a value from a key in a transaction and return the provided default value if it does not exist
proc getOrPut(d: Database; key, val: string): string {.
    ...raises: [Exception, Exception, KeyError], tags: [].}
Retrieves value of key as mutable copy or enters and returns val if not present
proc getOrPut(t: Transaction; key, val: string): string {.
    ...raises: [Exception, KeyError], tags: [].}
Retrieves value at key as mutable copy or enters and returns val if not present
proc hasKey(db: Database; key: string): bool {....raises: [Exception], tags: [].}
See if a key exists without fetching any data in a transaction
proc hasKey(t: Transaction; key: string): bool {....raises: [], tags: [].}
See if a key exists without fetching any data
proc hasKeyOrPut(d: Database; key, val: string): bool {.
    ...raises: [Exception, Exception], tags: [].}
Returns true if key is in the Database, otherwise inserts value.
proc hasKeyOrPut(t: Transaction; key, val: string): bool {.
    ...raises: [KeyError, Exception], tags: [].}
Returns true if key is in the transaction view of the database, otherwise inserts value.
proc initDatabase(db: Database; name = ""): Database {....raises: [Exception],
    tags: [].}
Open another database of a different name in an already-connected on-disk storage location.
proc initDatabase(filename = ""; name = ""; maxdbs = 255; size = 10485760): Database {.
    ...raises: [OSError, IOError, Exception], tags: [WriteDirEffect, ReadDirEffect].}
Connect to an on-disk storage location and open a database. If the path does not exist, a directory will be created.
proc initTransaction(db: Database): Transaction {....raises: [Exception], tags: [].}

Start a transaction from a database.

Reads and writes on the transaction will reflect the same point in time and will not be affected by other writes.

After reads, reset must be called on the transaction. After writes, commit must be called to perform all of the writes, or reset to perform none of them.

Caution: Calling neither reset nor commit on a transaction can block database access. This commonly happens when an exception is raised.
proc len(db: Database): int {....raises: [Exception], tags: [].}
Returns the number of key-value pairs in the database.
Note: This inits and resets a transaction under the hood
proc open(db: Database; name: string): Dbi {....raises: [Exception], tags: [].}
proc pop(d: Database; key: string; val: var string): bool {....raises: [Exception],
    tags: [].}
Delete value in database. If it existed, return true and place value into val
proc pop(t: Transaction; key: string; val: var string): bool {.
    ...raises: [Exception], tags: [].}
Delete value in database within transaction. If it existed, return true and place into val
proc take(d: Database; key: string; val: var string): bool {.
    ...raises: [Exception], tags: [].}
Alias for pop
proc take(t: Transaction; key: string; val: var string): bool {.
    ...raises: [Exception], tags: [].}
Alias for pop
proc toBlob(s: string): Blob {....raises: [], tags: [].}
Convert a string to a chunk of data, key or value, for LMDB
Note: If you want other data types than a string, implement this for the data type

Iterators

iterator keys(db: Database): string {....raises: [Exception, Exception], tags: [].}
Iterate over all keys pairs in a database.
Note: This inits and resets a transaction under the hood
iterator keys(t: Transaction): string {....raises: [Exception, Exception], tags: [].}
Iterate over all keys in a database with a transaction
iterator mpairs(db: Database): (string, var string) {.
    ...raises: [Exception, Exception], tags: [].}
Iterate over all key-value pairs in a database allowing the values to be modified
Note: This inits and resets a transaction under the hood
iterator mpairs(t: Transaction): (string, var string) {.
    ...raises: [Exception, Exception], tags: [].}
Iterate over all key-value pairs in a database with a transaction, allowing the values to be modified.
iterator mvalues(db: Database): var string {....raises: [Exception, Exception],
    tags: [].}
Iterate over all values in a database allowing modification
Note: This inits and resets a transaction under the hood
iterator mvalues(t: Transaction): var string {....raises: [Exception, Exception],
    tags: [].}
Iterate over all values in a database with a transaction, allowing the values to be modified.
iterator pairs(db: Database): (string, string) {....raises: [Exception, Exception],
    tags: [].}
Iterate over all values in a database
Note: This inits and resets a transaction under the hood
iterator pairs(t: Transaction): (string, string) {.
    ...raises: [Exception, Exception], tags: [].}
Iterate over all key-value pairs in a database with a transaction.
iterator values(db: Database): string {....raises: [Exception, Exception], tags: [].}
Iterate over all values in a database
Note: This inits and resets a transaction under the hood
iterator values(t: Transaction): string {....raises: [Exception, Exception],
    tags: [].}
Iterate over all values in a database with a transaction.

Templates

template clear(t: Transaction)
Remove all key-values pairs from the database, emptying it.
Note: The size of the database will stay the same on-disk but won't grow until more data than was in there before is added. It will shrink if it is copied.
template close(db: Database)
Close the database directory. This will free up some memory and make all databases that were created from the same directory unavailable. This is not necessary for many use cases.
Note: This creates and commits a transaction under the hood
template commit(t: Transaction)
Commit a transaction. This writes all changes made in the transaction to disk.
template contains(db: Database; key: string): bool
Alias for hasKey to support in syntax in transactions
template contains(t: Transaction; key: string): bool
Alias for hasKey to support in syntax
template del(t: Transaction; key: string)
Delete a value in a transaction
Note: LMDB requires you to delete by key and value. This proc fetches the value for you, giving you the more familiar interface.
template len(t: Transaction): int
Returns the number of key-value pairs in the database.
template reset(t: Transaction)
Reset a transaction. This throws away all changes made in the transaction. After only reading in a transaction, reset it as well.
Note: This is called reset because that is a pleasant and familiar term for reverting changes. The term differs from LMDB though, under the hood this calles mdb_abort, not mdb_reset- the latter does something else not covered by LimDB.