diff --git a/CHANGES.md b/CHANGES.md index bd1211a1..c4dd40e3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -21,6 +21,9 @@ // band needs to be mutable to set no-data value band.set_no_data_value(0.0)?; ``` + +* Implement wrapper for `OGR_L_TestCapability` + * * **Breaking**: Use `DatasetOptions` to pass as `Dataset::open_ex` parameters and add support for extended open flags. @@ -84,12 +87,17 @@ .unwrap(); ``` * +* Fixed potential race condition wrt. GDAL driver initialization + * * Add basic support to read overviews +* Added a `Dataset::build_overviews` method + * * BREAKING: update geo-types to 0.7.0. geo-types Coordinate now implement `Debug` * * Deprecated `SpatialRef::get_axis_mapping_strategy` - migrate to `SpatialRef::axis_mapping_strategy` instead. - +* Add support for reading and setting rasterband colour interpretations + * ## 0.7.1 * fix docs.rs build for gdal-sys * diff --git a/build.rs b/build.rs index 9b3f04c6..93da50e5 100644 --- a/build.rs +++ b/build.rs @@ -26,10 +26,10 @@ fn main() { let detected_version = Version::parse(semver_substring).expect("Could not parse gdal version!"); if detected_version.major < 2 { - panic!(format!( + panic!( "The GDAL crate requires a GDAL version >= 2.0.0. Found {}", detected_version.to_string() - )); + ); } println!("cargo:rustc-cfg=gdal_{}", detected_version.major); diff --git a/examples/read_write_ogr_datetime.rs b/examples/read_write_ogr_datetime.rs index 972d3f6c..bd0943e8 100644 --- a/examples/read_write_ogr_datetime.rs +++ b/examples/read_write_ogr_datetime.rs @@ -1,7 +1,5 @@ -use gdal::errors::Result; - #[cfg(feature = "datetime")] -fn run() -> Result<()> { +fn run() -> gdal::errors::Result<()> { use chrono::Duration; use gdal::vector::{Defn, Feature, FieldDefn, FieldValue}; use gdal::{Dataset, Driver}; @@ -60,11 +58,13 @@ fn run() -> Result<()> { } #[cfg(not(feature = "datetime"))] -fn run() -> Result<()> { +fn run() { println!("gdal crate was build without datetime support"); - Ok(()) } fn main() { + #[cfg(feature = "datetime")] run().unwrap(); + #[cfg(not(feature = "datetime"))] + run(); } diff --git a/gdal-sys/Cargo.toml b/gdal-sys/Cargo.toml index 7f1b35ff..3134e533 100644 --- a/gdal-sys/Cargo.toml +++ b/gdal-sys/Cargo.toml @@ -11,6 +11,6 @@ edition = "2018" libc = "0.2" [build-dependencies] -bindgen = { version = "0.56", optional = true } +bindgen = { version = "0.57", optional = true } pkg-config = "0.3" semver = "0.11" diff --git a/src/dataset.rs b/src/dataset.rs index 7d469b11..c6f99df0 100644 --- a/src/dataset.rs +++ b/src/dataset.rs @@ -1,18 +1,25 @@ -use std::{ffi::CString, ffi::NulError, path::Path, ptr, sync::Once}; +use ptr::null_mut; +use std::convert::TryInto; +use std::{ + ffi::NulError, + ffi::{CStr, CString}, + path::Path, + ptr, +}; +use crate::errors::*; use crate::utils::{_last_cpl_err, _last_null_pointer_err, _string}; +use crate::vector::sql; +use crate::vector::Geometry; use crate::{ gdal_major_object::MajorObject, raster::RasterBand, spatial_ref::SpatialRef, vector::Layer, Driver, Metadata, }; +use gdal_sys::OGRGeometryH; use gdal_sys::{ self, CPLErr, GDALAccess, GDALDatasetH, GDALMajorObjectH, OGRErr, OGRLayerH, OGRwkbGeometryType, }; use libc::{c_double, c_int, c_uint}; -use ptr::null_mut; - -use crate::errors::*; -use std::convert::TryInto; use bitflags::bitflags; @@ -21,7 +28,6 @@ use bitflags::bitflags; /// /// [GDALGetGeoTransform]: https://gdal.org/api/gdaldataset_cpp.html#classGDALDataset_1a5101119705f5fa2bc1344ab26f66fd1d pub type GeoTransform = [c_double; 6]; -static START: Once = Once::new(); /// Wrapper around a [`GDALDataset`][GDALDataset] object. /// @@ -38,14 +44,6 @@ pub struct Dataset { c_dataset: GDALDatasetH, } -pub fn _register_drivers() { - unsafe { - START.call_once(|| { - gdal_sys::GDALAllRegister(); - }); - } -} - // These are skipped by bindgen and manually updated. #[cfg(major_ge_2)] bitflags! { @@ -146,9 +144,12 @@ impl Dataset { } /// Open a dataset with extended options. See - /// [GDALOpenEx]. + /// [`GDALOpenEx`]. + /// + /// [`GDALOpenEx`]: https://gdal.org/doxygen/gdal_8h.html#a9cb8585d0b3c16726b08e25bcc94274a pub fn open_ex(path: &Path, options: DatasetOptions) -> Result { - _register_drivers(); + crate::driver::_register_drivers(); + let filename = path.to_string_lossy(); let c_filename = CString::new(filename.as_ref())?; let c_open_flags = options.open_flags.bits; @@ -315,6 +316,39 @@ impl Dataset { } } + /// Builds overviews for the current `Dataset`. See [`GDALBuildOverviews`]. + /// + /// # Arguments + /// * `resampling` - resampling method, as accepted by GDAL, e.g. `"CUBIC"` + /// * `overviews` - list of overview decimation factors, e.g. `&[2, 4, 8, 16, 32]` + /// * `bands` - list of bands to build the overviews for, or empty for all bands + /// + /// [`GDALBuildOverviews`]: https://gdal.org/doxygen/gdal_8h.html#a767f4456a6249594ee18ea53f68b7e80 + pub fn build_overviews( + &mut self, + resampling: &str, + overviews: &[i32], + bands: &[i32], + ) -> Result<()> { + let c_resampling = CString::new(resampling)?; + let rv = unsafe { + gdal_sys::GDALBuildOverviews( + self.c_dataset, + c_resampling.as_ptr(), + overviews.len() as i32, + overviews.as_ptr() as *mut i32, + bands.len() as i32, + bands.as_ptr() as *mut i32, + None, + null_mut(), + ) + }; + if rv != CPLErr::CE_None { + return Err(_last_cpl_err(rv)); + } + Ok(()) + } + fn child_layer(&self, c_layer: OGRLayerH) -> Layer { unsafe { Layer::from_c_layer(self, c_layer) } } @@ -508,6 +542,107 @@ impl Dataset { } Ok(Transaction::new(self)) } + + /// Execute a SQL query against the Dataset. It is equivalent to calling + /// [`GDALDatasetExecuteSQL`](https://gdal.org/api/raster_c_api.html#_CPPv421GDALDatasetExecuteSQL12GDALDatasetHPKc12OGRGeometryHPKc). + /// Returns a [`sql::ResultSet`], which can be treated just as any other [`Layer`]. + /// + /// Queries such as `ALTER TABLE`, `CREATE INDEX`, etc. have no [`sql::ResultSet`], and return + /// `None`, which is distinct from an empty [`sql::ResultSet`]. + /// + /// # Arguments + /// + /// * `query`: The SQL query + /// * `spatial_filter`: Limit results of the query to features that intersect the given + /// [`Geometry`] + /// * `dialect`: The dialect of SQL to use. See + /// + /// + /// # Example + /// + /// ``` + /// # use gdal::Dataset; + /// # use std::path::Path; + /// use gdal::vector::sql; + /// + /// let ds = Dataset::open(Path::new("fixtures/roads.geojson")).unwrap(); + /// let query = "SELECT kind, is_bridge, highway FROM roads WHERE highway = 'pedestrian'"; + /// let result_set = ds.execute_sql(query, None, sql::Dialect::DEFAULT).unwrap().unwrap(); + /// + /// assert_eq!(10, result_set.feature_count()); + /// + /// for feature in result_set.features() { + /// let highway = feature + /// .field("highway") + /// .unwrap() + /// .unwrap() + /// .into_string() + /// .unwrap(); + /// + /// assert_eq!("pedestrian", highway); + /// } + /// ``` + pub fn execute_sql>( + &self, + query: S, + spatial_filter: Option<&Geometry>, + dialect: sql::Dialect, + ) -> Result> { + let query = CString::new(query.as_ref())?; + + let dialect_c_str = match dialect { + sql::Dialect::DEFAULT => None, + sql::Dialect::OGR => Some(unsafe { CStr::from_bytes_with_nul_unchecked(sql::OGRSQL) }), + sql::Dialect::SQLITE => { + Some(unsafe { CStr::from_bytes_with_nul_unchecked(sql::SQLITE) }) + } + }; + + self._execute_sql(query, spatial_filter, dialect_c_str) + } + + fn _execute_sql( + &self, + query: CString, + spatial_filter: Option<&Geometry>, + dialect_c_str: Option<&CStr>, + ) -> Result> { + let mut filter_geom: OGRGeometryH = std::ptr::null_mut(); + + let dialect_ptr = match dialect_c_str { + None => std::ptr::null(), + Some(ref d) => d.as_ptr(), + }; + + if let Some(spatial_filter) = spatial_filter { + filter_geom = unsafe { spatial_filter.c_geometry() }; + } + + let c_dataset = unsafe { self.c_dataset() }; + + unsafe { gdal_sys::CPLErrorReset() }; + + let c_layer = unsafe { + gdal_sys::GDALDatasetExecuteSQL(c_dataset, query.as_ptr(), filter_geom, dialect_ptr) + }; + + let cpl_err = unsafe { gdal_sys::CPLGetLastErrorType() }; + + if cpl_err != CPLErr::CE_None { + return Err(_last_cpl_err(cpl_err)); + } + + if c_layer.is_null() { + return Ok(None); + } + + let layer = unsafe { Layer::from_c_layer(self, c_layer) }; + + Ok(Some(sql::ResultSet { + layer, + dataset: c_dataset, + })) + } } pub struct LayerIterator<'a> { diff --git a/src/errors.rs b/src/errors.rs index 157bc50b..d8a2e916 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -9,6 +9,8 @@ pub type Result = std::result::Result; pub enum GdalError { #[error("FfiNulError")] FfiNulError(#[from] std::ffi::NulError), + #[error("FfiIntoStringError")] + FfiIntoStringError(#[from] std::ffi::IntoStringError), #[error("StrUtf8Error")] StrUtf8Error(#[from] std::str::Utf8Error), #[cfg(feature = "ndarray")] diff --git a/src/raster/mod.rs b/src/raster/mod.rs index 6f1579be..8eba798c 100644 --- a/src/raster/mod.rs +++ b/src/raster/mod.rs @@ -4,7 +4,7 @@ mod rasterband; mod types; mod warp; -pub use rasterband::{Buffer, ByteBuffer, RasterBand}; +pub use rasterband::{Buffer, ByteBuffer, ColorInterpretation, RasterBand}; pub use types::{GDALDataType, GdalType}; pub use warp::reproject; diff --git a/src/raster/rasterband.rs b/src/raster/rasterband.rs index de999ebe..303f67ba 100644 --- a/src/raster/rasterband.rs +++ b/src/raster/rasterband.rs @@ -2,9 +2,10 @@ use crate::dataset::Dataset; use crate::gdal_major_object::MajorObject; use crate::metadata::Metadata; use crate::raster::{GDALDataType, GdalType}; -use crate::utils::{_last_cpl_err, _last_null_pointer_err}; -use gdal_sys::{self, CPLErr, GDALMajorObjectH, GDALRWFlag, GDALRasterBandH}; +use crate::utils::{_last_cpl_err, _last_null_pointer_err, _string}; +use gdal_sys::{self, CPLErr, GDALColorInterp, GDALMajorObjectH, GDALRWFlag, GDALRasterBandH}; use libc::c_int; +use std::ffi::CString; #[cfg(feature = "ndarray")] use ndarray::Array2; @@ -261,6 +262,23 @@ impl<'a> RasterBand<'a> { Ok(()) } + /// Returns the color interpretation of this band. + pub fn color_interpretation(&self) -> ColorInterpretation { + let interp_index = unsafe { gdal_sys::GDALGetRasterColorInterpretation(self.c_rasterband) }; + ColorInterpretation::from_c_int(interp_index).unwrap() + } + + /// Set the color interpretation for this band. + pub fn set_color_interpretation(&mut self, interp: ColorInterpretation) -> Result<()> { + let interp_index = interp.c_int(); + let rv = + unsafe { gdal_sys::GDALSetRasterColorInterpretation(self.c_rasterband, interp_index) }; + if rv != CPLErr::CE_None { + return Err(_last_cpl_err(rv)); + } + Ok(()) + } + /// Returns the scale of this band if set. pub fn scale(&self) -> Option { let mut pb_success = 1; @@ -338,3 +356,105 @@ impl Buffer { } pub type ByteBuffer = Buffer; + +/// Represents a color interpretation of a RasterBand +#[derive(Debug, PartialEq)] +pub enum ColorInterpretation { + /// Undefined + Undefined, + /// Grayscale + GrayIndex, + /// Paletted (see associated color table) + PaletteIndex, + /// Red band of RGBA image + RedBand, + /// Green band of RGBA image + GreenBand, + /// Blue band of RGBA image + BlueBand, + /// Alpha (0=transparent, 255=opaque) + AlphaBand, + /// Hue band of HLS image + HueBand, + /// Saturation band of HLS image + SaturationBand, + /// Lightness band of HLS image + LightnessBand, + /// Cyan band of CMYK image + CyanBand, + /// Magenta band of CMYK image + MagentaBand, + /// Yellow band of CMYK image + YellowBand, + /// Black band of CMYK image + BlackBand, + /// Y Luminance + YCbCrSpaceYBand, + /// Cb Chroma + YCbCrSpaceCbBand, + /// Cr Chroma + YCbCrSpaceCrBand, +} + +impl ColorInterpretation { + /// Creates a color interpretation from its C API int value. + pub fn from_c_int(color_interpretation: GDALColorInterp::Type) -> Option { + match color_interpretation { + GDALColorInterp::GCI_Undefined => Some(Self::Undefined), + GDALColorInterp::GCI_GrayIndex => Some(Self::GrayIndex), + GDALColorInterp::GCI_PaletteIndex => Some(Self::PaletteIndex), + GDALColorInterp::GCI_RedBand => Some(Self::RedBand), + GDALColorInterp::GCI_GreenBand => Some(Self::GreenBand), + GDALColorInterp::GCI_BlueBand => Some(Self::BlueBand), + GDALColorInterp::GCI_AlphaBand => Some(Self::AlphaBand), + GDALColorInterp::GCI_HueBand => Some(Self::HueBand), + GDALColorInterp::GCI_SaturationBand => Some(Self::SaturationBand), + GDALColorInterp::GCI_LightnessBand => Some(Self::LightnessBand), + GDALColorInterp::GCI_CyanBand => Some(Self::CyanBand), + GDALColorInterp::GCI_MagentaBand => Some(Self::MagentaBand), + GDALColorInterp::GCI_YellowBand => Some(Self::YellowBand), + GDALColorInterp::GCI_BlackBand => Some(Self::BlackBand), + GDALColorInterp::GCI_YCbCr_YBand => Some(Self::YCbCrSpaceYBand), + GDALColorInterp::GCI_YCbCr_CbBand => Some(Self::YCbCrSpaceCbBand), + GDALColorInterp::GCI_YCbCr_CrBand => Some(Self::YCbCrSpaceCrBand), + _ => None, + } + } + + /// Returns the C API int value of this color interpretation. + pub fn c_int(&self) -> GDALColorInterp::Type { + match self { + Self::Undefined => GDALColorInterp::GCI_Undefined, + Self::GrayIndex => GDALColorInterp::GCI_GrayIndex, + Self::PaletteIndex => GDALColorInterp::GCI_PaletteIndex, + Self::RedBand => GDALColorInterp::GCI_RedBand, + Self::GreenBand => GDALColorInterp::GCI_GreenBand, + Self::BlueBand => GDALColorInterp::GCI_BlueBand, + Self::AlphaBand => GDALColorInterp::GCI_AlphaBand, + Self::HueBand => GDALColorInterp::GCI_HueBand, + Self::SaturationBand => GDALColorInterp::GCI_SaturationBand, + Self::LightnessBand => GDALColorInterp::GCI_LightnessBand, + Self::CyanBand => GDALColorInterp::GCI_CyanBand, + Self::MagentaBand => GDALColorInterp::GCI_MagentaBand, + Self::YellowBand => GDALColorInterp::GCI_YellowBand, + Self::BlackBand => GDALColorInterp::GCI_BlackBand, + Self::YCbCrSpaceYBand => GDALColorInterp::GCI_YCbCr_YBand, + Self::YCbCrSpaceCbBand => GDALColorInterp::GCI_YCbCr_CbBand, + Self::YCbCrSpaceCrBand => GDALColorInterp::GCI_YCbCr_CrBand, + } + } + + /// Creates a color interpretation from its name. + pub fn from_name(name: &str) -> Result { + let c_str_interp_name = CString::new(name)?; + let interp_index = + unsafe { gdal_sys::GDALGetColorInterpretationByName(c_str_interp_name.as_ptr()) }; + Ok(Self::from_c_int(interp_index).unwrap()) + } + + /// Returns the name of this color interpretation. + pub fn name(&self) -> String { + let rv = unsafe { gdal_sys::GDALGetColorInterpretationName(self.c_int()) }; + _string(rv) + } +} diff --git a/src/raster/tests.rs b/src/raster/tests.rs index 374819f3..c77d724d 100644 --- a/src/raster/tests.rs +++ b/src/raster/tests.rs @@ -1,6 +1,6 @@ use crate::dataset::Dataset; use crate::metadata::Metadata; -use crate::raster::ByteBuffer; +use crate::raster::{ByteBuffer, ColorInterpretation}; use crate::Driver; use gdal_sys::GDALDataType; use std::path::Path; @@ -477,3 +477,36 @@ fn test_rasterband_lifetime() { drop(rasterband); assert!(overview.no_data_value().is_none()); } + +#[test] +fn test_get_rasterband_color_interp() { + let dataset = Dataset::open(fixture!("tinymarble.png")).unwrap(); + let rasterband = dataset.rasterband(1).unwrap(); + let band_interp = rasterband.color_interpretation(); + assert_eq!(band_interp, ColorInterpretation::RedBand); +} + +#[test] +fn test_set_rasterband_color_interp() { + let driver = Driver::get("MEM").unwrap(); + let dataset = driver.create("", 1, 1, 1).unwrap(); + let mut rasterband = dataset.rasterband(1).unwrap(); + rasterband + .set_color_interpretation(ColorInterpretation::AlphaBand) + .unwrap(); + let band_interp = rasterband.color_interpretation(); + assert_eq!(band_interp, ColorInterpretation::AlphaBand); +} + +#[test] +fn test_color_interp_names() { + assert_eq!(ColorInterpretation::AlphaBand.name(), "Alpha"); + assert_eq!( + ColorInterpretation::from_name("Alpha").unwrap(), + ColorInterpretation::AlphaBand + ); + assert_eq!( + ColorInterpretation::from_name("not a valid name").unwrap(), + ColorInterpretation::Undefined + ); +} diff --git a/src/vector/defn.rs b/src/vector/defn.rs index b460c630..797e86d4 100644 --- a/src/vector/defn.rs +++ b/src/vector/defn.rs @@ -11,6 +11,7 @@ use crate::errors::*; /// Layer definition /// /// Defines the fields available for features in a layer. +#[derive(Debug)] pub struct Defn { c_defn: OGRFeatureDefnH, } diff --git a/src/vector/feature.rs b/src/vector/feature.rs index d6d5f7ff..d29209e3 100644 --- a/src/vector/feature.rs +++ b/src/vector/feature.rs @@ -15,6 +15,7 @@ use crate::errors::*; use std::slice; /// OGR Feature +#[derive(Debug)] pub struct Feature<'a> { _defn: &'a Defn, c_feature: OGRFeatureH, @@ -72,12 +73,16 @@ impl<'a> Feature<'a> { /// If the field has an unsupported type, returns a [`GdalError::UnhandledFieldType`]. /// /// If the field is null, returns `None`. - pub fn field(&self, name: &str) -> Result> { - let c_name = CString::new(name)?; + pub fn field>(&self, name: S) -> Result> { + let c_name = CString::new(name.as_ref())?; + self._field(c_name) + } + + fn _field(&self, c_name: CString) -> Result> { let field_id = unsafe { gdal_sys::OGR_F_GetFieldIndex(self.c_feature, c_name.as_ptr()) }; if field_id == -1 { Err(GdalError::InvalidFieldName { - field_name: name.to_string(), + field_name: c_name.into_string()?, method_name: "OGR_F_GetFieldIndex", }) } else { diff --git a/src/vector/layer.rs b/src/vector/layer.rs index 5a65c4ac..df69a997 100644 --- a/src/vector/layer.rs +++ b/src/vector/layer.rs @@ -13,6 +13,73 @@ use std::{convert::TryInto, ffi::CString, marker::PhantomData}; use crate::errors::*; +/// Layer capabilities +pub enum LayerCaps { + /// Layer capability for random read + OLCRandomRead, + /// Layer capability for sequential write + OLCSequentialWrite, + /// Layer capability for random write + OLCRandomWrite, + /// Layer capability for fast spatial filter + OLCFastSpatialFilter, + /// Layer capability for fast feature count retrieval + OLCFastFeatureCount, + /// Layer capability for fast extent retrieval + OLCFastGetExtent, + /// Layer capability for field creation + OLCCreateField, + /// Layer capability for field deletion + OLCDeleteField, + /// Layer capability for field reordering + OLCReorderFields, + /// Layer capability for field alteration + OLCAlterFieldDefn, + /// Layer capability for transactions + OLCTransactions, + /// Layer capability for feature deletiond + OLCDeleteFeature, + /// Layer capability for setting next feature index + OLCFastSetNextByIndex, + /// Layer capability for strings returned with UTF-8 encoding + OLCStringsAsUTF8, + /// Layer capability for field ignoring + OLCIgnoreFields, + /// Layer capability for geometry field creation + OLCCreateGeomField, + /// Layer capability for curve geometries support + OLCCurveGeometries, + /// Layer capability for measured geometries support + OLCMeasuredGeometries, +} + +// Manage conversion to Gdal values +impl LayerCaps { + fn into_cstring(self) -> CString { + CString::new(match self { + Self::OLCRandomRead => "RandomRead", + Self::OLCSequentialWrite => "SequentialWrite", + Self::OLCRandomWrite => "RandomWrite", + Self::OLCFastSpatialFilter => "FastSpatialFilter", + Self::OLCFastFeatureCount => "FastFeatureCount", + Self::OLCFastGetExtent => "FastGetExtent", + Self::OLCCreateField => "CreateField", + Self::OLCDeleteField => "DeleteField", + Self::OLCReorderFields => "ReorderFields", + Self::OLCAlterFieldDefn => "AlterFieldDefn", + Self::OLCTransactions => "Transactions", + Self::OLCDeleteFeature => "DeleteFeature", + Self::OLCFastSetNextByIndex => "FastSetNextByIndex", + Self::OLCStringsAsUTF8 => "StringsAsUTF8", + Self::OLCIgnoreFields => "IgnoreFields", + Self::OLCCreateGeomField => "CreateGeomField", + Self::OLCCurveGeometries => "CurveGeometries", + Self::OLCMeasuredGeometries => "MeasuredGeometries", + }) + .unwrap() + } +} + /// Layer in a vector dataset /// /// ``` @@ -25,6 +92,7 @@ use crate::errors::*; /// // do something with each feature /// } /// ``` +#[derive(Debug)] pub struct Layer<'a> { c_layer: OGRLayerH, defn: Defn, @@ -106,6 +174,12 @@ impl<'a> Layer<'a> { _string(rv) } + pub fn has_capability(&self, capability: LayerCaps) -> bool { + unsafe { + gdal_sys::OGR_L_TestCapability(self.c_layer, capability.into_cstring().as_ptr()) == 1 + } + } + pub fn defn(&self) -> &Defn { &self.defn } diff --git a/src/vector/mod.rs b/src/vector/mod.rs index 2daa1ae0..3dbe64e8 100644 --- a/src/vector/mod.rs +++ b/src/vector/mod.rs @@ -22,12 +22,13 @@ mod geo_to_gdal; mod geometry; mod layer; mod ops; +pub mod sql; pub use defn::{Defn, Field, FieldIterator}; pub use feature::{Feature, FieldValue, FieldValueIterator}; pub use gdal_sys::{OGRFieldType, OGRwkbGeometryType}; pub use geometry::Geometry; -pub use layer::{FeatureIterator, FieldDefn, Layer}; +pub use layer::{FeatureIterator, FieldDefn, Layer, LayerCaps}; pub use ops::GeometryIntersection; use crate::errors::Result; diff --git a/src/vector/sql.rs b/src/vector/sql.rs new file mode 100644 index 00000000..67fcc602 --- /dev/null +++ b/src/vector/sql.rs @@ -0,0 +1,46 @@ +use std::ops::Deref; + +use gdal_sys::GDALDatasetH; + +use crate::vector::Layer; + +/// The result of a SQL query executed by +/// [`Dataset::execute_sql()`](crate::Dataset::execute_sql()). It is just a thin wrapper around a +/// [`Layer`], and you can treat it as such. +#[derive(Debug)] +pub struct ResultSet<'a> { + pub(crate) layer: Layer<'a>, + pub(crate) dataset: GDALDatasetH, +} + +impl<'a> Deref for ResultSet<'a> { + type Target = Layer<'a>; + + fn deref(&self) -> &Self::Target { + &self.layer + } +} + +impl<'a> Drop for ResultSet<'a> { + fn drop(&mut self) { + unsafe { gdal_sys::GDALDatasetReleaseResultSet(self.dataset, self.layer.c_layer()) }; + } +} + +/// Represents valid SQL dialects to use in SQL queries. See +/// +pub enum Dialect { + /// Use the default dialect. This is OGR SQL unless the underlying driver has a native dialect, + /// such as MySQL, Postgres, Oracle, etc. + DEFAULT, + + /// Explicitly choose OGR SQL regardless of if the underlying driver has a native dialect. + OGR, + + /// SQLite dialect. If the data set is not actually a SQLite database, then a virtual SQLite + /// table is created to execute the query. + SQLITE, +} + +pub(crate) const OGRSQL: &[u8] = b"OGRSQL\0"; +pub(crate) const SQLITE: &[u8] = b"SQLITE\0"; diff --git a/src/vector/vector_tests/mod.rs b/src/vector/vector_tests/mod.rs index ca06bb16..1fff5f1b 100644 --- a/src/vector/vector_tests/mod.rs +++ b/src/vector/vector_tests/mod.rs @@ -1,15 +1,17 @@ use super::{ - Feature, FeatureIterator, FieldValue, Geometry, Layer, OGRFieldType, OGRwkbGeometryType, + Feature, FeatureIterator, FieldValue, Geometry, Layer, LayerCaps::*, OGRFieldType, + OGRwkbGeometryType, }; use crate::spatial_ref::SpatialRef; use crate::{assert_almost_eq, Dataset, Driver}; -use std::path::Path; mod convert_geo; +mod sql; +#[macro_export] macro_rules! fixture { ($name:expr) => { - Path::new(file!()) + std::path::Path::new(file!()) .parent() .unwrap() .parent() @@ -63,6 +65,18 @@ fn test_layer_spatial_ref() { assert_eq!(srs.auth_code().unwrap(), 4326); } +#[test] +fn test_layer_capabilities() { + let mut ds = Dataset::open(fixture!("roads.geojson")).unwrap(); + let layer = ds.layer(0).unwrap(); + + assert_eq!(layer.has_capability(OLCFastSpatialFilter), false); + assert_eq!(layer.has_capability(OLCFastFeatureCount), true); + assert_eq!(layer.has_capability(OLCFastGetExtent), false); + assert_eq!(layer.has_capability(OLCRandomRead), true); + assert_eq!(layer.has_capability(OLCStringsAsUTF8), true); +} + fn ds_with_layer(ds_name: &str, layer_name: &str, f: F) where F: Fn(Layer), diff --git a/src/vector/vector_tests/sql.rs b/src/vector/vector_tests/sql.rs new file mode 100644 index 00000000..82fffc51 --- /dev/null +++ b/src/vector/vector_tests/sql.rs @@ -0,0 +1,133 @@ +use std::collections::HashSet; + +use crate::{ + fixture, + vector::{sql, Geometry}, + Dataset, +}; + +#[test] +fn test_sql() { + let ds = Dataset::open(fixture!("roads.geojson")).unwrap(); + let query = "SELECT kind, is_bridge, highway FROM roads WHERE highway = 'pedestrian'"; + let result_set = ds + .execute_sql(query, None, sql::Dialect::DEFAULT) + .unwrap() + .unwrap(); + + let field_names: HashSet<_> = result_set + .defn() + .fields() + .map(|field| field.name()) + .collect(); + + let mut correct_field_names = HashSet::new(); + correct_field_names.insert("kind".into()); + correct_field_names.insert("is_bridge".into()); + correct_field_names.insert("highway".into()); + + assert_eq!(correct_field_names, field_names); + assert_eq!(10, result_set.feature_count()); + + for feature in result_set.features() { + let highway = feature + .field("highway") + .unwrap() + .unwrap() + .into_string() + .unwrap(); + + assert_eq!("pedestrian", highway); + } +} + +#[test] +fn test_sql_with_spatial_filter() { + let query = "SELECT * FROM roads WHERE highway = 'pedestrian'"; + let ds = Dataset::open(fixture!("roads.geojson")).unwrap(); + let bbox = Geometry::bbox(26.1017, 44.4297, 26.1025, 44.4303).unwrap(); + let result_set = ds + .execute_sql(query, Some(&bbox), sql::Dialect::DEFAULT) + .unwrap() + .unwrap(); + + assert_eq!(2, result_set.feature_count()); + let mut correct_fids = HashSet::new(); + correct_fids.insert(252725993); + correct_fids.insert(23489656); + + let mut fids = HashSet::new(); + for feature in result_set.features() { + let highway = feature + .field("highway") + .unwrap() + .unwrap() + .into_string() + .unwrap(); + + assert_eq!("pedestrian", highway); + fids.insert(feature.fid().unwrap()); + } + + assert_eq!(correct_fids, fids); +} + +#[test] +fn test_sql_with_dialect() { + let query = "SELECT * FROM roads WHERE highway = 'pedestrian' and NumPoints(GEOMETRY) = 3"; + let ds = Dataset::open(fixture!("roads.geojson")).unwrap(); + let bbox = Geometry::bbox(26.1017, 44.4297, 26.1025, 44.4303).unwrap(); + let result_set = ds + .execute_sql(query, Some(&bbox), sql::Dialect::SQLITE) + .unwrap() + .unwrap(); + + assert_eq!(1, result_set.feature_count()); + let mut features: Vec<_> = result_set.features().collect(); + let feature = features.pop().unwrap(); + let highway = feature + .field("highway") + .unwrap() + .unwrap() + .into_string() + .unwrap(); + + assert_eq!("pedestrian", highway); +} + +#[test] +fn test_sql_empty_result() { + let ds = Dataset::open(fixture!("roads.geojson")).unwrap(); + let query = "SELECT kind, is_bridge, highway FROM roads WHERE highway = 'jazz hands 👐'"; + let result_set = ds + .execute_sql(query, None, sql::Dialect::DEFAULT) + .unwrap() + .unwrap(); + assert_eq!(0, result_set.feature_count()); + assert_eq!(0, result_set.features().count()); +} + +#[test] +fn test_sql_no_result() { + let ds = Dataset::open(fixture!("roads.geojson")).unwrap(); + let query = "ALTER TABLE roads ADD COLUMN fun integer"; + let result_set = ds.execute_sql(query, None, sql::Dialect::DEFAULT).unwrap(); + assert!(result_set.is_none()); +} + +#[test] +fn test_sql_bad_query() { + let ds = Dataset::open(fixture!("roads.geojson")).unwrap(); + + let query = "SELECT nope FROM roads"; + let result_set = ds.execute_sql(query, None, sql::Dialect::DEFAULT); + assert!(result_set.is_err()); + + let query = "SELECT nope FROM"; + let result_set = ds.execute_sql(query, None, sql::Dialect::DEFAULT); + assert!(result_set.is_err()); + + let query = "SELECT ninetynineredballoons(highway) FROM roads"; + let result_set = ds.execute_sql(query, None, sql::Dialect::DEFAULT); + assert!(result_set.is_err()); +}