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}