Skip to content

Commit 5793fa5

Browse files
committed
Add Dataset::maybe_run_in_batch for faster DS write when possible
1 parent ba73829 commit 5793fa5

File tree

3 files changed

+104
-1
lines changed

3 files changed

+104
-1
lines changed

CHANGES.md

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
- Add `Defn::field_index` and `Feature::field_index` ([#581](https://github.com/georust/gdal/pull/581))
2929
- Add `Defn::geometry_field_index` and `Feature::geometry_field_index` ([#594](https://github.com/georust/gdal/pull/594))
3030
- Add `Dataset::has_capability` for dataset capability check ([#581](https://github.com/georust/gdal/pull/585))
31+
- Add `Dataset::maybe_run_in_batch` which runs a user function inside a transaction if the dataset supports it, as an optimization for I/O; if the function fails, the transaction may or may not be rolled back ([#584](https://github.com/georust/gdal/pull/584))
3132

3233
### Fixed
3334

src/test_utils.rs

+35
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,41 @@ pub fn open_gpkg_for_update(path: &Path) -> (TempPath, Dataset) {
136136
(temp_path, ds)
137137
}
138138

139+
/// Copies the given file to a temporary file and opens it for writing. When the returned
140+
/// `TempPath` is dropped, the file is deleted.
141+
pub fn open_dataset_for_update(path: &Path) -> (TempPath, Dataset) {
142+
use std::fs;
143+
use std::io::Write;
144+
145+
let input_data = fs::read(path).unwrap();
146+
let (mut file, temp_path) = tempfile::Builder::new()
147+
// using the whole filename as suffix should be fine (can't
148+
// use .extension() for .shp.zip and such)
149+
.suffix(
150+
path.file_name()
151+
.unwrap_or_default()
152+
.to_string_lossy()
153+
.as_ref(),
154+
)
155+
.tempfile()
156+
.unwrap()
157+
.into_parts();
158+
file.write_all(&input_data).unwrap();
159+
// Close the temporary file so that Dataset can open it safely even if the filesystem uses
160+
// exclusive locking (Windows?).
161+
drop(file);
162+
163+
let ds = Dataset::open_ex(
164+
&temp_path,
165+
DatasetOptions {
166+
open_flags: GDALAccess::GA_Update.into(),
167+
..DatasetOptions::default()
168+
},
169+
)
170+
.unwrap();
171+
(temp_path, ds)
172+
}
173+
139174
/// Assert numerical difference between two expressions is less than
140175
/// 64-bit machine epsilon or a specified epsilon.
141176
///

src/vector/transaction.rs

+68-1
Original file line numberDiff line numberDiff line change
@@ -177,11 +177,52 @@ impl Dataset {
177177
}
178178
Ok(Transaction::new(self))
179179
}
180+
181+
/// Optionally start a transaction before running `func` for performance
182+
///
183+
/// This uses transaction if the dataset supports it, otherwise it
184+
/// runs the `func` function as it is on the dataset. If the
185+
/// closure results in error, and it is a transaction, it is
186+
/// rolled back. If the rollback fails, it not reported.
187+
pub fn maybe_run_in_batch(
188+
&mut self,
189+
mut func: impl FnMut(&mut Dataset) -> Result<()>,
190+
) -> Result<()> {
191+
let force = 0; // since this is for speed
192+
let rv = unsafe { gdal_sys::GDALDatasetStartTransaction(self.c_dataset(), force) };
193+
if rv == OGRErr::OGRERR_UNSUPPORTED_OPERATION {
194+
func(self)
195+
} else if rv == OGRErr::OGRERR_NONE {
196+
let res = func(self);
197+
if res.is_ok() {
198+
let rv = unsafe { gdal_sys::GDALDatasetCommitTransaction(self.c_dataset()) };
199+
if rv != OGRErr::OGRERR_NONE {
200+
Err(GdalError::OgrError {
201+
err: rv,
202+
method_name: "GDALDatasetCommitTransaction",
203+
})
204+
} else {
205+
res
206+
}
207+
} else {
208+
let _ = unsafe { gdal_sys::GDALDatasetRollbackTransaction(self.c_dataset()) };
209+
// ignore rollback error because it's not guaranteed to be
210+
// supported, and the `func` failure is more important.
211+
res
212+
}
213+
} else {
214+
// transaction supported but failed
215+
Err(GdalError::OgrError {
216+
err: rv,
217+
method_name: "GDALDatasetStartTransaction",
218+
})
219+
}
220+
}
180221
}
181222

182223
#[cfg(test)]
183224
mod tests {
184-
use crate::test_utils::{fixture, open_gpkg_for_update};
225+
use crate::test_utils::{fixture, open_dataset_for_update, open_gpkg_for_update};
185226
use crate::vector::{Geometry, LayerAccess};
186227
use crate::Dataset;
187228

@@ -241,4 +282,30 @@ mod tests {
241282
let mut ds = Dataset::open(fixture("roads.geojson")).unwrap();
242283
assert!(ds.start_transaction().is_err());
243284
}
285+
286+
#[test]
287+
fn test_maybe_run_in_batch() {
288+
let (_temp_path, mut ds) = open_gpkg_for_update(&fixture("poly.gpkg"));
289+
let orig_feature_count = ds.layer(0).unwrap().feature_count();
290+
291+
let res = ds.maybe_run_in_batch(|d| {
292+
let mut layer = d.layer(0).unwrap();
293+
layer.create_feature(polygon())
294+
});
295+
assert!(res.is_ok());
296+
assert_eq!(ds.layer(0).unwrap().feature_count(), orig_feature_count + 1);
297+
}
298+
299+
#[test]
300+
fn test_maybe_transaction_unsupported() {
301+
let (_temp_path, mut ds) = open_dataset_for_update(&fixture("roads.geojson"));
302+
let orig_feature_count = ds.layer(0).unwrap().feature_count();
303+
304+
let res = ds.maybe_run_in_batch(|d| {
305+
let mut layer = d.layer(0).unwrap();
306+
layer.create_feature(polygon())
307+
});
308+
assert!(res.is_ok());
309+
assert_eq!(ds.layer(0).unwrap().feature_count(), orig_feature_count + 1);
310+
}
244311
}

0 commit comments

Comments
 (0)