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::marker::PhantomData;
11use std::ops::{Bound, RangeBounds};
12use std::path::{Path, PathBuf};
13
14use serde::de::DeserializeOwned;
15use serde::Serialize;
16
17use crate::error::{IsamError, IsamResult};
18use crate::manager::TransactionManager;
19use crate::secondary_index::{DeriveKey, SecondaryIndexImpl};
20use crate::storage::IsamStorage;
21use crate::store::RecordRef;
22use crate::transaction::Transaction;
23
24// ── Path helpers (pub(crate) so storage.rs can use them) ─────────────────── //
25
26pub(crate) fn idb_path(base: &Path) -> PathBuf {
27    base.with_extension("idb")
28}
29
30pub(crate) fn idx_path(base: &Path) -> PathBuf {
31    base.with_extension("idx")
32}
33
34/// Convert a borrowed `Bound<&K>` into an owned `Bound<K>` by cloning.
35fn clone_bound<K: Clone>(b: Bound<&K>) -> Bound<K> {
36    match b {
37        Bound::Included(k) => Bound::Included(k.clone()),
38        Bound::Excluded(k) => Bound::Excluded(k.clone()),
39        Bound::Unbounded => Bound::Unbounded,
40    }
41}
42
43// ── Isam ─────────────────────────────────────────────────────────────────── //
44
45/// The public ISAM database handle.
46///
47/// `Isam` is `Clone` — every clone is another handle to the same underlying
48/// storage.  Thread safety is provided by `TransactionManager`.
49pub struct Isam<K, V> {
50    manager: TransactionManager<K, V>,
51}
52
53impl<K, V> Clone for Isam<K, V> {
54    fn clone(&self) -> Self {
55        Self {
56            manager: self.manager.clone(),
57        }
58    }
59}
60
61impl<K, V> Isam<K, V>
62where
63    K: Serialize + DeserializeOwned + Ord + Clone + 'static,
64    V: Serialize + DeserializeOwned + Clone + 'static,
65{
66    // ── Lifecycle ────────────────────────────────────────────────────────── //
67
68    /// Create a new, empty database at `path`.
69    ///
70    /// Two files are created: `<path>.idb` (data) and `<path>.idx` (index).
71    /// Any existing files at those paths are truncated.
72    ///
73    /// # Example
74    /// ```
75    /// # use tempfile::TempDir;
76    /// # use highlandcows_isam::Isam;
77    /// # let dir = TempDir::new().unwrap();
78    /// # let path = dir.path().join("db");
79    /// let db: Isam<u32, String> = Isam::create(&path).unwrap();
80    /// ```
81    pub fn create(path: impl AsRef<Path>) -> IsamResult<Self> {
82        Ok(Self {
83            manager: TransactionManager::create(path.as_ref())?,
84        })
85    }
86
87    /// Open an existing database at `path`.
88    ///
89    /// # Example
90    /// ```
91    /// # use tempfile::TempDir;
92    /// # use highlandcows_isam::Isam;
93    /// # let dir = TempDir::new().unwrap();
94    /// # let path = dir.path().join("db");
95    /// # Isam::<u32, String>::create(&path).unwrap();
96    /// let db: Isam<u32, String> = Isam::open(&path).unwrap();
97    /// ```
98    pub fn open(path: impl AsRef<Path>) -> IsamResult<Self> {
99        Ok(Self {
100            manager: TransactionManager::open(path.as_ref())?,
101        })
102    }
103
104    /// Begin a new transaction.
105    ///
106    /// The returned [`Transaction`] holds an exclusive lock on the database
107    /// until it is committed, rolled back, or dropped.  Dropping without
108    /// committing automatically rolls back all changes made in the transaction.
109    ///
110    /// # Example
111    /// ```
112    /// # use tempfile::TempDir;
113    /// # use highlandcows_isam::Isam;
114    /// # let dir = TempDir::new().unwrap();
115    /// # let path = dir.path().join("db");
116    /// # let db: Isam<u32, String> = Isam::create(&path).unwrap();
117    /// let mut txn = db.begin_transaction().unwrap();
118    /// // ... perform operations ...
119    /// txn.commit().unwrap();
120    /// ```
121    pub fn begin_transaction(&self) -> IsamResult<Transaction<'_, K, V>> {
122        self.manager.begin()
123    }
124
125    // ── CRUD ─────────────────────────────────────────────────────────────── //
126
127    /// Insert a new key-value pair.
128    ///
129    /// Returns [`IsamError::DuplicateKey`] if the key already exists.
130    ///
131    /// # Example
132    /// ```
133    /// # use tempfile::TempDir;
134    /// # use highlandcows_isam::Isam;
135    /// # let dir = TempDir::new().unwrap();
136    /// # let path = dir.path().join("db");
137    /// # let db: Isam<u32, String> = Isam::create(&path).unwrap();
138    /// let mut txn = db.begin_transaction().unwrap();
139    /// db.insert(&mut txn, 1u32, &"hello".to_string()).unwrap();
140    /// txn.commit().unwrap();
141    /// ```
142    pub fn insert(&self, txn: &mut Transaction<'_, K, V>, key: K, value: &V) -> IsamResult<()> {
143        {
144            let storage = txn.storage_mut();
145            let rec = storage.store.append(&key, value)?;
146            storage.index.insert(&key, rec)?;
147            for si in &mut storage.secondary_indices {
148                si.on_insert(&key, value)?;
149            }
150        }
151        txn.log_insert(key, value.clone());
152        Ok(())
153    }
154
155    /// Look up a key and return its value, or `None` if the key does not exist.
156    ///
157    /// # Example
158    /// ```
159    /// # use tempfile::TempDir;
160    /// # use highlandcows_isam::Isam;
161    /// # let dir = TempDir::new().unwrap();
162    /// # let path = dir.path().join("db");
163    /// # let db: Isam<u32, String> = Isam::create(&path).unwrap();
164    /// # let mut txn = db.begin_transaction().unwrap();
165    /// # db.insert(&mut txn, 1u32, &"hello".to_string()).unwrap();
166    /// # txn.commit().unwrap();
167    /// let mut txn = db.begin_transaction().unwrap();
168    /// assert_eq!(db.get(&mut txn, &1u32).unwrap(), Some("hello".to_string()));
169    /// assert_eq!(db.get(&mut txn, &99u32).unwrap(), None);
170    /// txn.commit().unwrap();
171    /// ```
172    pub fn get(&self, txn: &mut Transaction<'_, K, V>, key: &K) -> IsamResult<Option<V>> {
173        let storage = txn.storage_mut();
174        match storage.index.search(key)? {
175            None => Ok(None),
176            Some(rec) => Ok(Some(storage.store.read_value(rec)?)),
177        }
178    }
179
180    /// Replace the value for an existing key.
181    ///
182    /// Returns [`IsamError::KeyNotFound`] if the key does not exist.
183    ///
184    /// # Example
185    /// ```
186    /// # use tempfile::TempDir;
187    /// # use highlandcows_isam::Isam;
188    /// # let dir = TempDir::new().unwrap();
189    /// # let path = dir.path().join("db");
190    /// # let db: Isam<u32, String> = Isam::create(&path).unwrap();
191    /// # let mut txn = db.begin_transaction().unwrap();
192    /// # db.insert(&mut txn, 1u32, &"old".to_string()).unwrap();
193    /// # txn.commit().unwrap();
194    /// let mut txn = db.begin_transaction().unwrap();
195    /// db.update(&mut txn, 1u32, &"new".to_string()).unwrap();
196    /// assert_eq!(db.get(&mut txn, &1u32).unwrap(), Some("new".to_string()));
197    /// txn.commit().unwrap();
198    /// ```
199    pub fn update(&self, txn: &mut Transaction<'_, K, V>, key: K, value: &V) -> IsamResult<()> {
200        let (old_rec, old_value) = {
201            let storage = txn.storage_mut();
202            let old_rec = storage.index.search(&key)?.ok_or(IsamError::KeyNotFound)?;
203            let old_value: V = storage.store.read_value(old_rec)?;
204            (old_rec, old_value)
205        };
206        {
207            let storage = txn.storage_mut();
208            let new_rec = storage.store.append(&key, value)?;
209            storage.index.update(&key, new_rec)?;
210            for si in &mut storage.secondary_indices {
211                si.on_update(&key, &old_value, value)?;
212            }
213        }
214        txn.log_update(key, old_rec, old_value, value.clone());
215        Ok(())
216    }
217
218    /// Remove a key and its associated value.
219    ///
220    /// Returns [`IsamError::KeyNotFound`] if the key does not exist.
221    ///
222    /// # Example
223    /// ```
224    /// # use tempfile::TempDir;
225    /// # use highlandcows_isam::Isam;
226    /// # let dir = TempDir::new().unwrap();
227    /// # let path = dir.path().join("db");
228    /// # let db: Isam<u32, String> = Isam::create(&path).unwrap();
229    /// # let mut txn = db.begin_transaction().unwrap();
230    /// # db.insert(&mut txn, 1u32, &"hello".to_string()).unwrap();
231    /// # txn.commit().unwrap();
232    /// let mut txn = db.begin_transaction().unwrap();
233    /// db.delete(&mut txn, &1u32).unwrap();
234    /// assert_eq!(db.get(&mut txn, &1u32).unwrap(), None);
235    /// txn.commit().unwrap();
236    /// ```
237    pub fn delete(&self, txn: &mut Transaction<'_, K, V>, key: &K) -> IsamResult<()> {
238        let (old_rec, old_value) = {
239            let storage = txn.storage_mut();
240            let old_rec = storage.index.search(key)?.ok_or(IsamError::KeyNotFound)?;
241            let old_value: V = storage.store.read_value(old_rec)?;
242            (old_rec, old_value)
243        };
244        {
245            let storage = txn.storage_mut();
246            storage.index.delete(key)?;
247            storage.store.append_tombstone(key)?;
248            for si in &mut storage.secondary_indices {
249                si.on_delete(key, &old_value)?;
250            }
251        }
252        txn.log_delete(key.clone(), old_rec, old_value);
253        Ok(())
254    }
255
256    /// Return the smallest key in the database, or `None` if empty.
257    ///
258    /// # Example
259    /// ```
260    /// # use tempfile::TempDir;
261    /// # use highlandcows_isam::Isam;
262    /// # let dir = TempDir::new().unwrap();
263    /// # let path = dir.path().join("db");
264    /// # let db: Isam<u32, u32> = Isam::create(&path).unwrap();
265    /// # let mut txn = db.begin_transaction().unwrap();
266    /// # for k in [3u32, 1, 2] { db.insert(&mut txn, k, &k).unwrap(); }
267    /// # txn.commit().unwrap();
268    /// let mut txn = db.begin_transaction().unwrap();
269    /// assert_eq!(db.min_key(&mut txn).unwrap(), Some(1u32));
270    /// txn.commit().unwrap();
271    /// ```
272    pub fn min_key(&self, txn: &mut Transaction<'_, K, V>) -> IsamResult<Option<K>> {
273        txn.storage_mut().index.min_key()
274    }
275
276    /// Return the largest key in the database, or `None` if empty.
277    ///
278    /// # Example
279    /// ```
280    /// # use tempfile::TempDir;
281    /// # use highlandcows_isam::Isam;
282    /// # let dir = TempDir::new().unwrap();
283    /// # let path = dir.path().join("db");
284    /// # let db: Isam<u32, u32> = Isam::create(&path).unwrap();
285    /// # let mut txn = db.begin_transaction().unwrap();
286    /// # for k in [3u32, 1, 2] { db.insert(&mut txn, k, &k).unwrap(); }
287    /// # txn.commit().unwrap();
288    /// let mut txn = db.begin_transaction().unwrap();
289    /// assert_eq!(db.max_key(&mut txn).unwrap(), Some(3u32));
290    /// txn.commit().unwrap();
291    /// ```
292    pub fn max_key(&self, txn: &mut Transaction<'_, K, V>) -> IsamResult<Option<K>> {
293        txn.storage_mut().index.max_key()
294    }
295
296    // ── Iterators ────────────────────────────────────────────────────────── //
297
298    /// Return a key-ordered iterator over all records.
299    ///
300    /// The iterator borrows `txn` for its lifetime, so no other operations
301    /// can be performed on the database until the iterator is dropped.
302    ///
303    /// # Example
304    /// ```
305    /// # use tempfile::TempDir;
306    /// # use highlandcows_isam::Isam;
307    /// # let dir = TempDir::new().unwrap();
308    /// # let path = dir.path().join("db");
309    /// # let db: Isam<u32, u32> = Isam::create(&path).unwrap();
310    /// # let mut txn = db.begin_transaction().unwrap();
311    /// # for k in [3u32, 1, 2] { db.insert(&mut txn, k, &k).unwrap(); }
312    /// # txn.commit().unwrap();
313    /// let mut txn = db.begin_transaction().unwrap();
314    /// let keys: Vec<u32> = db.iter(&mut txn).unwrap()
315    ///     .map(|r| r.unwrap().0)
316    ///     .collect();
317    /// assert_eq!(keys, vec![1, 2, 3]);
318    /// txn.commit().unwrap();
319    /// ```
320    pub fn iter<'txn>(
321        &self,
322        txn: &'txn mut Transaction<'_, K, V>,
323    ) -> IsamResult<IsamIter<'txn, K, V>> {
324        let storage = txn.storage_mut();
325        let first_id = storage.index.first_leaf_id()?;
326        let (entries, next_id) = if first_id != 0 {
327            storage.index.read_leaf(first_id)?
328        } else {
329            (vec![], 0)
330        };
331        Ok(IsamIter {
332            storage: txn.storage_mut(),
333            buffer: entries,
334            buf_pos: 0,
335            next_leaf_id: next_id,
336        })
337    }
338
339    /// Return a key-ordered iterator over records whose keys fall within `range`.
340    ///
341    /// Accepts any of Rust's built-in range expressions: `a..b`, `a..=b`,
342    /// `a..`, `..b`, `..=b`, `..`.
343    ///
344    /// The iterator borrows `txn` for its lifetime, so no other operations
345    /// can be performed on the database until the iterator is dropped.
346    ///
347    /// # Example
348    /// ```
349    /// # use tempfile::TempDir;
350    /// # use highlandcows_isam::Isam;
351    /// # let dir = TempDir::new().unwrap();
352    /// # let path = dir.path().join("db");
353    /// # let db: Isam<u32, u32> = Isam::create(&path).unwrap();
354    /// # let mut txn = db.begin_transaction().unwrap();
355    /// # for k in 1u32..=10 { db.insert(&mut txn, k, &k).unwrap(); }
356    /// # txn.commit().unwrap();
357    /// let mut txn = db.begin_transaction().unwrap();
358    /// let keys: Vec<u32> = db.range(&mut txn, 3u32..=7).unwrap()
359    ///     .map(|r| r.unwrap().0)
360    ///     .collect();
361    /// assert_eq!(keys, vec![3, 4, 5, 6, 7]);
362    /// txn.commit().unwrap();
363    /// ```
364    pub fn range<'txn, R>(
365        &self,
366        txn: &'txn mut Transaction<'_, K, V>,
367        range: R,
368    ) -> IsamResult<RangeIter<'txn, K, V>>
369    where
370        R: RangeBounds<K>,
371    {
372        let start_bound = clone_bound(range.start_bound());
373        let end_bound = clone_bound(range.end_bound());
374
375        let storage = txn.storage_mut();
376
377        let start_leaf_id = match &start_bound {
378            Bound::Included(k) | Bound::Excluded(k) => storage.index.find_leaf_for_key(k)?,
379            Bound::Unbounded => storage.index.first_leaf_id()?,
380        };
381
382        let (entries, next_leaf_id) = if start_leaf_id != 0 {
383            storage.index.read_leaf(start_leaf_id)?
384        } else {
385            (vec![], 0)
386        };
387
388        let buf_pos = match &start_bound {
389            Bound::Included(k) => entries.partition_point(|(ek, _)| ek < k),
390            Bound::Excluded(k) => entries.partition_point(|(ek, _)| ek <= k),
391            Bound::Unbounded => 0,
392        };
393
394        Ok(RangeIter {
395            storage: txn.storage_mut(),
396            buffer: entries,
397            buf_pos,
398            next_leaf_id,
399            end_bound,
400        })
401    }
402
403    // ── Secondary indices ─────────────────────────────────────────────────── //
404
405    /// Register a secondary index on this database.
406    ///
407    /// Must be called **before** any writes.  All secondary indices must be
408    /// re-registered each time the database is opened.
409    ///
410    /// Returns a [`SecondaryIndexHandle`] that can be used to query the index.
411    ///
412    /// # Deadlock warning
413    /// Acquires the database lock internally.  Must not be called while a
414    /// [`Transaction`] is live on the same thread.
415    ///
416    /// # Example
417    /// ```
418    /// # use tempfile::TempDir;
419    /// use serde::{Serialize, Deserialize};
420    /// use highlandcows_isam::{Isam, DeriveKey};
421    ///
422    /// #[derive(Serialize, Deserialize, Clone)]
423    /// struct User { name: String, city: String }
424    ///
425    /// struct CityIndex;
426    /// impl DeriveKey<User> for CityIndex {
427    ///     type Key = String;
428    ///     fn derive(u: &User) -> String { u.city.clone() }
429    /// }
430    ///
431    /// # let dir = TempDir::new().unwrap();
432    /// # let path = dir.path().join("db");
433    /// let db: Isam<u64, User> = Isam::create(&path).unwrap();
434    ///
435    /// // Register before any writes; re-register on every open.
436    /// let city_idx = db.register_secondary_index("city", CityIndex).unwrap();
437    ///
438    /// let mut txn = db.begin_transaction().unwrap();
439    /// db.insert(&mut txn, 1, &User { name: "Alice".into(), city: "London".into() }).unwrap();
440    /// txn.commit().unwrap();
441    ///
442    /// let mut txn = db.begin_transaction().unwrap();
443    /// let results = city_idx.lookup(&mut txn, &"London".to_string()).unwrap();
444    /// assert_eq!(results.len(), 1);
445    /// txn.commit().unwrap();
446    /// ```
447    pub fn register_secondary_index<E>(
448        &self,
449        name: &str,
450        _extractor: E,
451    ) -> IsamResult<SecondaryIndexHandle<K, V, E::Key>>
452    where
453        E: DeriveKey<V>,
454        K: Send,
455        V: Send,
456    {
457        let mut storage = self
458            .manager
459            .storage
460            .lock()
461            .map_err(|_| IsamError::LockPoisoned)?;
462        let si = SecondaryIndexImpl::<K, V, E>::create_or_open(name, &storage.base_path)?;
463        storage.secondary_indices.push(Box::new(si));
464        Ok(SecondaryIndexHandle {
465            name: name.to_owned(),
466            _phantom: PhantomData,
467        })
468    }
469
470    // ── Schema versioning ────────────────────────────────────────────────── //
471
472    /// Return the key schema version stored in the index metadata.
473    ///
474    /// Schema versions are set by [`migrate_keys`](Self::migrate_keys) and
475    /// default to `0` for newly created databases.
476    ///
477    /// # Deadlock warning
478    /// Acquires the database lock internally.  Must not be called while a
479    /// [`Transaction`] is live on the same thread.
480    ///
481    /// # Example
482    /// ```
483    /// # use tempfile::TempDir;
484    /// # use highlandcows_isam::Isam;
485    /// # let dir = TempDir::new().unwrap();
486    /// # let path = dir.path().join("db");
487    /// # let db: Isam<u32, String> = Isam::create(&path).unwrap();
488    /// assert_eq!(db.key_schema_version().unwrap(), 0);
489    /// ```
490    pub fn key_schema_version(&self) -> IsamResult<u32> {
491        let guard = self.manager.storage.lock().map_err(|_| IsamError::LockPoisoned)?;
492        Ok(guard.index.key_schema_version())
493    }
494
495    /// Return the value schema version stored in the index metadata.
496    ///
497    /// Schema versions are set by [`migrate_values`](Self::migrate_values) and
498    /// default to `0` for newly created databases.
499    ///
500    /// # Deadlock warning
501    /// Acquires the database lock internally.  Must not be called while a
502    /// [`Transaction`] is live on the same thread.
503    ///
504    /// # Example
505    /// ```
506    /// # use tempfile::TempDir;
507    /// # use highlandcows_isam::Isam;
508    /// # let dir = TempDir::new().unwrap();
509    /// # let path = dir.path().join("db");
510    /// # let db: Isam<u32, String> = Isam::create(&path).unwrap();
511    /// assert_eq!(db.val_schema_version().unwrap(), 0);
512    /// ```
513    pub fn val_schema_version(&self) -> IsamResult<u32> {
514        let guard = self.manager.storage.lock().map_err(|_| IsamError::LockPoisoned)?;
515        Ok(guard.index.val_schema_version())
516    }
517
518    // ── Structural operations ─────────────────────────────────────────────── //
519
520    /// Compact the database, removing tombstones and stale values.
521    ///
522    /// Rewrites the data and index files atomically via temp-file rename,
523    /// then re-opens them in place.
524    ///
525    /// # Deadlock warning
526    /// Acquires the database lock internally.  Must not be called while a
527    /// [`Transaction`] is live on the same thread.  These operations are
528    /// intended for offline administration — commit or roll back all open
529    /// transactions before calling them.
530    ///
531    /// # Example
532    /// ```
533    /// # use tempfile::TempDir;
534    /// # use highlandcows_isam::Isam;
535    /// # let dir = TempDir::new().unwrap();
536    /// # let path = dir.path().join("db");
537    /// # let db: Isam<u32, String> = Isam::create(&path).unwrap();
538    /// # let mut txn = db.begin_transaction().unwrap();
539    /// # for i in 0u32..5 { db.insert(&mut txn, i, &i.to_string()).unwrap(); }
540    /// # for i in 0u32..3 { db.delete(&mut txn, &i).unwrap(); }
541    /// # txn.commit().unwrap();
542    /// // All transactions committed — safe to compact.
543    /// db.compact().unwrap();
544    /// ```
545    pub fn compact(&self) -> IsamResult<()> {
546        let mut storage = self
547            .manager
548            .storage
549            .lock()
550            .map_err(|_| IsamError::LockPoisoned)?;
551
552        let mut records: Vec<(Vec<u8>, Vec<u8>)> = Vec::new();
553        let first_id = storage.index.first_leaf_id()?;
554        let mut current_id = first_id;
555        while current_id != 0 {
556            let (entries, next_id) = storage.index.read_leaf(current_id)?;
557            for (_, rec) in &entries {
558                let (_status, key_bytes, val_bytes) = storage.store.read_record_raw(rec.offset)?;
559                records.push((key_bytes, val_bytes));
560            }
561            current_id = next_id;
562        }
563
564        let tmp_idb = storage.base_path.with_extension("idb.tmp");
565        let tmp_idx = storage.base_path.with_extension("idx.tmp");
566
567        let mut new_store = crate::store::DataStore::create(&tmp_idb)?;
568        let mut new_index: crate::index::BTree<K> = crate::index::BTree::create(&tmp_idx)?;
569
570        for (key_bytes, val_bytes) in &records {
571            let rec = new_store.write_raw_record(crate::store::STATUS_ALIVE, key_bytes, val_bytes)?;
572            let key: K = bincode::deserialize(key_bytes)?;
573            new_index.insert(&key, rec)?;
574        }
575
576        new_store.flush()?;
577        new_index.flush()?;
578        drop(new_store);
579        drop(new_index);
580
581        let base = storage.base_path.clone();
582        std::fs::rename(&tmp_idb, idb_path(&base))?;
583        std::fs::rename(&tmp_idx, idx_path(&base))?;
584
585        storage.store = crate::store::DataStore::open(&idb_path(&base))?;
586        storage.index = crate::index::BTree::open(&idx_path(&base))?;
587
588        Ok(())
589    }
590
591    /// Rewrite every value through `f`, bump the val schema version, and
592    /// return a ready-to-use `Isam<K, V2>`.  Consumes `self`.
593    ///
594    /// Records are rewritten to new temp files and atomically renamed into
595    /// place.  The key schema version is preserved.
596    ///
597    /// # Deadlock warning
598    /// Acquires the database lock internally.  Must not be called while a
599    /// [`Transaction`] is live on the same thread.  These operations are
600    /// intended for offline administration — commit or roll back all open
601    /// transactions before calling them.
602    ///
603    /// # Example
604    /// ```
605    /// # use tempfile::TempDir;
606    /// # use highlandcows_isam::Isam;
607    /// # let dir = TempDir::new().unwrap();
608    /// # let path = dir.path().join("db");
609    /// let db: Isam<u32, String> = Isam::create(&path).unwrap();
610    /// # let mut txn = db.begin_transaction().unwrap();
611    /// # db.insert(&mut txn, 1u32, &"42".to_string()).unwrap();
612    /// # txn.commit().unwrap();
613    /// // Migrate String values → u64, setting val schema version to 1.
614    /// let db2: Isam<u32, u64> = db
615    ///     .migrate_values(1, |s: String| Ok(s.parse::<u64>().unwrap()))
616    ///     .unwrap();
617    /// assert_eq!(db2.val_schema_version().unwrap(), 1);
618    /// ```
619    pub fn migrate_values<V2, F>(self, new_val_version: u32, mut f: F) -> IsamResult<Isam<K, V2>>
620    where
621        V2: Serialize + DeserializeOwned + Clone + 'static,
622        F: FnMut(V) -> IsamResult<V2>,
623    {
624        let mut storage = self
625            .manager
626            .storage
627            .lock()
628            .map_err(|_| IsamError::LockPoisoned)?;
629
630        let base_path = storage.base_path.clone();
631        let key_schema_v = storage.index.key_schema_version();
632
633        let mut records: Vec<(Vec<u8>, V)> = Vec::new();
634        let first_id = storage.index.first_leaf_id()?;
635        let mut current_id = first_id;
636        while current_id != 0 {
637            let (entries, next_id) = storage.index.read_leaf(current_id)?;
638            for (_, rec) in &entries {
639                let (_status, key_bytes, val_bytes) = storage.store.read_record_raw(rec.offset)?;
640                let v: V = bincode::deserialize(&val_bytes)?;
641                records.push((key_bytes, v));
642            }
643            current_id = next_id;
644        }
645
646        let mut transformed: Vec<(Vec<u8>, V2)> = Vec::with_capacity(records.len());
647        for (key_bytes, v) in records {
648            transformed.push((key_bytes, f(v)?));
649        }
650
651        let tmp_idb = base_path.with_extension("idb.tmp");
652        let tmp_idx = base_path.with_extension("idx.tmp");
653
654        let mut new_store = crate::store::DataStore::create(&tmp_idb)?;
655        let mut new_index: crate::index::BTree<K> = crate::index::BTree::create(&tmp_idx)?;
656        new_index.set_schema_versions(key_schema_v, new_val_version)?;
657
658        for (key_bytes, v2) in &transformed {
659            let val_bytes = bincode::serialize(v2)?;
660            let rec = new_store.write_raw_record(crate::store::STATUS_ALIVE, key_bytes, &val_bytes)?;
661            let key: K = bincode::deserialize(key_bytes)?;
662            new_index.insert(&key, rec)?;
663        }
664
665        new_store.flush()?;
666        new_index.flush()?;
667        drop(new_store);
668        drop(new_index);
669        drop(storage);
670
671        std::fs::rename(&tmp_idb, idb_path(&base_path))?;
672        std::fs::rename(&tmp_idx, idx_path(&base_path))?;
673
674        Isam::<K, V2>::open(&base_path)
675    }
676
677    /// Rewrite every key through `f`, bump the key schema version, re-sort by
678    /// `K2::Ord`, rebuild the index, and return a ready-to-use `Isam<K2, V>`.
679    /// Consumes `self`.
680    ///
681    /// Records are rewritten to new temp files and atomically renamed into
682    /// place.  The value schema version is preserved.
683    ///
684    /// # Deadlock warning
685    /// Acquires the database lock internally.  Must not be called while a
686    /// [`Transaction`] is live on the same thread.  These operations are
687    /// intended for offline administration — commit or roll back all open
688    /// transactions before calling them.
689    ///
690    /// # Example
691    /// ```
692    /// # use tempfile::TempDir;
693    /// # use highlandcows_isam::Isam;
694    /// # let dir = TempDir::new().unwrap();
695    /// # let path = dir.path().join("db");
696    /// let db: Isam<u32, String> = Isam::create(&path).unwrap();
697    /// # let mut txn = db.begin_transaction().unwrap();
698    /// # db.insert(&mut txn, 1u32, &"one".to_string()).unwrap();
699    /// # txn.commit().unwrap();
700    /// // Migrate u32 keys → String, setting key schema version to 1.
701    /// let db2: Isam<String, String> = db
702    ///     .migrate_keys(1, |k: u32| Ok(format!("{k}")))
703    ///     .unwrap();
704    /// assert_eq!(db2.key_schema_version().unwrap(), 1);
705    /// ```
706    pub fn migrate_keys<K2, F>(self, new_key_version: u32, mut f: F) -> IsamResult<Isam<K2, V>>
707    where
708        K2: Serialize + DeserializeOwned + Ord + Clone + 'static,
709        F: FnMut(K) -> IsamResult<K2>,
710    {
711        let mut storage = self
712            .manager
713            .storage
714            .lock()
715            .map_err(|_| IsamError::LockPoisoned)?;
716
717        let base_path = storage.base_path.clone();
718        let val_schema_v = storage.index.val_schema_version();
719
720        let mut records: Vec<(K2, Vec<u8>)> = Vec::new();
721        let first_id = storage.index.first_leaf_id()?;
722        let mut current_id = first_id;
723        while current_id != 0 {
724            let (entries, next_id) = storage.index.read_leaf(current_id)?;
725            for (k, rec) in &entries {
726                let (_status, _key_bytes, val_bytes) = storage.store.read_record_raw(rec.offset)?;
727                let k2 = f(k.clone())?;
728                records.push((k2, val_bytes));
729            }
730            current_id = next_id;
731        }
732
733        records.sort_by(|(a, _), (b, _)| a.cmp(b));
734
735        let tmp_idb = base_path.with_extension("idb.tmp");
736        let tmp_idx = base_path.with_extension("idx.tmp");
737
738        let mut new_store = crate::store::DataStore::create(&tmp_idb)?;
739        let mut new_index: crate::index::BTree<K2> = crate::index::BTree::create(&tmp_idx)?;
740        new_index.set_schema_versions(new_key_version, val_schema_v)?;
741
742        for (k2, val_bytes) in &records {
743            let key_bytes = bincode::serialize(k2)?;
744            let rec = new_store.write_raw_record(crate::store::STATUS_ALIVE, &key_bytes, val_bytes)?;
745            new_index.insert(k2, rec)?;
746        }
747
748        new_store.flush()?;
749        new_index.flush()?;
750        drop(new_store);
751        drop(new_index);
752        drop(storage);
753
754        std::fs::rename(&tmp_idb, idb_path(&base_path))?;
755        std::fs::rename(&tmp_idx, idx_path(&base_path))?;
756
757        Isam::<K2, V>::open(&base_path)
758    }
759}
760
761// ── IsamIter ──────────────────────────────────────────────────────────────── //
762
763/// Key-order iterator over all alive records.
764///
765/// Created by [`Isam::iter`].  Borrows the [`Transaction`] for its lifetime,
766/// preventing other operations until the iterator is dropped.
767pub struct IsamIter<'txn, K, V> {
768    storage: &'txn mut IsamStorage<K, V>,
769    buffer: Vec<(K, RecordRef)>,
770    buf_pos: usize,
771    next_leaf_id: u32,
772}
773
774impl<'txn, K, V> Iterator for IsamIter<'txn, K, V>
775where
776    K: Serialize + DeserializeOwned + Ord + Clone,
777    V: Serialize + DeserializeOwned,
778{
779    type Item = IsamResult<(K, V)>;
780
781    fn next(&mut self) -> Option<Self::Item> {
782        loop {
783            if self.buf_pos < self.buffer.len() {
784                let (key, rec) = self.buffer[self.buf_pos].clone();
785                self.buf_pos += 1;
786                return Some(self.storage.store.read_value(rec).map(|value| (key, value)));
787            }
788
789            if self.next_leaf_id == 0 {
790                return None;
791            }
792
793            match self.storage.index.read_leaf(self.next_leaf_id) {
794                Ok((entries, next_id)) => {
795                    self.buffer = entries;
796                    self.buf_pos = 0;
797                    self.next_leaf_id = next_id;
798                }
799                Err(e) => return Some(Err(e)),
800            }
801        }
802    }
803}
804
805// ── RangeIter ────────────────────────────────────────────────────────────── //
806
807/// Key-order iterator over records whose key falls within a given range.
808///
809/// Created by [`Isam::range`].  Borrows the [`Transaction`] for its lifetime,
810/// preventing other operations until the iterator is dropped.
811pub struct RangeIter<'txn, K, V> {
812    storage: &'txn mut IsamStorage<K, V>,
813    buffer: Vec<(K, RecordRef)>,
814    buf_pos: usize,
815    next_leaf_id: u32,
816    end_bound: Bound<K>,
817}
818
819impl<'txn, K, V> Iterator for RangeIter<'txn, K, V>
820where
821    K: Serialize + DeserializeOwned + Ord + Clone,
822    V: Serialize + DeserializeOwned,
823{
824    type Item = IsamResult<(K, V)>;
825
826    fn next(&mut self) -> Option<Self::Item> {
827        loop {
828            if self.buf_pos < self.buffer.len() {
829                let (key, rec) = self.buffer[self.buf_pos].clone();
830                self.buf_pos += 1;
831
832                let within = match &self.end_bound {
833                    Bound::Included(end) => &key <= end,
834                    Bound::Excluded(end) => &key < end,
835                    Bound::Unbounded => true,
836                };
837                if !within {
838                    return None;
839                }
840
841                return Some(self.storage.store.read_value(rec).map(|value| (key, value)));
842            }
843
844            if self.next_leaf_id == 0 {
845                return None;
846            }
847
848            match self.storage.index.read_leaf(self.next_leaf_id) {
849                Ok((entries, next_id)) => {
850                    self.buffer = entries;
851                    self.buf_pos = 0;
852                    self.next_leaf_id = next_id;
853                }
854                Err(e) => return Some(Err(e)),
855            }
856        }
857    }
858}
859
860// ── SecondaryIndexHandle ──────────────────────────────────────────────────── //
861
862/// An opaque handle to a registered secondary index, used for point lookups.
863///
864/// Obtained from [`Isam::register_secondary_index`].
865pub struct SecondaryIndexHandle<K, V, SK> {
866    name: String,
867    _phantom: PhantomData<fn() -> (K, V, SK)>,
868}
869
870impl<K, V, SK> SecondaryIndexHandle<K, V, SK>
871where
872    K: Serialize + DeserializeOwned + Ord + Clone,
873    V: Serialize + DeserializeOwned,
874    SK: Serialize + DeserializeOwned + Ord + Clone,
875{
876    /// Return all `(primary_key, value)` pairs whose secondary key equals `sk`.
877    ///
878    /// Results are returned in insertion order (not key order).  For a
879    /// non-existent secondary key the result is an empty `Vec`.
880    ///
881    /// # Example
882    /// ```
883    /// # use tempfile::TempDir;
884    /// use serde::{Serialize, Deserialize};
885    /// use highlandcows_isam::{Isam, DeriveKey};
886    ///
887    /// #[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
888    /// struct User { name: String, city: String }
889    ///
890    /// struct CityIndex;
891    /// impl DeriveKey<User> for CityIndex {
892    ///     type Key = String;
893    ///     fn derive(u: &User) -> String { u.city.clone() }
894    /// }
895    ///
896    /// # let dir = TempDir::new().unwrap();
897    /// # let path = dir.path().join("db");
898    /// let db: Isam<u64, User> = Isam::create(&path).unwrap();
899    /// let city_idx = db.register_secondary_index("city", CityIndex).unwrap();
900    ///
901    /// let mut txn = db.begin_transaction().unwrap();
902    /// db.insert(&mut txn, 1, &User { name: "Alice".into(), city: "London".into() }).unwrap();
903    /// db.insert(&mut txn, 2, &User { name: "Bob".into(),   city: "London".into() }).unwrap();
904    /// db.insert(&mut txn, 3, &User { name: "Carol".into(), city: "Paris".into()  }).unwrap();
905    /// txn.commit().unwrap();
906    ///
907    /// let mut txn = db.begin_transaction().unwrap();
908    /// let mut londoners = city_idx.lookup(&mut txn, &"London".to_string()).unwrap();
909    /// londoners.sort_by_key(|(pk, _)| *pk);
910    /// assert_eq!(londoners[0].0, 1);
911    /// assert_eq!(londoners[1].0, 2);
912    /// assert_eq!(city_idx.lookup(&mut txn, &"Berlin".to_string()).unwrap(), vec![]);
913    /// txn.commit().unwrap();
914    /// ```
915    pub fn lookup(
916        &self,
917        txn: &mut Transaction<'_, K, V>,
918        sk: &SK,
919    ) -> IsamResult<Vec<(K, V)>> {
920        let sk_bytes = bincode::serialize(sk)?;
921
922        // Step 1: look up primary keys in the secondary index.
923        let pks: Vec<K> = {
924            let storage = txn.storage_mut();
925            match storage.secondary_indices.iter_mut().find(|si| si.name() == self.name) {
926                None => Vec::new(),
927                Some(si) => si.lookup_primary_keys(&sk_bytes)?,
928            }
929        };
930
931        // Step 2: fetch each primary record.
932        let storage = txn.storage_mut();
933        let mut results = Vec::with_capacity(pks.len());
934        for pk in pks {
935            if let Some(rec) = storage.index.search(&pk)? {
936                let value = storage.store.read_value(rec)?;
937                results.push((pk, value));
938            }
939        }
940        Ok(results)
941    }
942}