A lightweight, embedded key-value database for mobile clients (i.e., iOS, Android), written in Rust.

Overview

ThetaDB API Crates.io Cocoapods Version SPM codecov

A lightweight, embedded key-value database for mobile clients (i.e., iOS, Android), written in Rust.

⚠️ Still in testing, not yet ready for production use.

Overview

ThetaDB is suitable for use on mobile clients with "High-Read, Low-Write" demands, it uses B+ Tree as the foundational layer for index management.

Inspired by Go's BoltDB, ThetaDB uses mmap, relying on the operating system to keep memory and database files in sync. ThetaDB also implements shadow paging to guarantee atomicity and durability of transactions, preventing data loss or damage to the internal structures of database.

Architecture

Installation

ThetaDB provides APIs for these languages: Rust, Swift, Kotlin.

Rust - Cargo

Add Thetadb as a dependency to your Cargo.toml:

[dependencies]
thetadb = 0.0.1

Swift - Package Manager

Add ThetaDB as a dependency to your Package.swift:

.package(url: "https://github.com/TangentW/ThetaDB.git", from: "0.0.1")

And then specify ThetaDB as a dependency of the Target in which you wish to use ThetaDB.

Swift - CocoaPods

Add the pod to your Podfile:

pod 'ThetaDB'

And then execute:

$ pod install

Kotlin

🚧 Yet To Be Developed.

Usage

ThetaDB's APIs are generally similar regardless of the language used.

Open Database

Use following way to open the database at the specified path. If the database file does not exist, ThetaDB will automatically create and initialize it.

Rust Examples
use thetadb::{Options, ThetaDB};

let path = "path/to/db.theta";

// The simplest way to open with default `Options`:
let db = ThetaDB::open(path)?;

// Open with `Options`:
let db = Options::new()
    .force_sync(true)
    .mempool_capacity(8)
    .open(path)?;
Swift Examples
import ThetaDB

let path = "path/to/db.theta"

// The simplest way to open with default `Options`:
let db = try ThetaDB(path: path)

// Open with `Options`:
let db = try ThetaDB(
    path: path,
    options: .init(
        forceSync: true,
        mempoolCapacity: 8
    )
)

ThetaDB has no specific requirements for filename and extension, but it is recommended to use theta as extension for easy identification.

ThetaDB will automatically close when the database instance is destroyed.

Get, Insert, Update, Delete

Rust Examples
// Insert a new key-value pair into database.
db.put(b"foo", b"foo")?;

// Check if the database contains a given key.
assert!(db.contains(b"foo")?);
assert!(!db.contains(b"unknown")?);

// Get the value associated with a given key.
assert_eq!(
    db.get(b"foo")?,
    Some(b"foo".to_vec())
);
assert_eq!(
    db.get(b"unknown")?,
    None
);

// Update an existing value associated with a given key.
db.put(b"foo", b"bar")?;
assert_eq!(
    db.get(b"foo")?,
    Some(b"bar".to_vec())
);

// Delete an existing key-value pair from database.
db.delete(b"foo")?;
assert!(!db.contains(b"foo")?);
assert_eq!(
    db.get(b"foo")?,
    None
);
Swift Examples
// Insert a new key-value pair into database.
try db.put("foo".data(using: .utf8)!, for: "foo")

// Check if the database contains a given key.
assert(try db.contains("foo"))
assert(try !db.contains("unknown"))

// Get the value associated with a given key.
assertEq(
    try db.get("foo"),
    "foo".data(using: .utf8)
)
assertEq(
    try db.get("unknown"),
    nil
)

// Update an existing value associated with a given key.
try db.put("bar".data(using: .utf8)!, for: "foo")
assertEq(
    try db.get("foo"),
    "bar".data(using: .utf8)
)

// Delete an existing key-value pair from database.
try db.delete("foo")
assert(try !db.contains("foo"))
assertEq(
    try db.get("foo"),
    nil
)

Transaction

ThetaDB has two kinds of transactions: Read-Only Transaction and Read-Write Transaction. The read-only transaction allows for read-only access and the read-write transaction allows modification.

ThetaDB allows a number of read-only transactions at a time but allows at most one read-write transaction at a time. When a read-write transaction is committing, it has exclusive access to the database until the commit is completed, at which point other transactions trying to access the database will be blocked. You can think of this situation as shared access and exclusive access to reader-writer lock.

Read-Only Transaction

Rust Examples
// Start a read-only transaction.
let tx = db.begin_tx()?;

// Then perform read-only access.
_ = tx.contains(b"foo")?;
_ = tx.get(b"foo")?;

// Or you can perform a read-only transaction using closure,
// with `view` method:
db.view(|tx| {
    _ = tx.contains(b"foo")?;
    _ = tx.get(b"foo")?;
    Ok(())
})?;
Swift Examples
// Start a read-only transaction.
let tx = try db.beginTx()

// Then perform read-only access.
_ = try tx.contains("foo")
_ = try tx.get("foo")

// Or you can perform a read-only transaction using closure,
// with `view` method:
try db.view {
    _ = try $0.contains("foo")
    _ = try $0.get("foo")
}

Read-Write Transaction

ThetaBD's read-write transactions are designed to automatically rollback, and therefore any changes made to the transaction will be discarded unless you explicity call the commit method.

Or you can perform a read-write transaction using closure, if no errors occur, then the transaction will be commit automatically after the closure call.

Rust Examples
// Start a read-write transaction.
let mut tx = db.begin_tx_mut()?;

// Then perform read-write access.
tx.put(b"hello", b"world")?;
_ = tx.get(b"hello")?;

// Finally, commit the transaction.
tx.commit()?;

// Or you can perform a read-write transaction using closure,
// with `update` method:
db.update(|tx| {
    tx.put(b"hello", b"world")?;
    _ = tx.get(b"hello")?;
    Ok(())
})?;
Swift Examples
// Start a read-write transaction.
let tx = try db.beginTxMut()

// Then perform read-write access.
try tx.put("world".data(using: .utf8)!, for: "hello")
_ = try tx.get("hello")

// Finally, commit the transaction.
try tx.commit()

// Or you can perform a read-write transaction using closure,
// with `update` method:
try db.update {
    try $0.put("world".data(using: .utf8)!, for: "hello")
    _ = try $0.get("hello")
}

Attention

❗️ Transaction instances are nonsendable, which means it's not safe to send them to another thread. Rust leverages Ownership system and the Send and Sync traits to enforce requirements automatically, whereas Swift requires us to manually ensure these guarantees.

❗️ Read-only transactions and read-write transaction must not overlap, otherwise a deadlock will be occurred.

😺 So ThetaDB recommends that if you want to use transactions, use the APIs with closure parameter (i.e., view, update).

Cursor

We can freely traverse the data in the ThetaDB using Cursor.

For instance, we can iterate over all the key-value pairs in the ThetaDB like this:

Rust Examples
// Forward traversal.
let mut cursor = db.first_cursor()?;
while let Some((key, value)) = cursor.key_value()? {
    println!("{:?} => {:?}", key, value);
    cursor.next()?;
}

// Backward traversal.
let mut cursor = db.last_cursor()?;
while let Some((key, value)) = cursor.key_value()? {
    println!("{:?} => {:?}", key, value);
    cursor.prev()?;
}
Swift Examples
// Forward traversal.
let cursor = try db.firstCursor()
while let (key, value) = try cursor.keyValue() {
    print("\(key) => \(value)")
    try cursor.next()
}

// Backward traversal.
let cursor2 = try db.lastCursor()
while let (key, value) = try cursor2.keyValue() {
    print("\(key) => \(value)")
    try cursor2.previous()
}

Or we can perform range queries on ThetaDB in this way:

Rust Examples
let mut cursor = db.cursor_from_key(b"C")?;
// Enable `let_chains` feature, should add `#![feature(let_chains)]`
// to the crate attributes.
while let Some((key, value)) = cursor.key_value()? && key != b"G" {
    println!("{:?} => {:?}", key, value);
    cursor.next()?;
}
Swift Examples
let cursor = try db.cursor(key: "C")
while let (key, value) = try cursor.keyValue(), key != "G" {
    print("\(key) => \(value)")
    try cursor.next()
}

Attention

❗️ Cursor is also a transaction (can be understood as a read-only transaction), so it also follows the transaction considerations mentioned above.

Benchmark

Benchmark

Lib Write (ms) Read (ms) Initialize (ms)
ThetaDB 109.85 15.70 0.72
ThetaDB In Tx 59.69 16.27 N/A
MMKV 193.41 17.39 52.28

Measure

Running on iPhone 12 mini 128 GB, iOS 16.1.2.

  • Write

    Measure: Put 10000 records with 5000 random keys and values from 1 to 10000 in bytes length.

    for i in 1 ... 10000 {
        let key = String(arc4random_uniform(5000))
        let value = Data(repeating: 1, count: i)
        // Put key-value...
    }
  • Read

    1. Put 10000 records with 5000 random keys and values from 1 to 10000 in bytes length.
    2. Measure: Get 10000 records with 5000 random keys.
     for _ in 0 ..< 10000 {
        let key = String(arc4random_uniform(5000))
        // Get record...
     }
  • Initialization

    1. Put 10000 records with 5000 random keys and values from 1 to 10000 in bytes length.
    2. Measure: Open then read a record with a random key.

License

ThetaDB is released under the MIT license. See LICENSE for more details.

You might also like...
A command-line tool to generate a list of required missing Android OS Project blobs.

aosp-missing-blobs aosp-missing-blobs is a nifty tool to identify required blobs (.so) that are missing from AOSP ROM builds, and to show which existi

Cross-platform casting SDK, support Android, Windows, Linux

mirror Cross-platform casting SDK, support Android, Windows, Linux Low-latency transport protocols use [SRT](https://github.com/Haivision/srt) Video:

Valq - macros for querying and extracting value from structured data by JavaScript-like syntax

valq   valq provides a macro for querying and extracting value from structured data in very concise manner, like the JavaScript syntax. Look & Feel: u

Execute Javascript code in the Dioxus, and get the return value ( for Dioxus )

Golde Dioxus Execute Javascript code in the Dioxus, and get the return value. This demo can help use Javascript to calc the + operator formula. use di

A crate providing a MemoryCell struct, which stores a current and previous value.

memcell What is a MemoryCell? A MemoryCell is a struct containing both a current and optional previous value. Definition #[derive(Debug, Clone)] pub s

This project returns Queried value from SOAP(XML) in form of JSON.

About This is project by team SSDD for HachNUThon (TechHolding). This project stores and allows updating SOAP(xml) data and responds to various querie

A minimalistic blog/portfolio starter written in Rust. No database, bloat or JS.

rlog Rlog is a minimalistic blog/portfolio starter project intended to be used for a personal blog. The project is built using only Rust, HTML and CSS

A lightweight and super fast cli todo program written in rust under 200 sloc
A lightweight and super fast cli todo program written in rust under 200 sloc

todo A lightweight and super fast cli todo program written in rust under 200 sloc installation AUR package: todo-bin use cargo build --release to comp

Lemurs - A lightweight TUI display/login manager written in Rust 🐒
Lemurs - A lightweight TUI display/login manager written in Rust 🐒

Lemurs 🐒 A TUI Display/Login Manager written in Rust WIP: Whilst the project is working and installable, there are still a lot of bugs and limitation

Owner
Tangent
A lonely monster.
Tangent
Simple template to use csr and ssr leptos with tauri for ios/android/windows/macos/linux and web dev

Tailwind-Leptos-Tauri Template Simple template to use csr and ssr leptos with tauri for ios/android/windows/macos/linux and web dev Just clone the rep

Victor Batarse 11 Mar 10, 2024
Super-lightweight Immediate-mode Embedded GUI framework, based on the awesome embedded-graphics library. Written in Rust.

Kolibri - A GUI framework made to be as lightweight as its namesake What is Kolibri? Kolibri is an embedded Immediate Mode GUI mini-framework very str

null 6 Jun 24, 2023
Dynamic, Type-Erased Key-Value Maps in Rust

Dynamic Objects in Rust Do you love Rust but are tired of being constrained by static typing when you need a map to hold values of different types? Do

Travis A. Wagner 12 Feb 25, 2024
A reliable key-value storage for modern software

Quick-KV A reliable key-value storage for modern software Features Binary Based Data-Store Serde Supported Data Types Thread Safe Links Documentation

null 3 Oct 11, 2023
A reconciliation service to sync a key-value map over multiple instances.

reconcile-rs Docs This crate provides a key-data map structure HRTree that can be used together with the reconciliation Service. Different instances c

Akvize 3 Nov 8, 2023
A minimal file exchange server designed for clients with browsers only.

XIAO-Files Xiao-Files is a minimal file exchange server designed for clients with browsers only. Example Let's say we have a host with IP 10.8.20.1, a

EvianZhang 3 Apr 2, 2024
Cross-platform file sharig application for desktop and mobile devices

Skylite Description Getting Started Dependencies Installing Executing program License Acknowledgments Description Cross platform file sharing applicat

Adeoye Adefemi 5 Nov 16, 2023
Helps cargo build and run apps for iOS

cargo-xcodebuild Helps cargo build and run apps for iOS. ?? ⚙️ ?? Setup You need to install Xcode (NOT just Command Line Tools!), xcodegen, cargo-xcod

Igor Shaposhnik 29 Nov 22, 2022
Middleware/ios shortcut to setup alarms automatically based on the first class

Webuntis alarm This is a small little "middleware" / web server intended to connect to a webuntis timetable used in german schools which i wrote when

raizo 3 Dec 30, 2022
A simple tool for extracting files from iOS backup archive.

iBackupExtractor A simple tool for extracting files from iOS backup archive. iOS backup files are not stored with their original directory layouts. Re

Cyandev 132 Oct 10, 2023