Skip to main content

noxu_db/
sequence.rs

1//! Sequence handle.
2//!
3//!
4//! A `Sequence` is an auto-incrementing (or decrementing) counter backed by
5//! a single key-value record in a `Database`.  The persistent record stores
6//! the *next* batch boundary so that multiple handles can share the same
7//! database key without requiring coordination on every call.
8//!
9//! ## Record format (ported exactly from the)
10//!
11//! ```text
12//! byte 0   : version (always 1)
13//! byte 1   : flags
14//!              bit 0 (FLAG_INCR) — sequence increments
15//!              bit 1 (FLAG_WRAP) — wrap-around allowed
16//!              bit 2 (FLAG_OVER) — overflow has occurred
17//! bytes 2+ : range_min  (big-endian i64, 8 bytes)
18//! bytes 10+: range_max  (big-endian i64, 8 bytes)
19//! bytes 18+: stored_value (big-endian i64, 8 bytes)
20//! ```
21//!
22//! Total: 26 bytes.  uses packed-long encoding; we use fixed big-endian
23//! i64 for simplicity (compatible with the noxu-persist pattern).
24
25use crate::database::Database;
26use crate::database_entry::DatabaseEntry;
27use crate::error::{NoxuError, Result};
28use crate::operation_status::OperationStatus;
29use crate::sequence_config::SequenceConfig;
30use crate::sequence_stats::SequenceStats;
31use crate::transaction::Transaction;
32use std::sync::Mutex;
33
34// ── record flags ──────────────────────────────────────────────────────────────
35const FLAG_INCR: u8 = 0x1;
36const FLAG_WRAP: u8 = 0x2;
37const FLAG_OVER: u8 = 0x4;
38
39/// Current on-disk record version.
40const CURRENT_VERSION: u8 = 1;
41
42/// Fixed size of the serialised sequence record.
43const RECORD_SIZE: usize = 26; // 1 + 1 + 8 + 8 + 8
44
45// ── mutable cache state, protected by a Mutex ────────────────────────────────
46struct CacheState {
47    /// Persistent fields (mirrored from the DB record).
48    wrap_allowed: bool,
49    increment: bool,
50    overflow: bool,
51    range_min: i64,
52    range_max: i64,
53    /// The value that was last written to the database (the batch boundary).
54    stored_value: i64,
55
56    /// Next value to hand out from the local cache.
57    cache_value: i64,
58    /// Last value reserved in the local cache (inclusive).
59    cache_last: i64,
60
61    /// Whether the cache has been filled at least once.
62    /// When false the first `get` always triggers a DB refill, regardless of
63    /// the `cache_value`/`cache_last` sentinel values (which can be ambiguous
64    /// for sequences at the i64 extremes).
65    cache_initialized: bool,
66
67    /// Statistics.
68    n_gets: u64,
69    n_cache_hits: u64,
70}
71
72/// A handle for manipulating a sequence record stored in a `Database`.
73///
74///
75///
76/// Multiple threads may share a single `Sequence` handle safely; all cache
77/// manipulation is protected by an internal `Mutex`.  For higher throughput
78/// open separate handles to the same database key.
79///
80/// # Example
81///
82/// ```ignore
83/// use noxu_db::{SequenceConfig, DatabaseEntry};
84///
85/// let config = SequenceConfig::new().with_allow_create(true);
86/// let key = DatabaseEntry::from_bytes(b"my_counter");
87/// let seq = db.open_sequence(&key, config).unwrap();
88///
89/// let v1 = seq.get(None, 1).unwrap();
90/// let v2 = seq.get(None, 1).unwrap();
91/// assert_eq!(v2, v1 + 1);
92/// ```
93pub struct Sequence<'db> {
94    /// The database backing the sequence.
95    db: &'db Database,
96    /// The key under which the sequence record is stored (owned copy).
97    key: Vec<u8>,
98    /// Cache size chosen at open time.
99    cache_size: i32,
100    /// All mutable state is behind a Mutex so `get` can take `&self`.
101    state: Mutex<CacheState>,
102}
103
104impl<'db> Sequence<'db> {
105    /// Opens (and optionally creates) a sequence.
106    ///
107    /// Called by `Database::open_sequence`.
108    pub(crate) fn open(
109        db: &'db Database,
110        key: &DatabaseEntry,
111        config: SequenceConfig,
112    ) -> Result<Self> {
113        // ── validate config ───────────────────────────────────────────────
114        if config.range_min >= config.range_max {
115            return Err(NoxuError::IllegalArgument(
116                "Minimum sequence value must be less than the maximum".into(),
117            ));
118        }
119        if config.initial_value > config.range_max
120            || config.initial_value < config.range_min
121        {
122            return Err(NoxuError::IllegalArgument(
123                "Initial sequence value is out of range".into(),
124            ));
125        }
126        // cache_size == 0 means no caching; any positive cache_size must fit
127        // within the range.  Use saturating_sub to avoid overflow when the
128        // range spans the full i64 range.
129        if config.cache_size > 0
130            && config.range_max.saturating_sub(config.range_min)
131                < config.cache_size as i64
132        {
133            return Err(NoxuError::IllegalArgument(
134                "The cache size is larger than the sequence range".into(),
135            ));
136        }
137
138        let key_bytes = key.get_data().unwrap_or(&[]).to_vec();
139        let key_entry = DatabaseEntry::from_bytes(&key_bytes);
140
141        // ── try to read an existing record ────────────────────────────────
142        let mut data_entry = DatabaseEntry::new();
143        let found = db.get(None, &key_entry, &mut data_entry)?
144            == OperationStatus::Success;
145
146        if found {
147            if config.allow_create && config.exclusive_create {
148                return Err(NoxuError::IllegalArgument(
149                    "ExclusiveCreate=true and the sequence record already exists."
150                        .into(),
151                ));
152            }
153            // Decode the existing record.
154            let rec = Self::decode_record(data_entry.data())?;
155            let cache_size = config.cache_size;
156            let (cache_value, cache_last) = Self::init_cache(&rec, cache_size);
157            return Ok(Sequence {
158                db,
159                key: key_bytes,
160                cache_size,
161                state: Mutex::new(CacheState {
162                    wrap_allowed: rec.wrap_allowed,
163                    increment: rec.increment,
164                    overflow: rec.overflow,
165                    range_min: rec.range_min,
166                    range_max: rec.range_max,
167                    stored_value: rec.stored_value,
168                    cache_value,
169                    cache_last,
170                    cache_initialized: false,
171                    n_gets: 0,
172                    n_cache_hits: 0,
173                }),
174            });
175        }
176
177        // ── record not found ──────────────────────────────────────────────
178        if !config.allow_create {
179            return Err(NoxuError::NotFound);
180        }
181
182        // Create a new record from the config.
183        let increment = !config.decrement;
184        let stored_value = config.initial_value;
185        let rec = PersistedSeq {
186            wrap_allowed: config.wrap,
187            increment,
188            overflow: false,
189            range_min: config.range_min,
190            range_max: config.range_max,
191            stored_value,
192        };
193        let encoded = Self::encode_record(&rec);
194        let data_entry = DatabaseEntry::from_bytes(&encoded);
195
196        // putNoOverwrite so a concurrent creator wins and we just read theirs.
197        let status = db.put_no_overwrite(None, &key_entry, &data_entry)?;
198        let final_rec = if status == OperationStatus::KeyExists {
199            // Lost the race — read the winner's record.
200            let mut d = DatabaseEntry::new();
201            if db.get(None, &key_entry, &mut d)? != OperationStatus::Success {
202                return Err(NoxuError::IllegalArgument(
203                    "Sequence record removed during open_sequence.".into(),
204                ));
205            }
206            Self::decode_record(d.data())?
207        } else {
208            rec
209        };
210
211        let cache_size = config.cache_size;
212        let (cache_value, cache_last) =
213            Self::init_cache(&final_rec, cache_size);
214        Ok(Sequence {
215            db,
216            key: key_bytes,
217            cache_size,
218            state: Mutex::new(CacheState {
219                wrap_allowed: final_rec.wrap_allowed,
220                increment: final_rec.increment,
221                overflow: final_rec.overflow,
222                range_min: final_rec.range_min,
223                range_max: final_rec.range_max,
224                stored_value: final_rec.stored_value,
225                cache_value,
226                cache_last,
227                cache_initialized: false,
228                n_gets: 0,
229                n_cache_hits: 0,
230            }),
231        })
232    }
233
234    // ── public API ────────────────────────────────────────────────────────────
235
236    /// Returns the next available element in the sequence and advances by
237    /// `delta`.
238    ///
239    ///
240    ///
241    /// `delta` must be > 0 and must fit within the configured range.
242    ///
243    /// The `txn` parameter, if provided, is used for the cache-refill database
244    /// write, making it participate in the caller's transaction.
245    /// `Sequence.get(Transaction txn, int delta)`.
246    pub fn get(&self, txn: Option<&Transaction>, delta: i32) -> Result<i64> {
247        if delta <= 0 {
248            return Err(NoxuError::IllegalArgument(
249                "Sequence delta must be greater than zero".into(),
250            ));
251        }
252
253        let mut state = self.state.lock().unwrap();
254
255        // "if (rangeMin > rangeMax - delta)" — use saturating to avoid
256        // overflow when range_max is near i64::MIN.
257        if state.range_min > state.range_max.saturating_sub(delta as i64) {
258            return Err(NoxuError::IllegalArgument(
259                "Sequence delta is larger than the range".into(),
260            ));
261        }
262
263        // ── check cache availability ──────────────────────────────────────
264        // If the cache has never been filled we always need a refill, even if
265        // the sentinel cache_last/cache_value happen to look non-empty (this
266        // can occur for sequences whose initial_value is at an i64 extreme).
267        let cache_available = if !state.cache_initialized {
268            0
269        } else if state.increment {
270            // cache_last < cache_value when empty: result is negative or zero,
271            // so need_refill will be true (delta > cache_available).
272            (state.cache_last - state.cache_value) + 1
273        } else {
274            (state.cache_value - state.cache_last) + 1
275        };
276        let need_refill = delta as i64 > cache_available;
277
278        // Check overflow unconditionally: when checked_add overflows i64 we
279        // set overflow=true but still serve the last batch from cache.  On
280        // the very next call (cache may still appear non-empty) we must error.
281        if state.overflow {
282            return Err(NoxuError::OperationNotAllowed(format!(
283                "Sequence overflow at {}",
284                state.stored_value
285            )));
286        }
287
288        if need_refill {
289            // ── refill the cache from the database ────────────────────────
290
291            let adjust = if delta > self.cache_size {
292                delta as i64
293            } else {
294                self.cache_size as i64
295            };
296
297            // How many values remain, inclusive of stored_value itself?
298            // stored_value is the first un-allocated value in the sequence, so
299            // the count of allocatable values is:
300            //   increment:  range_max - stored_value + 1
301            //   decrement:  stored_value - range_min + 1
302            // A negative result means stored_value has moved past the boundary
303            // (overflow has occurred).
304            //
305            // Potential overflow: range_max + 1 could overflow when
306            // range_max == i64::MAX, and similarly for range_min - 1 when
307            // range_min == i64::MIN.  Use checked_add/sub to cap at 0 in those
308            // cases — if stored_value == i64::MAX the +1 to range_max would
309            // overflow, but stored_value must have already been clamped by the
310            // checked_add above (overflow = true), so the early-return guard
311            // at the top of the block already handles that path.
312            // Compute how many values remain at stored_value (inclusive).
313            //
314            // For increment: stored_value must be ≤ range_max to have any.
315            //   avail = range_max - stored_value + 1  (clamp to i64::MAX)
316            // For decrement: stored_value must be ≥ range_min to have any.
317            //   avail = stored_value - range_min + 1  (clamp to i64::MAX)
318            //
319            // Overflow edge cases handled explicitly:
320            //   - If stored_value > range_max (increment) → exhausted → avail=0.
321            //   - If stored_value < range_min (decrement) → exhausted → avail=0.
322            //   - The subtraction itself can overflow when the range spans the
323            //     full i64 extent; use saturating to cap at i64::MAX.
324            let avail: i64 = if state.increment {
325                if state.stored_value > state.range_max {
326                    0
327                } else {
328                    // range_max - stored_value ≥ 0 and fits in i64 because
329                    // both are within i64; +1 may overflow only if the diff
330                    // is already i64::MAX (range is the full i64 span).
331                    state
332                        .range_max
333                        .saturating_sub(state.stored_value)
334                        .saturating_add(1)
335                }
336            } else {
337                if state.stored_value < state.range_min {
338                    0
339                } else {
340                    state
341                        .stored_value
342                        .saturating_sub(state.range_min)
343                        .saturating_add(1)
344                }
345            };
346
347            let actual_adjust: i64 = if avail < adjust {
348                if avail < delta as i64 {
349                    if state.wrap_allowed {
350                        // Wrap: reset stored_value to the opposite end.
351                        state.stored_value = if state.increment {
352                            state.range_min
353                        } else {
354                            state.range_max
355                        };
356                        // After wrapping, stored_value is at the opposite end.
357                        // Compute how many values are available in the full range.
358                        let full_avail = if state.increment {
359                            // range_max - range_min + 1 = full range size
360                            // Approximate with checked arithmetic; for extremes
361                            // the range must be small (validated at open time).
362                            state
363                                .range_max
364                                .saturating_sub(state.stored_value)
365                                .saturating_add(1)
366                        } else {
367                            state
368                                .stored_value
369                                .saturating_sub(state.range_min)
370                                .saturating_add(1)
371                        };
372                        full_avail.min(adjust)
373                    } else {
374                        // Range exhausted and wrap not allowed — error now,
375                        // matching SequenceImpl which throws immediately.
376                        return Err(NoxuError::OperationNotAllowed(format!(
377                            "Sequence overflow at {}",
378                            state.stored_value
379                        )));
380                    }
381                } else {
382                    // Not enough for a full cache batch but enough for delta.
383                    avail
384                }
385            } else {
386                adjust
387            };
388
389            // Apply the adjustment.
390            // Record the batch start (= old stored_value) before advancing.
391            // For increment: the batch covers [batch_start, batch_start + actual_adjust - 1].
392            // For decrement: the batch covers [batch_start - actual_adjust + 1, batch_start].
393            let batch_start = state.stored_value;
394            let signed_adjust =
395                if state.increment { actual_adjust } else { -actual_adjust };
396            // Use checked add: if the new stored_value would overflow i64 (only
397            // possible when range_max == i64::MAX or range_min == i64::MIN), we
398            // mark overflow immediately so the NEXT get returns an error.
399            match state.stored_value.checked_add(signed_adjust) {
400                Some(new_sv) => state.stored_value = new_sv,
401                None => {
402                    // Overflow past i64 bounds — mark it and use a sentinel.
403                    state.overflow = true;
404                    state.stored_value =
405                        if state.increment { i64::MAX } else { i64::MIN };
406                }
407            }
408
409            // Persist the new stored_value.
410            let rec = PersistedSeq {
411                wrap_allowed: state.wrap_allowed,
412                increment: state.increment,
413                overflow: state.overflow,
414                range_min: state.range_min,
415                range_max: state.range_max,
416                stored_value: state.stored_value,
417            };
418            let encoded = Self::encode_record(&rec);
419            let key_entry = DatabaseEntry::from_bytes(&self.key);
420            let data_entry = DatabaseEntry::from_bytes(&encoded);
421            self.db.put(txn, &key_entry, &data_entry)?;
422
423            // Update the local cache window using batch_start (pre-advance).
424            // cache_value = batch_start (first value to hand out)
425            // cache_last  = batch_start + actual_adjust - 1 (incr, inclusive end)
426            //             = batch_start - actual_adjust + 1 (decr, inclusive end)
427            // Use saturating arithmetic to avoid overflow at i64 extremes.
428            state.cache_value = batch_start;
429            state.cache_last = if state.increment {
430                batch_start.saturating_add(actual_adjust - 1)
431            } else {
432                batch_start.saturating_sub(actual_adjust - 1)
433            };
434            state.cache_initialized = true;
435        }
436
437        // ── serve from cache ──────────────────────────────────────────────
438        let ret_val = state.cache_value;
439        if state.increment {
440            state.cache_value = state.cache_value.saturating_add(delta as i64);
441        } else {
442            state.cache_value = state.cache_value.saturating_sub(delta as i64);
443        }
444
445        state.n_gets += 1;
446        if !need_refill {
447            state.n_cache_hits += 1;
448        }
449
450        Ok(ret_val)
451    }
452
453    /// Returns a snapshot of statistics for this handle.
454    ///
455    ///
456    pub fn get_stats(&self) -> SequenceStats {
457        let state = self.state.lock().unwrap();
458        SequenceStats {
459            n_gets: state.n_gets,
460            n_cache_hits: state.n_cache_hits,
461            current_value: state.stored_value,
462            cache_value: state.cache_value,
463            cache_last: state.cache_last,
464            range_min: state.range_min,
465            range_max: state.range_max,
466            cache_size: self.cache_size,
467        }
468    }
469
470    /// Closes the sequence handle.
471    ///
472    /// After calling this method the handle must not be used again.  Unused
473    /// cached values are discarded.
474    ///
475    ///
476    pub fn close(&self) -> Result<()> {
477        // Nothing to flush; the DB record already holds the batch boundary.
478        Ok(())
479    }
480
481    // ── helpers ───────────────────────────────────────────────────────────────
482
483    /// Initialise `(cache_value, cache_last)` from a just-read record so that
484    /// the cache appears empty and the first `get` call triggers a refill.
485    ///
486    /// does: `cacheLast = increment ? (storedValue - 1) : (storedValue + 1)`
487    fn init_cache(rec: &PersistedSeq, _cache_size: i32) -> (i64, i64) {
488        let cache_value = rec.stored_value;
489        // cache_last is set so the cache appears empty on first get:
490        //   increment: cache_last = stored_value - 1  (below cache_value)
491        //   decrement: cache_last = stored_value + 1  (above cache_value)
492        // Use saturating arithmetic to avoid overflow at i64 extremes.
493        let cache_last = if rec.increment {
494            rec.stored_value.saturating_sub(1)
495        } else {
496            rec.stored_value.saturating_add(1)
497        };
498        (cache_value, cache_last)
499    }
500
501    /// Encodes persistent sequence fields into a 26-byte array.
502    fn encode_record(rec: &PersistedSeq) -> [u8; RECORD_SIZE] {
503        let mut buf = [0u8; RECORD_SIZE];
504        buf[0] = CURRENT_VERSION;
505        let mut flags: u8 = 0;
506        if rec.increment {
507            flags |= FLAG_INCR;
508        }
509        if rec.wrap_allowed {
510            flags |= FLAG_WRAP;
511        }
512        if rec.overflow {
513            flags |= FLAG_OVER;
514        }
515        buf[1] = flags;
516        buf[2..10].copy_from_slice(&rec.range_min.to_be_bytes());
517        buf[10..18].copy_from_slice(&rec.range_max.to_be_bytes());
518        buf[18..26].copy_from_slice(&rec.stored_value.to_be_bytes());
519        buf
520    }
521
522    /// Decodes a 26-byte array into persistent sequence fields.
523    fn decode_record(data: &[u8]) -> Result<PersistedSeq> {
524        if data.len() < RECORD_SIZE {
525            return Err(NoxuError::IllegalArgument(format!(
526                "Sequence record too short: {} bytes (expected {})",
527                data.len(),
528                RECORD_SIZE
529            )));
530        }
531        // byte 0: version (ignored for forward compatibility)
532        let flags = data[1];
533        let increment = (flags & FLAG_INCR) != 0;
534        let wrap_allowed = (flags & FLAG_WRAP) != 0;
535        let overflow = (flags & FLAG_OVER) != 0;
536        let range_min = i64::from_be_bytes(data[2..10].try_into().unwrap());
537        let range_max = i64::from_be_bytes(data[10..18].try_into().unwrap());
538        let stored_value = i64::from_be_bytes(data[18..26].try_into().unwrap());
539        Ok(PersistedSeq {
540            wrap_allowed,
541            increment,
542            overflow,
543            range_min,
544            range_max,
545            stored_value,
546        })
547    }
548}
549
550// ── internal helper struct ────────────────────────────────────────────────────
551
552/// The subset of sequence state that is persisted to the database.
553struct PersistedSeq {
554    wrap_allowed: bool,
555    increment: bool,
556    overflow: bool,
557    range_min: i64,
558    range_max: i64,
559    stored_value: i64,
560}
561
562// ── tests ─────────────────────────────────────────────────────────────────────
563
564#[cfg(test)]
565mod tests {
566    use super::*;
567    use crate::database_config::DatabaseConfig;
568    use crate::environment::Environment;
569    use crate::environment_config::EnvironmentConfig;
570    use tempfile::TempDir;
571
572    fn setup() -> (TempDir, Environment) {
573        let dir = TempDir::new().unwrap();
574        let env = Environment::open(
575            EnvironmentConfig::new(dir.path().to_path_buf())
576                .with_allow_create(true)
577                .with_transactional(false),
578        )
579        .unwrap();
580        (dir, env)
581    }
582
583    fn open_db(env: &Environment) -> crate::database::Database {
584        env.open_database(
585            None,
586            "seqdb",
587            &DatabaseConfig::new().with_allow_create(true),
588        )
589        .unwrap()
590    }
591
592    #[test]
593    fn test_sequence_create_and_get() {
594        let (_dir, env) = setup();
595        let db = open_db(&env);
596
597        let key = DatabaseEntry::from_bytes(b"counter");
598        let config = SequenceConfig::new().with_allow_create(true);
599        let seq = db.open_sequence(&key, config).unwrap();
600
601        let v0 = seq.get(None, 1).unwrap();
602        let v1 = seq.get(None, 1).unwrap();
603        let v2 = seq.get(None, 1).unwrap();
604
605        // Must be monotonically increasing.
606        assert!(v1 > v0, "v1={v1} should be > v0={v0}");
607        assert!(v2 > v1, "v2={v2} should be > v1={v1}");
608    }
609
610    #[test]
611    fn test_sequence_five_values_monotonic() {
612        let (_dir, env) = setup();
613        let db = open_db(&env);
614
615        let key = DatabaseEntry::from_bytes(b"mono");
616        let config =
617            SequenceConfig::new().with_allow_create(true).with_cache_size(5);
618        let seq = db.open_sequence(&key, config).unwrap();
619
620        let mut prev = seq.get(None, 1).unwrap();
621        for _ in 0..4 {
622            let next = seq.get(None, 1).unwrap();
623            assert!(next > prev, "sequence not monotonic: {next} <= {prev}");
624            prev = next;
625        }
626    }
627
628    #[test]
629    fn test_sequence_delta_greater_than_one() {
630        let (_dir, env) = setup();
631        let db = open_db(&env);
632
633        let key = DatabaseEntry::from_bytes(b"delta");
634        let config = SequenceConfig::new()
635            .with_allow_create(true)
636            .with_initial_value(0)
637            .with_cache_size(0);
638        let seq = db.open_sequence(&key, config).unwrap();
639
640        let v0 = seq.get(None, 3).unwrap();
641        let v1 = seq.get(None, 3).unwrap();
642        assert_eq!(v1 - v0, 3);
643    }
644
645    #[test]
646    fn test_sequence_stats() {
647        let (_dir, env) = setup();
648        let db = open_db(&env);
649
650        let key = DatabaseEntry::from_bytes(b"stats");
651        let config =
652            SequenceConfig::new().with_allow_create(true).with_cache_size(10);
653        let seq = db.open_sequence(&key, config).unwrap();
654
655        seq.get(None, 1).unwrap();
656        seq.get(None, 1).unwrap();
657        seq.get(None, 1).unwrap();
658
659        let stats = seq.get_stats();
660        assert_eq!(stats.n_gets, 3);
661        assert_eq!(stats.range_min, i64::MIN);
662        assert_eq!(stats.range_max, i64::MAX);
663    }
664
665    #[test]
666    fn test_sequence_wrap() {
667        let (_dir, env) = setup();
668        let db = open_db(&env);
669
670        let key = DatabaseEntry::from_bytes(b"wrap");
671        // Small range [0, 4] with wrap enabled.
672        let config = SequenceConfig::new()
673            .with_allow_create(true)
674            .with_range(0, 4)
675            .with_wrap(true)
676            .with_initial_value(0)
677            .with_cache_size(0);
678        let seq = db.open_sequence(&key, config).unwrap();
679
680        // Consume all 5 values (0, 1, 2, 3, 4).
681        let mut values: Vec<i64> =
682            (0..5).map(|_| seq.get(None, 1).unwrap()).collect();
683
684        // Next call should wrap to 0 again.
685        let after_wrap = seq.get(None, 1).unwrap();
686        values.push(after_wrap);
687
688        // The wrapped value must be in [0, 4].
689        assert!(
690            (0..=4).contains(&after_wrap),
691            "wrapped value {after_wrap} not in [0, 4]"
692        );
693    }
694
695    #[test]
696    fn test_sequence_no_overwrite_on_existing() {
697        let (_dir, env) = setup();
698        let db = open_db(&env);
699
700        let key = DatabaseEntry::from_bytes(b"exist");
701        let config = SequenceConfig::new().with_allow_create(true);
702        let seq1 = db.open_sequence(&key, config.clone()).unwrap();
703        let v1 = seq1.get(None, 1).unwrap();
704
705        // Second open should succeed and continue from where seq1 left off.
706        let seq2 = db.open_sequence(&key, config).unwrap();
707        let v2 = seq2.get(None, 1).unwrap();
708        assert!(v2 >= v1, "seq2 should continue from seq1: v2={v2} v1={v1}");
709    }
710
711    #[test]
712    fn test_sequence_exclusive_create_fails_on_existing() {
713        let (_dir, env) = setup();
714        let db = open_db(&env);
715
716        let key = DatabaseEntry::from_bytes(b"excl");
717        db.open_sequence(&key, SequenceConfig::new().with_allow_create(true))
718            .unwrap();
719
720        let result = db.open_sequence(
721            &key,
722            SequenceConfig::new()
723                .with_allow_create(true)
724                .with_exclusive_create(true),
725        );
726        assert!(
727            result.is_err(),
728            "exclusive_create should fail when record exists"
729        );
730    }
731
732    #[test]
733    fn test_sequence_not_found_without_allow_create() {
734        let (_dir, env) = setup();
735        let db = open_db(&env);
736
737        let key = DatabaseEntry::from_bytes(b"missing");
738        let result = db.open_sequence(
739            &key,
740            SequenceConfig::new().with_allow_create(false),
741        );
742        assert!(result.is_err(), "should fail without allow_create");
743    }
744
745    #[test]
746    fn test_sequence_decrement() {
747        let (_dir, env) = setup();
748        let db = open_db(&env);
749
750        let key = DatabaseEntry::from_bytes(b"decr");
751        let config = SequenceConfig::new()
752            .with_allow_create(true)
753            .with_decrement(true)
754            .with_initial_value(100)
755            .with_cache_size(0);
756        let seq = db.open_sequence(&key, config).unwrap();
757
758        let v0 = seq.get(None, 1).unwrap();
759        let v1 = seq.get(None, 1).unwrap();
760        let v2 = seq.get(None, 1).unwrap();
761        assert!(v1 < v0, "decrement: v1={v1} should be < v0={v0}");
762        assert!(v2 < v1, "decrement: v2={v2} should be < v1={v1}");
763    }
764
765    #[test]
766    fn test_sequence_close() {
767        let (_dir, env) = setup();
768        let db = open_db(&env);
769
770        let key = DatabaseEntry::from_bytes(b"close");
771        let seq = db
772            .open_sequence(&key, SequenceConfig::new().with_allow_create(true))
773            .unwrap();
774        assert!(seq.close().is_ok());
775    }
776}