synadb 0.1.0

An AI-native embedded database
Documentation

SynaDB

CI Crates.io License: MIT

An AI-native embedded database.

An embedded, log-structured, columnar-mapped database engine written in Rust. Syna combines the embedded simplicity of SQLite, the columnar analytical speed of DuckDB, and the schema flexibility of MongoDB.

Features

  • Append-only log structure - Fast sequential writes, immutable history
  • Schema-free - Store heterogeneous data types without migrations
  • AI/ML optimized - Extract time-series data as contiguous tensors for PyTorch/TensorFlow
  • C-ABI interface - Use from Python, Node.js, C++, or any FFI-capable language
  • Delta & LZ4 compression - Minimize storage for time-series data
  • Crash recovery - Automatic index rebuild on open
  • Thread-safe - Concurrent read/write access with mutex-protected writes

Installation

Building from Source

# Clone the repository

git clone https://github.com/gtava5813/SynaDB.git

cd SynaDB


# Build release version

cargo build --release


# Run tests

cargo test

The compiled library will be at:

  • Linux: target/release/libsynadb.so
  • macOS: target/release/libsynadb.dylib
  • Windows: target/release/synadb.dll

Adding as a Dependency

[dependencies]

synadb = "0.1.0"

Quick Start

Rust Usage

use synadb::{synadb, Atom, Result};

fn main() -> Result<()> {
    // Open or create a database
    let mut db = synadb::new("my_data.db")?;
    
    // Write different data types
    db.append("temperature", Atom::Float(23.5))?;
    db.append("count", Atom::Int(42))?;
    db.append("name", Atom::Text("sensor-1".to_string()))?;
    db.append("raw_data", Atom::Bytes(vec![0x01, 0x02, 0x03]))?;
    
    // Read values back
    if let Some(temp) = db.get("temperature")? {
        println!("Temperature: {:?}", temp);
    }
    
    // Append more values to build history
    db.append("temperature", Atom::Float(24.1))?;
    db.append("temperature", Atom::Float(24.8))?;
    
    // Extract history as tensor for ML
    let history = db.get_history_floats("temperature")?;
    println!("Temperature history: {:?}", history); // [23.5, 24.1, 24.8]
    
    // Delete a key
    db.delete("count")?;
    assert!(db.get("count")?.is_none());
    
    // List all keys
    let keys = db.keys();
    println!("Keys: {:?}", keys);
    
    // Compact to reclaim space
    db.compact()?;
    
    // Close (optional - happens on drop)
    db.close()?;
    
    Ok(())
}

Python Usage (ctypes)

import ctypes
from ctypes import c_char_p, c_double, c_int64, c_int32, c_size_t, POINTER, byref

# Load the library
lib = ctypes.CDLL("./target/release/libsynadb.so")  # or .dylib/.dll

# Define function signatures
lib.syna_open.argtypes = [c_char_p]
lib.syna_open.restype = c_int32

lib.syna_close.argtypes = [c_char_p]
lib.syna_close.restype = c_int32

lib.syna_put_float.argtypes = [c_char_p, c_char_p, c_double]
lib.syna_put_float.restype = c_int64

lib.syna_get_float.argtypes = [c_char_p, c_char_p, POINTER(c_double)]
lib.syna_get_float.restype = c_int32

lib.syna_get_history_tensor.argtypes = [c_char_p, c_char_p, POINTER(c_size_t)]
lib.syna_get_history_tensor.restype = POINTER(c_double)

lib.syna_free_tensor.argtypes = [POINTER(c_double), c_size_t]
lib.syna_free_tensor.restype = None

lib.syna_delete.argtypes = [c_char_p, c_char_p]
lib.syna_delete.restype = c_int32

# Usage
db_path = b"my_data.db"

# Open database
result = lib.syna_open(db_path)
assert result == 1, f"Failed to open database: {result}"

# Write float values
lib.syna_put_float(db_path, b"temperature", 23.5)
lib.syna_put_float(db_path, b"temperature", 24.1)
lib.syna_put_float(db_path, b"temperature", 24.8)

# Read latest value
value = c_double()
result = lib.syna_get_float(db_path, b"temperature", byref(value))
if result == 1:
    print(f"Temperature: {value.value}")

# Get history as numpy-compatible array
length = c_size_t()
ptr = lib.syna_get_history_tensor(db_path, b"temperature", byref(length))
if ptr:
    # Convert to Python list (or use numpy.ctypeslib for zero-copy)
    history = [ptr[i] for i in range(length.value)]
    print(f"History: {history}")
    
    # Free the tensor memory
    lib.syna_free_tensor(ptr, length)

# Close database
lib.syna_close(db_path)

C/C++ Usage

#include "synadb.h"
#include <stdio.h>

int main() {
    const char* db_path = "my_data.db";
    
    // Open database
    int result = syna_open(db_path);
    if (result != 1) {
        fprintf(stderr, "Failed to open database: %d\n", result);
        return 1;
    }
    
    // Write values
    syna_put_float(db_path, "temperature", 23.5);
    syna_put_float(db_path, "temperature", 24.1);
    syna_put_int(db_path, "count", 42);
    syna_put_text(db_path, "name", "sensor-1");
    
    // Read float value
    double temp;
    if (syna_get_float(db_path, "temperature", &temp) == 1) {
        printf("Temperature: %f\n", temp);
    }
    
    // Get history tensor for ML
    size_t len;
    double* tensor = syna_get_history_tensor(db_path, "temperature", &len);
    if (tensor) {
        printf("History (%zu values):", len);
        for (size_t i = 0; i < len; i++) {
            printf(" %f", tensor[i]);
        }
        printf("\n");
        
        // Free tensor memory
        syna_free_tensor(tensor, len);
    }
    
    // Delete a key
    syna_delete(db_path, "count");
    
    // Compact database
    syna_compact(db_path);
    
    // Close database
    syna_close(db_path);
    
    return 0;
}

Compile with:

gcc -o myapp myapp.c -L./target/release -lsynadb -Wl,-rpath,./target/release

Data Types

Syna supports five atomic data types:

Type Rust C/FFI Description
Null Atom::Null N/A Absence of value
Float Atom::Float(f64) syna_put_float 64-bit floating point
Int Atom::Int(i64) syna_put_int 64-bit signed integer
Text Atom::Text(String) syna_put_text UTF-8 string
Bytes Atom::Bytes(Vec<u8>) syna_put_bytes Raw byte array

Configuration

use synadb::{synadb, DbConfig};

let config = DbConfig {
    enable_compression: true,   // LZ4 compression for large values
    enable_delta: true,         // Delta encoding for float sequences
    sync_on_write: true,        // fsync after each write (safer but slower)
};

let db = synadb::with_config("my_data.db", config)?;

Error Codes (FFI)

Code Constant Meaning
1 ERR_SUCCESS Operation successful
0 ERR_GENERIC Generic error
-1 ERR_DB_NOT_FOUND Database not in registry
-2 ERR_INVALID_PATH Invalid path or UTF-8
-3 ERR_IO I/O error
-4 ERR_SERIALIZATION Serialization error
-5 ERR_KEY_NOT_FOUND Key not found
-6 ERR_TYPE_MISMATCH Type mismatch on read
-100 ERR_INTERNAL_PANIC Internal panic

Architecture

Syna uses an append-only log structure inspired by the "physics of time" principle:

┌─────────────────────────────────────────────────────────────┐
│ Entry 0                                                     │
├──────────────┬──────────────────┬───────────────────────────┤
│ LogHeader    │ Key (UTF-8)      │ Value (bincode)           │
│ (15 bytes)   │ (key_len bytes)  │ (val_len bytes)           │
├──────────────┴──────────────────┴───────────────────────────┤
│ Entry 1 ...                                                 │
└─────────────────────────────────────────────────────────────┘
  • Writes: Always append to end of file (sequential I/O)
  • Reads: Use in-memory index for O(1) key lookup
  • Recovery: Scan file on open to rebuild index
  • Compaction: Rewrite file with only latest values

Contributing

See CONTRIBUTING.md for guidelines.

License

MIT License - see LICENSE for details.