highlandcows_isam/isam.rs
1/// `Isam<K, V>` — the public orchestration interface for an ISAM database.
2///
3/// `Isam` is a thin facade over `TransactionManager`. All CRUD operations
4/// take a `&mut Transaction` obtained from `begin_transaction()`.
5///
6/// ## Generic parameters
7///
8/// - `K` — key type; serializable, deserializable, ordered, cheap to clone.
9/// - `V` — value type; serializable and deserializable.
10use std::collections::HashSet;
11use std::marker::PhantomData;
12use std::ops::{Bound, RangeBounds};
13use std::path::{Path, PathBuf};
14use std::time::Duration;
15
16use serde::de::DeserializeOwned;
17use serde::Serialize;
18
19use crate::error::{IsamError, IsamResult};
20use crate::manager::TransactionManager;
21use crate::secondary_index::{AnySecondaryIndex, DeriveKey, SecondaryIndexImpl};
22use crate::storage::IsamStorage;
23use crate::store::RecordRef;
24use crate::transaction::Transaction;
25
26// ── Path helpers (pub(crate) so storage.rs can use them) ─────────────────── //
27
28pub(crate) fn idb_path(base: &Path) -> PathBuf {
29 base.with_extension("idb")
30}
31
32pub(crate) fn idx_path(base: &Path) -> PathBuf {
33 base.with_extension("idx")
34}
35
36/// Convert a borrowed `Bound<&K>` into an owned `Bound<K>` by cloning.
37fn clone_bound<K: Clone>(b: Bound<&K>) -> Bound<K> {
38 match b {
39 Bound::Included(k) => Bound::Included(k.clone()),
40 Bound::Excluded(k) => Bound::Excluded(k.clone()),
41 Bound::Unbounded => Bound::Unbounded,
42 }
43}
44
45/// Borrow the inner `Vec<u8>` of a `Bound<Vec<u8>>` as a `Bound<&[u8]>`.
46fn as_bound_bytes(b: &Bound<Vec<u8>>) -> Bound<&[u8]> {
47 match b {
48 Bound::Included(v) => Bound::Included(v.as_slice()),
49 Bound::Excluded(v) => Bound::Excluded(v.as_slice()),
50 Bound::Unbounded => Bound::Unbounded,
51 }
52}
53
54// ── Constants ────────────────────────────────────────────────────────────── //
55
56/// Default timeout for [`Isam::as_single_user`].
57///
58/// 30 seconds — long enough for typical in-flight transactions to finish,
59/// short enough to surface hung transactions rather than waiting forever.
60pub const DEFAULT_SINGLE_USER_TIMEOUT: Duration = Duration::from_secs(30);
61
62// ── SingleUserToken ───────────────────────────────────────────────────────── //
63
64/// An opaque capability token proving the caller is inside an
65/// [`Isam::as_single_user`] closure.
66///
67/// `SingleUserToken` can only be constructed by `as_single_user`; a reference
68/// to it is passed into the closure and must be forwarded to any administrative
69/// method that requires exclusive access. This enforces **at compile time**
70/// that [`Isam::compact`], [`Isam::migrate_values`], [`Isam::migrate_keys`],
71/// and [`Isam::migrate_index`] are never called outside of single-user mode.
72pub struct SingleUserToken(());
73
74// ── Isam ─────────────────────────────────────────────────────────────────── //
75
76/// The public ISAM database handle.
77///
78/// `Isam` is `Clone` — every clone is another handle to the same underlying
79/// storage. Thread safety is provided by `TransactionManager`.
80///
81/// ## Creating and opening databases
82///
83/// For databases without secondary indices, use [`Isam::create`] and [`Isam::open`].
84/// To attach secondary indices at construction time, use [`Isam::builder`].
85///
86/// ## Running transactions
87///
88/// For simple single-operation writes or reads, use the [`write`](Self::write) and
89/// [`read`](Self::read) helpers — they handle begin/commit/rollback automatically:
90///
91/// ```
92/// # use tempfile::TempDir;
93/// # use highlandcows_isam::Isam;
94/// # let dir = TempDir::new().unwrap();
95/// # let path = dir.path().join("db");
96/// # let db: Isam<u32, String> = Isam::create(&path).unwrap();
97/// db.write(|txn| db.insert(txn, 1u32, &"hello".to_string())).unwrap();
98/// let val = db.read(|txn| db.get(txn, &1u32)).unwrap();
99/// assert_eq!(val, Some("hello".to_string()));
100/// ```
101///
102/// For multi-operation transactions, use [`begin_transaction`](Self::begin_transaction) directly.
103pub struct Isam<K, V> {
104 manager: TransactionManager<K, V>,
105}
106
107impl<K, V> Clone for Isam<K, V> {
108 fn clone(&self) -> Self {
109 Self {
110 manager: self.manager.clone(),
111 }
112 }
113}
114
115impl<K, V> Isam<K, V>
116where
117 K: Serialize + DeserializeOwned + Ord + Clone + 'static,
118 V: Serialize + DeserializeOwned + Clone + 'static,
119{
120 // ── Lifecycle ────────────────────────────────────────────────────────── //
121
122 /// Return a builder for creating or opening a database with secondary indices.
123 ///
124 /// For databases without secondary indices, [`create`](Self::create) and
125 /// [`open`](Self::open) are simpler alternatives.
126 ///
127 /// # Example
128 /// ```
129 /// # use tempfile::TempDir;
130 /// use serde::{Serialize, Deserialize};
131 /// use highlandcows_isam::{Isam, DeriveKey};
132 ///
133 /// #[derive(Serialize, Deserialize, Clone)]
134 /// struct User { name: String, city: String }
135 ///
136 /// struct CityIndex;
137 /// impl DeriveKey<User> for CityIndex {
138 /// type Key = String;
139 /// fn derive(u: &User) -> String { u.city.clone() }
140 /// }
141 ///
142 /// # let dir = TempDir::new().unwrap();
143 /// # let path = dir.path().join("db");
144 /// let db = Isam::<u64, User>::builder()
145 /// .with_index("city", CityIndex)
146 /// .create(&path)
147 /// .unwrap();
148 ///
149 /// let city_idx = db.index::<CityIndex>("city");
150 /// ```
151 pub fn builder() -> IsamBuilder<K, V> {
152 IsamBuilder::default()
153 }
154
155 /// Create a new, empty database at `path` with no secondary indices.
156 ///
157 /// Two files are created: `<path>.idb` (data) and `<path>.idx` (index).
158 /// Any existing files at those paths are truncated.
159 ///
160 /// To register secondary indices, use [`builder`](Self::builder) instead.
161 ///
162 /// # Example
163 /// ```
164 /// # use tempfile::TempDir;
165 /// # use highlandcows_isam::Isam;
166 /// # let dir = TempDir::new().unwrap();
167 /// # let path = dir.path().join("db");
168 /// let db: Isam<u32, String> = Isam::create(&path).unwrap();
169 /// ```
170 pub fn create(path: impl AsRef<Path>) -> IsamResult<Self> {
171 Ok(Self {
172 manager: TransactionManager::create(path.as_ref())?,
173 })
174 }
175
176 /// Open an existing database at `path` with no secondary indices.
177 ///
178 /// To re-register secondary indices on open, use [`builder`](Self::builder) instead.
179 ///
180 /// # Example
181 /// ```
182 /// # use tempfile::TempDir;
183 /// # use highlandcows_isam::Isam;
184 /// # let dir = TempDir::new().unwrap();
185 /// # let path = dir.path().join("db");
186 /// # Isam::<u32, String>::create(&path).unwrap();
187 /// let db: Isam<u32, String> = Isam::open(&path).unwrap();
188 /// ```
189 pub fn open(path: impl AsRef<Path>) -> IsamResult<Self> {
190 Ok(Self {
191 manager: TransactionManager::open(path.as_ref())?,
192 })
193 }
194
195 // ── Single-user mode ─────────────────────────────────────────────────── //
196
197 /// Execute a closure in single-user mode.
198 ///
199 /// Sets the single-user flag immediately, then waits up to `timeout` for any
200 /// in-flight transaction on another thread to finish. Once exclusive access
201 /// is confirmed, `f` is called. Any other thread that attempts any database
202 /// operation while `f` is running receives [`IsamError::SingleUserMode`]
203 /// immediately (no blocking). The calling thread can continue to use `self`
204 /// normally inside `f`.
205 ///
206 /// Single-user mode is intended for administrative operations — compaction,
207 /// schema migration, and similar tasks — where you need to ensure no other
208 /// thread modifies the database concurrently. It is an in-process
209 /// mechanism only; multi-process exclusion is not supported.
210 ///
211 /// The return value of `f` is forwarded to the caller. Single-user mode is
212 /// released when `f` returns, including if `f` returns an error or panics.
213 ///
214 /// # Errors
215 ///
216 /// - [`IsamError::SingleUserMode`] — single-user mode is already active
217 /// (e.g. called recursively, or another thread holds it).
218 /// - [`IsamError::Timeout`] — an in-flight transaction did not finish within
219 /// `timeout`. This also occurs if the calling thread itself holds an open
220 /// [`Transaction`]: the transaction holds the storage lock, so the spin
221 /// will never succeed and the call will time out. Commit or roll back all
222 /// transactions on the calling thread before calling `as_single_user`.
223 ///
224 /// # Examples
225 ///
226 /// Running compaction — return `db` from the closure to keep using it:
227 /// ```
228 /// # use tempfile::TempDir;
229 /// # use highlandcows_isam::{Isam, DEFAULT_SINGLE_USER_TIMEOUT};
230 /// # let dir = TempDir::new().unwrap();
231 /// # let path = dir.path().join("db");
232 /// # let db: Isam<u32, String> = Isam::create(&path).unwrap();
233 /// # let mut txn = db.begin_transaction().unwrap();
234 /// # for i in 0u32..5 { db.insert(&mut txn, i, &i.to_string()).unwrap(); }
235 /// # for i in 0u32..3 { db.delete(&mut txn, &i).unwrap(); }
236 /// # txn.commit().unwrap();
237 /// let db = db.as_single_user(DEFAULT_SINGLE_USER_TIMEOUT, |token, db| {
238 /// db.compact(token)?;
239 /// Ok(db)
240 /// }).unwrap();
241 /// ```
242 ///
243 /// Migrating values to a new type — return the *new* handle from the
244 /// closure, not the original `db`:
245 /// ```
246 /// # use tempfile::TempDir;
247 /// # use highlandcows_isam::{Isam, DEFAULT_SINGLE_USER_TIMEOUT};
248 /// # let dir = TempDir::new().unwrap();
249 /// # let path = dir.path().join("db");
250 /// # let db: Isam<u32, String> = Isam::create(&path).unwrap();
251 /// // db is Isam<u32, String>; migrate to Isam<u32, Vec<u8>>.
252 /// let db: Isam<u32, Vec<u8>> =
253 /// db.as_single_user(DEFAULT_SINGLE_USER_TIMEOUT, |token, db| {
254 /// db.migrate_values(1, |s: String| Ok(s.into_bytes()), token)
255 /// }).unwrap();
256 /// ```
257 ///
258 /// # Note on error handling
259 ///
260 /// `as_single_user` consumes `self`. If it returns `Err` (e.g.
261 /// [`IsamError::Timeout`] or [`IsamError::SingleUserMode`]), the handle is
262 /// dropped. Clone before calling if you need to retry on failure:
263 ///
264 /// ```
265 /// # use tempfile::TempDir;
266 /// # use highlandcows_isam::{Isam, DEFAULT_SINGLE_USER_TIMEOUT};
267 /// # let dir = TempDir::new().unwrap();
268 /// # let path = dir.path().join("db");
269 /// # let db: Isam<u32, String> = Isam::create(&path).unwrap();
270 /// let result = db.clone().as_single_user(DEFAULT_SINGLE_USER_TIMEOUT, |token, db| {
271 /// db.compact(token)?;
272 /// Ok(db)
273 /// });
274 /// ```
275 pub fn as_single_user<F, T>(self, timeout: Duration, f: F) -> IsamResult<T>
276 where
277 F: FnOnce(&SingleUserToken, Isam<K, V>) -> IsamResult<T>,
278 {
279 let _guard = self.manager.enter_single_user_mode(timeout)?;
280 f(&SingleUserToken(()), self)
281 }
282
283 /// Return a [`SecondaryIndexHandle`] for the named index.
284 ///
285 /// The index must have been registered via
286 /// [`IsamBuilder::with_index`] when the database was created or opened.
287 /// No I/O is performed — the handle is just a typed wrapper around the
288 /// index name.
289 ///
290 /// # Example
291 /// ```
292 /// # use tempfile::TempDir;
293 /// use serde::{Serialize, Deserialize};
294 /// use highlandcows_isam::{Isam, DeriveKey};
295 ///
296 /// #[derive(Serialize, Deserialize, Clone)]
297 /// struct User { name: String, city: String }
298 ///
299 /// struct CityIndex;
300 /// impl DeriveKey<User> for CityIndex {
301 /// type Key = String;
302 /// fn derive(u: &User) -> String { u.city.clone() }
303 /// }
304 ///
305 /// # let dir = TempDir::new().unwrap();
306 /// # let path = dir.path().join("db");
307 /// let db = Isam::<u64, User>::builder()
308 /// .with_index("city", CityIndex)
309 /// .create(&path)
310 /// .unwrap();
311 ///
312 /// let city_idx = db.index::<CityIndex>("city");
313 ///
314 /// db.write(|txn| db.insert(txn, 1, &User { name: "Alice".into(), city: "London".into() })).unwrap();
315 ///
316 /// let results = db.read(|txn| city_idx.lookup(txn, &"London".to_string())).unwrap();
317 /// assert_eq!(results.len(), 1);
318 /// ```
319 pub fn index<E: DeriveKey<V>>(&self, name: &str) -> SecondaryIndexHandle<K, V, E::Key> {
320 SecondaryIndexHandle {
321 name: name.to_owned(),
322 _phantom: PhantomData,
323 }
324 }
325
326 /// Return information about all secondary indices registered on this database.
327 ///
328 /// # Deadlock warning
329 /// Acquires the database lock internally. Must not be called while a
330 /// [`Transaction`] is live on the same thread.
331 ///
332 /// # Example
333 /// ```
334 /// # use tempfile::TempDir;
335 /// use serde::{Serialize, Deserialize};
336 /// use highlandcows_isam::{Isam, DeriveKey};
337 ///
338 /// #[derive(Serialize, Deserialize, Clone)]
339 /// struct User { name: String, city: String }
340 ///
341 /// struct CityIndex;
342 /// impl DeriveKey<User> for CityIndex {
343 /// type Key = String;
344 /// fn derive(u: &User) -> String { u.city.clone() }
345 /// }
346 ///
347 /// # let dir = TempDir::new().unwrap();
348 /// # let path = dir.path().join("db");
349 /// let db = Isam::<u64, User>::builder()
350 /// .with_index("city", CityIndex)
351 /// .create(&path)
352 /// .unwrap();
353 ///
354 /// let indices = db.secondary_indices().unwrap();
355 /// assert_eq!(indices.len(), 1);
356 /// assert_eq!(indices[0].name, "city");
357 /// ```
358 pub fn secondary_indices(&self) -> IsamResult<Vec<IndexInfo>> {
359 let guard = self.manager.lock_storage()?;
360 Ok(guard
361 .secondary_indices
362 .iter()
363 .map(|si| IndexInfo {
364 name: si.name().to_owned(),
365 extractor_type: si.extractor_type_name(),
366 schema_version: si.stored_schema_version(),
367 })
368 .collect())
369 }
370
371 /// Begin a new transaction.
372 ///
373 /// The returned [`Transaction`] holds an exclusive lock on the database
374 /// until it is committed, rolled back, or dropped. Dropping without
375 /// committing automatically rolls back all changes made in the transaction.
376 ///
377 /// For simple single-operation use, prefer the [`write`](Self::write) and
378 /// [`read`](Self::read) helpers, which handle begin/commit/rollback
379 /// automatically.
380 ///
381 /// # Example
382 /// ```
383 /// # use tempfile::TempDir;
384 /// # use highlandcows_isam::Isam;
385 /// # let dir = TempDir::new().unwrap();
386 /// # let path = dir.path().join("db");
387 /// # let db: Isam<u32, String> = Isam::create(&path).unwrap();
388 /// let mut txn = db.begin_transaction().unwrap();
389 /// // ... perform operations ...
390 /// txn.commit().unwrap();
391 /// ```
392 pub fn begin_transaction(&self) -> IsamResult<Transaction<'_, K, V>> {
393 self.manager.begin()
394 }
395
396 /// Execute a write closure inside a transaction.
397 ///
398 /// Begins a transaction, passes it to `f`, then commits on `Ok` or rolls
399 /// back on `Err`. The return value of `f` is forwarded to the caller.
400 ///
401 /// Use this for inserts, updates, and deletes where you don't need to
402 /// manage the transaction lifetime manually.
403 ///
404 /// # Example
405 /// ```
406 /// # use tempfile::TempDir;
407 /// # use highlandcows_isam::Isam;
408 /// # let dir = TempDir::new().unwrap();
409 /// # let path = dir.path().join("db");
410 /// # let db: Isam<u32, String> = Isam::create(&path).unwrap();
411 /// db.write(|txn| db.insert(txn, 1u32, &"hello".to_string())).unwrap();
412 /// ```
413 pub fn write<F, T>(&self, f: F) -> IsamResult<T>
414 where
415 F: FnOnce(&mut Transaction<'_, K, V>) -> IsamResult<T>,
416 {
417 let mut txn = self.begin_transaction()?;
418 match f(&mut txn) {
419 Ok(val) => { txn.commit()?; Ok(val) }
420 Err(e) => { let _ = txn.rollback(); Err(e) }
421 }
422 }
423
424 /// Execute a read closure inside a transaction.
425 ///
426 /// Begins a transaction, passes it to `f`, then rolls back unconditionally
427 /// (since reads make no changes). The return value of `f` is forwarded to
428 /// the caller.
429 ///
430 /// # Example
431 /// ```
432 /// # use tempfile::TempDir;
433 /// # use highlandcows_isam::Isam;
434 /// # let dir = TempDir::new().unwrap();
435 /// # let path = dir.path().join("db");
436 /// # let db: Isam<u32, String> = Isam::create(&path).unwrap();
437 /// # db.write(|txn| db.insert(txn, 1u32, &"hello".to_string())).unwrap();
438 /// let val = db.read(|txn| db.get(txn, &1u32)).unwrap();
439 /// assert_eq!(val, Some("hello".to_string()));
440 /// ```
441 pub fn read<F, T>(&self, f: F) -> IsamResult<T>
442 where
443 F: FnOnce(&mut Transaction<'_, K, V>) -> IsamResult<T>,
444 {
445 let mut txn = self.begin_transaction()?;
446 let result = f(&mut txn);
447 let _ = txn.rollback();
448 result
449 }
450
451 // ── CRUD ─────────────────────────────────────────────────────────────── //
452
453 /// Insert a new key-value pair.
454 ///
455 /// Returns [`IsamError::DuplicateKey`] if the key already exists.
456 ///
457 /// # Example
458 /// ```
459 /// # use tempfile::TempDir;
460 /// # use highlandcows_isam::Isam;
461 /// # let dir = TempDir::new().unwrap();
462 /// # let path = dir.path().join("db");
463 /// # let db: Isam<u32, String> = Isam::create(&path).unwrap();
464 /// let mut txn = db.begin_transaction().unwrap();
465 /// db.insert(&mut txn, 1u32, &"hello".to_string()).unwrap();
466 /// txn.commit().unwrap();
467 /// ```
468 pub fn insert(&self, txn: &mut Transaction<'_, K, V>, key: K, value: &V) -> IsamResult<()> {
469 {
470 let storage = txn.storage_mut();
471 let rec = storage.store.append(&key, value)?;
472 storage.index.insert(&key, rec)?;
473 for si in &mut storage.secondary_indices {
474 si.on_insert(&key, value)?;
475 }
476 }
477 txn.log_insert(key, value.clone());
478 Ok(())
479 }
480
481 /// Look up a key and return its value, or `None` if the key does not exist.
482 ///
483 /// # Example
484 /// ```
485 /// # use tempfile::TempDir;
486 /// # use highlandcows_isam::Isam;
487 /// # let dir = TempDir::new().unwrap();
488 /// # let path = dir.path().join("db");
489 /// # let db: Isam<u32, String> = Isam::create(&path).unwrap();
490 /// # let mut txn = db.begin_transaction().unwrap();
491 /// # db.insert(&mut txn, 1u32, &"hello".to_string()).unwrap();
492 /// # txn.commit().unwrap();
493 /// let mut txn = db.begin_transaction().unwrap();
494 /// assert_eq!(db.get(&mut txn, &1u32).unwrap(), Some("hello".to_string()));
495 /// assert_eq!(db.get(&mut txn, &99u32).unwrap(), None);
496 /// txn.commit().unwrap();
497 /// ```
498 pub fn get(&self, txn: &mut Transaction<'_, K, V>, key: &K) -> IsamResult<Option<V>> {
499 let storage = txn.storage_mut();
500 match storage.index.search(key)? {
501 None => Ok(None),
502 Some(rec) => Ok(Some(storage.store.read_value(rec)?)),
503 }
504 }
505
506 /// Replace the value for an existing key.
507 ///
508 /// Returns [`IsamError::KeyNotFound`] if the key does not exist.
509 ///
510 /// # Example
511 /// ```
512 /// # use tempfile::TempDir;
513 /// # use highlandcows_isam::Isam;
514 /// # let dir = TempDir::new().unwrap();
515 /// # let path = dir.path().join("db");
516 /// # let db: Isam<u32, String> = Isam::create(&path).unwrap();
517 /// # let mut txn = db.begin_transaction().unwrap();
518 /// # db.insert(&mut txn, 1u32, &"old".to_string()).unwrap();
519 /// # txn.commit().unwrap();
520 /// let mut txn = db.begin_transaction().unwrap();
521 /// db.update(&mut txn, 1u32, &"new".to_string()).unwrap();
522 /// assert_eq!(db.get(&mut txn, &1u32).unwrap(), Some("new".to_string()));
523 /// txn.commit().unwrap();
524 /// ```
525 pub fn update(&self, txn: &mut Transaction<'_, K, V>, key: K, value: &V) -> IsamResult<()> {
526 let (old_rec, old_value) = {
527 let storage = txn.storage_mut();
528 let old_rec = storage.index.search(&key)?.ok_or(IsamError::KeyNotFound)?;
529 let old_value: V = storage.store.read_value(old_rec)?;
530 (old_rec, old_value)
531 };
532 {
533 let storage = txn.storage_mut();
534 let new_rec = storage.store.append(&key, value)?;
535 storage.index.update(&key, new_rec)?;
536 for si in &mut storage.secondary_indices {
537 si.on_update(&key, &old_value, value)?;
538 }
539 }
540 txn.log_update(key, old_rec, old_value, value.clone());
541 Ok(())
542 }
543
544 /// Remove a key and its associated value.
545 ///
546 /// Returns [`IsamError::KeyNotFound`] if the key does not exist.
547 ///
548 /// # Example
549 /// ```
550 /// # use tempfile::TempDir;
551 /// # use highlandcows_isam::Isam;
552 /// # let dir = TempDir::new().unwrap();
553 /// # let path = dir.path().join("db");
554 /// # let db: Isam<u32, String> = Isam::create(&path).unwrap();
555 /// # let mut txn = db.begin_transaction().unwrap();
556 /// # db.insert(&mut txn, 1u32, &"hello".to_string()).unwrap();
557 /// # txn.commit().unwrap();
558 /// let mut txn = db.begin_transaction().unwrap();
559 /// db.delete(&mut txn, &1u32).unwrap();
560 /// assert_eq!(db.get(&mut txn, &1u32).unwrap(), None);
561 /// txn.commit().unwrap();
562 /// ```
563 pub fn delete(&self, txn: &mut Transaction<'_, K, V>, key: &K) -> IsamResult<()> {
564 let (old_rec, old_value) = {
565 let storage = txn.storage_mut();
566 let old_rec = storage.index.search(key)?.ok_or(IsamError::KeyNotFound)?;
567 let old_value: V = storage.store.read_value(old_rec)?;
568 (old_rec, old_value)
569 };
570 {
571 let storage = txn.storage_mut();
572 storage.index.delete(key)?;
573 storage.store.append_tombstone(key)?;
574 for si in &mut storage.secondary_indices {
575 si.on_delete(key, &old_value)?;
576 }
577 }
578 txn.log_delete(key.clone(), old_rec, old_value);
579 Ok(())
580 }
581
582 /// Return the smallest key in the database, or `None` if empty.
583 ///
584 /// # Example
585 /// ```
586 /// # use tempfile::TempDir;
587 /// # use highlandcows_isam::Isam;
588 /// # let dir = TempDir::new().unwrap();
589 /// # let path = dir.path().join("db");
590 /// # let db: Isam<u32, u32> = Isam::create(&path).unwrap();
591 /// # let mut txn = db.begin_transaction().unwrap();
592 /// # for k in [3u32, 1, 2] { db.insert(&mut txn, k, &k).unwrap(); }
593 /// # txn.commit().unwrap();
594 /// let mut txn = db.begin_transaction().unwrap();
595 /// assert_eq!(db.min_key(&mut txn).unwrap(), Some(1u32));
596 /// txn.commit().unwrap();
597 /// ```
598 pub fn min_key(&self, txn: &mut Transaction<'_, K, V>) -> IsamResult<Option<K>> {
599 txn.storage_mut().index.min_key()
600 }
601
602 /// Return the largest key in the database, or `None` if empty.
603 ///
604 /// # Example
605 /// ```
606 /// # use tempfile::TempDir;
607 /// # use highlandcows_isam::Isam;
608 /// # let dir = TempDir::new().unwrap();
609 /// # let path = dir.path().join("db");
610 /// # let db: Isam<u32, u32> = Isam::create(&path).unwrap();
611 /// # let mut txn = db.begin_transaction().unwrap();
612 /// # for k in [3u32, 1, 2] { db.insert(&mut txn, k, &k).unwrap(); }
613 /// # txn.commit().unwrap();
614 /// let mut txn = db.begin_transaction().unwrap();
615 /// assert_eq!(db.max_key(&mut txn).unwrap(), Some(3u32));
616 /// txn.commit().unwrap();
617 /// ```
618 pub fn max_key(&self, txn: &mut Transaction<'_, K, V>) -> IsamResult<Option<K>> {
619 txn.storage_mut().index.max_key()
620 }
621
622 // ── Iterators ────────────────────────────────────────────────────────── //
623
624 /// Return a key-ordered iterator over all records.
625 ///
626 /// The iterator borrows `txn` for its lifetime, so no other operations
627 /// can be performed on the database until the iterator is dropped.
628 ///
629 /// # Example
630 /// ```
631 /// # use tempfile::TempDir;
632 /// # use highlandcows_isam::Isam;
633 /// # let dir = TempDir::new().unwrap();
634 /// # let path = dir.path().join("db");
635 /// # let db: Isam<u32, u32> = Isam::create(&path).unwrap();
636 /// # let mut txn = db.begin_transaction().unwrap();
637 /// # for k in [3u32, 1, 2] { db.insert(&mut txn, k, &k).unwrap(); }
638 /// # txn.commit().unwrap();
639 /// let mut txn = db.begin_transaction().unwrap();
640 /// let keys: Vec<u32> = db.iter(&mut txn).unwrap()
641 /// .map(|r| r.unwrap().0)
642 /// .collect();
643 /// assert_eq!(keys, vec![1, 2, 3]);
644 /// txn.commit().unwrap();
645 /// ```
646 pub fn iter<'txn>(
647 &self,
648 txn: &'txn mut Transaction<'_, K, V>,
649 ) -> IsamResult<IsamIter<'txn, K, V>> {
650 let storage = txn.storage_mut();
651 let first_id = storage.index.first_leaf_id()?;
652 let (entries, next_id) = if first_id != 0 {
653 storage.index.read_leaf(first_id)?
654 } else {
655 (vec![], 0)
656 };
657 Ok(IsamIter {
658 storage: txn.storage_mut(),
659 buffer: entries,
660 buf_pos: 0,
661 next_leaf_id: next_id,
662 })
663 }
664
665 /// Return a key-ordered iterator over records whose keys fall within `range`.
666 ///
667 /// Accepts any of Rust's built-in range expressions: `a..b`, `a..=b`,
668 /// `a..`, `..b`, `..=b`, `..`.
669 ///
670 /// The iterator borrows `txn` for its lifetime, so no other operations
671 /// can be performed on the database until the iterator is dropped.
672 ///
673 /// # Example
674 /// ```
675 /// # use tempfile::TempDir;
676 /// # use highlandcows_isam::Isam;
677 /// # let dir = TempDir::new().unwrap();
678 /// # let path = dir.path().join("db");
679 /// # let db: Isam<u32, u32> = Isam::create(&path).unwrap();
680 /// # let mut txn = db.begin_transaction().unwrap();
681 /// # for k in 1u32..=10 { db.insert(&mut txn, k, &k).unwrap(); }
682 /// # txn.commit().unwrap();
683 /// let mut txn = db.begin_transaction().unwrap();
684 /// let keys: Vec<u32> = db.range(&mut txn, 3u32..=7).unwrap()
685 /// .map(|r| r.unwrap().0)
686 /// .collect();
687 /// assert_eq!(keys, vec![3, 4, 5, 6, 7]);
688 /// txn.commit().unwrap();
689 /// ```
690 pub fn range<'txn, R>(
691 &self,
692 txn: &'txn mut Transaction<'_, K, V>,
693 range: R,
694 ) -> IsamResult<RangeIter<'txn, K, V>>
695 where
696 R: RangeBounds<K>,
697 {
698 let start_bound = clone_bound(range.start_bound());
699 let end_bound = clone_bound(range.end_bound());
700
701 let storage = txn.storage_mut();
702
703 let start_leaf_id = match &start_bound {
704 Bound::Included(k) | Bound::Excluded(k) => storage.index.find_leaf_for_key(k)?,
705 Bound::Unbounded => storage.index.first_leaf_id()?,
706 };
707
708 let (entries, next_leaf_id) = if start_leaf_id != 0 {
709 storage.index.read_leaf(start_leaf_id)?
710 } else {
711 (vec![], 0)
712 };
713
714 let buf_pos = match &start_bound {
715 Bound::Included(k) => entries.partition_point(|(ek, _)| ek < k),
716 Bound::Excluded(k) => entries.partition_point(|(ek, _)| ek <= k),
717 Bound::Unbounded => 0,
718 };
719
720 Ok(RangeIter {
721 storage: txn.storage_mut(),
722 buffer: entries,
723 buf_pos,
724 next_leaf_id,
725 end_bound,
726 })
727 }
728
729 // ── Schema versioning ────────────────────────────────────────────────── //
730
731 /// Return the key schema version stored in the index metadata.
732 ///
733 /// Schema versions are set by [`migrate_keys`](Self::migrate_keys) and
734 /// default to `0` for newly created databases.
735 ///
736 /// # Deadlock warning
737 /// Acquires the database lock internally. Must not be called while a
738 /// [`Transaction`] is live on the same thread.
739 ///
740 /// # Example
741 /// ```
742 /// # use tempfile::TempDir;
743 /// # use highlandcows_isam::Isam;
744 /// # let dir = TempDir::new().unwrap();
745 /// # let path = dir.path().join("db");
746 /// # let db: Isam<u32, String> = Isam::create(&path).unwrap();
747 /// assert_eq!(db.key_schema_version().unwrap(), 0);
748 /// ```
749 pub fn key_schema_version(&self) -> IsamResult<u32> {
750 let guard = self.manager.lock_storage()?;
751 Ok(guard.index.key_schema_version())
752 }
753
754 /// Return the value schema version stored in the index metadata.
755 ///
756 /// Schema versions are set by [`migrate_values`](Self::migrate_values) and
757 /// default to `0` for newly created databases.
758 ///
759 /// # Deadlock warning
760 /// Acquires the database lock internally. Must not be called while a
761 /// [`Transaction`] is live on the same thread.
762 ///
763 /// # Example
764 /// ```
765 /// # use tempfile::TempDir;
766 /// # use highlandcows_isam::Isam;
767 /// # let dir = TempDir::new().unwrap();
768 /// # let path = dir.path().join("db");
769 /// # let db: Isam<u32, String> = Isam::create(&path).unwrap();
770 /// assert_eq!(db.val_schema_version().unwrap(), 0);
771 /// ```
772 pub fn val_schema_version(&self) -> IsamResult<u32> {
773 let guard = self.manager.lock_storage()?;
774 Ok(guard.index.val_schema_version())
775 }
776
777 // ── Secondary index migration ─────────────────────────────────────────── //
778
779 /// Migrate a secondary index to a new schema version.
780 ///
781 /// This is the secondary index counterpart to [`migrate_values`](Self::migrate_values)
782 /// and [`migrate_keys`](Self::migrate_keys). Use it when the [`DeriveKey`]
783 /// derivation logic for a named secondary index has changed and the on-disk
784 /// index needs to be rebuilt to match.
785 ///
786 /// The named secondary index is cleared and repopulated by scanning all
787 /// primary records. For each record, `f` is applied to the stored value
788 /// before the registered [`DeriveKey`] extractor derives the secondary key,
789 /// letting you adapt the effective input to the updated derivation logic.
790 /// Pass the identity closure (`|v| Ok(v)`) for a plain rebuild with no
791 /// value transformation.
792 ///
793 /// After the rebuild, `new_version` is written into the `.sidx` metadata
794 /// so that [`Isam::secondary_indices`] reflects the current migration state
795 /// via [`IndexInfo::schema_version`].
796 ///
797 /// **Primary records are not modified.** Only the named secondary index
798 /// is affected; other secondary indices are left untouched.
799 ///
800 /// # Deadlock warning
801 /// Acquires the database lock internally. Must not be called while a
802 /// [`Transaction`] is live on the same thread — commit or roll back all
803 /// open transactions before calling [`as_single_user`](Self::as_single_user).
804 ///
805 /// # Example
806 /// ```
807 /// # use tempfile::TempDir;
808 /// use serde::{Serialize, Deserialize};
809 /// use highlandcows_isam::{Isam, DeriveKey, DEFAULT_SINGLE_USER_TIMEOUT};
810 ///
811 /// #[derive(Serialize, Deserialize, Clone)]
812 /// struct User { name: String, city: String }
813 ///
814 /// struct CityIndex;
815 /// impl DeriveKey<User> for CityIndex {
816 /// type Key = String;
817 /// // derive now normalizes to lowercase
818 /// fn derive(u: &User) -> String { u.city.to_lowercase() }
819 /// }
820 ///
821 /// # let dir = TempDir::new().unwrap();
822 /// # let path = dir.path().join("users");
823 /// let db = Isam::<u64, User>::builder()
824 /// .with_index("city", CityIndex)
825 /// .create(&path)
826 /// .unwrap();
827 ///
828 /// db.write(|txn| db.insert(txn, 1, &User { name: "Alice".into(), city: "London".into() }))
829 /// .unwrap();
830 ///
831 /// // Rebuild the "city" index, normalizing city names to lowercase
832 /// // so the index matches the updated DeriveKey logic.
833 /// let db = db.as_single_user(DEFAULT_SINGLE_USER_TIMEOUT, |token, db| {
834 /// db.migrate_index("city", 1, |mut u: User| {
835 /// u.city = u.city.to_lowercase();
836 /// Ok(u)
837 /// }, token)?;
838 /// Ok(db)
839 /// }).unwrap();
840 ///
841 /// let info = db.secondary_indices().unwrap();
842 /// assert_eq!(info[0].schema_version, 1);
843 /// ```
844 pub fn migrate_index<F>(&self, name: &str, new_version: u32, mut f: F, _token: &SingleUserToken) -> IsamResult<()>
845 where
846 F: FnMut(V) -> IsamResult<V>,
847 {
848 let mut storage = self.manager.lock_storage()?;
849
850 // Scan all primary records first, before mutating the index.
851 let mut records: Vec<(K, V)> = Vec::new();
852 let first_id = storage.index.first_leaf_id()?;
853 let mut current_id = first_id;
854 while current_id != 0 {
855 let (entries, next_id) = storage.index.read_leaf(current_id)?;
856 for (key, rec) in &entries {
857 let value: V = storage.store.read_value(*rec)?;
858 records.push((key.clone(), value));
859 }
860 current_id = next_id;
861 }
862
863 // Find the target index and reset it.
864 let si = storage
865 .secondary_indices
866 .iter_mut()
867 .find(|si| si.name() == name)
868 .ok_or_else(|| IsamError::IndexNotFound(name.to_owned()))?;
869 si.reset()?;
870
871 // Repopulate the index using f(value) as input to DeriveKey::derive.
872 for (key, value) in records {
873 let effective = f(value)?;
874 let si = storage
875 .secondary_indices
876 .iter_mut()
877 .find(|si| si.name() == name)
878 .unwrap();
879 si.on_insert(&key, &effective)?;
880 }
881
882 // Persist the new schema version.
883 let si = storage
884 .secondary_indices
885 .iter_mut()
886 .find(|si| si.name() == name)
887 .unwrap();
888 si.persist_schema_version(new_version)?;
889 si.fsync()?;
890
891 Ok(())
892 }
893
894 // ── Structural operations ─────────────────────────────────────────────── //
895
896 /// Compact the database, removing tombstones and stale values.
897 ///
898 /// Rewrites the data and index files atomically via temp-file rename,
899 /// then re-opens them in place.
900 ///
901 /// # Deadlock warning
902 /// Acquires the database lock internally. Must not be called while a
903 /// [`Transaction`] is live on the same thread — commit or roll back all
904 /// open transactions before calling [`as_single_user`](Self::as_single_user).
905 ///
906 /// # Example
907 /// ```
908 /// # use tempfile::TempDir;
909 /// # use highlandcows_isam::{Isam, DEFAULT_SINGLE_USER_TIMEOUT};
910 /// # let dir = TempDir::new().unwrap();
911 /// # let path = dir.path().join("db");
912 /// # let db: Isam<u32, String> = Isam::create(&path).unwrap();
913 /// # let mut txn = db.begin_transaction().unwrap();
914 /// # for i in 0u32..5 { db.insert(&mut txn, i, &i.to_string()).unwrap(); }
915 /// # for i in 0u32..3 { db.delete(&mut txn, &i).unwrap(); }
916 /// # txn.commit().unwrap();
917 /// db.as_single_user(DEFAULT_SINGLE_USER_TIMEOUT, |token, db| db.compact(token)).unwrap();
918 /// ```
919 pub fn compact(&self, _token: &SingleUserToken) -> IsamResult<()> {
920 let mut storage = self.manager.lock_storage()?;
921
922 let mut records: Vec<(Vec<u8>, Vec<u8>)> = Vec::new();
923 let first_id = storage.index.first_leaf_id()?;
924 let mut current_id = first_id;
925 while current_id != 0 {
926 let (entries, next_id) = storage.index.read_leaf(current_id)?;
927 for (_, rec) in &entries {
928 let (_status, key_bytes, val_bytes) = storage.store.read_record_raw(rec.offset)?;
929 records.push((key_bytes, val_bytes));
930 }
931 current_id = next_id;
932 }
933
934 let tmp_idb = storage.base_path.with_extension("idb.tmp");
935 let tmp_idx = storage.base_path.with_extension("idx.tmp");
936
937 let mut new_store = crate::store::DataStore::create(&tmp_idb)?;
938 let mut new_index: crate::index::BTree<K> = crate::index::BTree::create(&tmp_idx)?;
939
940 for (key_bytes, val_bytes) in &records {
941 let rec = new_store.write_raw_record(crate::store::STATUS_ALIVE, key_bytes, val_bytes)?;
942 let key: K = bincode::deserialize(key_bytes)?;
943 new_index.insert(&key, rec)?;
944 }
945
946 new_store.flush()?;
947 new_index.flush()?;
948 drop(new_store);
949 drop(new_index);
950
951 let base = storage.base_path.clone();
952 std::fs::rename(&tmp_idb, idb_path(&base))?;
953 std::fs::rename(&tmp_idx, idx_path(&base))?;
954
955 storage.store = crate::store::DataStore::open(&idb_path(&base))?;
956 storage.index = crate::index::BTree::open(&idx_path(&base))?;
957
958 Ok(())
959 }
960
961 /// Rewrite every value through `f`, bump the val schema version, and
962 /// return a ready-to-use `Isam<K, V2>`. Consumes `self`.
963 ///
964 /// Records are rewritten to new temp files and atomically renamed into
965 /// place. The key schema version is preserved.
966 ///
967 /// # Deadlock warning
968 /// Acquires the database lock internally. Must not be called while a
969 /// [`Transaction`] is live on the same thread — commit or roll back all
970 /// open transactions before calling [`as_single_user`](Self::as_single_user).
971 ///
972 /// # Example
973 /// ```
974 /// # use tempfile::TempDir;
975 /// # use highlandcows_isam::{Isam, DEFAULT_SINGLE_USER_TIMEOUT};
976 /// # let dir = TempDir::new().unwrap();
977 /// # let path = dir.path().join("db");
978 /// let db: Isam<u32, String> = Isam::create(&path).unwrap();
979 /// # let mut txn = db.begin_transaction().unwrap();
980 /// # db.insert(&mut txn, 1u32, &"42".to_string()).unwrap();
981 /// # txn.commit().unwrap();
982 /// // Migrate String values → u64, setting val schema version to 1.
983 /// let db2: Isam<u32, u64> = db
984 /// .as_single_user(DEFAULT_SINGLE_USER_TIMEOUT, |token, db| {
985 /// db.migrate_values(1, |s: String| Ok(s.parse::<u64>().unwrap()), token)
986 /// })
987 /// .unwrap();
988 /// assert_eq!(db2.val_schema_version().unwrap(), 1);
989 /// ```
990 pub fn migrate_values<V2, F>(self, new_val_version: u32, mut f: F, _token: &SingleUserToken) -> IsamResult<Isam<K, V2>>
991 where
992 V2: Serialize + DeserializeOwned + Clone + 'static,
993 F: FnMut(V) -> IsamResult<V2>,
994 {
995 let mut storage = self.manager.lock_storage()?;
996
997 let base_path = storage.base_path.clone();
998 let key_schema_v = storage.index.key_schema_version();
999
1000 let mut records: Vec<(Vec<u8>, V)> = Vec::new();
1001 let first_id = storage.index.first_leaf_id()?;
1002 let mut current_id = first_id;
1003 while current_id != 0 {
1004 let (entries, next_id) = storage.index.read_leaf(current_id)?;
1005 for (_, rec) in &entries {
1006 let (_status, key_bytes, val_bytes) = storage.store.read_record_raw(rec.offset)?;
1007 let v: V = bincode::deserialize(&val_bytes)?;
1008 records.push((key_bytes, v));
1009 }
1010 current_id = next_id;
1011 }
1012
1013 let mut transformed: Vec<(Vec<u8>, V2)> = Vec::with_capacity(records.len());
1014 for (key_bytes, v) in records {
1015 transformed.push((key_bytes, f(v)?));
1016 }
1017
1018 let tmp_idb = base_path.with_extension("idb.tmp");
1019 let tmp_idx = base_path.with_extension("idx.tmp");
1020
1021 let mut new_store = crate::store::DataStore::create(&tmp_idb)?;
1022 let mut new_index: crate::index::BTree<K> = crate::index::BTree::create(&tmp_idx)?;
1023 new_index.set_schema_versions(key_schema_v, new_val_version)?;
1024
1025 for (key_bytes, v2) in &transformed {
1026 let val_bytes = bincode::serialize(v2)?;
1027 let rec = new_store.write_raw_record(crate::store::STATUS_ALIVE, key_bytes, &val_bytes)?;
1028 let key: K = bincode::deserialize(key_bytes)?;
1029 new_index.insert(&key, rec)?;
1030 }
1031
1032 new_store.flush()?;
1033 new_index.flush()?;
1034 drop(new_store);
1035 drop(new_index);
1036 drop(storage);
1037
1038 std::fs::rename(&tmp_idb, idb_path(&base_path))?;
1039 std::fs::rename(&tmp_idx, idx_path(&base_path))?;
1040
1041 Isam::<K, V2>::open(&base_path)
1042 }
1043
1044 /// Rewrite every key through `f`, bump the key schema version, re-sort by
1045 /// `K2::Ord`, rebuild the index, and return a ready-to-use `Isam<K2, V>`.
1046 /// Consumes `self`.
1047 ///
1048 /// Records are rewritten to new temp files and atomically renamed into
1049 /// place. The value schema version is preserved.
1050 ///
1051 /// # Deadlock warning
1052 /// Acquires the database lock internally. Must not be called while a
1053 /// [`Transaction`] is live on the same thread — commit or roll back all
1054 /// open transactions before calling [`as_single_user`](Self::as_single_user).
1055 ///
1056 /// # Example
1057 /// ```
1058 /// # use tempfile::TempDir;
1059 /// # use highlandcows_isam::{Isam, DEFAULT_SINGLE_USER_TIMEOUT};
1060 /// # let dir = TempDir::new().unwrap();
1061 /// # let path = dir.path().join("db");
1062 /// let db: Isam<u32, String> = Isam::create(&path).unwrap();
1063 /// # let mut txn = db.begin_transaction().unwrap();
1064 /// # db.insert(&mut txn, 1u32, &"one".to_string()).unwrap();
1065 /// # txn.commit().unwrap();
1066 /// // Migrate u32 keys → String, setting key schema version to 1.
1067 /// let db2: Isam<String, String> = db
1068 /// .as_single_user(DEFAULT_SINGLE_USER_TIMEOUT, |token, db| {
1069 /// db.migrate_keys(1, |k: u32| Ok(format!("{k}")), token)
1070 /// })
1071 /// .unwrap();
1072 /// assert_eq!(db2.key_schema_version().unwrap(), 1);
1073 /// ```
1074 pub fn migrate_keys<K2, F>(self, new_key_version: u32, mut f: F, _token: &SingleUserToken) -> IsamResult<Isam<K2, V>>
1075 where
1076 K2: Serialize + DeserializeOwned + Ord + Clone + 'static,
1077 F: FnMut(K) -> IsamResult<K2>,
1078 {
1079 let mut storage = self.manager.lock_storage()?;
1080
1081 let base_path = storage.base_path.clone();
1082 let val_schema_v = storage.index.val_schema_version();
1083
1084 let mut records: Vec<(K2, Vec<u8>)> = Vec::new();
1085 let first_id = storage.index.first_leaf_id()?;
1086 let mut current_id = first_id;
1087 while current_id != 0 {
1088 let (entries, next_id) = storage.index.read_leaf(current_id)?;
1089 for (k, rec) in &entries {
1090 let (_status, _key_bytes, val_bytes) = storage.store.read_record_raw(rec.offset)?;
1091 let k2 = f(k.clone())?;
1092 records.push((k2, val_bytes));
1093 }
1094 current_id = next_id;
1095 }
1096
1097 records.sort_by(|(a, _), (b, _)| a.cmp(b));
1098
1099 let tmp_idb = base_path.with_extension("idb.tmp");
1100 let tmp_idx = base_path.with_extension("idx.tmp");
1101
1102 let mut new_store = crate::store::DataStore::create(&tmp_idb)?;
1103 let mut new_index: crate::index::BTree<K2> = crate::index::BTree::create(&tmp_idx)?;
1104 new_index.set_schema_versions(new_key_version, val_schema_v)?;
1105
1106 for (k2, val_bytes) in &records {
1107 let key_bytes = bincode::serialize(k2)?;
1108 let rec = new_store.write_raw_record(crate::store::STATUS_ALIVE, &key_bytes, val_bytes)?;
1109 new_index.insert(k2, rec)?;
1110 }
1111
1112 new_store.flush()?;
1113 new_index.flush()?;
1114 drop(new_store);
1115 drop(new_index);
1116 drop(storage);
1117
1118 std::fs::rename(&tmp_idb, idb_path(&base_path))?;
1119 std::fs::rename(&tmp_idx, idx_path(&base_path))?;
1120
1121 Isam::<K2, V>::open(&base_path)
1122 }
1123}
1124
1125// ── IsamIter ──────────────────────────────────────────────────────────────── //
1126
1127/// Key-order iterator over all alive records.
1128///
1129/// Created by [`Isam::iter`]. Borrows the [`Transaction`] for its lifetime,
1130/// preventing other operations until the iterator is dropped.
1131pub struct IsamIter<'txn, K, V> {
1132 storage: &'txn mut IsamStorage<K, V>,
1133 buffer: Vec<(K, RecordRef)>,
1134 buf_pos: usize,
1135 next_leaf_id: u32,
1136}
1137
1138impl<'txn, K, V> Iterator for IsamIter<'txn, K, V>
1139where
1140 K: Serialize + DeserializeOwned + Ord + Clone,
1141 V: Serialize + DeserializeOwned,
1142{
1143 type Item = IsamResult<(K, V)>;
1144
1145 fn next(&mut self) -> Option<Self::Item> {
1146 loop {
1147 if self.buf_pos < self.buffer.len() {
1148 let (key, rec) = self.buffer[self.buf_pos].clone();
1149 self.buf_pos += 1;
1150 return Some(self.storage.store.read_value(rec).map(|value| (key, value)));
1151 }
1152
1153 if self.next_leaf_id == 0 {
1154 return None;
1155 }
1156
1157 match self.storage.index.read_leaf(self.next_leaf_id) {
1158 Ok((entries, next_id)) => {
1159 self.buffer = entries;
1160 self.buf_pos = 0;
1161 self.next_leaf_id = next_id;
1162 }
1163 Err(e) => return Some(Err(e)),
1164 }
1165 }
1166 }
1167}
1168
1169// ── RangeIter ────────────────────────────────────────────────────────────── //
1170
1171/// Key-order iterator over records whose key falls within a given range.
1172///
1173/// Created by [`Isam::range`]. Borrows the [`Transaction`] for its lifetime,
1174/// preventing other operations until the iterator is dropped.
1175pub struct RangeIter<'txn, K, V> {
1176 storage: &'txn mut IsamStorage<K, V>,
1177 buffer: Vec<(K, RecordRef)>,
1178 buf_pos: usize,
1179 next_leaf_id: u32,
1180 end_bound: Bound<K>,
1181}
1182
1183impl<'txn, K, V> Iterator for RangeIter<'txn, K, V>
1184where
1185 K: Serialize + DeserializeOwned + Ord + Clone,
1186 V: Serialize + DeserializeOwned,
1187{
1188 type Item = IsamResult<(K, V)>;
1189
1190 fn next(&mut self) -> Option<Self::Item> {
1191 loop {
1192 if self.buf_pos < self.buffer.len() {
1193 let (key, rec) = self.buffer[self.buf_pos].clone();
1194 self.buf_pos += 1;
1195
1196 let within = match &self.end_bound {
1197 Bound::Included(end) => &key <= end,
1198 Bound::Excluded(end) => &key < end,
1199 Bound::Unbounded => true,
1200 };
1201 if !within {
1202 return None;
1203 }
1204
1205 return Some(self.storage.store.read_value(rec).map(|value| (key, value)));
1206 }
1207
1208 if self.next_leaf_id == 0 {
1209 return None;
1210 }
1211
1212 match self.storage.index.read_leaf(self.next_leaf_id) {
1213 Ok((entries, next_id)) => {
1214 self.buffer = entries;
1215 self.buf_pos = 0;
1216 self.next_leaf_id = next_id;
1217 }
1218 Err(e) => return Some(Err(e)),
1219 }
1220 }
1221 }
1222}
1223
1224// ── SecondaryIndexHandle ──────────────────────────────────────────────────── //
1225
1226/// An opaque handle to a secondary index, used for point lookups.
1227///
1228/// Obtained from [`Isam::index`].
1229pub struct SecondaryIndexHandle<K, V, SK> {
1230 name: String,
1231 _phantom: PhantomData<fn() -> (K, V, SK)>,
1232}
1233
1234impl<K, V, SK> SecondaryIndexHandle<K, V, SK>
1235where
1236 K: Serialize + DeserializeOwned + Ord + Clone,
1237 V: Serialize + DeserializeOwned,
1238 SK: Serialize + DeserializeOwned + Ord + Clone,
1239{
1240 /// Return all `(primary_key, value)` pairs whose secondary key equals `sk`.
1241 ///
1242 /// Results are returned in insertion order (not key order). For a
1243 /// non-existent secondary key the result is an empty `Vec`.
1244 ///
1245 /// # Example
1246 /// ```
1247 /// # use tempfile::TempDir;
1248 /// use serde::{Serialize, Deserialize};
1249 /// use highlandcows_isam::{Isam, DeriveKey};
1250 ///
1251 /// #[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
1252 /// struct User { name: String, city: String }
1253 ///
1254 /// struct CityIndex;
1255 /// impl DeriveKey<User> for CityIndex {
1256 /// type Key = String;
1257 /// fn derive(u: &User) -> String { u.city.clone() }
1258 /// }
1259 ///
1260 /// # let dir = TempDir::new().unwrap();
1261 /// # let path = dir.path().join("db");
1262 /// let db = Isam::<u64, User>::builder()
1263 /// .with_index("city", CityIndex)
1264 /// .create(&path)
1265 /// .unwrap();
1266 /// let city_idx = db.index::<CityIndex>("city");
1267 ///
1268 /// let mut txn = db.begin_transaction().unwrap();
1269 /// db.insert(&mut txn, 1, &User { name: "Alice".into(), city: "London".into() }).unwrap();
1270 /// db.insert(&mut txn, 2, &User { name: "Bob".into(), city: "London".into() }).unwrap();
1271 /// db.insert(&mut txn, 3, &User { name: "Carol".into(), city: "Paris".into() }).unwrap();
1272 /// txn.commit().unwrap();
1273 ///
1274 /// let mut txn = db.begin_transaction().unwrap();
1275 /// let mut londoners = city_idx.lookup(&mut txn, &"London".to_string()).unwrap();
1276 /// londoners.sort_by_key(|(pk, _)| *pk);
1277 /// assert_eq!(londoners[0].0, 1);
1278 /// assert_eq!(londoners[1].0, 2);
1279 /// assert_eq!(city_idx.lookup(&mut txn, &"Berlin".to_string()).unwrap(), vec![]);
1280 /// txn.commit().unwrap();
1281 /// ```
1282 pub fn lookup(
1283 &self,
1284 txn: &mut Transaction<'_, K, V>,
1285 sk: &SK,
1286 ) -> IsamResult<Vec<(K, V)>> {
1287 let sk_bytes = bincode::serialize(sk)?;
1288
1289 // Step 1: look up primary keys in the secondary index.
1290 let pks: Vec<K> = {
1291 let storage = txn.storage_mut();
1292 match storage.secondary_indices.iter_mut().find(|si| si.name() == self.name) {
1293 None => Vec::new(),
1294 Some(si) => si.lookup_primary_keys(&sk_bytes)?,
1295 }
1296 };
1297
1298 // Step 2: fetch each primary record.
1299 let storage = txn.storage_mut();
1300 let mut results = Vec::with_capacity(pks.len());
1301 for pk in pks {
1302 if let Some(rec) = storage.index.search(&pk)? {
1303 let value = storage.store.read_value(rec)?;
1304 results.push((pk, value));
1305 }
1306 }
1307 Ok(results)
1308 }
1309
1310 /// Return all `(secondary_key, records)` pairs whose secondary key falls
1311 /// within `range`, in secondary-key order.
1312 ///
1313 /// `range` accepts any of Rust's built-in range expressions: `a..b`,
1314 /// `a..=b`, `a..`, `..b`, `..=b`, `..`.
1315 ///
1316 /// Each item is `(SK, Vec<(K, V)>)` — one entry per distinct secondary key,
1317 /// with all primary records that produced that key grouped together.
1318 ///
1319 /// # Example
1320 /// ```
1321 /// # use tempfile::TempDir;
1322 /// use serde::{Serialize, Deserialize};
1323 /// use highlandcows_isam::{Isam, DeriveKey};
1324 ///
1325 /// #[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
1326 /// struct User { name: String, city: String }
1327 ///
1328 /// struct CityIndex;
1329 /// impl DeriveKey<User> for CityIndex {
1330 /// type Key = String;
1331 /// fn derive(u: &User) -> String { u.city.clone() }
1332 /// }
1333 ///
1334 /// # let dir = TempDir::new().unwrap();
1335 /// # let path = dir.path().join("users");
1336 /// let db = Isam::<u64, User>::builder()
1337 /// .with_index("city", CityIndex)
1338 /// .create(&path)
1339 /// .unwrap();
1340 /// let city_idx = db.index::<CityIndex>("city");
1341 ///
1342 /// db.write(|txn| {
1343 /// db.insert(txn, 1, &User { name: "Alice".into(), city: "London".into() })?;
1344 /// db.insert(txn, 2, &User { name: "Bob".into(), city: "London".into() })?;
1345 /// db.insert(txn, 3, &User { name: "Carol".into(), city: "Paris".into() })
1346 /// }).unwrap();
1347 ///
1348 /// // All cities from "L" onward.
1349 /// let mut txn = db.begin_transaction().unwrap();
1350 /// let results = city_idx.range(&mut txn, "London".to_string()..).unwrap();
1351 /// txn.commit().unwrap();
1352 /// assert_eq!(results.len(), 2); // London and Paris
1353 /// assert_eq!(results[0].0, "London");
1354 /// assert_eq!(results[1].0, "Paris");
1355 /// ```
1356 pub fn range<R>(
1357 &self,
1358 txn: &mut Transaction<'_, K, V>,
1359 range: R,
1360 ) -> IsamResult<Vec<(SK, Vec<(K, V)>)>>
1361 where
1362 R: RangeBounds<SK>,
1363 {
1364 let start_bound: Bound<Vec<u8>> = match range.start_bound() {
1365 Bound::Included(sk) => Bound::Included(bincode::serialize(sk)?),
1366 Bound::Excluded(sk) => Bound::Excluded(bincode::serialize(sk)?),
1367 Bound::Unbounded => Bound::Unbounded,
1368 };
1369 let end_bound: Bound<Vec<u8>> = match range.end_bound() {
1370 Bound::Included(sk) => Bound::Included(bincode::serialize(sk)?),
1371 Bound::Excluded(sk) => Bound::Excluded(bincode::serialize(sk)?),
1372 Bound::Unbounded => Bound::Unbounded,
1373 };
1374
1375 let sk_pks: Vec<(Vec<u8>, Vec<K>)> = {
1376 let storage = txn.storage_mut();
1377 match storage.secondary_indices.iter_mut().find(|si| si.name() == self.name) {
1378 None => Vec::new(),
1379 Some(si) => si.range_primary_keys(
1380 as_bound_bytes(&start_bound),
1381 as_bound_bytes(&end_bound),
1382 )?,
1383 }
1384 };
1385
1386 let storage = txn.storage_mut();
1387 let mut results = Vec::with_capacity(sk_pks.len());
1388 for (sk_bytes, pks) in sk_pks {
1389 let sk: SK = bincode::deserialize(&sk_bytes)?;
1390 let mut records = Vec::with_capacity(pks.len());
1391 for pk in pks {
1392 if let Some(rec) = storage.index.search(&pk)? {
1393 let value = storage.store.read_value(rec)?;
1394 records.push((pk, value));
1395 }
1396 }
1397 results.push((sk, records));
1398 }
1399
1400 Ok(results)
1401 }
1402}
1403
1404// ── IndexInfo ─────────────────────────────────────────────────────────────── //
1405
1406/// Metadata about a registered secondary index.
1407///
1408/// Returned by [`Isam::secondary_indices`].
1409#[derive(Debug, Clone)]
1410pub struct IndexInfo {
1411 /// The name the index was registered under.
1412 pub name: String,
1413 /// Fully-qualified type name of the [`DeriveKey`] extractor implementation.
1414 ///
1415 /// Provided by [`std::any::type_name`] — suitable for display and logging,
1416 /// but not for persistent storage (the value may change across compiler
1417 /// versions or if the type is renamed or moved).
1418 pub extractor_type: &'static str,
1419 /// The index schema version stored in the `.sidx` metadata.
1420 ///
1421 /// Set to `0` for newly created indices and updated by
1422 /// [`Isam::migrate_index`]. Use this to confirm that a migration has
1423 /// been applied, or to detect indices that predate schema versioning.
1424 pub schema_version: u32,
1425}
1426
1427// ── IsamBuilder ───────────────────────────────────────────────────────────── //
1428
1429/// Builder for creating or opening an [`Isam`] database with secondary indices.
1430///
1431/// Obtain a builder via [`Isam::builder`]. Call [`with_index`](Self::with_index)
1432/// for each secondary index, then [`create`](Self::create) or [`open`](Self::open).
1433pub struct IsamBuilder<K, V> {
1434 factories: Vec<(String, Box<dyn FnOnce(&Path) -> IsamResult<Box<dyn AnySecondaryIndex<K, V>>>>)>,
1435 rebuild: HashSet<String>,
1436 _phantom: PhantomData<(K, V)>,
1437}
1438
1439impl<K, V> Default for IsamBuilder<K, V> {
1440 fn default() -> Self {
1441 Self {
1442 factories: Vec::new(),
1443 rebuild: HashSet::new(),
1444 _phantom: PhantomData,
1445 }
1446 }
1447}
1448
1449impl<K, V> IsamBuilder<K, V>
1450where
1451 K: Serialize + DeserializeOwned + Ord + Clone + Send + 'static,
1452 V: Serialize + DeserializeOwned + Clone + Send + 'static,
1453{
1454 /// Register a secondary index to be opened or created alongside the database.
1455 ///
1456 /// `name` must be unique within a database. The extractor value is used only
1457 /// to infer the `DeriveKey` implementation — it is not stored.
1458 ///
1459 /// After construction, obtain a typed handle for querying via [`Isam::index`].
1460 pub fn with_index<E>(mut self, name: &str, _extractor: E) -> Self
1461 where
1462 E: DeriveKey<V>,
1463 {
1464 let owned = name.to_owned();
1465 let owned2 = owned.clone();
1466 self.factories.push((owned, Box::new(move |base: &Path| {
1467 let si = SecondaryIndexImpl::<K, V, E>::create_or_open(&owned2, base)?;
1468 Ok(Box::new(si) as Box<dyn AnySecondaryIndex<K, V>>)
1469 })));
1470 self
1471 }
1472
1473 /// Mark a secondary index to be fully rebuilt from primary data during [`open`](Self::open).
1474 ///
1475 /// The existing `.sidb`/`.sidx` files for `name` are deleted at open time
1476 /// and repopulated by scanning all primary records. The index must also be
1477 /// registered via [`with_index`](Self::with_index).
1478 ///
1479 /// # When to use
1480 ///
1481 /// Call this whenever the [`DeriveKey`] extractor logic has changed and the
1482 /// on-disk index is therefore stale. Without a rebuild, queries against a
1483 /// stale index will silently return incorrect results.
1484 ///
1485 /// # Example
1486 /// ```
1487 /// # use tempfile::TempDir;
1488 /// use serde::{Serialize, Deserialize};
1489 /// use highlandcows_isam::{Isam, DeriveKey};
1490 ///
1491 /// #[derive(Serialize, Deserialize, Clone)]
1492 /// struct User { name: String, city: String }
1493 ///
1494 /// struct CityIndex;
1495 /// impl DeriveKey<User> for CityIndex {
1496 /// type Key = String;
1497 /// fn derive(u: &User) -> String { u.city.clone() }
1498 /// }
1499 ///
1500 /// # let dir = TempDir::new().unwrap();
1501 /// # let path = dir.path().join("db");
1502 /// # Isam::<u64, User>::builder().with_index("city", CityIndex).create(&path).unwrap();
1503 /// // Reopen and force a full rebuild of the "city" index.
1504 /// let db = Isam::<u64, User>::builder()
1505 /// .with_index("city", CityIndex)
1506 /// .rebuild_index("city")
1507 /// .open(&path)
1508 /// .unwrap();
1509 /// ```
1510 pub fn rebuild_index(mut self, name: &str) -> Self {
1511 self.rebuild.insert(name.to_owned());
1512 self
1513 }
1514
1515 /// Create a new, empty database at `path` with the registered indices.
1516 ///
1517 /// # Example
1518 /// ```
1519 /// # use tempfile::TempDir;
1520 /// use serde::{Serialize, Deserialize};
1521 /// use highlandcows_isam::{Isam, DeriveKey};
1522 ///
1523 /// #[derive(Serialize, Deserialize, Clone)]
1524 /// struct User { name: String, city: String }
1525 ///
1526 /// struct CityIndex;
1527 /// impl DeriveKey<User> for CityIndex {
1528 /// type Key = String;
1529 /// fn derive(u: &User) -> String { u.city.clone() }
1530 /// }
1531 ///
1532 /// # let dir = TempDir::new().unwrap();
1533 /// # let path = dir.path().join("db");
1534 /// let db = Isam::<u64, User>::builder()
1535 /// .with_index("city", CityIndex)
1536 /// .create(&path)
1537 /// .unwrap();
1538 /// ```
1539 pub fn create(self, path: impl AsRef<Path>) -> IsamResult<Isam<K, V>> {
1540 let path = path.as_ref();
1541 let mut storage = IsamStorage::create(path)?;
1542 for (_name, factory) in self.factories {
1543 storage.secondary_indices.push(factory(path)?);
1544 }
1545 Ok(Isam {
1546 manager: TransactionManager::from_storage(storage),
1547 })
1548 }
1549
1550 /// Open an existing database at `path` with the registered indices.
1551 ///
1552 /// Secondary indices must be registered again on every open — the index
1553 /// name links the handle to the files on disk, but the extractor type is
1554 /// not persisted.
1555 ///
1556 /// # Example
1557 /// ```
1558 /// # use tempfile::TempDir;
1559 /// use serde::{Serialize, Deserialize};
1560 /// use highlandcows_isam::{Isam, DeriveKey};
1561 ///
1562 /// #[derive(Serialize, Deserialize, Clone)]
1563 /// struct User { name: String, city: String }
1564 ///
1565 /// struct CityIndex;
1566 /// impl DeriveKey<User> for CityIndex {
1567 /// type Key = String;
1568 /// fn derive(u: &User) -> String { u.city.clone() }
1569 /// }
1570 ///
1571 /// # let dir = TempDir::new().unwrap();
1572 /// # let path = dir.path().join("db");
1573 /// # Isam::<u64, User>::builder().with_index("city", CityIndex).create(&path).unwrap();
1574 /// let db = Isam::<u64, User>::builder()
1575 /// .with_index("city", CityIndex)
1576 /// .open(&path)
1577 /// .unwrap();
1578 /// let city_idx = db.index::<CityIndex>("city");
1579 /// ```
1580 pub fn open(self, path: impl AsRef<Path>) -> IsamResult<Isam<K, V>> {
1581 use crate::secondary_index::{sidb_path, sidx_path};
1582
1583 let path = path.as_ref();
1584 let mut storage = IsamStorage::open(path)?;
1585
1586 // Delete stale files for any indices marked for rebuild so the
1587 // factories below recreate them fresh.
1588 for name in &self.rebuild {
1589 let sidb = sidb_path(path, name);
1590 let sidx = sidx_path(path, name);
1591 if sidb.exists() { std::fs::remove_file(&sidb)?; }
1592 if sidx.exists() { std::fs::remove_file(&sidx)?; }
1593 }
1594
1595 for (_name, factory) in self.factories {
1596 storage.secondary_indices.push(factory(path)?);
1597 }
1598
1599 // Populate rebuilt indices by scanning all primary records.
1600 if !self.rebuild.is_empty() {
1601 let first_id = storage.index.first_leaf_id()?;
1602 let mut current_id = first_id;
1603 while current_id != 0 {
1604 let (entries, next_id) = storage.index.read_leaf(current_id)?;
1605 for (key, rec) in &entries {
1606 let value: V = storage.store.read_value(*rec)?;
1607 for si in &mut storage.secondary_indices {
1608 if self.rebuild.contains(si.name()) {
1609 si.on_insert(key, &value)?;
1610 }
1611 }
1612 }
1613 current_id = next_id;
1614 }
1615 for si in &mut storage.secondary_indices {
1616 if self.rebuild.contains(si.name()) {
1617 si.fsync()?;
1618 }
1619 }
1620 }
1621
1622 Ok(Isam {
1623 manager: TransactionManager::from_storage(storage),
1624 })
1625 }
1626}