Skip to main content

Isam

Struct Isam 

Source
pub struct Isam<K, V> { /* private fields */ }
Expand description

The public ISAM database handle.

Isam is Clone — every clone is another handle to the same underlying storage. Thread safety is provided by TransactionManager.

§Creating and opening databases

For databases without secondary indices, use Isam::create and Isam::open. To attach secondary indices at construction time, use Isam::builder.

§Running transactions

For simple single-operation writes or reads, use the write and read helpers — they handle begin/commit/rollback automatically:

db.write(|txn| db.insert(txn, 1u32, &"hello".to_string())).unwrap();
let val = db.read(|txn| db.get(txn, &1u32)).unwrap();
assert_eq!(val, Some("hello".to_string()));

For multi-operation transactions, use begin_transaction directly.

Implementations§

Source§

impl<K, V> Isam<K, V>
where K: Serialize + DeserializeOwned + Ord + Clone + 'static, V: Serialize + DeserializeOwned + Clone + 'static,

Source

pub fn builder() -> IsamBuilder<K, V>

Return a builder for creating or opening a database with secondary indices.

For databases without secondary indices, create and open are simpler alternatives.

§Example
use serde::{Serialize, Deserialize};
use highlandcows_isam::{Isam, DeriveKey};

#[derive(Serialize, Deserialize, Clone)]
struct User { name: String, city: String }

struct CityIndex;
impl DeriveKey<User> for CityIndex {
    type Key = String;
    fn derive(u: &User) -> String { u.city.clone() }
}

let db = Isam::<u64, User>::builder()
    .with_index("city", CityIndex)
    .create(&path)
    .unwrap();

let city_idx = db.index::<CityIndex>("city");
Source

pub fn create(path: impl AsRef<Path>) -> IsamResult<Self>

Create a new, empty database at path with no secondary indices.

Two files are created: <path>.idb (data) and <path>.idx (index). Any existing files at those paths are truncated.

To register secondary indices, use builder instead.

§Example
let db: Isam<u32, String> = Isam::create(&path).unwrap();
Source

pub fn open(path: impl AsRef<Path>) -> IsamResult<Self>

Open an existing database at path with no secondary indices.

To re-register secondary indices on open, use builder instead.

§Example
let db: Isam<u32, String> = Isam::open(&path).unwrap();
Source

pub fn as_single_user<F, T>(self, timeout: Duration, f: F) -> IsamResult<T>
where F: FnOnce(&SingleUserToken, Isam<K, V>) -> IsamResult<T>,

Execute a closure in single-user mode.

Sets the single-user flag immediately, then waits up to timeout for any in-flight transaction on another thread to finish. Once exclusive access is confirmed, f is called. Any other thread that attempts any database operation while f is running receives IsamError::SingleUserMode immediately (no blocking). The calling thread can continue to use self normally inside f.

Single-user mode is intended for administrative operations — compaction, schema migration, and similar tasks — where you need to ensure no other thread modifies the database concurrently. It is an in-process mechanism only; multi-process exclusion is not supported.

The return value of f is forwarded to the caller. Single-user mode is released when f returns, including if f returns an error or panics.

§Errors
  • IsamError::SingleUserMode — single-user mode is already active (e.g. called recursively, or another thread holds it).
  • IsamError::Timeout — an in-flight transaction did not finish within timeout. This also occurs if the calling thread itself holds an open Transaction: the transaction holds the storage lock, so the spin will never succeed and the call will time out. Commit or roll back all transactions on the calling thread before calling as_single_user.
§Examples

Running compaction — return db from the closure to keep using it:

let db = db.as_single_user(DEFAULT_SINGLE_USER_TIMEOUT, |token, db| {
    db.compact(token)?;
    Ok(db)
}).unwrap();

Migrating values to a new type — return the new handle from the closure, not the original db:

// db is Isam<u32, String>; migrate to Isam<u32, Vec<u8>>.
let db: Isam<u32, Vec<u8>> =
    db.as_single_user(DEFAULT_SINGLE_USER_TIMEOUT, |token, db| {
        db.migrate_values(1, |s: String| Ok(s.into_bytes()), token)
    }).unwrap();
§Note on error handling

as_single_user consumes self. If it returns Err (e.g. IsamError::Timeout or IsamError::SingleUserMode), the handle is dropped. Clone before calling if you need to retry on failure:

let result = db.clone().as_single_user(DEFAULT_SINGLE_USER_TIMEOUT, |token, db| {
    db.compact(token)?;
    Ok(db)
});
Source

pub fn index<E: DeriveKey<V>>( &self, name: &str, ) -> SecondaryIndexHandle<K, V, E::Key>

Return a SecondaryIndexHandle for the named index.

The index must have been registered via IsamBuilder::with_index when the database was created or opened. No I/O is performed — the handle is just a typed wrapper around the index name.

§Example
use serde::{Serialize, Deserialize};
use highlandcows_isam::{Isam, DeriveKey};

#[derive(Serialize, Deserialize, Clone)]
struct User { name: String, city: String }

struct CityIndex;
impl DeriveKey<User> for CityIndex {
    type Key = String;
    fn derive(u: &User) -> String { u.city.clone() }
}

let db = Isam::<u64, User>::builder()
    .with_index("city", CityIndex)
    .create(&path)
    .unwrap();

let city_idx = db.index::<CityIndex>("city");

db.write(|txn| db.insert(txn, 1, &User { name: "Alice".into(), city: "London".into() })).unwrap();

let results = db.read(|txn| city_idx.lookup(txn, &"London".to_string())).unwrap();
assert_eq!(results.len(), 1);
Source

pub fn secondary_indices(&self) -> IsamResult<Vec<IndexInfo>>

Return information about all secondary indices registered on this database.

§Deadlock warning

Acquires the database lock internally. Must not be called while a Transaction is live on the same thread.

§Example
use serde::{Serialize, Deserialize};
use highlandcows_isam::{Isam, DeriveKey};

#[derive(Serialize, Deserialize, Clone)]
struct User { name: String, city: String }

struct CityIndex;
impl DeriveKey<User> for CityIndex {
    type Key = String;
    fn derive(u: &User) -> String { u.city.clone() }
}

let db = Isam::<u64, User>::builder()
    .with_index("city", CityIndex)
    .create(&path)
    .unwrap();

let indices = db.secondary_indices().unwrap();
assert_eq!(indices.len(), 1);
assert_eq!(indices[0].name, "city");
Source

pub fn begin_transaction(&self) -> IsamResult<Transaction<'_, K, V>>

Begin a new transaction.

The returned Transaction holds an exclusive lock on the database until it is committed, rolled back, or dropped. Dropping without committing automatically rolls back all changes made in the transaction.

For simple single-operation use, prefer the write and read helpers, which handle begin/commit/rollback automatically.

§Example
let mut txn = db.begin_transaction().unwrap();
// ... perform operations ...
txn.commit().unwrap();
Source

pub fn write<F, T>(&self, f: F) -> IsamResult<T>
where F: FnOnce(&mut Transaction<'_, K, V>) -> IsamResult<T>,

Execute a write closure inside a transaction.

Begins a transaction, passes it to f, then commits on Ok or rolls back on Err. The return value of f is forwarded to the caller.

Use this for inserts, updates, and deletes where you don’t need to manage the transaction lifetime manually.

§Example
db.write(|txn| db.insert(txn, 1u32, &"hello".to_string())).unwrap();
Source

pub fn read<F, T>(&self, f: F) -> IsamResult<T>
where F: FnOnce(&mut Transaction<'_, K, V>) -> IsamResult<T>,

Execute a read closure inside a transaction.

Begins a transaction, passes it to f, then rolls back unconditionally (since reads make no changes). The return value of f is forwarded to the caller.

§Example
let val = db.read(|txn| db.get(txn, &1u32)).unwrap();
assert_eq!(val, Some("hello".to_string()));
Source

pub fn insert( &self, txn: &mut Transaction<'_, K, V>, key: K, value: &V, ) -> IsamResult<()>

Insert a new key-value pair.

Returns IsamError::DuplicateKey if the key already exists.

§Example
let mut txn = db.begin_transaction().unwrap();
db.insert(&mut txn, 1u32, &"hello".to_string()).unwrap();
txn.commit().unwrap();
Source

pub fn get( &self, txn: &mut Transaction<'_, K, V>, key: &K, ) -> IsamResult<Option<V>>

Look up a key and return its value, or None if the key does not exist.

§Example
let mut txn = db.begin_transaction().unwrap();
assert_eq!(db.get(&mut txn, &1u32).unwrap(), Some("hello".to_string()));
assert_eq!(db.get(&mut txn, &99u32).unwrap(), None);
txn.commit().unwrap();
Source

pub fn update( &self, txn: &mut Transaction<'_, K, V>, key: K, value: &V, ) -> IsamResult<()>

Replace the value for an existing key.

Returns IsamError::KeyNotFound if the key does not exist.

§Example
let mut txn = db.begin_transaction().unwrap();
db.update(&mut txn, 1u32, &"new".to_string()).unwrap();
assert_eq!(db.get(&mut txn, &1u32).unwrap(), Some("new".to_string()));
txn.commit().unwrap();
Source

pub fn delete(&self, txn: &mut Transaction<'_, K, V>, key: &K) -> IsamResult<()>

Remove a key and its associated value.

Returns IsamError::KeyNotFound if the key does not exist.

§Example
let mut txn = db.begin_transaction().unwrap();
db.delete(&mut txn, &1u32).unwrap();
assert_eq!(db.get(&mut txn, &1u32).unwrap(), None);
txn.commit().unwrap();
Source

pub fn min_key(&self, txn: &mut Transaction<'_, K, V>) -> IsamResult<Option<K>>

Return the smallest key in the database, or None if empty.

§Example
let mut txn = db.begin_transaction().unwrap();
assert_eq!(db.min_key(&mut txn).unwrap(), Some(1u32));
txn.commit().unwrap();
Source

pub fn max_key(&self, txn: &mut Transaction<'_, K, V>) -> IsamResult<Option<K>>

Return the largest key in the database, or None if empty.

§Example
let mut txn = db.begin_transaction().unwrap();
assert_eq!(db.max_key(&mut txn).unwrap(), Some(3u32));
txn.commit().unwrap();
Source

pub fn iter<'txn>( &self, txn: &'txn mut Transaction<'_, K, V>, ) -> IsamResult<IsamIter<'txn, K, V>>

Return a key-ordered iterator over all records.

The iterator borrows txn for its lifetime, so no other operations can be performed on the database until the iterator is dropped.

§Example
let mut txn = db.begin_transaction().unwrap();
let keys: Vec<u32> = db.iter(&mut txn).unwrap()
    .map(|r| r.unwrap().0)
    .collect();
assert_eq!(keys, vec![1, 2, 3]);
txn.commit().unwrap();
Source

pub fn range<'txn, R>( &self, txn: &'txn mut Transaction<'_, K, V>, range: R, ) -> IsamResult<RangeIter<'txn, K, V>>
where R: RangeBounds<K>,

Return a key-ordered iterator over records whose keys fall within range.

Accepts any of Rust’s built-in range expressions: a..b, a..=b, a.., ..b, ..=b, ...

The iterator borrows txn for its lifetime, so no other operations can be performed on the database until the iterator is dropped.

§Example
let mut txn = db.begin_transaction().unwrap();
let keys: Vec<u32> = db.range(&mut txn, 3u32..=7).unwrap()
    .map(|r| r.unwrap().0)
    .collect();
assert_eq!(keys, vec![3, 4, 5, 6, 7]);
txn.commit().unwrap();
Source

pub fn key_schema_version(&self) -> IsamResult<u32>

Return the key schema version stored in the index metadata.

Schema versions are set by migrate_keys and default to 0 for newly created databases.

§Deadlock warning

Acquires the database lock internally. Must not be called while a Transaction is live on the same thread.

§Example
assert_eq!(db.key_schema_version().unwrap(), 0);
Source

pub fn val_schema_version(&self) -> IsamResult<u32>

Return the value schema version stored in the index metadata.

Schema versions are set by migrate_values and default to 0 for newly created databases.

§Deadlock warning

Acquires the database lock internally. Must not be called while a Transaction is live on the same thread.

§Example
assert_eq!(db.val_schema_version().unwrap(), 0);
Source

pub fn migrate_index<F>( &self, name: &str, new_version: u32, f: F, _token: &SingleUserToken, ) -> IsamResult<()>
where F: FnMut(V) -> IsamResult<V>,

Migrate a secondary index to a new schema version.

This is the secondary index counterpart to migrate_values and migrate_keys. Use it when the DeriveKey derivation logic for a named secondary index has changed and the on-disk index needs to be rebuilt to match.

The named secondary index is cleared and repopulated by scanning all primary records. For each record, f is applied to the stored value before the registered DeriveKey extractor derives the secondary key, letting you adapt the effective input to the updated derivation logic. Pass the identity closure (|v| Ok(v)) for a plain rebuild with no value transformation.

After the rebuild, new_version is written into the .sidx metadata so that Isam::secondary_indices reflects the current migration state via IndexInfo::schema_version.

Primary records are not modified. Only the named secondary index is affected; other secondary indices are left untouched.

§Deadlock warning

Acquires the database lock internally. Must not be called while a Transaction is live on the same thread — commit or roll back all open transactions before calling as_single_user.

§Example
use serde::{Serialize, Deserialize};
use highlandcows_isam::{Isam, DeriveKey, DEFAULT_SINGLE_USER_TIMEOUT};

#[derive(Serialize, Deserialize, Clone)]
struct User { name: String, city: String }

struct CityIndex;
impl DeriveKey<User> for CityIndex {
    type Key = String;
    // derive now normalizes to lowercase
    fn derive(u: &User) -> String { u.city.to_lowercase() }
}

let db = Isam::<u64, User>::builder()
    .with_index("city", CityIndex)
    .create(&path)
    .unwrap();

db.write(|txn| db.insert(txn, 1, &User { name: "Alice".into(), city: "London".into() }))
    .unwrap();

// Rebuild the "city" index, normalizing city names to lowercase
// so the index matches the updated DeriveKey logic.
let db = db.as_single_user(DEFAULT_SINGLE_USER_TIMEOUT, |token, db| {
    db.migrate_index("city", 1, |mut u: User| {
        u.city = u.city.to_lowercase();
        Ok(u)
    }, token)?;
    Ok(db)
}).unwrap();

let info = db.secondary_indices().unwrap();
assert_eq!(info[0].schema_version, 1);
Source

pub fn compact(&self, _token: &SingleUserToken) -> IsamResult<()>

Compact the database, removing tombstones and stale values.

Rewrites the data and index files atomically via temp-file rename, then re-opens them in place.

§Deadlock warning

Acquires the database lock internally. Must not be called while a Transaction is live on the same thread — commit or roll back all open transactions before calling as_single_user.

§Example
db.as_single_user(DEFAULT_SINGLE_USER_TIMEOUT, |token, db| db.compact(token)).unwrap();
Source

pub fn migrate_values<V2, F>( self, new_val_version: u32, f: F, _token: &SingleUserToken, ) -> IsamResult<Isam<K, V2>>
where V2: Serialize + DeserializeOwned + Clone + 'static, F: FnMut(V) -> IsamResult<V2>,

Rewrite every value through f, bump the val schema version, and return a ready-to-use Isam<K, V2>. Consumes self.

Records are rewritten to new temp files and atomically renamed into place. The key schema version is preserved.

§Deadlock warning

Acquires the database lock internally. Must not be called while a Transaction is live on the same thread — commit or roll back all open transactions before calling as_single_user.

§Example
let db: Isam<u32, String> = Isam::create(&path).unwrap();
// Migrate String values → u64, setting val schema version to 1.
let db2: Isam<u32, u64> = db
    .as_single_user(DEFAULT_SINGLE_USER_TIMEOUT, |token, db| {
        db.migrate_values(1, |s: String| Ok(s.parse::<u64>().unwrap()), token)
    })
    .unwrap();
assert_eq!(db2.val_schema_version().unwrap(), 1);
Source

pub fn migrate_keys<K2, F>( self, new_key_version: u32, f: F, _token: &SingleUserToken, ) -> IsamResult<Isam<K2, V>>
where K2: Serialize + DeserializeOwned + Ord + Clone + 'static, F: FnMut(K) -> IsamResult<K2>,

Rewrite every key through f, bump the key schema version, re-sort by K2::Ord, rebuild the index, and return a ready-to-use Isam<K2, V>. Consumes self.

Records are rewritten to new temp files and atomically renamed into place. The value schema version is preserved.

§Deadlock warning

Acquires the database lock internally. Must not be called while a Transaction is live on the same thread — commit or roll back all open transactions before calling as_single_user.

§Example
let db: Isam<u32, String> = Isam::create(&path).unwrap();
// Migrate u32 keys → String, setting key schema version to 1.
let db2: Isam<String, String> = db
    .as_single_user(DEFAULT_SINGLE_USER_TIMEOUT, |token, db| {
        db.migrate_keys(1, |k: u32| Ok(format!("{k}")), token)
    })
    .unwrap();
assert_eq!(db2.key_schema_version().unwrap(), 1);

Trait Implementations§

Source§

impl<K, V> Clone for Isam<K, V>

Source§

fn clone(&self) -> Self

Returns a duplicate of the value. Read more
1.0.0 · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more

Auto Trait Implementations§

§

impl<K, V> Freeze for Isam<K, V>

§

impl<K, V> RefUnwindSafe for Isam<K, V>

§

impl<K, V> Send for Isam<K, V>
where V: Send, K: Send,

§

impl<K, V> Sync for Isam<K, V>
where V: Send, K: Send,

§

impl<K, V> Unpin for Isam<K, V>

§

impl<K, V> UnsafeUnpin for Isam<K, V>

§

impl<K, V> UnwindSafe for Isam<K, V>

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.