mirror of
https://github.com/launchbadge/sqlx.git
synced 2025-10-02 15:25:32 +00:00
feat(postgres): add geometry path (#3716)
* feat: add geometry path * fix: paths to pg point * test: remove array tests for path * Fix readme: uuid feature is gating for all repos (#3720) The readme previously stated that the uuid feature is only for postres but it actually also gates the functionality in mysql and sqlite. * Replace some futures_util APIs with std variants (#3721) * feat(sqlx-cli): Add flag to disable automatic loading of .env files (#3724) * Add flag to disable automatic loading of .env files * Update sqlx-cli/src/opt.rs Co-authored-by: Austin Bonander <austin.bonander@gmail.com> --------- Co-authored-by: Austin Bonander <austin.bonander@gmail.com> * chore: expose bstr feature (#3714) * chore: replace rustls-pemfile with rustls-pki-types (#3725) * QueryBuilder: add `debug_assert` when `push_values` is passed an empty set of tuples (#3734) * throw a warning in tracing so that the empty tuples would be noticed * use debug assertion to throw a panic in debug mode * fix: merge conflicts * chore(cli): remove unused async-trait crate from dependencies (#3754) * Update pull_request_template.md * Fix example calculation (#3741) * Avoid privilege requirements by using an advisory lock in test setup (postgres). (#3753) * feat(sqlx-postgres): use advisory lock to avoid setup race condition * fix(sqlx-postgres): numeric hex constants not supported before postgres 16 * Small doc correction. (#3755) When sqlx-core/src/from_row.rs was updated to implement FromRow for tuples of up to 16 values, a comment was left stating that it was implemented up to tuples of 9 values. * Update FAQ.md * refactor(cli): replace promptly with dialoguer (#3669) * docs(pool): recommend actix-web ThinData over Data to avoid two Arcs (#3762) Both actix_web::web::Data and sqlx::PgPool internally wrap an Arc. Thus, using Data<PgPool> as an extractor in an actix-web route handler results in two Arcs wrapping the data of interest, which isn't ideal. Actix-web 4.9.0 introduced a new web::ThinData extractor for cases like this, where the data is already wrapped in an `Arc` (or is otherwise similarly cheap and sensible to simply clone), which doesn't wrap the inner value in a (second) Arc. Since the new extractor is better suited to the task, suggest it in place of web::Data when giving an example on how to share a pool. * fix: merge conflicts * fix: use types mod from main * fix: merge conflicts * fix: merge conflicts * fix: merge conflicts * fix: ordering of types mod * fix: path import * test: no array test for path --------- Co-authored-by: Jon Thacker <thacker.jon@gmail.com> Co-authored-by: Paolo Barbolini <paolo.barbolini@m4ss.net> Co-authored-by: Ben Wilber <benwilber@users.noreply.github.com> Co-authored-by: Austin Bonander <austin.bonander@gmail.com> Co-authored-by: joeydewaal <99046430+joeydewaal@users.noreply.github.com> Co-authored-by: tottoto <tottotodev@gmail.com> Co-authored-by: Ethan Wang <mailme@ethanavania.org> Co-authored-by: Stefan Schindler <dns2utf8@users.noreply.github.com> Co-authored-by: kildrens <5198060+kildrens@users.noreply.github.com> Co-authored-by: Marti Serra <marti.serra+github@protonmail.com> Co-authored-by: Jonas Malaco <jonas@protocubo.io>
This commit is contained in:
parent
c5ea6c4435
commit
5c573e15eb
@ -40,6 +40,8 @@ impl_type_checking!(
|
||||
|
||||
sqlx::postgres::types::PgBox,
|
||||
|
||||
sqlx::postgres::types::PgPath,
|
||||
|
||||
#[cfg(feature = "uuid")]
|
||||
sqlx::types::Uuid,
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
pub mod r#box;
|
||||
pub mod line;
|
||||
pub mod line_segment;
|
||||
pub mod path;
|
||||
pub mod point;
|
||||
|
372
sqlx-postgres/src/types/geometry/path.rs
Normal file
372
sqlx-postgres/src/types/geometry/path.rs
Normal file
@ -0,0 +1,372 @@
|
||||
use crate::decode::Decode;
|
||||
use crate::encode::{Encode, IsNull};
|
||||
use crate::error::BoxDynError;
|
||||
use crate::types::{PgPoint, Type};
|
||||
use crate::{PgArgumentBuffer, PgHasArrayType, PgTypeInfo, PgValueFormat, PgValueRef, Postgres};
|
||||
use sqlx_core::bytes::Buf;
|
||||
use sqlx_core::Error;
|
||||
use std::mem;
|
||||
use std::str::FromStr;
|
||||
|
||||
const BYTE_WIDTH: usize = mem::size_of::<f64>();
|
||||
|
||||
/// ## Postgres Geometric Path type
|
||||
///
|
||||
/// Description: Open path or Closed path (similar to polygon)
|
||||
/// Representation: Open `[(x1,y1),...]`, Closed `((x1,y1),...)`
|
||||
///
|
||||
/// Paths are represented by lists of connected points. Paths can be open, where the first and last points in the list are considered not connected, or closed, where the first and last points are considered connected.
|
||||
/// Values of type path are specified using any of the following syntaxes:
|
||||
/// ```text
|
||||
/// [ ( x1 , y1 ) , ... , ( xn , yn ) ]
|
||||
/// ( ( x1 , y1 ) , ... , ( xn , yn ) )
|
||||
/// ( x1 , y1 ) , ... , ( xn , yn )
|
||||
/// ( x1 , y1 , ... , xn , yn )
|
||||
/// x1 , y1 , ... , xn , yn
|
||||
/// ```
|
||||
/// where the points are the end points of the line segments comprising the path. Square brackets `([])` indicate an open path, while parentheses `(())` indicate a closed path.
|
||||
/// When the outermost parentheses are omitted, as in the third through fifth syntaxes, a closed path is assumed.
|
||||
///
|
||||
/// See https://www.postgresql.org/docs/16/datatype-geometric.html#DATATYPE-GEOMETRIC-PATHS
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct PgPath {
|
||||
pub closed: bool,
|
||||
pub points: Vec<PgPoint>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
struct Header {
|
||||
is_closed: bool,
|
||||
length: usize,
|
||||
}
|
||||
|
||||
impl Type<Postgres> for PgPath {
|
||||
fn type_info() -> PgTypeInfo {
|
||||
PgTypeInfo::with_name("path")
|
||||
}
|
||||
}
|
||||
|
||||
impl PgHasArrayType for PgPath {
|
||||
fn array_type_info() -> PgTypeInfo {
|
||||
PgTypeInfo::with_name("_path")
|
||||
}
|
||||
}
|
||||
|
||||
impl<'r> Decode<'r, Postgres> for PgPath {
|
||||
fn decode(value: PgValueRef<'r>) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
|
||||
match value.format() {
|
||||
PgValueFormat::Text => Ok(PgPath::from_str(value.as_str()?)?),
|
||||
PgValueFormat::Binary => Ok(PgPath::from_bytes(value.as_bytes()?)?),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'q> Encode<'q, Postgres> for PgPath {
|
||||
fn produces(&self) -> Option<PgTypeInfo> {
|
||||
Some(PgTypeInfo::with_name("path"))
|
||||
}
|
||||
|
||||
fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result<IsNull, BoxDynError> {
|
||||
self.serialize(buf)?;
|
||||
Ok(IsNull::No)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for PgPath {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let closed = !s.contains('[');
|
||||
let sanitised = s.replace(['(', ')', '[', ']', ' '], "");
|
||||
let parts = sanitised.split(',').collect::<Vec<_>>();
|
||||
|
||||
let mut points = vec![];
|
||||
|
||||
if parts.len() % 2 != 0 {
|
||||
return Err(Error::Decode(
|
||||
format!("Unmatched pair in PATH: {}", s).into(),
|
||||
));
|
||||
}
|
||||
|
||||
for chunk in parts.chunks_exact(2) {
|
||||
if let [x_str, y_str] = chunk {
|
||||
let x = parse_float_from_str(x_str, "could not get x")?;
|
||||
let y = parse_float_from_str(y_str, "could not get y")?;
|
||||
|
||||
let point = PgPoint { x, y };
|
||||
points.push(point);
|
||||
}
|
||||
}
|
||||
|
||||
if !points.is_empty() {
|
||||
return Ok(PgPath { points, closed });
|
||||
}
|
||||
|
||||
Err(Error::Decode(
|
||||
format!("could not get path from {}", s).into(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl PgPath {
|
||||
fn header(&self) -> Header {
|
||||
Header {
|
||||
is_closed: self.closed,
|
||||
length: self.points.len(),
|
||||
}
|
||||
}
|
||||
|
||||
fn from_bytes(mut bytes: &[u8]) -> Result<Self, BoxDynError> {
|
||||
let header = Header::try_read(&mut bytes)?;
|
||||
|
||||
if bytes.len() != header.data_size() {
|
||||
return Err(format!(
|
||||
"expected {} bytes after header, got {}",
|
||||
header.data_size(),
|
||||
bytes.len()
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
if bytes.len() % BYTE_WIDTH * 2 != 0 {
|
||||
return Err(format!(
|
||||
"data length not divisible by pairs of {BYTE_WIDTH}: {}",
|
||||
bytes.len()
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
let mut out_points = Vec::with_capacity(bytes.len() / (BYTE_WIDTH * 2));
|
||||
|
||||
while bytes.has_remaining() {
|
||||
let point = PgPoint {
|
||||
x: bytes.get_f64(),
|
||||
y: bytes.get_f64(),
|
||||
};
|
||||
out_points.push(point)
|
||||
}
|
||||
Ok(PgPath {
|
||||
closed: header.is_closed,
|
||||
points: out_points,
|
||||
})
|
||||
}
|
||||
|
||||
fn serialize(&self, buff: &mut PgArgumentBuffer) -> Result<(), BoxDynError> {
|
||||
let header = self.header();
|
||||
buff.reserve(header.data_size());
|
||||
header.try_write(buff)?;
|
||||
|
||||
for point in &self.points {
|
||||
buff.extend_from_slice(&point.x.to_be_bytes());
|
||||
buff.extend_from_slice(&point.y.to_be_bytes());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn serialize_to_vec(&self) -> Vec<u8> {
|
||||
let mut buff = PgArgumentBuffer::default();
|
||||
self.serialize(&mut buff).unwrap();
|
||||
buff.to_vec()
|
||||
}
|
||||
}
|
||||
|
||||
impl Header {
|
||||
const HEADER_WIDTH: usize = mem::size_of::<i8>() + mem::size_of::<i32>();
|
||||
|
||||
fn data_size(&self) -> usize {
|
||||
self.length * BYTE_WIDTH * 2
|
||||
}
|
||||
|
||||
fn try_read(buf: &mut &[u8]) -> Result<Self, String> {
|
||||
if buf.len() < Self::HEADER_WIDTH {
|
||||
return Err(format!(
|
||||
"expected PATH data to contain at least {} bytes, got {}",
|
||||
Self::HEADER_WIDTH,
|
||||
buf.len()
|
||||
));
|
||||
}
|
||||
|
||||
let is_closed = buf.get_i8();
|
||||
let length = buf.get_i32();
|
||||
|
||||
let length = usize::try_from(length).ok().ok_or_else(|| {
|
||||
format!(
|
||||
"received PATH data length: {length}. Expected length between 0 and {}",
|
||||
usize::MAX
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(Self {
|
||||
is_closed: is_closed != 0,
|
||||
length,
|
||||
})
|
||||
}
|
||||
|
||||
fn try_write(&self, buff: &mut PgArgumentBuffer) -> Result<(), String> {
|
||||
let is_closed = self.is_closed as i8;
|
||||
|
||||
let length = i32::try_from(self.length).map_err(|_| {
|
||||
format!(
|
||||
"PATH length exceeds allowed maximum ({} > {})",
|
||||
self.length,
|
||||
i32::MAX
|
||||
)
|
||||
})?;
|
||||
|
||||
buff.extend(is_closed.to_be_bytes());
|
||||
buff.extend(length.to_be_bytes());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_float_from_str(s: &str, error_msg: &str) -> Result<f64, Error> {
|
||||
s.parse().map_err(|_| Error::Decode(error_msg.into()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod path_tests {
|
||||
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::types::PgPoint;
|
||||
|
||||
use super::PgPath;
|
||||
|
||||
const PATH_CLOSED_BYTES: &[u8] = &[
|
||||
1, 0, 0, 0, 2, 63, 240, 0, 0, 0, 0, 0, 0, 64, 0, 0, 0, 0, 0, 0, 0, 64, 8, 0, 0, 0, 0, 0, 0,
|
||||
64, 16, 0, 0, 0, 0, 0, 0,
|
||||
];
|
||||
|
||||
const PATH_OPEN_BYTES: &[u8] = &[
|
||||
0, 0, 0, 0, 2, 63, 240, 0, 0, 0, 0, 0, 0, 64, 0, 0, 0, 0, 0, 0, 0, 64, 8, 0, 0, 0, 0, 0, 0,
|
||||
64, 16, 0, 0, 0, 0, 0, 0,
|
||||
];
|
||||
|
||||
const PATH_UNEVEN_POINTS: &[u8] = &[
|
||||
0, 0, 0, 0, 2, 63, 240, 0, 0, 0, 0, 0, 0, 64, 0, 0, 0, 0, 0, 0, 0, 64, 8, 0, 0, 0, 0, 0, 0,
|
||||
64, 16, 0, 0,
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn can_deserialise_path_type_bytes_closed() {
|
||||
let path = PgPath::from_bytes(PATH_CLOSED_BYTES).unwrap();
|
||||
assert_eq!(
|
||||
path,
|
||||
PgPath {
|
||||
closed: true,
|
||||
points: vec![PgPoint { x: 1.0, y: 2.0 }, PgPoint { x: 3.0, y: 4.0 }]
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cannot_deserialise_path_type_uneven_point_bytes() {
|
||||
let path = PgPath::from_bytes(PATH_UNEVEN_POINTS);
|
||||
assert!(path.is_err());
|
||||
|
||||
if let Err(err) = path {
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
format!("expected 32 bytes after header, got 28")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_deserialise_path_type_bytes_open() {
|
||||
let path = PgPath::from_bytes(PATH_OPEN_BYTES).unwrap();
|
||||
assert_eq!(
|
||||
path,
|
||||
PgPath {
|
||||
closed: false,
|
||||
points: vec![PgPoint { x: 1.0, y: 2.0 }, PgPoint { x: 3.0, y: 4.0 }]
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_deserialise_path_type_str_first_syntax() {
|
||||
let path = PgPath::from_str("[( 1, 2), (3, 4 )]").unwrap();
|
||||
assert_eq!(
|
||||
path,
|
||||
PgPath {
|
||||
closed: false,
|
||||
points: vec![PgPoint { x: 1., y: 2. }, PgPoint { x: 3., y: 4. }]
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cannot_deserialise_path_type_str_uneven_points_first_syntax() {
|
||||
let input_str = "[( 1, 2), (3)]";
|
||||
let path = PgPath::from_str(input_str);
|
||||
|
||||
assert!(path.is_err());
|
||||
|
||||
if let Err(err) = path {
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
format!("error occurred while decoding: Unmatched pair in PATH: {input_str}")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_deserialise_path_type_str_second_syntax() {
|
||||
let path = PgPath::from_str("(( 1, 2), (3, 4 ))").unwrap();
|
||||
assert_eq!(
|
||||
path,
|
||||
PgPath {
|
||||
closed: true,
|
||||
points: vec![PgPoint { x: 1., y: 2. }, PgPoint { x: 3., y: 4. }]
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_deserialise_path_type_str_third_syntax() {
|
||||
let path = PgPath::from_str("(1, 2), (3, 4 )").unwrap();
|
||||
assert_eq!(
|
||||
path,
|
||||
PgPath {
|
||||
closed: true,
|
||||
points: vec![PgPoint { x: 1., y: 2. }, PgPoint { x: 3., y: 4. }]
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_deserialise_path_type_str_fourth_syntax() {
|
||||
let path = PgPath::from_str("1, 2, 3, 4").unwrap();
|
||||
assert_eq!(
|
||||
path,
|
||||
PgPath {
|
||||
closed: true,
|
||||
points: vec![PgPoint { x: 1., y: 2. }, PgPoint { x: 3., y: 4. }]
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_deserialise_path_type_str_float() {
|
||||
let path = PgPath::from_str("(1.1, 2.2), (3.3, 4.4)").unwrap();
|
||||
assert_eq!(
|
||||
path,
|
||||
PgPath {
|
||||
closed: true,
|
||||
points: vec![PgPoint { x: 1.1, y: 2.2 }, PgPoint { x: 3.3, y: 4.4 }]
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_serialise_path_type() {
|
||||
let path = PgPath {
|
||||
closed: true,
|
||||
points: vec![PgPoint { x: 1., y: 2. }, PgPoint { x: 3., y: 4. }],
|
||||
};
|
||||
assert_eq!(path.serialize_to_vec(), PATH_CLOSED_BYTES,)
|
||||
}
|
||||
}
|
@ -25,6 +25,7 @@
|
||||
//! | [`PgLine`] | LINE |
|
||||
//! | [`PgLSeg`] | LSEG |
|
||||
//! | [`PgBox`] | BOX |
|
||||
//! | [`PgPath`] | PATH |
|
||||
//! | [`PgHstore`] | HSTORE |
|
||||
//!
|
||||
//! <sup>1</sup> SQLx generally considers `CITEXT` to be compatible with `String`, `&str`, etc.,
|
||||
@ -262,6 +263,7 @@ pub use citext::PgCiText;
|
||||
pub use cube::PgCube;
|
||||
pub use geometry::line::PgLine;
|
||||
pub use geometry::line_segment::PgLSeg;
|
||||
pub use geometry::path::PgPath;
|
||||
pub use geometry::point::PgPoint;
|
||||
pub use geometry::r#box::PgBox;
|
||||
pub use hstore::PgHstore;
|
||||
|
@ -524,6 +524,12 @@ test_type!(_box<Vec<sqlx::postgres::types::PgBox>>(Postgres,
|
||||
"array[box('1,2,3,4'),box('((1.1, 2.2), (3.3, 4.4))')]" @= vec![sqlx::postgres::types::PgBox { upper_right_x: 3., upper_right_y: 4., lower_left_x: 1., lower_left_y: 2. }, sqlx::postgres::types::PgBox { upper_right_x: 3.3, upper_right_y: 4.4, lower_left_x: 1.1, lower_left_y: 2.2 }],
|
||||
));
|
||||
|
||||
#[cfg(any(postgres_12, postgres_13, postgres_14, postgres_15))]
|
||||
test_type!(path<sqlx::postgres::types::PgPath>(Postgres,
|
||||
"path('((1.0, 2.0), (3.0,4.0))')" == sqlx::postgres::types::PgPath { closed: true, points: vec![ sqlx::postgres::types::PgPoint { x: 1., y: 2. }, sqlx::postgres::types::PgPoint { x: 3. , y: 4. } ]},
|
||||
"path('[(1.0, 2.0), (3.0,4.0)]')" == sqlx::postgres::types::PgPath { closed: false, points: vec![ sqlx::postgres::types::PgPoint { x: 1., y: 2. }, sqlx::postgres::types::PgPoint { x: 3. , y: 4. } ]},
|
||||
));
|
||||
|
||||
#[cfg(feature = "rust_decimal")]
|
||||
test_type!(decimal<sqlx::types::Decimal>(Postgres,
|
||||
"0::numeric" == sqlx::types::Decimal::from_str("0").unwrap(),
|
||||
|
Loading…
x
Reference in New Issue
Block a user