@@ -119,6 +119,19 @@ impl<PdC: PdClient> Transaction<PdC> {
119119 }
120120 }
121121
122+ /// Get the start timestamp of the transaction.
123+ ///
124+ /// This is the TSO timestamp at which the transaction reads. The
125+ /// transaction's writes become visible at its *commit* timestamp
126+ /// (returned by [`commit`](Transaction::commit)), not at `start_ts`.
127+ ///
128+ /// The `physical` component is a cluster-wide wall clock in
129+ /// milliseconds, useful as a consistent clock reading across processes
130+ /// (e.g. checking lease or deadline expiry).
131+ pub fn start_ts ( & self ) -> Timestamp {
132+ self . timestamp . clone ( )
133+ }
134+
122135 /// Create a new 'get' request
123136 ///
124137 /// Once resolved this request will result in the fetching of the value associated with the
@@ -448,6 +461,70 @@ impl<PdC: PdClient> Transaction<PdC> {
448461 . map ( KvPair :: into_key) )
449462 }
450463
464+ /// Create a 'scan' request without a limit.
465+ ///
466+ /// Once resolved this request will result in a `Vec` of ALL key-value pairs
467+ /// that lie in the specified range, ordered by key.
468+ ///
469+ /// Internally the range is fetched in batches of `SCAN_UNBOUNDED_BATCH_SIZE`
470+ /// pairs, so no single RPC carries an unbounded limit and each response
471+ /// message stays bounded in size. This is the equivalent of client-go's
472+ /// `KVSnapshot.Iter(start, end)`.
473+ ///
474+ /// # Examples
475+ ///
476+ /// ```rust,no_run
477+ /// # use tikv_client::{Key, KvPair, Value, Config, TransactionClient};
478+ /// # use futures::prelude::*;
479+ /// # futures::executor::block_on(async {
480+ /// # let client = TransactionClient::new(vec!["192.168.0.100", "192.168.0.101"]).await.unwrap();
481+ /// let mut txn = client.begin_optimistic().await.unwrap();
482+ /// let key1: Key = b"foo".to_vec().into();
483+ /// let key2: Key = b"bar".to_vec().into();
484+ /// let result: Vec<KvPair> = txn
485+ /// .scan_unbounded(key1..key2)
486+ /// .await
487+ /// .unwrap()
488+ /// .collect();
489+ /// // Finish the transaction...
490+ /// txn.commit().await.unwrap();
491+ /// # });
492+ /// ```
493+ pub async fn scan_unbounded (
494+ & mut self ,
495+ range : impl Into < BoundRange > ,
496+ ) -> Result < impl Iterator < Item = KvPair > > {
497+ debug ! ( "invoking transactional scan_unbounded request" ) ;
498+ let ( start, end) = range. into ( ) . into_keys ( ) ;
499+ let mut start = start;
500+ let mut out: Vec < KvPair > = Vec :: new ( ) ;
501+ loop {
502+ let page: Vec < KvPair > = self
503+ . scan ( ( start. clone ( ) , end. clone ( ) ) , SCAN_UNBOUNDED_BATCH_SIZE )
504+ . await ?
505+ . collect ( ) ;
506+ // Termination follows client-go's Scanner (txnsnapshot/scan.go):
507+ // a short page means the range is exhausted — the plan layer fans
508+ // a scan out to every region in the range and each region returns
509+ // up to `limit` pairs, so a merged page smaller than the batch can
510+ // only happen when no region has more data. A full page means more
511+ // data may remain; advance past the last returned key with
512+ // `next_key()` (the same key+'\x00' successor trick client-go
513+ // uses) and fetch again. If the total is an exact multiple of the
514+ // batch size, this costs one extra empty page fetch, same as
515+ // client-go discovering EOF on its next getData.
516+ let full_page = page. len ( ) as u32 == SCAN_UNBOUNDED_BATCH_SIZE ;
517+ if let Some ( last) = page. last ( ) {
518+ start = last. key ( ) . clone ( ) . next_key ( ) ;
519+ }
520+ out. extend ( page) ;
521+ if !full_page {
522+ break ;
523+ }
524+ }
525+ Ok ( out. into_iter ( ) )
526+ }
527+
451528 /// Sets the value associated with the given key.
452529 ///
453530 /// # Examples
@@ -1128,6 +1205,9 @@ const DEFAULT_HEARTBEAT_INTERVAL: Duration = Duration::from_millis(MAX_TTL / 2);
11281205/// TiKV recommends each RPC packet should be less than around 1MB. We keep KV size of
11291206/// each request below 16KB.
11301207pub const TXN_COMMIT_BATCH_SIZE : u64 = 16 * 1024 ;
1208+
1209+ /// Batch size used internally by `scan_unbounded` to paginate through a range.
1210+ const SCAN_UNBOUNDED_BATCH_SIZE : u32 = 1024 ;
11311211const TTL_FACTOR : f64 = 6000.0 ;
11321212
11331213/// Optimistic or pessimistic transaction.
@@ -1701,6 +1781,7 @@ impl From<u8> for TransactionStatus {
17011781#[ cfg( test) ]
17021782mod tests {
17031783 use std:: any:: Any ;
1784+ use std:: collections:: BTreeMap ;
17041785 use std:: io;
17051786 use std:: sync:: atomic:: AtomicUsize ;
17061787 use std:: sync:: atomic:: Ordering ;
@@ -1717,6 +1798,7 @@ mod tests {
17171798 use crate :: proto:: pdpb:: Timestamp ;
17181799 use crate :: request:: Keyspace ;
17191800 use crate :: transaction:: HeartbeatOption ;
1801+ use crate :: KvPair ;
17201802 use crate :: TimestampExt ;
17211803 use crate :: Transaction ;
17221804 use crate :: TransactionOptions ;
@@ -1870,4 +1952,85 @@ mod tests {
18701952 "expected a lifecycle log carrying start_ts {start_ts}; captured: {logs:?}"
18711953 ) ;
18721954 }
1955+
1956+ #[ tokio:: test]
1957+ async fn start_ts_returns_transaction_timestamp ( ) {
1958+ let ts = Timestamp {
1959+ physical : 1_700_000_000_123 ,
1960+ logical : 42 ,
1961+ ..Default :: default ( )
1962+ } ;
1963+ let txn = Transaction :: new (
1964+ ts. clone ( ) ,
1965+ Arc :: new ( MockPdClient :: default ( ) ) ,
1966+ TransactionOptions :: new_optimistic ( ) . read_only ( ) ,
1967+ Keyspace :: Disable ,
1968+ ) ;
1969+ assert_eq ! ( txn. start_ts( ) , ts) ;
1970+ }
1971+
1972+ #[ tokio:: test]
1973+ async fn scan_unbounded_paginates_through_range ( ) {
1974+ // 2500 pairs force 3 pages at the internal batch size of 1024.
1975+ let data: BTreeMap < Vec < u8 > , Vec < u8 > > = ( 0 ..2500u32 )
1976+ . map ( |i| {
1977+ (
1978+ format ! ( "k{i:04}" ) . into_bytes ( ) ,
1979+ format ! ( "v{i}" ) . into_bytes ( ) ,
1980+ )
1981+ } )
1982+ . collect ( ) ;
1983+ let scan_calls = Arc :: new ( AtomicUsize :: new ( 0 ) ) ;
1984+ let scan_calls_cloned = scan_calls. clone ( ) ;
1985+ let pd_client = Arc :: new ( MockPdClient :: new ( MockKvClient :: with_dispatch_hook (
1986+ move |req : & dyn Any | {
1987+ let scan = req
1988+ . downcast_ref :: < kvrpcpb:: ScanRequest > ( )
1989+ . expect ( "only scan requests are expected" ) ;
1990+ scan_calls_cloned. fetch_add ( 1 , Ordering :: SeqCst ) ;
1991+ assert_eq ! ( scan. limit, super :: SCAN_UNBOUNDED_BATCH_SIZE ) ;
1992+ let mut pairs = Vec :: new ( ) ;
1993+ for ( k, v) in data. range ( scan. start_key . clone ( ) ..) {
1994+ if !scan. end_key . is_empty ( ) && k. as_slice ( ) >= scan. end_key . as_slice ( ) {
1995+ break ;
1996+ }
1997+ if pairs. len ( ) >= scan. limit as usize {
1998+ break ;
1999+ }
2000+ pairs. push ( kvrpcpb:: KvPair {
2001+ key : k. clone ( ) ,
2002+ value : v. clone ( ) ,
2003+ ..Default :: default ( )
2004+ } ) ;
2005+ }
2006+ Ok ( Box :: new ( kvrpcpb:: ScanResponse {
2007+ pairs,
2008+ ..Default :: default ( )
2009+ } ) as Box < dyn Any > )
2010+ } ,
2011+ ) ) ) ;
2012+
2013+ let mut txn = Transaction :: new (
2014+ Timestamp :: default ( ) ,
2015+ pd_client,
2016+ TransactionOptions :: new_optimistic ( ) . read_only ( ) ,
2017+ Keyspace :: Disable ,
2018+ ) ;
2019+ let result: Vec < KvPair > = txn
2020+ . scan_unbounded ( "k0000" . to_owned ( ) ..="k9999" . to_owned ( ) )
2021+ . await
2022+ . unwrap ( )
2023+ . collect ( ) ;
2024+
2025+ assert_eq ! ( result. len( ) , 2500 ) ;
2026+ assert_eq ! ( scan_calls. load( Ordering :: SeqCst ) , 3 ) ;
2027+ let keys: Vec < & [ u8 ] > = result. iter ( ) . map ( |p| p. key ( ) . into ( ) ) . collect ( ) ;
2028+ let mut sorted = keys. clone ( ) ;
2029+ sorted. sort_unstable ( ) ;
2030+ assert_eq ! ( keys, sorted, "pairs must come back in key order" ) ;
2031+ assert_eq ! ( keys. first( ) . unwrap( ) , & b"k0000" ) ;
2032+ assert_eq ! ( keys. last( ) . unwrap( ) , & b"k2499" ) ;
2033+ assert_eq ! ( result[ 0 ] . value( ) , & b"v0" . to_vec( ) ) ;
2034+ assert_eq ! ( result[ 2499 ] . value( ) , & b"v2499" . to_vec( ) ) ;
2035+ }
18732036}
0 commit comments