char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
78f4748
main
1use std:: io; 2use std:: ops:: Bound ; 3use std:: path:: Path ; 4use std:: sync::{ Arc , Mutex , MutexGuard }; 5 6use redb::{ 7Database , DatabaseError , ReadOnlyDatabase , ReadableDatabase , ReadableTable , 8ReadableTableMetadata , TableDefinition , TableHandle , 9}; 10 11// Keyspace `name` lives in table "ks:{name}", mapping key bytes to (commit 12// counter, value bytes). The prefix keeps keyspaces apart from META, which 13// holds the commit counter under "" and adapter metadata under any other key. 14// Not "meta": pre-unification Web Storage files have a table by that name. 15const META : TableDefinition < & str , u64 > =TableDefinition :: new ( "kv_meta" ); 16const COUNTER_KEY : & str ="" ; 17const KEYSPACE_PREFIX : & str ="ks:" ; 18 19type Bytes =&' static [ u8 ]; 20type Entry =( u64 , Bytes ); 21 22fn keyspace_table_name ( name : & str ) ->String { 23format! ( "{KEYSPACE_PREFIX}{name}" ) 24} 25 26pub fn stamp_string ( counter : u64 ) ->String { 27 counter. to_string () 28} 29 30pub fn parse_stamp ( stamp : & str ) -> io:: Result < u64 > { 31// Only the canonical rendering parses, so equal stamps are equal strings. 32// (u64::from_str alone would also accept a leading '+' or zeros.) 33if !stamp. bytes (). all ( |b| b. is_ascii_digit ()) ||( stamp. len () >1 && stamp. starts_with ( '0' )) { 34return Err ( io:: Error :: new ( 35 io:: ErrorKind :: InvalidInput , 36"Invalid kv versionstamp" , 37)); 38} 39 stamp 40. parse () 41. map_err ( |_| io:: Error :: new ( io:: ErrorKind :: InvalidInput , "Invalid kv versionstamp" )) 42} 43 44pub fn check_keyspace_name ( name : & str ) -> io:: Result <()> { 45if name. is_empty () { 46return Err ( io:: Error :: new ( 47 io:: ErrorKind :: InvalidInput , 48"Keyspace name must not be empty" , 49)); 50} 51Ok (()) 52} 53 54// "" is the commit counter's slot, so adapter keys cannot touch it. 55fn check_meta_key ( key : & str ) -> io:: Result <()> { 56if key. is_empty () { 57return Err ( io:: Error :: new ( 58 io:: ErrorKind :: InvalidInput , 59"kv metadata keys must be non-empty" , 60)); 61} 62Ok (()) 63} 64 65# [ derive ( Debug , Clone , Copy , PartialEq , Eq )] 66pub enum Access { 67Shared , 68Exclusive , 69ReadOnly , 70} 71 72impl Access { 73pub fn parse ( value : & str ) -> io:: Result < Self > { 74match value{ 75"shared" =>Ok ( Access :: Shared ), 76"exclusive" =>Ok ( Access :: Exclusive ), 77"read-only" =>Ok ( Access :: ReadOnly ), 78 _ =>Err ( io:: Error :: new ( 79 io:: ErrorKind :: InvalidInput , 80"access must be \"shared\", \"exclusive\" or \"read-only\"" , 81)), 82} 83} 84 85pub fn writes ( self ) ->bool { 86self !=Access :: ReadOnly 87} 88} 89 90enum Held { 91// Shared mode never holds the file: every operation opens and closes it, 92// serialized by the store mutex so same-process opens cannot collide. 93Shared { path : std:: path:: PathBuf }, 94Exclusive { db : Database }, 95// A missing file stays unopened so read-only never creates anything; 96// reads then behave as empty. 97ReadOnly { db : Option < ReadOnlyDatabase > }, 98} 99 100// A stored entry: raw value bytes plus the opaque versionstamp of the 101// commit that wrote it. 102pub type StoredEntry =( Vec < u8 >, String ); 103 104// A listed entry: key plus StoredEntry. 105pub type ListedEntry =( Vec < u8 >, Vec < u8 >, String ); 106 107/// Outcome of a [`Store::write_tx`] closure: commit the transaction or drop 108/// it uncommitted. Aborting changes nothing, including keyspace registration. 109pub enum Decision < T > { 110Commit ( T ), 111Abort ( T ), 112} 113 114struct StoreInner { 115// Serializes all operations on this database. Shared mode relies on this 116// for same-process safety; clones of one Store share the same mutex, so 117// closures must not re-enter the Store or they will deadlock. 118mutex : Mutex <()>, 119held : Held , 120} 121 122fn lock ( mutex : & Mutex <()>) ->MutexGuard < ' _ , ()> { 123 mutex. lock (). unwrap_or_else ( |error| error. into_inner ()) 124} 125 126/// Synchronous byte-oriented redb backend. Cheap to clone and share across 127/// threads; every operation runs to completion under one mutex. 128# [ derive ( Clone )] 129pub struct Store { 130inner : Arc < StoreInner >, 131} 132 133impl std:: fmt:: Debug for Store { 134fn fmt ( & self , f : & mut std:: fmt:: Formatter < ' _ >) -> std:: fmt:: Result { 135 f. debug_struct ( "Store" ). finish_non_exhaustive () 136} 137} 138 139fn db_error ( path : & Path , error : DatabaseError ) -> io:: Error { 140match error{ 141DatabaseError :: DatabaseAlreadyOpen => io:: Error :: new ( 142 io:: ErrorKind :: AlreadyExists , 143format! ( "kv database is already open: {}" , path. display ()), 144), 145DatabaseError :: Storage ( redb:: StorageError :: Io ( error)) 146if error. kind () == io:: ErrorKind :: NotFound => 147{ 148 io:: Error :: new ( 149 io:: ErrorKind :: NotFound , 150format! ( "kv database does not exist: {}" , path. display ()), 151) 152} 153 error => io:: Error :: other ( format! ( "kv database at {}: {error}" , path. display ())), 154} 155} 156 157fn is_empty_file ( path : & Path ) ->bool { 158 std:: fs:: metadata ( path). is_ok_and ( |metadata| metadata. len () ==0 ) 159} 160 161fn create_database ( path : & Path ) ->Result < Database , DatabaseError > { 162match Database :: create ( path) { 163// A crash between creating the file and initializing it leaves an 164// empty file; retry from scratch rather than reporting corruption. 165Err ( _) if is_empty_file ( path) =>{ 166let _ = std:: fs:: remove_file ( path); 167Database :: create ( path) 168} 169 result => result, 170} 171} 172 173fn open_database ( path : & Path , create : bool ) -> io:: Result < Database > { 174if let Some ( parent) = path. parent () 175 && !parent. as_os_str (). is_empty () 176 && !parent. exists () 177{ 178if !create{ 179return Err ( io:: Error :: new ( 180 io:: ErrorKind :: NotFound , 181format! ( "kv database does not exist: {}" , path. display ()), 182)); 183} 184return Err ( io:: Error :: new ( 185 io:: ErrorKind :: NotFound , 186format! ( 187"kv database parent directory does not exist: {}" , 188 parent. display () 189), 190)); 191} 192if path. is_dir () { 193return Err ( io:: Error :: new ( 194 io:: ErrorKind :: InvalidInput , 195format! ( "kv database path is a directory: {}" , path. display ()), 196)); 197} 198let result =if create{ 199create_database ( path) 200} else if !path. exists () ||is_empty_file ( path) { 201return Err ( io:: Error :: new ( 202 io:: ErrorKind :: NotFound , 203format! ( "kv database does not exist: {}" , path. display ()), 204)); 205} else { 206Database :: open ( path) 207}; 208 result. map_err ( |error|db_error ( path, error)) 209} 210 211// Missing or empty files stay unopened so read-only never creates anything. 212fn read_only_database ( path : & Path ) ->Result < Option < ReadOnlyDatabase >, DatabaseError > { 213if !path. exists () ||is_empty_file ( path) { 214return Ok ( None ); 215} 216ReadOnlyDatabase :: open ( path). map ( Some ) 217} 218 219impl Store { 220pub fn open ( path : & Path , access : Access , create : bool ) -> io:: Result < Self > { 221// Lock conflicts surface as DatabaseAlreadyOpen via db_error; redb 222// locks are non-blocking, so there is no spin or wait here. 223let held =match access{ 224Access :: ReadOnly if create =>{ 225return Err ( io:: Error :: new ( 226 io:: ErrorKind :: InvalidInput , 227"read-only kv databases cannot be created" , 228)); 229} 230Access :: Shared =>{ 231// Probe once so open() itself reports missing, locked or 232// corrupt files instead of deferring the failure to the first 233// operation. 234open_database ( path, create) ?; 235Held :: Shared { path : path. to_owned () } 236} 237Access :: Exclusive =>Held :: Exclusive { db : open_database ( path, create) ?}, 238Access :: ReadOnly =>match read_only_database ( path) { 239Err ( DatabaseError :: RepairAborted ) =>{ 240return Err ( io:: Error :: other ( format! ( 241"kv database at {} needs repair; open it with write access first" , 242 path. display () 243))); 244} 245 result =>Held :: ReadOnly { 246db : result. map_err ( |error|db_error ( path, error)) ?, 247}, 248}, 249}; 250Ok ( Store :: new ( held)) 251} 252 253/// Open read-only, repairing an unclean shutdown first. ReadOnlyDatabase 254/// cannot repair on its own, so a RepairAborted open is followed by 255/// exactly one writable open (which repairs) and a single read-only 256/// retry; if the file still needs repair afterwards, that error is 257/// returned. Missing or empty files open as empty without creating 258/// anything, like [`Store::open`] with [`Access::ReadOnly`]. 259pub fn open_read_only_repairing ( path : & Path ) -> io:: Result < Self > { 260let db =match read_only_database ( path) { 261Err ( DatabaseError :: RepairAborted ) =>{ 262Database :: open ( path). map_err ( |error|db_error ( path, error)) ?; 263read_only_database ( path) 264} 265 result => result, 266}; 267Ok ( Store :: new ( Held :: ReadOnly { 268db : db. map_err ( |error|db_error ( path, error)) ?, 269})) 270} 271 272fn new ( held : Held ) ->Self { 273Store { 274inner : Arc :: new ( StoreInner { mutex : Mutex :: new (()), held}), 275} 276} 277} 278 279/// Read-only view of one transaction. Constructed only by [`Store::read_tx`]. 280pub struct ReadTx < ' a > { 281// None for a database file that does not exist: every read is empty. 282txn : Option < &' a redb:: ReadTransaction >, 283} 284 285/// Read-write view of one transaction. Constructed only by 286/// [`Store::write_tx`]; entry mutations allocate one commit stamp per 287/// transaction, lazily on the first mutation, so a batch of writes shares 288/// exactly one stamp. 289pub struct WriteTx < ' a > { 290txn : &' a redb:: WriteTransaction , 291counter : Option < u64 >, 292} 293 294fn commit_counter ( meta : & redb:: Table < ' _ , & str , u64 >) -> io:: Result < u64 > { 295Ok ( meta 296. get ( COUNTER_KEY ) 297. map_err ( io:: Error :: other) ? 298. map_or ( 0 , |counter| counter. value ())) 299} 300 301// Allocates the transaction's stamp on first use. Entry writes share one 302// counter bump per transaction; metadata-only work never allocates one. 303fn bump_counter ( meta : & mut redb:: Table < ' _ , & str , u64 >, slot : & mut Option < u64 >) -> io:: Result < u64 > { 304if let Some ( counter) =* slot{ 305return Ok ( counter); 306} 307let counter =commit_counter ( meta) ? +1 ; 308 meta. insert ( COUNTER_KEY , counter) 309. map_err ( io:: Error :: other) ?; 310* slot =Some ( counter); 311Ok ( counter) 312} 313 314fn read_meta ( meta : & impl ReadableTable < &' static str , u64 >, key : & str ) -> io:: Result < Option < u64 >> { 315check_meta_key ( key) ?; 316Ok ( meta 317. get ( key) 318. map_err ( io:: Error :: other) ? 319. map ( |value| value. value ())) 320} 321 322// The entry type is dictated by redb's range iterator. 323# [ allow ( clippy :: type_complexity )] 324fn decode_entry ( 325entry : Result <( redb:: AccessGuard < ' _ , Bytes >, redb:: AccessGuard < ' _ , Entry >), redb:: StorageError >, 326) -> io:: Result < ListedEntry > { 327let ( key, stored) = entry. map_err ( io:: Error :: other) ?; 328let ( counter, value) = stored. value (); 329Ok (( key. value (). to_vec (), value. to_vec (), stamp_string ( counter))) 330} 331 332// Exclusive end bound for a prefix: increment the last non-0xff byte. A 333// prefix of all 0xff has no successor, so the range stays unbounded above. 334fn prefix_successor ( prefix : & [ u8 ]) ->Option < Vec < u8 >> { 335let mut end = prefix. to_vec (); 336while end. last () ==Some ( & 0xff ) { 337 end. pop (); 338} 339let last = end. last_mut () ?; 340* last +=1 ; 341Some ( end) 342} 343 344const MAX_LIST : usize =10_000 ; 345 346fn open_or_absent < T >( result : Result < T , redb:: TableError >) -> io:: Result < Option < T >> { 347match result{ 348Ok ( table) =>Ok ( Some ( table)), 349Err ( redb:: TableError :: TableDoesNotExist ( _)) =>Ok ( None ), 350Err ( error) =>Err ( io:: Error :: other ( error)), 351} 352} 353 354// The transaction kinds ReadTx and WriteTx read through, so each read is 355// written once. An absent table reads as empty and is never created. 356trait Tables { 357fn table_names ( & self ) -> io:: Result < Vec < String >>; 358 359type Table < ' t , K : redb:: Key +' static , V : redb:: Value +' static >: ReadableTable < K , V > 360where 361Self : ' t ; 362 363fn table < K : redb:: Key +' static , V : redb:: Value +' static >( 364& self , 365definition : TableDefinition < K , V >, 366) -> io:: Result < Option < Self :: Table < ' _ , K , V >>>; 367} 368 369impl Tables for Option < & redb:: ReadTransaction > { 370fn table_names ( & self ) -> io:: Result < Vec < String >> { 371let Some ( txn) =self else { 372return Ok ( Vec :: new ()); 373}; 374let tables = txn. list_tables (). map_err ( io:: Error :: other) ?; 375Ok ( tables. map ( |table| table. name (). to_owned ()). collect ()) 376} 377 378type Table < ' t , K : redb:: Key +' static , V : redb:: Value +' static > 379 = redb:: ReadOnlyTable < K , V > 380where 381Self : ' t ; 382 383fn table < K : redb:: Key +' static , V : redb:: Value +' static >( 384& self , 385definition : TableDefinition < K , V >, 386) -> io:: Result < Option < redb:: ReadOnlyTable < K , V >>> { 387match self { 388Some ( txn) =>open_or_absent ( txn. open_table ( definition)), 389None =>Ok ( None ), 390} 391} 392} 393 394impl Tables for redb:: WriteTransaction { 395type Table < ' t , K : redb:: Key +' static , V : redb:: Value +' static > = redb:: Table < ' t , K , V >; 396 397fn table_names ( & self ) -> io:: Result < Vec < String >> { 398let tables =self . list_tables (). map_err ( io:: Error :: other) ?; 399Ok ( tables. map ( |table| table. name (). to_owned ()). collect ()) 400} 401 402// open_table creates a missing table, so check first: reads, checks and 403// deletes inside a write transaction must not materialize anything. 404fn table < K : redb:: Key +' static , V : redb:: Value +' static >( 405& self , 406definition : TableDefinition < K , V >, 407) -> io:: Result < Option < redb:: Table < ' _ , K , V >>> { 408if !self 409. table_names () ? 410. iter () 411. any ( |name| name == definition. name ()) 412{ 413return Ok ( None ); 414} 415self . open_table ( definition) 416. map ( Some ) 417. map_err ( io:: Error :: other) 418} 419} 420 421fn keyspace_table < ' t , T : Tables >( 422txn : &' t T , 423keyspace : & str , 424) -> io:: Result < Option < T :: Table < ' t , Bytes , Entry >>> { 425check_keyspace_name ( keyspace) ?; 426 txn. table ( TableDefinition :: new ( & keyspace_table_name ( keyspace))) 427} 428 429fn get_entry ( txn : & impl Tables , keyspace : & str , key : & [ u8 ]) -> io:: Result < Option < StoredEntry >> { 430let Some ( table) =keyspace_table ( txn, keyspace) ?else { 431return Ok ( None ); 432}; 433let Some ( entry) = table. get ( key). map_err ( io:: Error :: other) ?else { 434return Ok ( None ); 435}; 436let ( counter, value) = entry. value (); 437Ok ( Some (( value. to_vec (), stamp_string ( counter)))) 438} 439 440fn scan ( 441txn : & impl Tables , 442keyspace : & str , 443bounds : ( Bound < & [ u8 ]>, Bound < & [ u8 ]>), 444limit : usize , 445reverse : bool , 446) -> io:: Result < Vec < ListedEntry >> { 447let Some ( table) =keyspace_table ( txn, keyspace) ?else { 448return Ok ( Vec :: new ()); 449}; 450let range = table. range ::< & [ u8 ]>( bounds). map_err ( io:: Error :: other) ?; 451let range: Box < dyn Iterator < Item =_ >> =if reverse{ 452Box :: new ( range. rev ()) 453} else { 454Box :: new ( range) 455}; 456 range. take ( limit). map ( decode_entry). collect () 457} 458 459fn entries ( txn : & impl Tables , keyspace : & str ) -> io:: Result < Vec < ListedEntry >> { 460scan ( 461 txn, 462 keyspace, 463( Bound :: Unbounded , Bound :: Unbounded ), 464 usize:: MAX , 465false , 466) 467} 468 469fn count ( txn : & impl Tables , keyspace : & str ) -> io:: Result < u64 > { 470keyspace_table ( txn, keyspace) ?. map_or ( Ok ( 0 ), |table| table. len (). map_err ( io:: Error :: other)) 471} 472 473fn list_keyspaces ( txn : & impl Tables ) -> io:: Result < Vec < String >> { 474let mut names: Vec < String > = txn 475. table_names () ? 476. into_iter () 477. filter_map ( |name| name. strip_prefix ( KEYSPACE_PREFIX ). map ( str:: to_owned)) 478. collect (); 479 names. sort (); 480Ok ( names) 481} 482 483fn get_meta ( txn : & impl Tables , key : & str ) -> io:: Result < Option < u64 >> { 484 txn. table ( META ) ? 485. map_or ( Ok ( None ), |meta|read_meta ( & meta, key)) 486} 487 488impl ReadTx < ' _ > { 489pub fn get ( & self , keyspace : & str , key : & [ u8 ]) -> io:: Result < Option < StoredEntry >> { 490get_entry ( & self . txn , keyspace, key) 491} 492 493pub fn entries ( & self , keyspace : & str ) -> io:: Result < Vec < ListedEntry >> { 494entries ( & self . txn , keyspace) 495} 496 497/// Entries within `[start, end)` and under `prefix`, at most `limit` 498/// (capped at 10000) of them. 499# [ allow ( clippy :: too_many_arguments )] 500pub fn list ( 501& self , 502keyspace : & str , 503start : Option < & [ u8 ]>, 504end : Option < & [ u8 ]>, 505prefix : Option < & [ u8 ]>, 506limit : usize , 507reverse : bool , 508) -> io:: Result < Vec < ListedEntry >> { 509// Intersect explicit bounds with the prefix range; the prefix end 510// is the successor exclusive bound (absent when all bytes are 0xff). 511let prefix_end = prefix. and_then ( prefix_successor); 512let lower =match ( start, prefix) { 513( Some ( a), Some ( b)) =>Bound :: Included ( a. max ( b)), 514( Some ( a), None ) =>Bound :: Included ( a), 515( None , Some ( b)) =>Bound :: Included ( b), 516( None , None ) =>Bound :: Unbounded , 517}; 518let upper =match ( end, prefix_end. as_deref ()) { 519( Some ( a), Some ( b)) =>Bound :: Excluded ( a. min ( b)), 520( Some ( a), None ) =>Bound :: Excluded ( a), 521( None , Some ( b)) =>Bound :: Excluded ( b), 522( None , None ) =>Bound :: Unbounded , 523}; 524scan ( 525& self . txn , 526 keyspace, 527( lower, upper), 528 limit. min ( MAX_LIST ), 529 reverse, 530) 531} 532 533pub fn count ( & self , keyspace : & str ) -> io:: Result < u64 > { 534count ( & self . txn , keyspace) 535} 536 537pub fn list_keyspaces ( & self ) -> io:: Result < Vec < String >> { 538list_keyspaces ( & self . txn ) 539} 540 541pub fn get_meta ( & self , key : & str ) -> io:: Result < Option < u64 >> { 542get_meta ( & self . txn , key) 543} 544} 545 546impl WriteTx < ' _ > { 547pub fn get ( & self , keyspace : & str , key : & [ u8 ]) -> io:: Result < Option < StoredEntry >> { 548get_entry ( self . txn , keyspace, key) 549} 550 551pub fn put ( & mut self , keyspace : & str , key : & [ u8 ], value : & [ u8 ]) -> io:: Result < String > { 552check_keyspace_name ( keyspace) ?; 553let counter ={ 554let mut meta =self . txn . open_table ( META ). map_err ( io:: Error :: other) ?; 555bump_counter ( & mut meta, & mut self . counter ) ? 556}; 557let name =keyspace_table_name ( keyspace); 558let mut table =self 559. txn 560. open_table ( TableDefinition ::< Bytes , Entry >:: new ( & name)) 561. map_err ( io:: Error :: other) ?; 562 table 563. insert ( key, ( counter, value)) 564. map_err ( io:: Error :: other) ?; 565Ok ( stamp_string ( counter)) 566} 567 568pub fn delete ( & mut self , keyspace : & str , key : & [ u8 ]) -> io:: Result < bool > { 569let Some ( mut table) =keyspace_table ( self . txn , keyspace) ?else { 570return Ok ( false ); 571}; 572if table. remove ( key). map_err ( io:: Error :: other) ?. is_none () { 573return Ok ( false ); 574} 575drop ( table); 576let mut meta =self . txn . open_table ( META ). map_err ( io:: Error :: other) ?; 577bump_counter ( & mut meta, & mut self . counter ) ?; 578Ok ( true ) 579} 580 581/// Empty the keyspace, materializing it if it did not exist. 582pub fn clear ( & mut self , keyspace : & str ) -> io:: Result <()> { 583check_keyspace_name ( keyspace) ?; 584let name =keyspace_table_name ( keyspace); 585let definition =TableDefinition ::< Bytes , Entry >:: new ( & name); 586self . txn 587. delete_table ( definition) 588. map_err ( io:: Error :: other) ?; 589self . txn . open_table ( definition). map_err ( io:: Error :: other) ?; 590Ok (()) 591} 592 593pub fn entries ( & self , keyspace : & str ) -> io:: Result < Vec < ListedEntry >> { 594entries ( self . txn , keyspace) 595} 596 597pub fn count ( & self , keyspace : & str ) -> io:: Result < u64 > { 598count ( self . txn , keyspace) 599} 600 601pub fn list_keyspaces ( & self ) -> io:: Result < Vec < String >> { 602list_keyspaces ( self . txn ) 603} 604 605pub fn get_meta ( & self , key : & str ) -> io:: Result < Option < u64 >> { 606get_meta ( self . txn , key) 607} 608 609pub fn put_meta ( & mut self , key : & str , value : u64 ) -> io:: Result <()> { 610check_meta_key ( key) ?; 611let mut meta =self . txn . open_table ( META ). map_err ( io:: Error :: other) ?; 612 meta. insert ( key, value). map_err ( io:: Error :: other) ?; 613Ok (()) 614} 615 616/// This transaction's commit stamp, if an entry mutation allocated one. 617/// Check-only work leaves it empty; entry writes share exactly one stamp. 618pub fn stamp ( & self ) ->Option < String > { 619self . counter . map ( stamp_string) 620} 621 622/// Allocate the commit stamp without writing an entry, for commits that 623/// must carry a stamp even though no entry changed. 624pub fn allocate_stamp ( & mut self ) -> io:: Result < String > { 625let mut meta =self . txn . open_table ( META ). map_err ( io:: Error :: other) ?; 626Ok ( stamp_string ( bump_counter ( & mut meta, & mut self . counter ) ?)) 627} 628} 629 630impl Store { 631/// Run read-only work in one read transaction. 632pub fn read_tx < T >( & self , work : impl FnOnce ( & ReadTx ) -> io:: Result < T >) -> io:: Result < T > { 633let _guard =lock ( & self . inner . mutex ); 634let mut opened =None ; 635let txn =match & self . inner . held { 636Held :: Shared { path} =>match open_database ( path, false ) { 637Ok ( db) =>Some ( opened. insert ( db). begin_read ()), 638// A shared file removed since open reads as empty, like a 639// missing read-only file. 640Err ( error) if error. kind () == io:: ErrorKind :: NotFound =>None , 641Err ( error) =>return Err ( error), 642}, 643Held :: Exclusive { db} =>Some ( db. begin_read ()), 644Held :: ReadOnly { db} => db. as_ref (). map ( ReadableDatabase :: begin_read), 645}; 646let txn = txn. transpose (). map_err ( io:: Error :: other) ?; 647work ( & ReadTx { txn : txn. as_ref () }) 648} 649 650/// Run read-write work in one write transaction, committing on 651/// [`Decision::Commit`] and dropping everything uncommitted on 652/// [`Decision::Abort`]. 653pub fn write_tx < T >( 654& self , 655work : impl FnOnce ( & mut WriteTx ) -> io:: Result < Decision < T >>, 656) -> io:: Result < T > { 657let _guard =lock ( & self . inner . mutex ); 658let mut opened =None ; 659let db =match & self . inner . held { 660Held :: Shared { path} =>&* opened. insert ( open_database ( path, false ) ?), 661Held :: Exclusive { db} => db, 662Held :: ReadOnly { ..} =>{ 663return Err ( io:: Error :: new ( 664 io:: ErrorKind :: PermissionDenied , 665"kv database is open read-only" , 666)); 667} 668}; 669let txn = db. begin_write (). map_err ( io:: Error :: other) ?; 670match work ( & mut WriteTx { txn : & txn, counter : None }) ?{ 671Decision :: Commit ( value) => txn. commit (). map_err ( io:: Error :: other). map ( |() | value), 672Decision :: Abort ( value) =>Ok ( value), 673} 674} 675}