diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 8aec39f0d..3c75a87f5 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -1,42 +1,94 @@ -name: RocksDB build +name: RocksDB CI on: [push, pull_request] jobs: - test-librocksdb-sys: - name: Test librocksdb-sys crate - runs-on: - - windows-latest + fmt: + name: Rustfmt + runs-on: ubuntu-latest steps: - name: Checkout sources uses: actions/checkout@v2 + - name: Install rust + uses: actions-rs/toolchain@v1 with: - submodules: true - - - name: Install dependencies - run: choco install llvm -y - - - name: Run librocksdb-sys tests + toolchain: stable + components: rustfmt + profile: minimal + override: true + - name: Run rustfmt uses: actions-rs/cargo@v1 with: - command: test - args: --manifest-path=librocksdb-sys/Cargo.toml + command: fmt + args: --all -- --check - test-rocksdb: - name: Test rocksdb crate - runs-on: - - windows-latest + clippy: + name: Clippy + runs-on: ubuntu-latest steps: - name: Checkout sources uses: actions/checkout@v2 with: submodules: true + - name: Install rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + components: clippy + profile: minimal + override: true + - name: Run clippy + uses: actions-rs/clippy-check@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + args: -- -D warnings + + audit: + name: Security audit + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/audit-check@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + test: + name: ${{ matrix.build }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + build: [Linux, macOS, Windows] + include: + - build: Linux + os: ubuntu-latest + - build: macOS + os: macos-latest + - build: Windows + os: windows-latest + steps: + - name: Checkout sources + uses: actions/checkout@v2 + with: + submodules: true + - name: Install rust + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ matrix.rust || 'stable' }} + target: ${{ matrix.target }} + profile: minimal + override: true + - name: Remove msys64 # Workaround to resolve link error with C:\msys64\mingw64\bin\libclang.dll + if: runner.os == 'Windows' + run: Remove-Item -LiteralPath "C:\msys64\" -Force -Recurse - name: Install dependencies + if: runner.os == 'Windows' run: choco install llvm -y - + - name: Run librocksdb-sys tests + uses: actions-rs/cargo@v1 + with: + command: test + args: --manifest-path=librocksdb-sys/Cargo.toml - name: Run rocksdb tests uses: actions-rs/cargo@v1 with: command: test - args: -- --skip test_iterator_outlive_db diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index c92fcf452..000000000 --- a/.travis.yml +++ /dev/null @@ -1,19 +0,0 @@ -language: rust -dist: bionic - -os: -- linux -- osx - -rust: - - stable - -install: - - rustup component add rustfmt - - rustfmt -V - - rustup component add clippy - - cargo clippy --version - -script: - - .travis/lints.sh - - .travis/tests.sh diff --git a/.travis/lints.sh b/.travis/lints.sh deleted file mode 100755 index 0a2018bb3..000000000 --- a/.travis/lints.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -# Run cargo fmt and cargo clippy only on OSX host - -if [[ ${TRAVIS_OS_NAME} == "osx" ]]; then - cargo fmt --all -- --check - cargo clippy --all --tests -- -D warnings -fi diff --git a/.travis/tests.sh b/.travis/tests.sh deleted file mode 100755 index ad10c011e..000000000 --- a/.travis/tests.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash - -cargo test --manifest-path=librocksdb-sys/Cargo.toml -cargo test -- --skip test_iterator_outlive_db diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c91e88c5..5f36a23fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,39 @@ ## [Unreleased] -* Export the `DEFAULT_COLUMN_FAMILY_NAME` constant. +* Add `DB::cancel_all_background_work` method (stanislav-tkach) +* Bump `librocksdb-sys` up to 6.13.3 (aleksuss) +* Add `multi_get`, `multi_get_opt`, `multi_get_cf` and `multi_get_cf_opt` `DB` methods (stanislav-tkach) + +## 0.15.0 (2020-08-25) + +* Fix building rocksdb library on windows host (aleksuss) +* Add github actions CI for windows build (aleksuss) +* Update doc for Options::set_compression_type (wqfish) +* Add clippy linter in CI (aleksuss) +* Use DBPath for backup_restore test (wqfish) +* Allow to build RocksDB with a different stdlib (calavera) +* Add some doc-comments and tiny refactoring (aleksuss) +* Expose `open_with_ttl`. (calavera) +* Fixed build for `x86_64-linux-android` that doesn't support PCLMUL (vimmerru) +* Add support for `SstFileWriter` and `DB::ingest_external_file` (methyl) +* Add set_max_log_file_size and set_recycle_log_file_num to the Options (stanislav-tkach) +* Export the `DEFAULT_COLUMN_FAMILY_NAME` constant (stanislav-tkach) +* Fix slice transformers with no in_domain callback (nelhage) +* Don't segfault on failed a merge operator (nelhage) +* Adding read/write/db/compaction options (linxGnu) +* Add dbpath and env options (linxGnu) +* Add compaction filter factory API (unrealhoang) +* Add link stdlib when linking prebuilt rocksdb (unrealhoang) +* Support fetching sst files metadata, delete files in range, get mem usage (linxGnu) +* Do not set rerun-if-changed=build.rs (xu-cheng) +* Use pretty_assertions in tests (stanislav-tkach) +* librocksdb-sys: update rocksdb to 6.11.4 (ordian) +* Adding backup engine info (linxGnu) +* Implement `Clone` trait for `Options` (stanislav-tkach) +* Added `Send` implementation to `WriteBatch` (stanislav-tkach) +* Extend github actions (stanislav-tkach) +* Avoid copy for merge operator result using delete_callback (xuchen-plus) ## 0.14.0 (2020-04-22) diff --git a/Cargo.toml b/Cargo.toml index 1a34ee8b7..9531f1438 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "rocksdb" description = "Rust wrapper for Facebook's RocksDB embeddable database" -version = "0.14.0" +version = "0.15.0" edition = "2018" authors = ["Tyler Neely ", "David Greenberg "] repository = "https://github.com/rust-rocksdb/rust-rocksdb" @@ -27,8 +27,9 @@ bzip2 = ["librocksdb-sys/bzip2"] [dependencies] libc = "0.2" -librocksdb-sys = { path = "librocksdb-sys", version = "6.10.2" } +librocksdb-sys = { path = "librocksdb-sys", version = "6.11.4" } [dev-dependencies] trybuild = "1.0.21" tempfile = "3.1.0" +pretty_assertions = "0.6.1" diff --git a/librocksdb-sys/Cargo.toml b/librocksdb-sys/Cargo.toml index b175c7fd7..5ec782db9 100644 --- a/librocksdb-sys/Cargo.toml +++ b/librocksdb-sys/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librocksdb-sys" -version = "6.10.2" +version = "6.11.4" edition = "2018" authors = ["Karl Hobley ", "Arkadiy Paronyan "] license = "MIT/Apache-2.0/BSD-3-Clause" @@ -9,7 +9,6 @@ readme = "README.md" repository = "https://github.com/rust-rocksdb/rust-rocksdb" keywords = [ "bindings", "ffi", "rocksdb" ] categories = [ "api-bindings", "database", "external-ffi-bindings" ] - links = "rocksdb" [features] @@ -30,5 +29,5 @@ uuid = { version = "0.8", features = ["v4"] } [build-dependencies] cc = { version = "^1.0", features = ["parallel"] } -bindgen = "0.54" +bindgen = "0.55" glob = "0.3" diff --git a/librocksdb-sys/build.rs b/librocksdb-sys/build.rs index ef94d4ebe..6bee3b76e 100644 --- a/librocksdb-sys/build.rs +++ b/librocksdb-sys/build.rs @@ -147,10 +147,14 @@ fn build_rocksdb() { lib_sources = lib_sources .iter() .cloned() - .filter(|file| match *file { - "port/port_posix.cc" | "env/env_posix.cc" | "env/fs_posix.cc" - | "env/io_posix.cc" => false, - _ => true, + .filter(|file| { + !matches!( + *file, + "port/port_posix.cc" + | "env/env_posix.cc" + | "env/fs_posix.cc" + | "env/io_posix.cc" + ) }) .collect::>(); diff --git a/librocksdb-sys/build_version.cc b/librocksdb-sys/build_version.cc index 309700fa8..03e7541bb 100644 --- a/librocksdb-sys/build_version.cc +++ b/librocksdb-sys/build_version.cc @@ -1,4 +1,4 @@ #include "build_version.h" -const char* rocksdb_build_git_sha = "rocksdb_build_git_sha:@50f206ad84fa6cd516bad1bea93c03ae0655b314@"; -const char* rocksdb_build_git_date = "rocksdb_build_git_date:@2020/07/29 16:07:40@"; +const char* rocksdb_build_git_sha = "rocksdb_build_git_sha:@48bfca38f6f175435052a59791922a1a453d9609@"; +const char* rocksdb_build_git_date = "rocksdb_build_git_date:@2020/07/15 17:54:15@"; const char* rocksdb_build_compile_date = __DATE__; diff --git a/librocksdb-sys/rocksdb_lib_sources.txt b/librocksdb-sys/rocksdb_lib_sources.txt index 0c813bad3..716b03d17 100644 --- a/librocksdb-sys/rocksdb_lib_sources.txt +++ b/librocksdb-sys/rocksdb_lib_sources.txt @@ -251,4 +251,3 @@ utilities/transactions/write_unprepared_txn_db.cc utilities/ttl/db_ttl_impl.cc utilities/write_batch_with_index/write_batch_with_index.cc utilities/write_batch_with_index/write_batch_with_index_internal.cc - diff --git a/librocksdb-sys/tests/ffi.rs b/librocksdb-sys/tests/ffi.rs index 84f8688bb..07419ba14 100644 --- a/librocksdb-sys/tests/ffi.rs +++ b/librocksdb-sys/tests/ffi.rs @@ -89,21 +89,25 @@ macro_rules! CheckCondition { }; } -unsafe fn CheckEqual(expected: *const c_char, v: *const c_char, n: size_t) { - if expected.is_null() && v.is_null() { - // ok +unsafe fn CheckEqual(expected: *const c_char, actual: *const c_char, n: size_t) { + let is_equal = if expected.is_null() && actual.is_null() { + true } else if !expected.is_null() - && !v.is_null() + && !actual.is_null() && n == strlen(expected) - && memcmp(expected as *const c_void, v as *const c_void, n) == 0 + && memcmp(expected as *const c_void, actual as *const c_void, n) == 0 { - // ok + true } else { + false + }; + + if !is_equal { panic!( "{}: expected '{}', got '{}'", phase, rstr(strndup(expected, n)), - rstr(strndup(v, 5)) + rstr(strndup(actual, 5)) ); } } diff --git a/src/backup.rs b/src/backup.rs index 1b3f0aa9e..f84e0e931 100644 --- a/src/backup.rs +++ b/src/backup.rs @@ -15,10 +15,25 @@ use crate::{ffi, Error, DB}; -use libc::c_int; +use libc::{c_int, c_uchar}; use std::ffi::CString; use std::path::Path; +/// Represents information of a backup including timestamp of the backup +/// and the size (please note that sum of all backups' sizes is bigger than the actual +/// size of the backup directory because some data is shared by multiple backups). +/// Backups are identified by their always-increasing IDs. +pub struct BackupEngineInfo { + /// Timestamp of the backup + pub timestamp: i64, + /// ID of the backup + pub backup_id: u32, + /// Size of the backup + pub size: u64, + /// Number of files related to the backup + pub num_files: u32, +} + pub struct BackupEngine { inner: *mut ffi::rocksdb_backup_engine_t, } @@ -58,10 +73,28 @@ impl BackupEngine { Ok(BackupEngine { inner: be }) } + /// Captures the state of the database in the latest backup. + /// + /// Note: no flush before backup is performed. User might want to + /// use `create_new_backup_flush` instead. pub fn create_new_backup(&mut self, db: &DB) -> Result<(), Error> { + self.create_new_backup_flush(db, false) + } + + /// Captures the state of the database in the latest backup. + /// + /// Set flush_before_backup=true to avoid losing unflushed key/value + /// pairs from the memtable. + pub fn create_new_backup_flush( + &mut self, + db: &DB, + flush_before_backup: bool, + ) -> Result<(), Error> { unsafe { - ffi_try!(ffi::rocksdb_backup_engine_create_new_backup( - self.inner, db.inner, + ffi_try!(ffi::rocksdb_backup_engine_create_new_backup_flush( + self.inner, + db.inner, + flush_before_backup as c_uchar, )); Ok(()) } @@ -137,6 +170,52 @@ impl BackupEngine { } Ok(()) } + + /// Checks that each file exists and that the size of the file matches our + /// expectations. it does not check file checksum. + /// + /// If this BackupEngine created the backup, it compares the files' current + /// sizes against the number of bytes written to them during creation. + /// Otherwise, it compares the files' current sizes against their sizes when + /// the BackupEngine was opened. + pub fn verify_backup(&self, backup_id: u32) -> Result<(), Error> { + unsafe { + ffi_try!(ffi::rocksdb_backup_engine_verify_backup( + self.inner, backup_id, + )); + } + Ok(()) + } + + /// Get a list of all backups together with information on timestamp of the backup + /// and the size (please note that sum of all backups' sizes is bigger than the actual + /// size of the backup directory because some data is shared by multiple backups). + /// Backups are identified by their always-increasing IDs. + /// + /// You can perform this function safely, even with other BackupEngine performing + /// backups on the same directory + pub fn get_backup_info(&self) -> Vec { + unsafe { + let i = ffi::rocksdb_backup_engine_get_backup_info(self.inner); + + let n = ffi::rocksdb_backup_engine_info_count(i); + + let mut info = Vec::with_capacity(n as usize); + for index in 0..n { + info.push(BackupEngineInfo { + timestamp: ffi::rocksdb_backup_engine_info_timestamp(i, index), + backup_id: ffi::rocksdb_backup_engine_info_backup_id(i, index), + size: ffi::rocksdb_backup_engine_info_size(i, index), + num_files: ffi::rocksdb_backup_engine_info_number_files(i, index), + }) + } + + // destroy backup info object + ffi::rocksdb_backup_engine_info_destroy(i); + + info + } + } } impl BackupEngineOptions { diff --git a/src/db.rs b/src/db.rs index 38b84cad8..e49fe366e 100644 --- a/src/db.rs +++ b/src/db.rs @@ -564,6 +564,102 @@ impl DB { self.get_pinned_cf_opt(cf, key, &ReadOptions::default()) } + /// Return the values associated with the given keys. + pub fn multi_get(&self, keys: I) -> Result>, Error> + where + K: AsRef<[u8]>, + I: IntoIterator, + { + self.multi_get_opt(keys, &ReadOptions::default()) + } + + /// Return the values associated with the given keys using read options. + pub fn multi_get_opt( + &self, + keys: I, + readopts: &ReadOptions, + ) -> Result>, Error> + where + K: AsRef<[u8]>, + I: IntoIterator, + { + let (keys, keys_sizes): (Vec>, Vec<_>) = keys + .into_iter() + .map(|k| (Box::from(k.as_ref()), k.as_ref().len())) + .unzip(); + let ptr_keys: Vec<_> = keys.iter().map(|k| k.as_ptr() as *const c_char).collect(); + + let mut values = vec![ptr::null_mut(); keys.len()]; + let mut values_sizes = vec![0_usize; keys.len()]; + unsafe { + ffi_try!(ffi::rocksdb_multi_get( + self.inner, + readopts.inner, + ptr_keys.len(), + ptr_keys.as_ptr(), + keys_sizes.as_ptr(), + values.as_mut_ptr(), + values_sizes.as_mut_ptr(), + )); + } + + Ok(convert_values(values, values_sizes)) + } + + /// Return the values associated with the given keys and column families. + pub fn multi_get_cf<'c, K, I>(&self, keys: I) -> Result>, Error> + where + K: AsRef<[u8]>, + I: IntoIterator, + { + self.multi_get_cf_opt(keys, &ReadOptions::default()) + } + + /// Return the values associated with the given keys and column families using read options. + pub fn multi_get_cf_opt<'c, K, I>( + &self, + keys: I, + readopts: &ReadOptions, + ) -> Result>, Error> + where + K: AsRef<[u8]>, + I: IntoIterator, + { + let mut boxed_keys: Vec> = Vec::new(); + let mut keys_sizes = Vec::new(); + let mut column_families = Vec::new(); + for (cf, key) in keys { + boxed_keys.push(Box::from(key.as_ref())); + keys_sizes.push(key.as_ref().len()); + column_families.push(cf); + } + let ptr_keys: Vec<_> = boxed_keys + .iter() + .map(|k| k.as_ptr() as *const c_char) + .collect(); + let ptr_cfs: Vec<_> = column_families + .iter() + .map(|c| c.inner as *const _) + .collect(); + + let mut values = vec![ptr::null_mut(); boxed_keys.len()]; + let mut values_sizes = vec![0_usize; boxed_keys.len()]; + unsafe { + ffi_try!(ffi::rocksdb_multi_get_cf( + self.inner, + readopts.inner, + ptr_cfs.as_ptr(), + ptr_keys.len(), + ptr_keys.as_ptr(), + keys_sizes.as_ptr(), + values.as_mut_ptr(), + values_sizes.as_mut_ptr(), + )); + } + + Ok(convert_values(values, values_sizes)) + } + pub fn create_cf>(&mut self, name: N, opts: &Options) -> Result<(), Error> { let cf_name = if let Ok(c) = CString::new(name.as_ref().as_bytes()) { c @@ -1005,21 +1101,7 @@ impl DB { } pub fn set_options(&self, opts: &[(&str, &str)]) -> Result<(), Error> { - let copts = opts - .iter() - .map(|(name, value)| { - let cname = match CString::new(name.as_bytes()) { - Ok(cname) => cname, - Err(e) => return Err(Error::new(format!("Invalid option name `{}`", e))), - }; - let cvalue = match CString::new(value.as_bytes()) { - Ok(cvalue) => cvalue, - Err(e) => return Err(Error::new(format!("Invalid option value: `{}`", e))), - }; - Ok((cname, cvalue)) - }) - .collect::, Error>>()?; - + let copts = convert_options(opts)?; let cnames: Vec<*const c_char> = copts.iter().map(|opt| opt.0.as_ptr()).collect(); let cvalues: Vec<*const c_char> = copts.iter().map(|opt| opt.1.as_ptr()).collect(); let count = opts.len() as i32; @@ -1034,6 +1116,27 @@ impl DB { Ok(()) } + pub fn set_options_cf( + &self, + cf_handle: &ColumnFamily, + opts: &[(&str, &str)], + ) -> Result<(), Error> { + let copts = convert_options(opts)?; + let cnames: Vec<*const c_char> = copts.iter().map(|opt| opt.0.as_ptr()).collect(); + let cvalues: Vec<*const c_char> = copts.iter().map(|opt| opt.1.as_ptr()).collect(); + let count = opts.len() as i32; + unsafe { + ffi_try!(ffi::rocksdb_set_options_cf( + self.inner, + cf_handle.inner, + count, + cnames.as_ptr(), + cvalues.as_ptr(), + )); + } + Ok(()) + } + /// Retrieves a RocksDB property by name. /// /// Full list of properties could be find @@ -1361,6 +1464,13 @@ impl DB { Ok(()) } } + + /// Request stopping background work, if wait is true wait until it's done. + pub fn cancel_all_background_work(&self, wait: bool) { + unsafe { + ffi::rocksdb_cancel_all_background_work(self.inner, wait as u8); + } + } } impl Drop for DB { @@ -1398,3 +1508,33 @@ pub struct LiveFile { /// Number of deletions/tomb key(s) in the file pub num_deletions: u64, } + +fn convert_options(opts: &[(&str, &str)]) -> Result, Error> { + opts.iter() + .map(|(name, value)| { + let cname = match CString::new(name.as_bytes()) { + Ok(cname) => cname, + Err(e) => return Err(Error::new(format!("Invalid option name `{}`", e))), + }; + let cvalue = match CString::new(value.as_bytes()) { + Ok(cvalue) => cvalue, + Err(e) => return Err(Error::new(format!("Invalid option value: `{}`", e))), + }; + Ok((cname, cvalue)) + }) + .collect() +} + +fn convert_values(values: Vec<*mut c_char>, values_sizes: Vec) -> Vec> { + values + .into_iter() + .zip(values_sizes.into_iter()) + .map(|(v, s)| { + let value = unsafe { slice::from_raw_parts(v as *const u8, s) }.into(); + unsafe { + ffi::rocksdb_free(v as *mut c_void); + } + value + }) + .collect() +} diff --git a/src/db_options.rs b/src/db_options.rs index 33a2e1d6c..7317f18c3 100644 --- a/src/db_options.rs +++ b/src/db_options.rs @@ -344,6 +344,16 @@ impl Drop for Options { } } +impl Clone for Options { + fn clone(&self) -> Self { + let inner = unsafe { ffi::rocksdb_options_create_copy(self.inner) }; + if inner.is_null() { + panic!("Could not copy RocksDB options"); + } + Self { inner } + } +} + impl Drop for BlockBasedOptions { fn drop(&mut self) { unsafe { @@ -935,26 +945,50 @@ impl Options { } } - pub fn set_merge_operator( + pub fn set_merge_operator_associative( &mut self, name: &str, - full_merge_fn: MergeFn, - partial_merge_fn: Option, + full_merge_fn: F, + ) { + let cb = Box::new(MergeOperatorCallback { + name: CString::new(name.as_bytes()).unwrap(), + full_merge_fn: full_merge_fn.clone(), + partial_merge_fn: full_merge_fn, + }); + + unsafe { + let mo = ffi::rocksdb_mergeoperator_create( + Box::into_raw(cb) as _, + Some(merge_operator::destructor_callback::), + Some(full_merge_callback::), + Some(partial_merge_callback::), + None, + Some(merge_operator::name_callback::), + ); + ffi::rocksdb_options_set_merge_operator(self.inner, mo); + } + } + + pub fn set_merge_operator( + &mut self, + name: &str, + full_merge_fn: F, + partial_merge_fn: PF, ) { let cb = Box::new(MergeOperatorCallback { name: CString::new(name.as_bytes()).unwrap(), full_merge_fn, - partial_merge_fn: partial_merge_fn.unwrap_or(full_merge_fn), + partial_merge_fn, }); unsafe { let mo = ffi::rocksdb_mergeoperator_create( - mem::transmute(cb), - Some(merge_operator::destructor_callback), - Some(full_merge_callback), - Some(partial_merge_callback), + Box::into_raw(cb) as _, + Some(merge_operator::destructor_callback::), + Some(full_merge_callback::), + Some(partial_merge_callback::), None, - Some(merge_operator::name_callback), + Some(merge_operator::name_callback::), ); ffi::rocksdb_options_set_merge_operator(self.inner, mo); } @@ -964,8 +998,8 @@ impl Options { since = "0.5.0", note = "add_merge_operator has been renamed to set_merge_operator" )] - pub fn add_merge_operator(&mut self, name: &str, merge_fn: MergeFn) { - self.set_merge_operator(name, merge_fn, None); + pub fn add_merge_operator(&mut self, name: &str, merge_fn: F) { + self.set_merge_operator_associative(name, merge_fn); } /// Sets a compaction filter used to determine if entries should be kept, changed, @@ -2208,7 +2242,7 @@ impl Options { /// If not zero, dump rocksdb.stats to RocksDB to LOG every `stats_persist_period_sec`. /// /// Default: `600` (10 mins) - /// + /// /// # Examples /// /// ``` @@ -3422,7 +3456,7 @@ mod tests { } #[test] - fn test_set_stats_persist_period_sec(){ + fn test_set_stats_persist_period_sec() { let mut opts = Options::default(); opts.enable_statistics(); opts.set_stats_persist_period_sec(5); diff --git a/src/lib.rs b/src/lib.rs index aaa05291c..74a902d6f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -68,7 +68,8 @@ // False positive: WebSocket clippy::doc_markdown, clippy::missing_safety_doc, - clippy::needless_pass_by_value + clippy::needless_pass_by_value, + clippy::option_if_let_else, )] #[macro_use] @@ -163,7 +164,7 @@ mod test { use super::{ BlockBasedOptions, ColumnFamily, ColumnFamilyDescriptor, DBIterator, DBRawIterator, IngestExternalFileOptions, Options, PlainTableFactoryOptions, ReadOptions, Snapshot, - SstFileWriter, WriteOptions, DB, + SstFileWriter, WriteBatch, WriteOptions, DB, }; #[test] @@ -188,6 +189,7 @@ mod test { is_send::(); is_send::(); is_send::(); + is_send::(); } #[test] diff --git a/src/merge_operator.rs b/src/merge_operator.rs index 0ef12c498..e78a23b69 100644 --- a/src/merge_operator.rs +++ b/src/merge_operator.rs @@ -41,7 +41,7 @@ //!let mut opts = Options::default(); //! //!opts.create_if_missing(true); -//!opts.set_merge_operator("test operator", concat_merge, None); +//!opts.set_merge_operator_associative("test operator", concat_merge); //!{ //! let db = DB::open(&opts, path).unwrap(); //! let p = db.put(b"k1", b"a"); @@ -61,24 +61,47 @@ use std::mem; use std::ptr; use std::slice; -pub type MergeFn = fn(&[u8], Option<&[u8]>, &mut MergeOperands) -> Option>; +pub trait MergeFn: + Fn(&[u8], Option<&[u8]>, &mut MergeOperands) -> Option> + Send + Sync + 'static +{ +} +impl MergeFn for F where + F: Fn(&[u8], Option<&[u8]>, &mut MergeOperands) -> Option> + Send + Sync + 'static +{ +} -pub struct MergeOperatorCallback { +pub struct MergeOperatorCallback { pub name: CString, - pub full_merge_fn: MergeFn, - pub partial_merge_fn: MergeFn, + pub full_merge_fn: F, + pub partial_merge_fn: PF, } -pub unsafe extern "C" fn destructor_callback(raw_cb: *mut c_void) { - let _: Box = mem::transmute(raw_cb); +pub unsafe extern "C" fn destructor_callback(raw_cb: *mut c_void) { + let _: Box> = + Box::from_raw(raw_cb as *mut MergeOperatorCallback); +} + +pub unsafe extern "C" fn delete_callback( + _raw_cb: *mut c_void, + value: *const c_char, + value_length: size_t, +) { + if !value.is_null() { + let _ = Box::from_raw(slice::from_raw_parts_mut( + value as *mut u8, + value_length as usize, + )); + } } -pub unsafe extern "C" fn name_callback(raw_cb: *mut c_void) -> *const c_char { - let cb = &mut *(raw_cb as *mut MergeOperatorCallback); +pub unsafe extern "C" fn name_callback( + raw_cb: *mut c_void, +) -> *const c_char { + let cb = &mut *(raw_cb as *mut MergeOperatorCallback); cb.name.as_ptr() } -pub unsafe extern "C" fn full_merge_callback( +pub unsafe extern "C" fn full_merge_callback( raw_cb: *mut c_void, raw_key: *const c_char, key_len: size_t, @@ -90,7 +113,7 @@ pub unsafe extern "C" fn full_merge_callback( success: *mut u8, new_value_length: *mut size_t, ) -> *mut c_char { - let cb = &mut *(raw_cb as *mut MergeOperatorCallback); + let cb = &mut *(raw_cb as *mut MergeOperatorCallback); let operands = &mut MergeOperands::new(operands_list, operands_list_len, num_operands); let key = slice::from_raw_parts(raw_key as *const u8, key_len as usize); let oldval = if existing_value.is_null() { @@ -101,23 +124,21 @@ pub unsafe extern "C" fn full_merge_callback( existing_value_len as usize, )) }; - if let Some(mut result) = (cb.full_merge_fn)(key, oldval, operands) { - result.shrink_to_fit(); - // TODO(tan) investigate zero-copy techniques to improve performance - let buf = libc::malloc(result.len() as size_t); - assert!(!buf.is_null()); - *new_value_length = result.len() as size_t; - *success = 1 as u8; - ptr::copy(result.as_ptr() as *mut c_void, &mut *buf, result.len()); - buf as *mut c_char - } else { - *new_value_length = 0; - *success = 0 as u8; - ptr::null_mut() as *mut c_char - } + (cb.full_merge_fn)(key, oldval, operands).map_or_else( + || { + *new_value_length = 0; + *success = 0_u8; + ptr::null_mut() as *mut c_char + }, + |result| { + *new_value_length = result.len() as size_t; + *success = 1_u8; + Box::into_raw(result.into_boxed_slice()) as *mut c_char + }, + ) } -pub unsafe extern "C" fn partial_merge_callback( +pub unsafe extern "C" fn partial_merge_callback( raw_cb: *mut c_void, raw_key: *const c_char, key_len: size_t, @@ -127,23 +148,21 @@ pub unsafe extern "C" fn partial_merge_callback( success: *mut u8, new_value_length: *mut size_t, ) -> *mut c_char { - let cb = &mut *(raw_cb as *mut MergeOperatorCallback); + let cb = &mut *(raw_cb as *mut MergeOperatorCallback); let operands = &mut MergeOperands::new(operands_list, operands_list_len, num_operands); let key = slice::from_raw_parts(raw_key as *const u8, key_len as usize); - if let Some(mut result) = (cb.partial_merge_fn)(key, None, operands) { - result.shrink_to_fit(); - // TODO(tan) investigate zero-copy techniques to improve performance - let buf = libc::malloc(result.len() as size_t); - assert!(!buf.is_null()); - *new_value_length = result.len() as size_t; - *success = 1 as u8; - ptr::copy(result.as_ptr() as *mut c_void, &mut *buf, result.len()); - buf as *mut c_char - } else { - *new_value_length = 0; - *success = 0 as u8; - ptr::null_mut::() - } + (cb.partial_merge_fn)(key, None, operands).map_or_else( + || { + *new_value_length = 0; + *success = 0_u8; + ptr::null_mut::() + }, + |result| { + *new_value_length = result.len() as size_t; + *success = 1_u8; + Box::into_raw(result.into_boxed_slice()) as *mut c_char + }, + ) } pub struct MergeOperands { diff --git a/src/slice_transform.rs b/src/slice_transform.rs index 396012f8f..38c1f77fb 100644 --- a/src/slice_transform.rs +++ b/src/slice_transform.rs @@ -111,9 +111,6 @@ pub unsafe extern "C" fn in_domain_callback( ) -> u8 { let cb = &mut *(raw_cb as *mut TransformCallback); let key = slice::from_raw_parts(raw_key as *const u8, key_len as usize); - if let Some(in_domain) = cb.in_domain_fn { - in_domain(key) as u8 - } else { - 0xff - } + cb.in_domain_fn + .map_or(0xff, |in_domain| in_domain(key) as u8) } diff --git a/src/write_batch.rs b/src/write_batch.rs index c07fe6b82..a2566bd36 100644 --- a/src/write_batch.rs +++ b/src/write_batch.rs @@ -279,3 +279,5 @@ impl Drop for WriteBatch { unsafe { ffi::rocksdb_writebatch_destroy(self.inner) } } } + +unsafe impl Send for WriteBatch {} diff --git a/tests/test_backup.rs b/tests/test_backup.rs index 572bcd280..c3e488520 100644 --- a/tests/test_backup.rs +++ b/tests/test_backup.rs @@ -14,6 +14,8 @@ mod util; +use pretty_assertions::assert_eq; + use rocksdb::{ backup::{BackupEngine, BackupEngineOptions, RestoreOptions}, DB, @@ -36,6 +38,14 @@ fn backup_restore() { let mut backup_engine = BackupEngine::open(&backup_opts, &backup_path).unwrap(); assert!(backup_engine.create_new_backup(&db).is_ok()); + // check backup info + let info = backup_engine.get_backup_info(); + assert!(!info.is_empty()); + info.iter().for_each(|i| { + assert!(backup_engine.verify_backup(i.backup_id).is_ok()); + assert!(i.size > 0); + }); + let mut restore_option = RestoreOptions::default(); restore_option.set_keep_log_files(false); // true to keep log files let restore_status = backup_engine.restore_from_latest_backup( diff --git a/tests/test_checkpoint.rs b/tests/test_checkpoint.rs index 0c63bb7c1..01e4dc5d0 100644 --- a/tests/test_checkpoint.rs +++ b/tests/test_checkpoint.rs @@ -14,6 +14,8 @@ mod util; +use pretty_assertions::assert_eq; + use rocksdb::{checkpoint::Checkpoint, Options, DB}; use util::DBPath; @@ -41,10 +43,10 @@ pub fn test_single_checkpoint() { // Verify checkpoint let cp = DB::open_default(&cp1_path).unwrap(); - assert_eq!(*cp.get(b"k1").unwrap().unwrap(), *b"v1"); - assert_eq!(*cp.get(b"k2").unwrap().unwrap(), *b"v2"); - assert_eq!(*cp.get(b"k3").unwrap().unwrap(), *b"v3"); - assert_eq!(*cp.get(b"k4").unwrap().unwrap(), *b"v4"); + assert_eq!(cp.get(b"k1").unwrap().unwrap(), b"v1"); + assert_eq!(cp.get(b"k2").unwrap().unwrap(), b"v2"); + assert_eq!(cp.get(b"k3").unwrap().unwrap(), b"v3"); + assert_eq!(cp.get(b"k4").unwrap().unwrap(), b"v4"); } #[test] @@ -71,10 +73,10 @@ pub fn test_multi_checkpoints() { // Verify checkpoint let cp = DB::open_default(&cp1_path).unwrap(); - assert_eq!(*cp.get(b"k1").unwrap().unwrap(), *b"v1"); - assert_eq!(*cp.get(b"k2").unwrap().unwrap(), *b"v2"); - assert_eq!(*cp.get(b"k3").unwrap().unwrap(), *b"v3"); - assert_eq!(*cp.get(b"k4").unwrap().unwrap(), *b"v4"); + assert_eq!(cp.get(b"k1").unwrap().unwrap(), b"v1"); + assert_eq!(cp.get(b"k2").unwrap().unwrap(), b"v2"); + assert_eq!(cp.get(b"k3").unwrap().unwrap(), b"v3"); + assert_eq!(cp.get(b"k4").unwrap().unwrap(), b"v4"); // Change some existing keys db.put(b"k1", b"modified").unwrap(); @@ -92,8 +94,8 @@ pub fn test_multi_checkpoints() { // Verify second checkpoint let cp = DB::open_default(&cp2_path).unwrap(); - assert_eq!(*cp.get(b"k1").unwrap().unwrap(), *b"modified"); - assert_eq!(*cp.get(b"k2").unwrap().unwrap(), *b"changed"); - assert_eq!(*cp.get(b"k5").unwrap().unwrap(), *b"v5"); - assert_eq!(*cp.get(b"k6").unwrap().unwrap(), *b"v6"); + assert_eq!(cp.get(b"k1").unwrap().unwrap(), b"modified"); + assert_eq!(cp.get(b"k2").unwrap().unwrap(), b"changed"); + assert_eq!(cp.get(b"k5").unwrap().unwrap(), b"v5"); + assert_eq!(cp.get(b"k6").unwrap().unwrap(), b"v6"); } diff --git a/tests/test_column_family.rs b/tests/test_column_family.rs index fc9e9b289..95bb138e1 100644 --- a/tests/test_column_family.rs +++ b/tests/test_column_family.rs @@ -14,6 +14,8 @@ mod util; +use pretty_assertions::assert_eq; + use rocksdb::{ColumnFamilyDescriptor, MergeOperands, Options, DB, DEFAULT_COLUMN_FAMILY_NAME}; use util::DBPath; @@ -25,7 +27,7 @@ fn test_column_family() { { let mut opts = Options::default(); opts.create_if_missing(true); - opts.set_merge_operator("test operator", test_provided_merge, None); + opts.set_merge_operator_associative("test operator", test_provided_merge); let mut db = DB::open(&opts, &n).unwrap(); let opts = Options::default(); match db.create_cf("cf1", &opts) { @@ -39,7 +41,7 @@ fn test_column_family() { // should fail to open db without specifying same column families { let mut opts = Options::default(); - opts.set_merge_operator("test operator", test_provided_merge, None); + opts.set_merge_operator_associative("test operator", test_provided_merge); match DB::open(&opts, &n) { Ok(_db) => panic!( "should not have opened DB successfully without \ @@ -56,7 +58,7 @@ fn test_column_family() { // should properly open db when specyfing all column families { let mut opts = Options::default(); - opts.set_merge_operator("test operator", test_provided_merge, None); + opts.set_merge_operator_associative("test operator", test_provided_merge); match DB::open_cf(&opts, &n, &["cf1"]) { Ok(_db) => println!("successfully opened db with column family"), Err(e) => panic!("failed to open db with column family: {}", e), @@ -135,7 +137,7 @@ fn test_merge_operator() { // TODO should be able to write, read, merge, batch, and iterate over a cf { let mut opts = Options::default(); - opts.set_merge_operator("test operator", test_provided_merge, None); + opts.set_merge_operator_associative("test operator", test_provided_merge); let db = match DB::open_cf(&opts, &n, &["cf1"]) { Ok(db) => { println!("successfully opened db with column family"); diff --git a/tests/test_compationfilter.rs b/tests/test_compationfilter.rs index 11835c0b7..f5e206dbc 100644 --- a/tests/test_compationfilter.rs +++ b/tests/test_compationfilter.rs @@ -14,6 +14,8 @@ mod util; +use pretty_assertions::assert_eq; + use rocksdb::{CompactionDecision, Options, DB}; use util::DBPath; diff --git a/tests/test_db.rs b/tests/test_db.rs index cf4a4b914..d9ec85b24 100644 --- a/tests/test_db.rs +++ b/tests/test_db.rs @@ -14,15 +14,16 @@ mod util; +use std::{mem, sync::Arc, thread, time::Duration}; + +use pretty_assertions::assert_eq; + use rocksdb::{ perf::get_memory_usage_stats, BlockBasedOptions, BottommostLevelCompaction, Cache, CompactOptions, DBCompactionStyle, Env, Error, FifoCompactOptions, IteratorMode, Options, PerfContext, PerfMetric, ReadOptions, SliceTransform, Snapshot, UniversalCompactOptions, UniversalCompactionStopStyle, WriteBatch, DB, }; -use std::sync::Arc; -use std::time::Duration; -use std::{mem, thread}; use util::DBPath; #[test] @@ -335,6 +336,47 @@ fn set_option_test() { } } +#[test] +fn set_option_cf_test() { + let path = DBPath::new("_rust_rocksdb_set_options_cftest"); + { + let mut opts = Options::default(); + opts.create_if_missing(true); + opts.create_missing_column_families(true); + let db = DB::open_cf(&opts, &path, vec!["cf1"]).unwrap(); + let cf = db.cf_handle("cf1").unwrap(); + // set an option to valid values + assert!(db + .set_options_cf(cf, &[("disable_auto_compactions", "true")]) + .is_ok()); + assert!(db + .set_options_cf(cf, &[("disable_auto_compactions", "false")]) + .is_ok()); + // invalid names/values should result in an error + assert!(db + .set_options_cf(cf, &[("disable_auto_compactions", "INVALID_VALUE")]) + .is_err()); + assert!(db + .set_options_cf(cf, &[("INVALID_NAME", "INVALID_VALUE")]) + .is_err()); + // option names/values must not contain NULLs + assert!(db + .set_options_cf(cf, &[("disable_auto_compactions", "true\0")]) + .is_err()); + assert!(db + .set_options_cf(cf, &[("disable_auto_compactions\0", "true")]) + .is_err()); + // empty options are not allowed + assert!(db.set_options_cf(cf, &[]).is_err()); + // multiple options can be set in a single API call + let multiple_options = [ + ("paranoid_file_checks", "true"), + ("report_bg_io_stats", "true"), + ]; + db.set_options(&multiple_options).unwrap(); + } +} + #[test] fn test_sequence_number() { let path = DBPath::new("_rust_rocksdb_test_sequence_number"); @@ -738,7 +780,7 @@ fn get_with_cache_and_bulkload_test() { assert!(DB::open(&opts, &path).is_err()); } - // disable all threads + // disable all threads { // create new options let mut opts = Options::default(); @@ -826,3 +868,50 @@ fn delete_range_test() { assert!(db.get_cf(cf1, b"k3").unwrap().is_none()); } } + +#[test] +fn multi_get() { + let path = DBPath::new("_rust_rocksdb_multi_get"); + + { + let db = DB::open_default(&path).unwrap(); + db.put(b"k1", b"v1").unwrap(); + db.put(b"k2", b"v2").unwrap(); + + let values = db + .multi_get(&[b"k0", b"k1", b"k2"]) + .expect("multi_get failed"); + assert_eq!(3, values.len()); + assert!(values[0].is_empty()); + assert_eq!(values[1], b"v1"); + assert_eq!(values[2], b"v2"); + } +} + +#[test] +fn multi_get_cf() { + let path = DBPath::new("_rust_rocksdb_multi_get_cf"); + + { + let mut opts = Options::default(); + opts.create_if_missing(true); + opts.create_missing_column_families(true); + let db = DB::open_cf(&opts, &path, &["cf0", "cf1", "cf2"]).unwrap(); + + let cf0 = db.cf_handle("cf0").unwrap(); + + let cf1 = db.cf_handle("cf1").unwrap(); + db.put_cf(cf1, b"k1", b"v1").unwrap(); + + let cf2 = db.cf_handle("cf2").unwrap(); + db.put_cf(cf2, b"k2", b"v2").unwrap(); + + let values = db + .multi_get_cf(vec![(cf0, b"k0"), (cf1, b"k1"), (cf2, b"k2")]) + .expect("multi_get failed"); + assert_eq!(3, values.len()); + assert!(values[0].is_empty()); + assert_eq!(values[1], b"v1"); + assert_eq!(values[2], b"v2"); + } +} diff --git a/tests/test_iterator.rs b/tests/test_iterator.rs index f15c61186..60d5b7731 100644 --- a/tests/test_iterator.rs +++ b/tests/test_iterator.rs @@ -14,6 +14,8 @@ mod util; +use pretty_assertions::assert_eq; + use rocksdb::{Direction, IteratorMode, MemtableFactory, Options, DB}; use util::DBPath; diff --git a/tests/test_merge_operator.rs b/tests/test_merge_operator.rs index de544da94..021eb2a53 100644 --- a/tests/test_merge_operator.rs +++ b/tests/test_merge_operator.rs @@ -14,6 +14,9 @@ mod util; +use pretty_assertions::assert_eq; + +use rocksdb::merge_operator::MergeFn; use rocksdb::{DBCompactionStyle, MergeOperands, Options, DB}; use util::DBPath; @@ -44,7 +47,7 @@ fn merge_test() { let db_path = DBPath::new("_rust_rocksdb_merge_test"); let mut opts = Options::default(); opts.create_if_missing(true); - opts.set_merge_operator("test operator", test_provided_merge, None); + opts.set_merge_operator_associative("test operator", test_provided_merge); let db = DB::open(&opts, &db_path).unwrap(); let p = db.put(b"k1", b"a"); @@ -157,7 +160,7 @@ fn counting_merge_test() { opts.set_merge_operator( "sort operator", test_counting_full_merge, - Some(test_counting_partial_merge), + test_counting_partial_merge, ); let db = Arc::new(DB::open(&opts, &db_path).unwrap()); @@ -276,7 +279,7 @@ fn failed_merge_test() { let db_path = DBPath::new("_rust_rocksdb_failed_merge_test"); let mut opts = Options::default(); opts.create_if_missing(true); - opts.set_merge_operator("test operator", test_failing_merge, None); + opts.set_merge_operator_associative("test operator", test_failing_merge); let db = DB::open(&opts, &db_path).expect("open with a merge operator"); db.put(b"key", b"value").expect("put_ok"); @@ -288,3 +291,72 @@ fn failed_merge_test() { } } } + +fn make_merge_max_with_limit(limit: u64) -> impl MergeFn + Clone { + move |_key: &[u8], first: Option<&[u8]>, rest: &mut MergeOperands| { + let max = first + .into_iter() + .chain(rest) + .map(|slice| { + let mut bytes: [u8; 8] = Default::default(); + bytes.clone_from_slice(slice); + u64::from_ne_bytes(bytes) + }) + .fold(0, u64::max); + let new_value = max.min(limit); + Some(Vec::from(new_value.to_ne_bytes().as_ref())) + } +} + +#[test] +fn test_merge_state() { + use {Options, DB}; + let path = "_rust_rocksdb_mergetest_state"; + let mut opts = Options::default(); + opts.create_if_missing(true); + opts.set_merge_operator_associative("max-limit-12", make_merge_max_with_limit(12)); + { + let db = DB::open(&opts, path).unwrap(); + let p = db.put(b"k1", 1u64.to_ne_bytes()); + assert!(p.is_ok()); + let _ = db.merge(b"k1", 7u64.to_ne_bytes()); + let m = db.merge(b"k1", 64u64.to_ne_bytes()); + assert!(m.is_ok()); + match db.get(b"k1") { + Ok(Some(value)) => { + let mut bytes: [u8; 8] = Default::default(); + bytes.copy_from_slice(&value); + assert_eq!(u64::from_ne_bytes(bytes), 12); + } + Err(_) => println!("error reading value"), + _ => panic!("value not present"), + } + + assert!(db.delete(b"k1").is_ok()); + assert!(db.get(b"k1").unwrap().is_none()); + } + assert!(DB::destroy(&opts, path).is_ok()); + + opts.set_merge_operator_associative("max-limit-128", make_merge_max_with_limit(128)); + { + let db = DB::open(&opts, path).unwrap(); + let p = db.put(b"k1", 1u64.to_ne_bytes()); + assert!(p.is_ok()); + let _ = db.merge(b"k1", 7u64.to_ne_bytes()); + let m = db.merge(b"k1", 64u64.to_ne_bytes()); + assert!(m.is_ok()); + match db.get(b"k1") { + Ok(Some(value)) => { + let mut bytes: [u8; 8] = Default::default(); + bytes.copy_from_slice(&value); + assert_eq!(u64::from_ne_bytes(bytes), 64); + } + Err(_) => println!("error reading value"), + _ => panic!("value not present"), + } + + assert!(db.delete(b"k1").is_ok()); + assert!(db.get(b"k1").unwrap().is_none()); + } + assert!(DB::destroy(&opts, path).is_ok()); +} diff --git a/tests/test_multithreaded.rs b/tests/test_multithreaded.rs index ec3d9a184..db99758f8 100644 --- a/tests/test_multithreaded.rs +++ b/tests/test_multithreaded.rs @@ -14,9 +14,9 @@ mod util; +use std::{sync::Arc, thread}; + use rocksdb::DB; -use std::sync::Arc; -use std::thread; use util::DBPath; const N: usize = 100_000; diff --git a/tests/test_pinnable_slice.rs b/tests/test_pinnable_slice.rs index bda4165f8..d11744071 100644 --- a/tests/test_pinnable_slice.rs +++ b/tests/test_pinnable_slice.rs @@ -14,6 +14,8 @@ mod util; +use pretty_assertions::assert_eq; + use rocksdb::{Options, DB}; use util::DBPath; diff --git a/tests/test_property.rs b/tests/test_property.rs index 4e04e51b9..2b9d39944 100644 --- a/tests/test_property.rs +++ b/tests/test_property.rs @@ -14,6 +14,8 @@ mod util; +use pretty_assertions::assert_eq; + use rocksdb::{Options, DB}; use util::DBPath; diff --git a/tests/test_raw_iterator.rs b/tests/test_raw_iterator.rs index e2ea63ce6..b9efbab2b 100644 --- a/tests/test_raw_iterator.rs +++ b/tests/test_raw_iterator.rs @@ -14,6 +14,8 @@ mod util; +use pretty_assertions::assert_eq; + use rocksdb::DB; use util::DBPath; diff --git a/tests/test_rocksdb_options.rs b/tests/test_rocksdb_options.rs index 18f57fdc9..8089b3db2 100644 --- a/tests/test_rocksdb_options.rs +++ b/tests/test_rocksdb_options.rs @@ -14,8 +14,9 @@ mod util; -use rocksdb::{BlockBasedOptions, DataBlockIndexType, Options, ReadOptions, DB}; use std::{fs, io::Read as _}; + +use rocksdb::{BlockBasedOptions, DataBlockIndexType, Options, ReadOptions, DB}; use util::DBPath; #[test] diff --git a/tests/test_slice_transform.rs b/tests/test_slice_transform.rs index 68868605a..c2633d8d1 100644 --- a/tests/test_slice_transform.rs +++ b/tests/test_slice_transform.rs @@ -14,6 +14,8 @@ mod util; +use pretty_assertions::assert_eq; + use rocksdb::{Options, SliceTransform, DB}; use util::DBPath; diff --git a/tests/test_sst_file_writer.rs b/tests/test_sst_file_writer.rs index 43b0f8bfe..5376f5ab7 100644 --- a/tests/test_sst_file_writer.rs +++ b/tests/test_sst_file_writer.rs @@ -14,8 +14,9 @@ mod util; -use rocksdb::{Error, Options, SstFileWriter, DB}; +use pretty_assertions::assert_eq; +use rocksdb::{Error, Options, SstFileWriter, DB}; use util::DBPath; #[test] diff --git a/tests/test_write_batch.rs b/tests/test_write_batch.rs index 695633ad0..c1627aad5 100644 --- a/tests/test_write_batch.rs +++ b/tests/test_write_batch.rs @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +use pretty_assertions::assert_eq; + use rocksdb::WriteBatch; #[test]