diff --git a/README.md b/README.md index cd6edb4f..804740ac 100644 --- a/README.md +++ b/README.md @@ -253,6 +253,27 @@ types. The driver currently supports the following conversions: (optional) MACADDR + + + geo::Point<f64> + (optional) + + POINT + + + + geo::Bbox<f64> + (optional) + + BOX + + + + geo::LineString<f64> + (optional) + + PATH @@ -316,3 +337,21 @@ and `FromSql` implementations for `bit-vec`'s `BitVec` type. [MACADDR](http://www.postgresql.org/docs/9.4/static/datatype-net-types.html#DATATYPE-MACADDR) support is provided optionally by the `with-eui48` feature, which adds `ToSql` and `FromSql` implementations for `eui48`'s `MacAddress` type. + +### POINT type + +[POINT](https://www.postgresql.org/docs/9.4/static/datatype-geometric.html#AEN6799) +support is provided optionally by the `with-geo` feature, which adds `ToSql` and `FromSql` implementations for `geo`'s `Point` type. + +### BOX type + +[BOX](https://www.postgresql.org/docs/9.4/static/datatype-geometric.html#AEN6883) +support is provided optionally by the `with-geo` feature, which adds `ToSql` and `FromSql` implementations for `geo`'s `Bbox` type. + +### PATH type + +[PATH](https://www.postgresql.org/docs/9.4/static/datatype-geometric.html#AEN6912) +support is provided optionally by the `with-geo` feature, which adds `ToSql` and `FromSql` implementations for `geo`'s `LineString` type. +Paths converted from LineString are always treated as "open" paths. Use the +[pclose](https://www.postgresql.org/docs/8.2/static/functions-geometry.html#FUNCTIONS-GEOMETRY-FUNC-TABLE) +geometric function to insert a closed path. diff --git a/postgres-shared/Cargo.toml b/postgres-shared/Cargo.toml index 8b30e491..e9073cfe 100644 --- a/postgres-shared/Cargo.toml +++ b/postgres-shared/Cargo.toml @@ -10,6 +10,7 @@ repository = "https://github.com/sfackler/rust-postgres" with-bit-vec = ["bit-vec"] with-chrono = ["chrono"] with-eui48 = ["eui48"] +with-geo = ["geo"] with-rustc-serialize = ["rustc-serialize"] with-serde_json = ["serde_json"] with-time = ["time"] @@ -24,6 +25,7 @@ postgres-protocol = "0.2" bit-vec = { version = "0.4", optional = true } chrono = { version = "0.3", optional = true } eui48 = { version = "0.1", optional = true } +geo = { version = "0.4", optional = true } rustc-serialize = { version = "0.3", optional = true } serde_json = { version = "0.9", optional = true } time = { version = "0.1.14", optional = true } diff --git a/postgres-shared/src/types/geo.rs b/postgres-shared/src/types/geo.rs new file mode 100644 index 00000000..13080495 --- /dev/null +++ b/postgres-shared/src/types/geo.rs @@ -0,0 +1,104 @@ +extern crate geo; + +use postgres_protocol::types; +use self::geo::{Bbox, LineString, Point}; +use std::error::Error; + +use types::{FromSql, ToSql, IsNull, Type}; + +impl FromSql for Point { + fn from_sql(_: &Type, raw: &[u8]) -> Result> { + if raw.len() != 16 { + return Err("invalid message length".into()); + } + + let x = types::float8_from_sql(&raw[0..8])?; + let y = types::float8_from_sql(&raw[8..16])?; + Ok(Point::new(x, y)) + } + + accepts!(Type::Point); +} + +impl ToSql for Point { + fn to_sql(&self, _: &Type, out: &mut Vec) -> Result> { + types::float8_to_sql(self.x(), out); + types::float8_to_sql(self.y(), out); + Ok(IsNull::No) + } + + accepts!(Type::Point); + to_sql_checked!(); +} + +impl FromSql for Bbox { + fn from_sql(_: &Type, raw: &[u8]) -> Result> { + if raw.len() != 32 { + return Err("invalid message length".into()); + } + + let xmax = types::float8_from_sql(&raw[0..8])?; + let ymax = types::float8_from_sql(&raw[8..16])?; + let xmin = types::float8_from_sql(&raw[16..24])?; + let ymin = types::float8_from_sql(&raw[24..32])?; + Ok(Bbox{xmax: xmax, ymax: ymax, xmin: xmin, ymin: ymin}) + } + + accepts!(Type::Box); +} + +impl ToSql for Bbox { + fn to_sql(&self, _: &Type, out: &mut Vec) -> Result> { + types::float8_to_sql(self.xmax, out); + types::float8_to_sql(self.ymax, out); + types::float8_to_sql(self.xmin, out); + types::float8_to_sql(self.ymin, out); + Ok(IsNull::No) + } + + accepts!(Type::Box); + to_sql_checked!(); +} + +impl FromSql for LineString { + fn from_sql(_: &Type, raw: &[u8]) -> Result> { + if raw.len() < 5 { + return Err("invalid message length".into()); + } + + // let _ = types::bool_from_sql(&raw[0..1])?; // is path open or closed + let n_points = types::int4_from_sql(&raw[1..5])? as usize; + let raw_points = &raw[5..raw.len()]; + if raw_points.len() != 16 * n_points { + return Err("invalid message length".into()); + } + + let mut offset = 0; + let mut points = Vec::with_capacity(n_points); + for _ in 0..n_points { + let x = types::float8_from_sql(&raw_points[offset..offset+8])?; + let y = types::float8_from_sql(&raw_points[offset+8..offset+16])?; + points.push(Point::new(x, y)); + offset += 16; + } + Ok(LineString(points)) + } + + accepts!(Type::Path); +} + +impl ToSql for LineString { + fn to_sql(&self, _: &Type, out: &mut Vec) -> Result> { + let closed = false; // always encode an open path from LineString + types::bool_to_sql(closed, out); + types::int4_to_sql(self.0.len() as i32, out); + for point in &self.0 { + types::float8_to_sql(point.x(), out); + types::float8_to_sql(point.y(), out); + } + Ok(IsNull::No) + } + + accepts!(Type::Path); + to_sql_checked!(); +} diff --git a/postgres-shared/src/types/mod.rs b/postgres-shared/src/types/mod.rs index 5a7a8f9c..76ba41fa 100644 --- a/postgres-shared/src/types/mod.rs +++ b/postgres-shared/src/types/mod.rs @@ -73,6 +73,8 @@ mod serde_json; mod chrono; #[cfg(feature = "with-eui48")] mod eui48; +#[cfg(feature = "with-geo")] +mod geo; mod special; mod type_gen; diff --git a/postgres/Cargo.toml b/postgres/Cargo.toml index 86ccbeec..47270e2b 100644 --- a/postgres/Cargo.toml +++ b/postgres/Cargo.toml @@ -24,6 +24,7 @@ path = "tests/test.rs" with-bit-vec = ["postgres-shared/with-bit-vec"] with-chrono = ["postgres-shared/with-chrono"] with-eui48 = ["postgres-shared/with-eui48"] +with-geo = ["postgres-shared/with-geo"] with-rustc-serialize = ["postgres-shared/with-rustc-serialize"] with-serde_json = ["postgres-shared/with-serde_json"] with-time = ["postgres-shared/with-time"] @@ -57,6 +58,7 @@ url = "1.0" bit-vec = "0.4" chrono = "0.3" eui48 = "0.1" +geo = "0.4" rustc-serialize = "0.3" serde_json = "0.9" time = "0.1.14" diff --git a/postgres/tests/types/geo.rs b/postgres/tests/types/geo.rs new file mode 100644 index 00000000..7b6afc0d --- /dev/null +++ b/postgres/tests/types/geo.rs @@ -0,0 +1,28 @@ +extern crate geo; + +use self::geo::{Bbox, LineString, Point}; +use types::test_type; + +#[test] +fn test_point_params() { + test_type("POINT", + &[(Some(Point::new(0.0, 0.0)), "POINT(0, 0)"), + (Some(Point::new(-3.14, 1.618)), "POINT(-3.14, 1.618)"), + (None, "NULL")]); +} + +#[test] +fn test_box_params() { + test_type("BOX", + &[(Some(Bbox{xmax: 160.0, ymax: 69701.5615, xmin: -3.14, ymin: 1.618}), + "BOX(POINT(160.0, 69701.5615), POINT(-3.14, 1.618))"), + (None, "NULL")]); +} + +#[test] +fn test_path_params() { + let points = vec![Point::new(0.0, 0.0), Point::new(-3.14, 1.618), Point::new(160.0, 69701.5615)]; + test_type("PATH", + &[(Some(LineString(points)),"path '((0, 0), (-3.14, 1.618), (160.0, 69701.5615))'"), + (None, "NULL")]); +} diff --git a/postgres/tests/types/mod.rs b/postgres/tests/types/mod.rs index 719f5d39..7634452a 100644 --- a/postgres/tests/types/mod.rs +++ b/postgres/tests/types/mod.rs @@ -23,6 +23,8 @@ mod rustc_serialize; mod serde_json; #[cfg(feature = "with-chrono")] mod chrono; +#[cfg(feature = "with-geo")] +mod geo; fn test_type(sql_type: &str, checks: &[(T, S)]) { let conn = or_panic!(Connection::connect("postgres://postgres@localhost", TlsMode::None)); diff --git a/tokio-postgres/Cargo.toml b/tokio-postgres/Cargo.toml index 22f9e521..4f5fbb03 100644 --- a/tokio-postgres/Cargo.toml +++ b/tokio-postgres/Cargo.toml @@ -13,6 +13,7 @@ keywords = ["database", "postgres", "postgresql", "sql", "async"] with-bit-vec = ["postgres-shared/with-bit-vec"] with-chrono = ["postgres-shared/with-chrono"] with-eui48 = ["postgres-shared/with-eui48"] +with-geo = ["postgres-shared/with-geo"] with-rustc-serialize = ["postgres-shared/with-rustc-serialize"] with-serde_json = ["postgres-shared/with-serde_json"] with-time = ["postgres-shared/with-time"]