Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Support geometric types in Postgres #242

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ tls = [ "sqlx-core/tls" ]
# intended mainly for CI and docs
all = [ "tls", "all-database", "all-type" ]
all-database = [ "mysql", "sqlite", "postgres" ]
all-type = [ "bigdecimal", "json", "time", "chrono", "ipnetwork", "uuid" ]
all-type = [ "bigdecimal", "json", "time", "chrono", "ipnetwork", "uuid", "geo" ]

# runtime
runtime-async-std = [ "sqlx-core/runtime-async-std", "sqlx-macros/runtime-async-std" ]
Expand All @@ -60,6 +60,7 @@ ipnetwork = [ "sqlx-core/ipnetwork", "sqlx-macros/ipnetwork" ]
uuid = [ "sqlx-core/uuid", "sqlx-macros/uuid" ]
json = [ "sqlx-core/json", "sqlx-macros/json" ]
time = [ "sqlx-core/time", "sqlx-macros/time" ]
geo = [ "sqlx-core/geo", "sqlx-macros/geo" ]

[dependencies]
sqlx-core = { version = "0.3.4", path = "sqlx-core", default-features = false }
Expand Down
1 change: 1 addition & 0 deletions sqlx-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ futures-channel = { version = "0.3.4", default-features = false, features = [ "a
futures-core = { version = "0.3.4", default-features = false }
futures-util = { version = "0.3.4", default-features = false }
generic-array = { version = "0.12.3", default-features = false, optional = true }
geo = { version = "0.13.0", optional = true }
hex = "0.4.2"
hmac = { version = "0.7.1", default-features = false, optional = true }
ipnetwork = { version = "0.16.0", default-features = false, optional = true }
Expand Down
18 changes: 18 additions & 0 deletions sqlx-core/src/io/buf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,12 @@ pub trait Buf<'a> {

fn get_u32<T: ByteOrder>(&mut self) -> io::Result<u32>;

fn get_f32<T: ByteOrder>(&mut self) -> io::Result<f32>;

fn get_u64<T: ByteOrder>(&mut self) -> io::Result<u64>;

fn get_f64<T: ByteOrder>(&mut self) -> io::Result<f64>;

fn get_str(&mut self, len: usize) -> io::Result<&'a str>;

fn get_str_nul(&mut self) -> io::Result<&'a str>;
Expand Down Expand Up @@ -100,13 +104,27 @@ impl<'a> Buf<'a> for &'a [u8] {
Ok(val)
}

fn get_f32<T: ByteOrder>(&mut self) -> io::Result<f32> {
let val = T::read_f32(*self);
self.advance(4);

Ok(val)
}

fn get_u64<T: ByteOrder>(&mut self) -> io::Result<u64> {
let val = T::read_u64(*self);
self.advance(8);

Ok(val)
}

fn get_f64<T: ByteOrder>(&mut self) -> io::Result<f64> {
let val = T::read_f64(*self);
self.advance(8);

Ok(val)
}

fn get_str(&mut self, len: usize) -> io::Result<&'a str> {
str::from_utf8(self.get_bytes(len)?)
.map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))
Expand Down
16 changes: 16 additions & 0 deletions sqlx-core/src/io/buf_mut.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,12 @@ pub trait BufMut {

fn put_u32<T: ByteOrder>(&mut self, val: u32);

fn put_f32<T: ByteOrder>(&mut self, val: f32);

fn put_u64<T: ByteOrder>(&mut self, val: u64);

fn put_f64<T: ByteOrder>(&mut self, val: f64);

fn put_bytes(&mut self, val: &[u8]);

fn put_str(&mut self, val: &str);
Expand Down Expand Up @@ -64,12 +68,24 @@ impl BufMut for Vec<u8> {
self.extend_from_slice(&buf);
}

fn put_f32<T: ByteOrder>(&mut self, val: f32) {
let mut buf = [0; 4];
T::write_f32(&mut buf, val);
self.extend_from_slice(&buf);
}

fn put_u64<T: ByteOrder>(&mut self, val: u64) {
let mut buf = [0; 8];
T::write_u64(&mut buf, val);
self.extend_from_slice(&buf);
}

fn put_f64<T: ByteOrder>(&mut self, val: f64) {
let mut buf = [0; 8];
T::write_f64(&mut buf, val);
self.extend_from_slice(&buf);
}

fn put_bytes(&mut self, val: &[u8]) {
self.extend_from_slice(val);
}
Expand Down
6 changes: 6 additions & 0 deletions sqlx-core/src/postgres/protocol/type_id.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ impl TypeId {
pub(crate) const CIDR: TypeId = TypeId(650);
pub(crate) const INET: TypeId = TypeId(869);

pub(crate) const POINT: TypeId = TypeId(600);
pub(crate) const LSEG: TypeId = TypeId(601);

// Arrays

pub(crate) const ARRAY_BOOL: TypeId = TypeId(1000);
Expand Down Expand Up @@ -81,6 +84,9 @@ impl TypeId {
pub(crate) const ARRAY_CIDR: TypeId = TypeId(651);
pub(crate) const ARRAY_INET: TypeId = TypeId(1041);

pub(crate) const ARRAY_POINT: TypeId = TypeId(1017);
pub(crate) const ARRAY_LSEG: TypeId = TypeId(1018);

// JSON

pub(crate) const JSON: TypeId = TypeId(114);
Expand Down
120 changes: 120 additions & 0 deletions sqlx-core/src/postgres/types/geo.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
use crate::decode::Decode;
use crate::encode::Encode;
use crate::io::Buf;
use crate::postgres::protocol::TypeId;
use crate::postgres::{PgData, PgRawBuffer, PgTypeInfo, PgValue, Postgres};
use crate::types::Type;
use byteorder::BigEndian;
use geo::{Coordinate, Line};
use std::mem;

// <https://www.postgresql.org/docs/12/datatype-geometric.html>

impl Type<Postgres> for Coordinate<f64> {
fn type_info() -> PgTypeInfo {
PgTypeInfo::new(TypeId::POINT, "POINT")
}
}

impl<'de> Decode<'de, Postgres> for Coordinate<f64> {
fn decode(value: PgValue<'de>) -> crate::Result<Self> {
match value.try_get()? {
PgData::Binary(buf) => decode_coordinate_binary(buf),

PgData::Text(s) => {
let parens: &[_] = &['(', ')'];
let mut s = s.trim_matches(parens).split(',');

match (s.next(), s.next()) {
(Some(x), Some(y)) => {
let x = x.parse().map_err(crate::Error::decode)?;
let y = y.parse().map_err(crate::Error::decode)?;

Ok((x, y).into())
}

_ => Err(crate::Error::Decode(
format!("expecting a value with the format \"(x,y)\"").into(),
)),
}
}
}
}
}

fn decode_coordinate_binary(mut buf: &[u8]) -> crate::Result<Coordinate<f64>> {
let x = buf.get_f64::<BigEndian>()?;

let y = buf.get_f64::<BigEndian>()?;

Ok((x, y).into())
}

impl Encode<Postgres> for Coordinate<f64> {
fn encode(&self, buf: &mut PgRawBuffer) {
Encode::<Postgres>::encode(&self.x, buf);
Encode::<Postgres>::encode(&self.y, buf);
}

fn size_hint(&self) -> usize {
2 * mem::size_of::<f64>()
}
}

impl Type<Postgres> for Line<f64> {
fn type_info() -> PgTypeInfo {
PgTypeInfo::new(TypeId::LSEG, "LSEG")
}
}

impl<'de> Decode<'de, Postgres> for Line<f64> {
fn decode(value: PgValue<'de>) -> crate::Result<Self> {
match value.try_get()? {
PgData::Binary(mut buf) => {
let start = decode_coordinate_binary(buf)?;
buf.advance(Encode::<Postgres>::size_hint(&start));
let end = decode_coordinate_binary(buf)?;

Ok(Line::new(start, end))
}

// TODO: is there no way to make this make use of the Decode for Coordinate?
PgData::Text(s) => {
let brackets: &[_] = &['[', ']'];
let mut s = s
.trim_matches(brackets)
.split(|c| c == '(' || c == ')' || c == ',')
.filter_map(|part| if part == "" { None } else { Some(part) });

match (s.next(), s.next(), s.next(), s.next()) {
(Some(x1), Some(y1), Some(x2), Some(y2)) => {
let x1 = x1.parse().map_err(crate::Error::decode)?;
let y1 = y1.parse().map_err(crate::Error::decode)?;
let x2 = x2.parse().map_err(crate::Error::decode)?;
let y2 = y2.parse().map_err(crate::Error::decode)?;

let start = Coordinate::from((x1, y1));
let end = Coordinate::from((x2, y2));

Ok(Line::new(start, end))
}

_ => Err(crate::Error::Decode(
format!("expecting a value with the format \"[(x,y),(x,y)]\"").into(),
)),
}
}
}
}
}

impl Encode<Postgres> for Line<f64> {
fn encode(&self, buf: &mut PgRawBuffer) {
Encode::<Postgres>::encode(&self.start, buf);
Encode::<Postgres>::encode(&self.end, buf);
}

fn size_hint(&self) -> usize {
2 * Encode::<Postgres>::size_hint(&self.start)
}
}
15 changes: 15 additions & 0 deletions sqlx-core/src/postgres/types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,18 @@
//!
//! [`Json<T>`]: crate::types::Json
//!
//! ### [`geo`](https://crates.io/crates/geo)
//!
//! Requires the `geo` Cargo feature flag.
//!
//! | Rust type | Postgres type(s) |
//! |---------------------------------------|------------------------------------------------------|
//! | `geo::Coordinate<f64>` | POINT |
//! | `geo::Line<f64>` | LSEG |
//! | `geo::Rect<f64>` | BOX |
//! | `geo::LineString<f64>` | PATH |
//! | `geo::Polygon<f64>` | POLYGON |
//!
//! # [Composite types](https://www.postgresql.org/docs/current/rowtypes.html)
//!
//! User-defined composite types are supported through a derive for `Type`.
Expand Down Expand Up @@ -162,6 +174,9 @@ mod json;
#[cfg(feature = "ipnetwork")]
mod ipnetwork;

#[cfg(feature = "geo")]
mod geo;

// Implement `Decode` for all postgres types
// The concept of a nullable `RawValue` is db-specific
// `Type` is implemented generically at src/types.rs
Expand Down
1 change: 1 addition & 0 deletions sqlx-macros/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ time = [ "sqlx/time" ]
ipnetwork = [ "sqlx/ipnetwork" ]
uuid = [ "sqlx/uuid" ]
json = [ "sqlx/json", "serde_json" ]
geo = [ "sqlx/geo" ]

[dependencies]
async-std = { version = "1.5.0", default-features = false, optional = true }
Expand Down