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,
impl<K, V> Isam<K, V>where
K: Serialize + DeserializeOwned + Ord + Clone + 'static,
V: Serialize + DeserializeOwned + Clone + 'static,
Sourcepub fn builder() -> IsamBuilder<K, V>
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");Sourcepub fn create(path: impl AsRef<Path>) -> IsamResult<Self>
pub fn create(path: impl AsRef<Path>) -> IsamResult<Self>
Sourcepub fn open(path: impl AsRef<Path>) -> IsamResult<Self>
pub fn open(path: impl AsRef<Path>) -> IsamResult<Self>
Sourcepub fn as_single_user<F, T>(self, timeout: Duration, f: F) -> IsamResult<T>
pub fn as_single_user<F, T>(self, timeout: Duration, f: F) -> 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 withintimeout. This also occurs if the calling thread itself holds an openTransaction: 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 callingas_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)
});Sourcepub fn index<E: DeriveKey<V>>(
&self,
name: &str,
) -> SecondaryIndexHandle<K, V, E::Key>
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);Sourcepub fn secondary_indices(&self) -> IsamResult<Vec<IndexInfo>>
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");Sourcepub fn begin_transaction(&self) -> IsamResult<Transaction<'_, K, V>>
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();Sourcepub fn write<F, T>(&self, f: F) -> IsamResult<T>
pub fn write<F, T>(&self, f: F) -> 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();Sourcepub fn read<F, T>(&self, f: F) -> IsamResult<T>
pub fn read<F, T>(&self, f: F) -> 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()));Sourcepub fn insert(
&self,
txn: &mut Transaction<'_, K, V>,
key: K,
value: &V,
) -> IsamResult<()>
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();Sourcepub fn get(
&self,
txn: &mut Transaction<'_, K, V>,
key: &K,
) -> IsamResult<Option<V>>
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();Sourcepub fn update(
&self,
txn: &mut Transaction<'_, K, V>,
key: K,
value: &V,
) -> IsamResult<()>
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();Sourcepub fn delete(&self, txn: &mut Transaction<'_, K, V>, key: &K) -> IsamResult<()>
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();Sourcepub fn min_key(&self, txn: &mut Transaction<'_, K, V>) -> IsamResult<Option<K>>
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();Sourcepub fn max_key(&self, txn: &mut Transaction<'_, K, V>) -> IsamResult<Option<K>>
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();Sourcepub fn iter<'txn>(
&self,
txn: &'txn mut Transaction<'_, K, V>,
) -> IsamResult<IsamIter<'txn, K, V>>
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();Sourcepub fn range<'txn, R>(
&self,
txn: &'txn mut Transaction<'_, K, V>,
range: R,
) -> IsamResult<RangeIter<'txn, K, V>>where
R: RangeBounds<K>,
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();Sourcepub fn key_schema_version(&self) -> IsamResult<u32>
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);Sourcepub fn val_schema_version(&self) -> IsamResult<u32>
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);Sourcepub fn migrate_index<F>(
&self,
name: &str,
new_version: u32,
f: F,
_token: &SingleUserToken,
) -> IsamResult<()>where
F: FnMut(V) -> IsamResult<V>,
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);Sourcepub fn compact(&self, _token: &SingleUserToken) -> IsamResult<()>
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();Sourcepub fn migrate_values<V2, F>(
self,
new_val_version: u32,
f: F,
_token: &SingleUserToken,
) -> IsamResult<Isam<K, V2>>
pub fn migrate_values<V2, F>( self, new_val_version: u32, f: F, _token: &SingleUserToken, ) -> IsamResult<Isam<K, 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);Sourcepub fn migrate_keys<K2, F>(
self,
new_key_version: u32,
f: F,
_token: &SingleUserToken,
) -> IsamResult<Isam<K2, V>>
pub fn migrate_keys<K2, F>( self, new_key_version: u32, f: F, _token: &SingleUserToken, ) -> IsamResult<Isam<K2, V>>
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);