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}