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/// 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}