Skip to main content

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