Skip to content

Commit e071073

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

File tree

3 files changed

+101
-1
lines changed

3 files changed

+101
-1
lines changed

CHANGES.md

Lines changed: 1 addition & 0 deletions
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

Lines changed: 35 additions & 0 deletions
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

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,11 +177,49 @@ 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(&mut self, func: impl Fn(&Dataset) -> Result<()>) -> Result<()> {
188+
let force = 0; // since this is for speed
189+
let rv = unsafe { gdal_sys::GDALDatasetStartTransaction(self.c_dataset(), force) };
190+
if rv == OGRErr::OGRERR_UNSUPPORTED_OPERATION {
191+
func(self)
192+
} else if rv == OGRErr::OGRERR_NONE {
193+
let res = func(self);
194+
if res.is_ok() {
195+
let rv = unsafe { gdal_sys::GDALDatasetCommitTransaction(self.c_dataset()) };
196+
if rv != OGRErr::OGRERR_NONE {
197+
Err(GdalError::OgrError {
198+
err: rv,
199+
method_name: "GDALDatasetCommitTransaction",
200+
})
201+
} else {
202+
res
203+
}
204+
} else {
205+
let _ = unsafe { gdal_sys::GDALDatasetRollbackTransaction(self.c_dataset()) };
206+
// ignore rollback error because it's not guaranteed to be
207+
// supported, and the `func` failure is more important.
208+
res
209+
}
210+
} else {
211+
// transaction supported but failed
212+
Err(GdalError::OgrError {
213+
err: rv,
214+
method_name: "GDALDatasetStartTransaction",
215+
})
216+
}
217+
}
180218
}
181219

182220
#[cfg(test)]
183221
mod tests {
184-
use crate::test_utils::{fixture, open_gpkg_for_update};
222+
use crate::test_utils::{fixture, open_dataset_for_update, open_gpkg_for_update};
185223
use crate::vector::{Geometry, LayerAccess};
186224
use crate::Dataset;
187225

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

0 commit comments

Comments
 (0)