From a962b730951f4cf979873ca71d6da3e542187993 Mon Sep 17 00:00:00 2001 From: Gaurav Atreya Date: Fri, 1 Nov 2024 20:11:51 -0400 Subject: [PATCH] Add MaybeTransaction for better writing speed when supported --- src/test_utils.rs | 35 ++++++++++ src/vector/transaction.rs | 136 +++++++++++++++++++++++++++++++++++++- 2 files changed, 170 insertions(+), 1 deletion(-) diff --git a/src/test_utils.rs b/src/test_utils.rs index 65a44863..8ef0d3c1 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -136,6 +136,41 @@ pub fn open_gpkg_for_update(path: &Path) -> (TempPath, Dataset) { (temp_path, ds) } +/// Copies the given file to a temporary file and opens it for writing. When the returned +/// `TempPath` is dropped, the file is deleted. +pub fn open_dataset_for_update(path: &Path) -> (TempPath, Dataset) { + use std::fs; + use std::io::Write; + + let input_data = fs::read(path).unwrap(); + let (mut file, temp_path) = tempfile::Builder::new() + // using the whole filename as suffix should be fine (can't + // use .extension() for .shp.zip and such) + .suffix( + path.file_name() + .unwrap_or_default() + .to_string_lossy() + .as_ref(), + ) + .tempfile() + .unwrap() + .into_parts(); + file.write_all(&input_data).unwrap(); + // Close the temporary file so that Dataset can open it safely even if the filesystem uses + // exclusive locking (Windows?). + drop(file); + + let ds = Dataset::open_ex( + &temp_path, + DatasetOptions { + open_flags: GDALAccess::GA_Update.into(), + ..DatasetOptions::default() + }, + ) + .unwrap(); + (temp_path, ds) +} + /// Assert numerical difference between two expressions is less than /// 64-bit machine epsilon or a specified epsilon. /// diff --git a/src/vector/transaction.rs b/src/vector/transaction.rs index 2719e210..412a1e37 100644 --- a/src/vector/transaction.rs +++ b/src/vector/transaction.rs @@ -179,9 +179,93 @@ impl Dataset { } } +/// Represents maybe a transaction on a dataset for speed reasons. +/// +/// It can be committed by calling [`commit`](MaybeTransaction::commit), but unline [`Transaction`] it can not be rolled back. The transaction part is to make the process speedy when possible. If the [`Dataset`] does not support transaction, it does nothing. +/// +/// If the transaction is not explicitly committed when it is dropped, it is implicitely committed, but you will not know if it fails. +/// +/// The transaction holds a mutable borrow on the `Dataset` that it was created from, so during the +/// lifetime of the transaction you will need to access the dataset by dereferencing the +/// `MaybeTransaction` through its [`Deref`] or [`DerefMut`] implementations. +#[derive(Debug)] +pub struct MaybeTransaction<'a> { + dataset: &'a mut Dataset, + is_transaction: bool, + commit_on_drop: bool, +} + +impl<'a> MaybeTransaction<'a> { + fn new(dataset: &'a mut Dataset, is_transaction: bool) -> Self { + MaybeTransaction { + dataset, + is_transaction, + commit_on_drop: true, + } + } + + pub fn is_transaction(&self) -> bool { + self.is_transaction + } + + /// Commits this transaction. + /// + /// If the commit fails, will return [`OGRErr::OGRERR_FAILURE`]. + /// + /// Depending on drivers, this may or may not abort layer sequential readings that are active. + pub fn commit(mut self) -> Result<()> { + if !self.is_transaction { + return Ok(()); + } + + let rv = unsafe { gdal_sys::GDALDatasetCommitTransaction(self.dataset.c_dataset()) }; + self.commit_on_drop = false; + if rv != OGRErr::OGRERR_NONE { + return Err(GdalError::OgrError { + err: rv, + method_name: "GDALDatasetCommitTransaction", + }); + } + Ok(()) + } +} + +impl<'a> Deref for MaybeTransaction<'a> { + type Target = Dataset; + + fn deref(&self) -> &Self::Target { + self.dataset + } +} + +impl<'a> DerefMut for MaybeTransaction<'a> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.dataset + } +} + +impl Dataset { + pub fn maybe_start_transaction(&mut self) -> MaybeTransaction<'_> { + let force = 1; + let rv = unsafe { gdal_sys::GDALDatasetStartTransaction(self.c_dataset(), force) }; + + MaybeTransaction::new(self, rv == OGRErr::OGRERR_NONE) + } +} + +impl<'a> Drop for MaybeTransaction<'a> { + fn drop(&mut self) { + if self.commit_on_drop { + // We silently swallow any errors, because we have no way to report them from a drop + // function apart from panicking. + unsafe { gdal_sys::GDALDatasetCommitTransaction(self.dataset.c_dataset()) }; + } + } +} + #[cfg(test)] mod tests { - use crate::test_utils::{fixture, open_gpkg_for_update}; + use crate::test_utils::{fixture, open_dataset_for_update, open_gpkg_for_update}; use crate::vector::{Geometry, LayerAccess}; use crate::Dataset; @@ -241,4 +325,54 @@ mod tests { let mut ds = Dataset::open(fixture("roads.geojson")).unwrap(); assert!(ds.start_transaction().is_err()); } + + #[test] + fn test_maybe_start_transaction() { + let (_temp_path, mut ds) = open_gpkg_for_update(&fixture("poly.gpkg")); + let txn = ds.maybe_start_transaction(); + assert!(txn.is_transaction()); + let mut ds = Dataset::open(fixture("roads.geojson")).unwrap(); + let txn = ds.maybe_start_transaction(); + assert!(!txn.is_transaction()); + } + + #[test] + fn test_maybe_transaction_commit() { + let (_temp_path, mut ds) = open_gpkg_for_update(&fixture("poly.gpkg")); + let orig_feature_count = ds.layer(0).unwrap().feature_count(); + + let txn = ds.maybe_start_transaction(); + let mut layer = txn.layer(0).unwrap(); + layer.create_feature(polygon()).unwrap(); + assert!(txn.commit().is_ok()); + + assert_eq!(ds.layer(0).unwrap().feature_count(), orig_feature_count + 1); + } + + #[test] + fn test_maybe_transaction_commit_unsupported() { + let (_temp_path, mut ds) = open_dataset_for_update(&fixture("roads.geojson")); + let orig_feature_count = ds.layer(0).unwrap().feature_count(); + + let txn = ds.maybe_start_transaction(); + let mut layer = txn.layer(0).unwrap(); + layer.create_feature(polygon()).unwrap(); + assert!(txn.commit().is_ok()); + + assert_eq!(ds.layer(0).unwrap().feature_count(), orig_feature_count + 1); + } + + #[test] + fn test_maybe_transaction_implicit_commit() { + let (_temp_path, mut ds) = open_gpkg_for_update(&fixture("poly.gpkg")); + let orig_feature_count = ds.layer(0).unwrap().feature_count(); + + { + let txn = ds.maybe_start_transaction(); + let mut layer = txn.layer(0).unwrap(); + layer.create_feature(polygon()).unwrap(); + } // txn is dropped here. + + assert_eq!(ds.layer(0).unwrap().feature_count(), orig_feature_count + 1); + } }