@@ -3,7 +3,7 @@ use crate::mvcc::cursor::{static_iterator_hack, MvccIterator};
33#[ cfg( any( test, injected_yields) ) ]
44use crate :: mvcc:: yield_hooks:: { ProvidesYieldContext , YieldContext , YieldPointMarker } ;
55use crate :: mvcc:: yield_points:: { inject_transition_failure, inject_transition_yield} ;
6- use crate :: schema:: { Schema , Table } ;
6+ use crate :: schema:: { Schema , Sequence , Table } ;
77use crate :: state_machine:: StateMachine ;
88use crate :: state_machine:: StateTransition ;
99use crate :: state_machine:: TransitionResult ;
@@ -44,7 +44,7 @@ use crossbeam_skiplist::map::Entry;
4444use crossbeam_skiplist:: SkipMap ;
4545use rustc_hash:: FxHashMap as HashMap ;
4646use rustc_hash:: FxHashSet as HashSet ;
47- use std:: collections:: BTreeSet ;
47+ use std:: collections:: { BTreeSet , HashMap as StdHashMap } ;
4848use std:: fmt:: Debug ;
4949use std:: marker:: PhantomData ;
5050use std:: ops:: Bound ;
@@ -80,6 +80,20 @@ pub mod tests;
8080/// Sentinel value for `MvStore::exclusive_tx` indicating no exclusive transaction is active.
8181const NO_EXCLUSIVE_TX : u64 = 0 ;
8282
83+ /// Convert a sequence backing-table row into the exclusive upper bound used by
84+ /// sync scans. This is intentionally tailored to ascending non-CYCLE sequences,
85+ /// which is the shape used by AUTOINCREMENT CDC ids.
86+ pub ( crate ) fn first_unsafe_sequence_watermark ( seq : & Sequence , value : i64 , is_called : bool ) -> i64 {
87+ if !is_called {
88+ return value;
89+ }
90+ if seq. increment_by > 0 && !seq. cycle {
91+ value. checked_add ( seq. increment_by ) . unwrap_or ( value)
92+ } else {
93+ value
94+ }
95+ }
96+
8397#[ cfg( not( any( test, injected_yields) ) ) ]
8498struct YieldContext ;
8599
@@ -3437,6 +3451,15 @@ pub struct MvStore<Clock: LogicalClock> {
34373451 /// deadlock.
34383452 last_global_header_ts : AtomicU64 ,
34393453 table_id_to_last_rowid : RwLock < HashMap < MVTableId , Arc < RowidAllocator > > > ,
3454+ /// Per-sequence first value not guaranteed safe to read past based only on
3455+ /// durable/current sequence state. Active allocations can lower this.
3456+ sequence_watermarks : Mutex < HashMap < String , i64 > > ,
3457+ /// Per-sequence minimum allocated value for each active transaction.
3458+ ///
3459+ /// This is in-memory and therefore only correct while all MVCC writers for a
3460+ /// database live in one process. Multi-process MVCC will need a shared
3461+ /// coordination mechanism before sync can rely on this watermark.
3462+ sequence_allocations : Mutex < HashMap < String , StdHashMap < TxID , i64 > > > ,
34403463}
34413464
34423465impl < Clock : LogicalClock > MvStore < Clock > {
@@ -3512,6 +3535,8 @@ impl<Clock: LogicalClock> MvStore<Clock> {
35123535 last_committed_tx_ts : AtomicU64 :: new ( 0 ) ,
35133536 last_global_header_ts : AtomicU64 :: new ( 0 ) ,
35143537 table_id_to_last_rowid : RwLock :: new ( HashMap :: default ( ) ) ,
3538+ sequence_watermarks : Mutex :: new ( HashMap :: default ( ) ) ,
3539+ sequence_allocations : Mutex :: new ( HashMap :: default ( ) ) ,
35153540 }
35163541 }
35173542
@@ -4744,6 +4769,7 @@ impl<Clock: LogicalClock> MvStore<Clock> {
47444769 }
47454770
47464771 pub fn remove_tx ( & self , tx_id : TxID ) {
4772+ self . remove_sequence_allocations ( tx_id) ;
47474773 if let Some ( entry) = self . txs . get ( & tx_id) {
47484774 let tx = entry. value ( ) ;
47494775 if let TransactionState :: Committed ( commit_ts) = tx. state . load ( ) {
@@ -4768,6 +4794,87 @@ impl<Clock: LogicalClock> MvStore<Clock> {
47684794 self . blocking_checkpoint_lock . unlock ( ) ;
47694795 }
47704796
4797+ pub fn register_sequence_allocation (
4798+ & self ,
4799+ tx_id : TxID ,
4800+ sequence_name : & str ,
4801+ sequence_value : i64 ,
4802+ ) -> Result < ( ) > {
4803+ let Some ( tx) = self . txs . get ( & tx_id) else {
4804+ return Err ( LimboError :: NoSuchTransactionID ( tx_id. to_string ( ) ) ) ;
4805+ } ;
4806+ turso_assert ! (
4807+ matches!(
4808+ tx. value( ) . state. load( ) ,
4809+ TransactionState :: Active | TransactionState :: Preparing ( _)
4810+ ) ,
4811+ "sequence allocation must be registered while the transaction is active or preparing"
4812+ ) ;
4813+
4814+ let sequence_name = crate :: util:: normalize_ident ( sequence_name) ;
4815+ let mut allocations = self . sequence_allocations . lock ( ) ;
4816+ let tx_allocations = allocations. entry ( sequence_name) . or_default ( ) ;
4817+ tx_allocations
4818+ . entry ( tx_id)
4819+ . and_modify ( |value| * value = ( * value) . min ( sequence_value) )
4820+ . or_insert ( sequence_value) ;
4821+ Ok ( ( ) )
4822+ }
4823+
4824+ pub fn set_sequence_watermark ( & self , sequence_name : & str , watermark : i64 ) {
4825+ let sequence_name = crate :: util:: normalize_ident ( sequence_name) ;
4826+ self . sequence_watermarks
4827+ . lock ( )
4828+ . insert ( sequence_name, watermark) ;
4829+ }
4830+
4831+ /// Returns the first sequence value that is not safe for cursor scans to pass.
4832+ ///
4833+ /// Readers can safely consume rows with sequence values less than this
4834+ /// watermark. The value is the minimum of the current sequence boundary and
4835+ /// any lower value already allocated by an active transaction.
4836+ pub fn sequence_watermark ( & self , sequence_name : & str ) -> Option < i64 > {
4837+ let sequence_name = crate :: util:: normalize_ident ( sequence_name) ;
4838+ let mut allocations = self . sequence_allocations . lock ( ) ;
4839+ let mut remove_allocations = false ;
4840+ let active_watermark = {
4841+ allocations
4842+ . get_mut ( & sequence_name)
4843+ . and_then ( |tx_allocations| {
4844+ tx_allocations. retain ( |tx_id, _| {
4845+ self . txs . get ( tx_id) . is_some_and ( |tx| {
4846+ matches ! (
4847+ tx. value( ) . state. load( ) ,
4848+ TransactionState :: Active | TransactionState :: Preparing ( _)
4849+ )
4850+ } )
4851+ } ) ;
4852+ let watermark = tx_allocations. values ( ) . copied ( ) . min ( ) ;
4853+ if tx_allocations. is_empty ( ) {
4854+ remove_allocations = true ;
4855+ }
4856+ watermark
4857+ } )
4858+ } ;
4859+ if remove_allocations {
4860+ allocations. remove ( & sequence_name) ;
4861+ }
4862+ let current_watermark = self . sequence_watermarks . lock ( ) . get ( & sequence_name) . copied ( ) ;
4863+ match ( current_watermark, active_watermark) {
4864+ ( Some ( current) , Some ( active) ) => Some ( current. min ( active) ) ,
4865+ ( Some ( current) , None ) => Some ( current) ,
4866+ ( None , active) => active,
4867+ }
4868+ }
4869+
4870+ fn remove_sequence_allocations ( & self , tx_id : TxID ) {
4871+ let mut allocations = self . sequence_allocations . lock ( ) ;
4872+ allocations. retain ( |_, tx_allocations| {
4873+ tx_allocations. remove ( & tx_id) ;
4874+ !tx_allocations. is_empty ( )
4875+ } ) ;
4876+ }
4877+
47714878 /// Atomically retire a committed tx: clear the connection's mv_tx_id cache
47724879 /// for `db_id`, then remove the tx from `txs`. Pairs the two mutations so
47734880 /// no concurrent observer (or in-flight statement) can see the divergent
0 commit comments