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