Skip to content

Commit ed789c6

Browse files
authored
Merge pull request #153 from dead10ck/sql
Implement SQL queries
2 parents 838a685 + 926ccff commit ed789c6

File tree

9 files changed

+308
-10
lines changed

9 files changed

+308
-10
lines changed

src/dataset.rs

Lines changed: 113 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,25 @@
1-
use std::{ffi::CString, ffi::NulError, path::Path, ptr};
1+
use ptr::null_mut;
2+
use std::convert::TryInto;
3+
use std::{
4+
ffi::NulError,
5+
ffi::{CStr, CString},
6+
path::Path,
7+
ptr,
8+
};
29

10+
use crate::errors::*;
311
use crate::utils::{_last_cpl_err, _last_null_pointer_err, _string};
12+
use crate::vector::sql;
13+
use crate::vector::Geometry;
414
use crate::{
515
gdal_major_object::MajorObject, raster::RasterBand, spatial_ref::SpatialRef, vector::Layer,
616
Driver, Metadata,
717
};
18+
use gdal_sys::OGRGeometryH;
819
use gdal_sys::{
920
self, CPLErr, GDALAccess, GDALDatasetH, GDALMajorObjectH, OGRErr, OGRLayerH, OGRwkbGeometryType,
1021
};
1122
use libc::{c_double, c_int, c_uint};
12-
use ptr::null_mut;
13-
14-
use crate::errors::*;
15-
use std::convert::TryInto;
1623

1724
use bitflags::bitflags;
1825

@@ -535,6 +542,107 @@ impl Dataset {
535542
}
536543
Ok(Transaction::new(self))
537544
}
545+
546+
/// Execute a SQL query against the Dataset. It is equivalent to calling
547+
/// [`GDALDatasetExecuteSQL`](https://gdal.org/api/raster_c_api.html#_CPPv421GDALDatasetExecuteSQL12GDALDatasetHPKc12OGRGeometryHPKc).
548+
/// Returns a [`sql::ResultSet`], which can be treated just as any other [`Layer`].
549+
///
550+
/// Queries such as `ALTER TABLE`, `CREATE INDEX`, etc. have no [`sql::ResultSet`], and return
551+
/// `None`, which is distinct from an empty [`sql::ResultSet`].
552+
///
553+
/// # Arguments
554+
///
555+
/// * `query`: The SQL query
556+
/// * `spatial_filter`: Limit results of the query to features that intersect the given
557+
/// [`Geometry`]
558+
/// * `dialect`: The dialect of SQL to use. See
559+
/// <https://gdal.org/user/ogr_sql_sqlite_dialect.html>
560+
///
561+
/// # Example
562+
///
563+
/// ```
564+
/// # use gdal::Dataset;
565+
/// # use std::path::Path;
566+
/// use gdal::vector::sql;
567+
///
568+
/// let ds = Dataset::open(Path::new("fixtures/roads.geojson")).unwrap();
569+
/// let query = "SELECT kind, is_bridge, highway FROM roads WHERE highway = 'pedestrian'";
570+
/// let result_set = ds.execute_sql(query, None, sql::Dialect::DEFAULT).unwrap().unwrap();
571+
///
572+
/// assert_eq!(10, result_set.feature_count());
573+
///
574+
/// for feature in result_set.features() {
575+
/// let highway = feature
576+
/// .field("highway")
577+
/// .unwrap()
578+
/// .unwrap()
579+
/// .into_string()
580+
/// .unwrap();
581+
///
582+
/// assert_eq!("pedestrian", highway);
583+
/// }
584+
/// ```
585+
pub fn execute_sql<S: AsRef<str>>(
586+
&self,
587+
query: S,
588+
spatial_filter: Option<&Geometry>,
589+
dialect: sql::Dialect,
590+
) -> Result<Option<sql::ResultSet>> {
591+
let query = CString::new(query.as_ref())?;
592+
593+
let dialect_c_str = match dialect {
594+
sql::Dialect::DEFAULT => None,
595+
sql::Dialect::OGR => Some(unsafe { CStr::from_bytes_with_nul_unchecked(sql::OGRSQL) }),
596+
sql::Dialect::SQLITE => {
597+
Some(unsafe { CStr::from_bytes_with_nul_unchecked(sql::SQLITE) })
598+
}
599+
};
600+
601+
self._execute_sql(query, spatial_filter, dialect_c_str)
602+
}
603+
604+
fn _execute_sql(
605+
&self,
606+
query: CString,
607+
spatial_filter: Option<&Geometry>,
608+
dialect_c_str: Option<&CStr>,
609+
) -> Result<Option<sql::ResultSet>> {
610+
let mut filter_geom: OGRGeometryH = std::ptr::null_mut();
611+
612+
let dialect_ptr = match dialect_c_str {
613+
None => std::ptr::null(),
614+
Some(ref d) => d.as_ptr(),
615+
};
616+
617+
if let Some(spatial_filter) = spatial_filter {
618+
filter_geom = unsafe { spatial_filter.c_geometry() };
619+
}
620+
621+
let c_dataset = unsafe { self.c_dataset() };
622+
623+
unsafe { gdal_sys::CPLErrorReset() };
624+
625+
let c_layer = unsafe {
626+
gdal_sys::GDALDatasetExecuteSQL(c_dataset, query.as_ptr(), filter_geom, dialect_ptr)
627+
};
628+
629+
let cpl_err = unsafe { gdal_sys::CPLGetLastErrorType() };
630+
631+
if cpl_err != CPLErr::CE_None {
632+
return Err(_last_cpl_err(cpl_err));
633+
}
634+
635+
if c_layer.is_null() {
636+
return Ok(None);
637+
}
638+
639+
let layer = unsafe { Layer::from_c_layer(self, c_layer) };
640+
641+
Ok(Some(sql::ResultSet {
642+
layer,
643+
dataset: c_dataset,
644+
}))
645+
}
538646
}
539647

540648
pub struct LayerIterator<'a> {

src/errors.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ pub type Result<T> = std::result::Result<T, GdalError>;
99
pub enum GdalError {
1010
#[error("FfiNulError")]
1111
FfiNulError(#[from] std::ffi::NulError),
12+
#[error("FfiIntoStringError")]
13+
FfiIntoStringError(#[from] std::ffi::IntoStringError),
1214
#[error("StrUtf8Error")]
1315
StrUtf8Error(#[from] std::str::Utf8Error),
1416
#[cfg(feature = "ndarray")]

src/vector/defn.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use crate::errors::*;
1111
/// Layer definition
1212
///
1313
/// Defines the fields available for features in a layer.
14+
#[derive(Debug)]
1415
pub struct Defn {
1516
c_defn: OGRFeatureDefnH,
1617
}

src/vector/feature.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use crate::errors::*;
1515
use std::slice;
1616

1717
/// OGR Feature
18+
#[derive(Debug)]
1819
pub struct Feature<'a> {
1920
_defn: &'a Defn,
2021
c_feature: OGRFeatureH,
@@ -72,12 +73,16 @@ impl<'a> Feature<'a> {
7273
/// If the field has an unsupported type, returns a [`GdalError::UnhandledFieldType`].
7374
///
7475
/// If the field is null, returns `None`.
75-
pub fn field(&self, name: &str) -> Result<Option<FieldValue>> {
76-
let c_name = CString::new(name)?;
76+
pub fn field<S: AsRef<str>>(&self, name: S) -> Result<Option<FieldValue>> {
77+
let c_name = CString::new(name.as_ref())?;
78+
self._field(c_name)
79+
}
80+
81+
fn _field(&self, c_name: CString) -> Result<Option<FieldValue>> {
7782
let field_id = unsafe { gdal_sys::OGR_F_GetFieldIndex(self.c_feature, c_name.as_ptr()) };
7883
if field_id == -1 {
7984
Err(GdalError::InvalidFieldName {
80-
field_name: name.to_string(),
85+
field_name: c_name.into_string()?,
8186
method_name: "OGR_F_GetFieldIndex",
8287
})
8388
} else {

src/vector/layer.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ use crate::errors::*;
2525
/// // do something with each feature
2626
/// }
2727
/// ```
28+
#[derive(Debug)]
2829
pub struct Layer<'a> {
2930
c_layer: OGRLayerH,
3031
defn: Defn,

src/vector/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ mod geo_to_gdal;
2222
mod geometry;
2323
mod layer;
2424
mod ops;
25+
pub mod sql;
2526

2627
pub use defn::{Defn, Field, FieldIterator};
2728
pub use feature::{Feature, FieldValue, FieldValueIterator};

src/vector/sql.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
use std::ops::Deref;
2+
3+
use gdal_sys::GDALDatasetH;
4+
5+
use crate::vector::Layer;
6+
7+
/// The result of a SQL query executed by
8+
/// [`Dataset::execute_sql()`](crate::Dataset::execute_sql()). It is just a thin wrapper around a
9+
/// [`Layer`], and you can treat it as such.
10+
#[derive(Debug)]
11+
pub struct ResultSet<'a> {
12+
pub(crate) layer: Layer<'a>,
13+
pub(crate) dataset: GDALDatasetH,
14+
}
15+
16+
impl<'a> Deref for ResultSet<'a> {
17+
type Target = Layer<'a>;
18+
19+
fn deref(&self) -> &Self::Target {
20+
&self.layer
21+
}
22+
}
23+
24+
impl<'a> Drop for ResultSet<'a> {
25+
fn drop(&mut self) {
26+
unsafe { gdal_sys::GDALDatasetReleaseResultSet(self.dataset, self.layer.c_layer()) };
27+
}
28+
}
29+
30+
/// Represents valid SQL dialects to use in SQL queries. See
31+
/// <https://gdal.org/user/ogr_sql_sqlite_dialect.html>
32+
pub enum Dialect {
33+
/// Use the default dialect. This is OGR SQL unless the underlying driver has a native dialect,
34+
/// such as MySQL, Postgres, Oracle, etc.
35+
DEFAULT,
36+
37+
/// Explicitly choose OGR SQL regardless of if the underlying driver has a native dialect.
38+
OGR,
39+
40+
/// SQLite dialect. If the data set is not actually a SQLite database, then a virtual SQLite
41+
/// table is created to execute the query.
42+
SQLITE,
43+
}
44+
45+
pub(crate) const OGRSQL: &[u8] = b"OGRSQL\0";
46+
pub(crate) const SQLITE: &[u8] = b"SQLITE\0";

src/vector/vector_tests/mod.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@ use super::{
33
};
44
use crate::spatial_ref::SpatialRef;
55
use crate::{assert_almost_eq, Dataset, Driver};
6-
use std::path::Path;
76

87
mod convert_geo;
8+
mod sql;
99

10+
#[macro_export]
1011
macro_rules! fixture {
1112
($name:expr) => {
12-
Path::new(file!())
13+
std::path::Path::new(file!())
1314
.parent()
1415
.unwrap()
1516
.parent()

src/vector/vector_tests/sql.rs

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
use std::collections::HashSet;
2+
3+
use crate::{
4+
fixture,
5+
vector::{sql, Geometry},
6+
Dataset,
7+
};
8+
9+
#[test]
10+
fn test_sql() {
11+
let ds = Dataset::open(fixture!("roads.geojson")).unwrap();
12+
let query = "SELECT kind, is_bridge, highway FROM roads WHERE highway = 'pedestrian'";
13+
let result_set = ds
14+
.execute_sql(query, None, sql::Dialect::DEFAULT)
15+
.unwrap()
16+
.unwrap();
17+
18+
let field_names: HashSet<_> = result_set
19+
.defn()
20+
.fields()
21+
.map(|field| field.name())
22+
.collect();
23+
24+
let mut correct_field_names = HashSet::new();
25+
correct_field_names.insert("kind".into());
26+
correct_field_names.insert("is_bridge".into());
27+
correct_field_names.insert("highway".into());
28+
29+
assert_eq!(correct_field_names, field_names);
30+
assert_eq!(10, result_set.feature_count());
31+
32+
for feature in result_set.features() {
33+
let highway = feature
34+
.field("highway")
35+
.unwrap()
36+
.unwrap()
37+
.into_string()
38+
.unwrap();
39+
40+
assert_eq!("pedestrian", highway);
41+
}
42+
}
43+
44+
#[test]
45+
fn test_sql_with_spatial_filter() {
46+
let query = "SELECT * FROM roads WHERE highway = 'pedestrian'";
47+
let ds = Dataset::open(fixture!("roads.geojson")).unwrap();
48+
let bbox = Geometry::bbox(26.1017, 44.4297, 26.1025, 44.4303).unwrap();
49+
let result_set = ds
50+
.execute_sql(query, Some(&bbox), sql::Dialect::DEFAULT)
51+
.unwrap()
52+
.unwrap();
53+
54+
assert_eq!(2, result_set.feature_count());
55+
let mut correct_fids = HashSet::new();
56+
correct_fids.insert(252725993);
57+
correct_fids.insert(23489656);
58+
59+
let mut fids = HashSet::new();
60+
for feature in result_set.features() {
61+
let highway = feature
62+
.field("highway")
63+
.unwrap()
64+
.unwrap()
65+
.into_string()
66+
.unwrap();
67+
68+
assert_eq!("pedestrian", highway);
69+
fids.insert(feature.fid().unwrap());
70+
}
71+
72+
assert_eq!(correct_fids, fids);
73+
}
74+
75+
#[test]
76+
fn test_sql_with_dialect() {
77+
let query = "SELECT * FROM roads WHERE highway = 'pedestrian' and NumPoints(GEOMETRY) = 3";
78+
let ds = Dataset::open(fixture!("roads.geojson")).unwrap();
79+
let bbox = Geometry::bbox(26.1017, 44.4297, 26.1025, 44.4303).unwrap();
80+
let result_set = ds
81+
.execute_sql(query, Some(&bbox), sql::Dialect::SQLITE)
82+
.unwrap()
83+
.unwrap();
84+
85+
assert_eq!(1, result_set.feature_count());
86+
let mut features: Vec<_> = result_set.features().collect();
87+
let feature = features.pop().unwrap();
88+
let highway = feature
89+
.field("highway")
90+
.unwrap()
91+
.unwrap()
92+
.into_string()
93+
.unwrap();
94+
95+
assert_eq!("pedestrian", highway);
96+
}
97+
98+
#[test]
99+
fn test_sql_empty_result() {
100+
let ds = Dataset::open(fixture!("roads.geojson")).unwrap();
101+
let query = "SELECT kind, is_bridge, highway FROM roads WHERE highway = 'jazz hands 👐'";
102+
let result_set = ds
103+
.execute_sql(query, None, sql::Dialect::DEFAULT)
104+
.unwrap()
105+
.unwrap();
106+
assert_eq!(0, result_set.feature_count());
107+
assert_eq!(0, result_set.features().count());
108+
}
109+
110+
#[test]
111+
fn test_sql_no_result() {
112+
let ds = Dataset::open(fixture!("roads.geojson")).unwrap();
113+
let query = "ALTER TABLE roads ADD COLUMN fun integer";
114+
let result_set = ds.execute_sql(query, None, sql::Dialect::DEFAULT).unwrap();
115+
assert!(result_set.is_none());
116+
}
117+
118+
#[test]
119+
fn test_sql_bad_query() {
120+
let ds = Dataset::open(fixture!("roads.geojson")).unwrap();
121+
122+
let query = "SELECT nope FROM roads";
123+
let result_set = ds.execute_sql(query, None, sql::Dialect::DEFAULT);
124+
assert!(result_set.is_err());
125+
126+
let query = "SELECT nope FROM";
127+
let result_set = ds.execute_sql(query, None, sql::Dialect::DEFAULT);
128+
assert!(result_set.is_err());
129+
130+
let query = "SELECT ninetynineredballoons(highway) FROM roads";
131+
let result_set = ds.execute_sql(query, None, sql::Dialect::DEFAULT);
132+
assert!(result_set.is_err());
133+
}

0 commit comments

Comments
 (0)