diff --git a/lib/src/sql/array.rs b/lib/src/sql/array.rs index d5f94b8e..9b43b8c6 100644 --- a/lib/src/sql/array.rs +++ b/lib/src/sql/array.rs @@ -42,6 +42,12 @@ impl From> for Array { } } +impl From> for Array { + fn from(v: Vec) -> Self { + Self(v.into_iter().map(Value::from).collect()) + } +} + impl From> for Array { fn from(v: Vec<&str>) -> Self { Self(v.into_iter().map(Value::from).collect()) diff --git a/lib/src/sql/geometry.rs b/lib/src/sql/geometry.rs index e039ea34..d4118f2f 100644 --- a/lib/src/sql/geometry.rs +++ b/lib/src/sql/geometry.rs @@ -1,9 +1,13 @@ #![allow(clippy::derived_hash_with_manual_eq)] +use crate::sql::array::Array; use crate::sql::comment::mightbespace; -use crate::sql::common::{closebracket, closeparentheses, commas, openbracket, openparentheses}; +use crate::sql::common::{ + closebraces, closebracket, closeparentheses, commas, openbraces, openbracket, openparentheses, +}; use crate::sql::error::IResult; use crate::sql::fmt::Fmt; +use crate::sql::value::Value; use geo::algorithm::contains::Contains; use geo::algorithm::intersects::Intersects; use geo::{Coord, LineString, Point, Polygon}; @@ -40,35 +44,122 @@ pub enum Geometry { // Add new variants here } +impl Geometry { + /// Check if this is a Point + pub fn is_point(&self) -> bool { + matches!(self, Self::Point(_)) + } + /// Check if this is a Line + pub fn is_line(&self) -> bool { + matches!(self, Self::Line(_)) + } + /// Check if this is a Polygon + pub fn is_polygon(&self) -> bool { + matches!(self, Self::Polygon(_)) + } + /// Check if this is a MultiPoint + pub fn is_multipoint(&self) -> bool { + matches!(self, Self::MultiPoint(_)) + } + /// Check if this is a MultiLine + pub fn is_multiline(&self) -> bool { + matches!(self, Self::MultiLine(_)) + } + /// Check if this is a MultiPolygon + pub fn is_multipolygon(&self) -> bool { + matches!(self, Self::MultiPolygon(_)) + } + /// Check if this is not a Collection + pub fn is_geometry(&self) -> bool { + !matches!(self, Self::Collection(_)) + } + /// Check if this is a Collection + pub fn is_collection(&self) -> bool { + matches!(self, Self::Collection(_)) + } + /// Get the type of this Geometry as text + pub fn as_type(&self) -> &'static str { + match self { + Self::Point(_) => "Point", + Self::Line(_) => "LineString", + Self::Polygon(_) => "Polygon", + Self::MultiPoint(_) => "MultiPoint", + Self::MultiLine(_) => "MultiLineString", + Self::MultiPolygon(_) => "MultiPolygon", + Self::Collection(_) => "GeometryCollection", + } + } + /// Get the raw coordinates of this Geometry as an Array + pub fn as_coordinates(&self) -> Value { + fn point(v: &Point) -> Value { + Array::from(vec![v.x(), v.y()]).into() + } + + fn line(v: &LineString) -> Value { + v.points().map(|v| point(&v)).collect::>().into() + } + + fn polygon(v: &Polygon) -> Value { + once(v.exterior()).chain(v.interiors()).map(line).collect::>().into() + } + + fn multipoint(v: &MultiPoint) -> Value { + v.iter().map(point).collect::>().into() + } + + fn multiline(v: &MultiLineString) -> Value { + v.iter().map(line).collect::>().into() + } + + fn multipolygon(v: &MultiPolygon) -> Value { + v.iter().map(polygon).collect::>().into() + } + + fn collection(v: &[Geometry]) -> Value { + v.iter().map(Geometry::as_coordinates).collect::>().into() + } + + match self { + Self::Point(v) => point(v), + Self::Line(v) => line(v), + Self::Polygon(v) => polygon(v), + Self::MultiPoint(v) => multipoint(v), + Self::MultiLine(v) => multiline(v), + Self::MultiPolygon(v) => multipolygon(v), + Self::Collection(v) => collection(v), + } + } +} + impl PartialOrd for Geometry { #[rustfmt::skip] fn partial_cmp(&self, other: &Self) -> Option { - fn coord(coord: &Coord) -> (f64, f64) { - coord.x_y() + fn coord(v: &Coord) -> (f64, f64) { + v.x_y() } - fn point(point: &Point) -> (f64, f64) { - coord(&point.0) + fn point(v: &Point) -> (f64, f64) { + coord(&v.0) } - fn line(line: &LineString) -> impl Iterator + '_ { - line.into_iter().map(coord) + fn line(v: &LineString) -> impl Iterator + '_ { + v.into_iter().map(coord) } - fn polygon(polygon: &Polygon) -> impl Iterator + '_ { - polygon.interiors().iter().chain(once(polygon.exterior())).flat_map(line) + fn polygon(v: &Polygon) -> impl Iterator + '_ { + v.interiors().iter().chain(once(v.exterior())).flat_map(line) } - fn multi_point(multi_point: &MultiPoint) -> impl Iterator + '_ { - multi_point.iter().map(point) + fn multipoint(v: &MultiPoint) -> impl Iterator + '_ { + v.iter().map(point) } - fn multi_line(multi_line: &MultiLineString) -> impl Iterator + '_ { - multi_line.iter().flat_map(line) + fn multiline(v: &MultiLineString) -> impl Iterator + '_ { + v.iter().flat_map(line) } - fn multi_polygon(multi_polygon: &MultiPolygon) -> impl Iterator + '_ { - multi_polygon.iter().flat_map(polygon) + fn multipolygon(v: &MultiPolygon) -> impl Iterator + '_ { + v.iter().flat_map(polygon) } match (self, other) { @@ -125,9 +216,9 @@ impl PartialOrd for Geometry { (Self::Point(a), Self::Point(b)) => point(a).partial_cmp(&point(b)), (Self::Line(a), Self::Line(b)) => line(a).partial_cmp(line(b)), (Self::Polygon(a), Self::Polygon(b)) => polygon(a).partial_cmp(polygon(b)), - (Self::MultiPoint(a), Self::MultiPoint(b)) => multi_point(a).partial_cmp(multi_point(b)), - (Self::MultiLine(a), Self::MultiLine(b)) => multi_line(a).partial_cmp(multi_line(b)), - (Self::MultiPolygon(a), Self::MultiPolygon(b)) => multi_polygon(a).partial_cmp(multi_polygon(b)), + (Self::MultiPoint(a), Self::MultiPoint(b)) => multipoint(a).partial_cmp(multipoint(b)), + (Self::MultiLine(a), Self::MultiLine(b)) => multiline(a).partial_cmp(multiline(b)), + (Self::MultiPolygon(a), Self::MultiPolygon(b)) => multipolygon(a).partial_cmp(multipolygon(b)), (Self::Collection(a), Self::Collection(b)) => a.partial_cmp(b), } } @@ -547,13 +638,11 @@ fn simple(i: &str) -> IResult<&str, Geometry> { } fn normal(i: &str) -> IResult<&str, Geometry> { - let (i, _) = char('{')(i)?; - let (i, _) = mightbespace(i)?; + let (i, _) = openbraces(i)?; let (i, v) = alt((point, line, polygon, multipoint, multiline, multipolygon, collection))(i)?; let (i, _) = mightbespace(i)?; let (i, _) = opt(char(','))(i)?; - let (i, _) = mightbespace(i)?; - let (i, _) = char('}')(i)?; + let (i, _) = closebraces(i)?; Ok((i, v)) } @@ -706,15 +795,11 @@ fn polygon_vals(i: &str) -> IResult<&str, Polygon> { let (i, e) = line_vals(i)?; let (i, _) = mightbespace(i)?; let (i, _) = opt(char(','))(i)?; + let (i, _) = mightbespace(i)?; + let (i, v) = separated_list0(commas, line_vals)(i)?; + let (i, _) = mightbespace(i)?; + let (i, _) = opt(char(','))(i)?; let (i, _) = closebracket(i)?; - let (i, v) = separated_list0(commas, |i| { - let (i, _) = openbracket(i)?; - let (i, v) = line_vals(i)?; - let (i, _) = mightbespace(i)?; - let (i, _) = opt(char(','))(i)?; - let (i, _) = closebracket(i)?; - Ok((i, v)) - })(i)?; Ok((i, Polygon::new(e, v))) } @@ -876,10 +961,63 @@ mod tests { #[test] fn simple() { - let sql = "(51.509865, -0.118092)"; + let sql = "(-0.118092, 51.509865)"; let res = geometry(sql); assert!(res.is_ok()); let out = res.unwrap().1; - assert_eq!("(51.509865, -0.118092)", format!("{}", out)); + assert_eq!("(-0.118092, 51.509865)", format!("{}", out)); + } + + #[test] + fn point() { + let sql = r#"{ + type: 'Point', + coordinates: [-0.118092, 51.509865] + }"#; + let res = geometry(sql); + assert!(res.is_ok()); + let out = res.unwrap().1; + assert_eq!("(-0.118092, 51.509865)", format!("{}", out)); + } + + #[test] + fn polygon_exterior() { + let sql = r#"{ + type: 'Polygon', + coordinates: [ + [ + [-0.38314819, 51.37692386], [0.1785278, 51.37692386], + [0.1785278, 51.61460570], [-0.38314819, 51.61460570], + [-0.38314819, 51.37692386] + ] + ] + }"#; + let res = geometry(sql); + assert!(res.is_ok()); + let out = res.unwrap().1; + assert_eq!("{ type: 'Polygon', coordinates: [[[-0.38314819, 51.37692386], [0.1785278, 51.37692386], [0.1785278, 51.6146057], [-0.38314819, 51.6146057], [-0.38314819, 51.37692386]]] }", format!("{}", out)); + } + + #[test] + fn polygon_interior() { + let sql = r#"{ + type: 'Polygon', + coordinates: [ + [ + [-0.38314819, 51.37692386], [0.1785278, 51.37692386], + [0.1785278, 51.61460570], [-0.38314819, 51.61460570], + [-0.38314819, 51.37692386] + ], + [ + [-0.38314819, 51.37692386], [0.1785278, 51.37692386], + [0.1785278, 51.61460570], [-0.38314819, 51.61460570], + [-0.38314819, 51.37692386] + ] + ] + }"#; + let res = geometry(sql); + assert!(res.is_ok()); + let out = res.unwrap().1; + assert_eq!("{ type: 'Polygon', coordinates: [[[-0.38314819, 51.37692386], [0.1785278, 51.37692386], [0.1785278, 51.6146057], [-0.38314819, 51.6146057], [-0.38314819, 51.37692386]], [[[-0.38314819, 51.37692386], [0.1785278, 51.37692386], [0.1785278, 51.6146057], [-0.38314819, 51.6146057], [-0.38314819, 51.37692386]]]] }", format!("{}", out)); } } diff --git a/lib/src/sql/ident.rs b/lib/src/sql/ident.rs index ca5cd331..d2ad041c 100644 --- a/lib/src/sql/ident.rs +++ b/lib/src/sql/ident.rs @@ -51,10 +51,22 @@ impl Ident { pub fn to_raw(&self) -> String { self.0.to_string() } - /// Returns a yield if an alias is specified + /// Checks if this field is the `id` field pub(crate) fn is_id(&self) -> bool { self.0.as_str() == "id" } + /// Checks if this field is the `type` field + pub(crate) fn is_type(&self) -> bool { + self.0.as_str() == "type" + } + /// Checks if this field is the `coordinates` field + pub(crate) fn is_coordinates(&self) -> bool { + self.0.as_str() == "coordinates" + } + /// Checks if this field is the `geometries` field + pub(crate) fn is_geometries(&self) -> bool { + self.0.as_str() == "geometries" + } } impl Display for Ident { diff --git a/lib/src/sql/value/get.rs b/lib/src/sql/value/get.rs index cc0092c0..7b67a02a 100644 --- a/lib/src/sql/value/get.rs +++ b/lib/src/sql/value/get.rs @@ -26,6 +26,24 @@ impl Value { match path.first() { // Get the current path part Some(p) => match self { + // Current path part is a geometry + Value::Geometry(v) => match p { + // If this is the 'type' field then continue + Part::Field(f) if f.is_type() => { + Value::from(v.as_type()).get(ctx, opt, path.next()).await + } + // If this is the 'coordinates' field then continue + Part::Field(f) if f.is_coordinates() && v.is_geometry() => { + v.as_coordinates().get(ctx, opt, path.next()).await + } + // If this is the 'geometries' field then continue + Part::Field(f) if f.is_geometries() && v.is_collection() => { + v.as_coordinates().get(ctx, opt, path.next()).await + } + // otherwise return none + _ => Ok(Value::None), + }, + // Current path part is a future Value::Future(v) => { // Check how many path parts are remaining diff --git a/lib/tests/geometry.rs b/lib/tests/geometry.rs index 3cef9d88..b6e86be4 100644 --- a/lib/tests/geometry.rs +++ b/lib/tests/geometry.rs @@ -309,3 +309,163 @@ async fn geometry_multipolygon() -> Result<(), Error> { // Ok(()) } + +#[tokio::test] +async fn geometry_inner_access() -> Result<(), Error> { + let sql = " + SELECT type, coordinates[0] as lng, coordinates[1] AS lat FROM type::point([-0.118092, 51.509865]); + SELECT type, coordinates[0] as lng, coordinates[1] AS lat FROM (-0.118092, 51.509865); + SELECT coordinates FROM { + type: 'Polygon', + coordinates: [ + [ + [-0.38314819, 51.37692386], [0.1785278, 51.37692386], + [0.1785278, 51.61460570], [-0.38314819, 51.61460570], + [-0.38314819, 51.37692386], + ] + ], + }; + SELECT coordinates FROM { + type: 'Polygon', + coordinates: [ + [ + [-0.38314819, 51.37692386], [0.1785278, 51.37692386], + [0.1785278, 51.61460570], [-0.38314819, 51.61460570], + [-0.38314819, 51.37692386], + ], + [ + [-0.38314819, 51.37692386], [-0.38314819, 51.61460570], + [-0.38314819, 51.37692386], + ], + [ + [110.38314819, 110.37692386], [110.38314819, 110.61460570], + [110.38314819, 110.37692386], + ] + ], + }; + "; + let dbs = Datastore::new("memory").await?; + let ses = Session::for_kv().with_ns("test").with_db("test"); + let res = &mut dbs.execute(sql, &ses, None, false).await?; + assert_eq!(res.len(), 4); + // + let tmp = res.remove(0).result?; + let val = Value::parse( + r#"[ + { + lat: 51.509865, + lng: -0.118092, + type: 'Point' + } + ]"#, + ); + assert_eq!(tmp, val); + // + let tmp = res.remove(0).result?; + let val = Value::parse( + r#"[ + { + lat: 51.509865, + lng: -0.118092, + type: 'Point' + } + ]"#, + ); + assert_eq!(tmp, val); + // + let tmp = res.remove(0).result?; + let val = Value::parse( + r#"[ + { + coordinates: [ + [ + [ + -0.38314819, + 51.37692386 + ], + [ + 0.1785278, + 51.37692386 + ], + [ + 0.1785278, + 51.6146057 + ], + [ + -0.38314819, + 51.6146057 + ], + [ + -0.38314819, + 51.37692386 + ] + ] + ] + } + ]"#, + ); + assert_eq!(tmp, val); + // + let tmp = res.remove(0).result?; + let val = Value::parse( + r#"[ + { + coordinates: [ + [ + [ + -0.38314819, + 51.37692386 + ], + [ + 0.1785278, + 51.37692386 + ], + [ + 0.1785278, + 51.6146057 + ], + [ + -0.38314819, + 51.6146057 + ], + [ + -0.38314819, + 51.37692386 + ] + ], + [ + [ + -0.38314819, + 51.37692386 + ], + [ + -0.38314819, + 51.6146057 + ], + [ + -0.38314819, + 51.37692386 + ] + ], + [ + [ + 110.38314819, + 110.37692386 + ], + [ + 110.38314819, + 110.6146057 + ], + [ + 110.38314819, + 110.37692386 + ] + ] + ] + } + ]"#, + ); + assert_eq!(tmp, val); + // + Ok(()) +}