From 60e424cb317b422f55c1a3fb9fe9e0a57bc22fce Mon Sep 17 00:00:00 2001 From: Mirro Mutth Date: Thu, 22 Feb 2024 19:39:28 +0900 Subject: [PATCH] feat(mysql): add support for Geometry subtypes --- sqlx-mysql/src/types/geometry.rs | 129 +++++++++++++++-- tests/mysql/types.rs | 235 +++++++++++++++++++++---------- 2 files changed, 277 insertions(+), 87 deletions(-) diff --git a/sqlx-mysql/src/types/geometry.rs b/sqlx-mysql/src/types/geometry.rs index 0851fa19e1..6124f106ff 100644 --- a/sqlx-mysql/src/types/geometry.rs +++ b/sqlx-mysql/src/types/geometry.rs @@ -1,6 +1,10 @@ -use geo_types::Geometry; +use geo_types::{ + Error, Geometry, GeometryCollection, LineString, MultiLineString, MultiPoint, MultiPolygon, + Point, Polygon, +}; use geozero::wkb::{FromWkb, WkbDialect}; use geozero::{GeozeroGeometry, ToWkb}; +use std::any::type_name; use crate::decode::Decode; use crate::encode::{Encode, IsNull}; @@ -10,23 +14,26 @@ use crate::protocol::text::ColumnType; use crate::types::Type; use crate::{MySql, MySqlTypeInfo, MySqlValueRef}; -/// Define a type that can be used to represent a `GEOMETRY` field. -/// -/// Note: Only `Geometry` is implemented with geozero::GeozeroGeometry. -impl Type for Geometry { - fn type_info() -> MySqlTypeInfo { - // MySQL does not allow to execute with a Geometry parameter for now. - // MySQL reports: 1210 (HY000): Incorrect arguments to mysqld_stmt_execute - // MariaDB does not report errors but does not work properly. - // So we use the `Blob` type to pass Geometry parameters. - MySqlTypeInfo::binary(ColumnType::Blob) - } +macro_rules! impl_mysql_type { + ($name:ident) => { + impl Type for $name { + fn type_info() -> MySqlTypeInfo { + // MySQL does not allow to execute with a Geometry parameter for now. + // MySQL reports: 1210 (HY000): Incorrect arguments to mysqld_stmt_execute + // MariaDB does not report errors but does not work properly. + // So we use the `Blob` type to pass Geometry parameters. + MySqlTypeInfo::binary(ColumnType::Blob) + } - fn compatible(ty: &MySqlTypeInfo) -> bool { - ty.r#type == ColumnType::Geometry || <&[u8] as Type>::compatible(ty) - } + fn compatible(ty: &MySqlTypeInfo) -> bool { + ty.r#type == ColumnType::Geometry || <&[u8] as Type>::compatible(ty) + } + } + }; } +impl_mysql_type!(Geometry); + const ENCODE_ERR: &str = "failed to encode value as Geometry to WKB; the most likely cause is that the value is not a valid geometry"; impl Encode<'_, MySql> for Geometry { @@ -48,3 +55,95 @@ impl Decode<'_, MySql> for Geometry { Ok(FromWkb::from_wkb(&mut bytes, WkbDialect::MySQL)?) } } + +/// Encode a subtype of [`Geometry`] into a MySQL value. +/// +/// Override [`Encode::encode`] for each subtype to avoid the overhead of cloning the value. +macro_rules! impl_encode_subtype { + ($name:ident) => { + impl Encode<'_, MySql> for $name { + fn encode(self, buf: &mut Vec) -> IsNull { + Geometry::::$name(self).encode(buf) + } + + fn encode_by_ref(&self, buf: &mut Vec) -> IsNull { + Geometry::::$name(self.clone()).encode(buf) + } + } + }; +} + +/// Decode a subtype of [`Geometry`] from a MySQL value. +/// +/// All decodable geometry types in MySQL: `GEOMETRY`, `POINT`, `LINESTRING`, `POLYGON`, `MULTIPOINT`, +/// `MULTILINESTRING`, `MULTIPOLYGON`, `GEOMETRYCOLLECTION`. +/// +/// [`Line`], [`Rect`], and [`Triangle`] can be encoded, but MySQL has no corresponding types. +/// This means, their [`TryFrom>`] will always return [`Err`], so they are not decodable. +/// +/// [`Line`]: geo_types::geometry::Line +/// [`Rect`]: geo_types::geometry::Rect +/// [`Triangle`]: geo_types::geometry::Triangle +macro_rules! impl_decode_subtype { + ($name:ident) => { + impl Decode<'_, MySql> for $name { + fn decode(value: MySqlValueRef<'_>) -> Result { + Ok( as Decode<'_, MySql>>::decode(value)?.try_into()?) + } + } + }; +} + +macro_rules! impls_subtype { + ($name:ident) => { + impl_mysql_type!($name); + impl_encode_subtype!($name); + impl_decode_subtype!($name); + }; + + // GeometryCollection is a special case + // Deprecated `GeometryCollection::from(single_geom)` produces unexpected results + // TODO: remove it when GeometryCollection::from(single_geom) is removed + ($name:ident, $n:ident => $($t:tt)+) => { + impl_mysql_type!($name); + impl_encode_subtype!($name); + + impl Decode<'_, MySql> for $name { + fn decode(value: MySqlValueRef<'_>) -> Result { + let $n = as Decode<'_, MySql>>::decode(value)?; + + $($t)+ + } + } + }; +} + +impls_subtype!(Point); +impls_subtype!(LineString); +impls_subtype!(Polygon); +impls_subtype!(MultiPoint); +impls_subtype!(MultiLineString); +impls_subtype!(MultiPolygon); + +macro_rules! geometry_collection_mismatch { + ($name:ident) => { + Err(Error::MismatchedGeometry { + expected: type_name::>(), + found: type_name::>(), + } + .into()) + }; +} + +impls_subtype!(GeometryCollection, geom => match geom { + Geometry::GeometryCollection(gc) => Ok(gc), + Geometry::Point(_) => geometry_collection_mismatch!(Point), + Geometry::Line(_) => geometry_collection_mismatch!(Line), + Geometry::LineString(_) => geometry_collection_mismatch!(LineString), + Geometry::Polygon(_) => geometry_collection_mismatch!(Polygon), + Geometry::MultiPoint(_) => geometry_collection_mismatch!(MultiPoint), + Geometry::MultiLineString(_) => geometry_collection_mismatch!(MultiLineString), + Geometry::MultiPolygon(_) => geometry_collection_mismatch!(MultiPolygon), + Geometry::Rect(_) => geometry_collection_mismatch!(Rect), + Geometry::Triangle(_) => geometry_collection_mismatch!(Triangle), +}); diff --git a/tests/mysql/types.rs b/tests/mysql/types.rs index 0d67a9f0d1..b4e1117ca2 100644 --- a/tests/mysql/types.rs +++ b/tests/mysql/types.rs @@ -289,7 +289,10 @@ mod json_tests { #[cfg(feature = "geometry")] mod geometry_tests { - use geo_types::{Coord, Geometry, GeometryCollection, LineString, Point, Polygon}; + use geo_types::{ + line_string, point, polygon, Geometry, GeometryCollection, LineString, MultiPoint, Point, + Polygon, + }; use sqlx_test::test_type; use super::*; @@ -297,61 +300,127 @@ mod geometry_tests { test_type!(geometry_point>( MySql, "SELECT CAST({0} <=> ? AS SIGNED INTEGER), CAST({0} AS BINARY) as _2, ? as _3", - "ST_GeomFromText('POINT(1 1)')" == Geometry::Point(Point(Coord { x: 1.0, y: 1.0 })), + "ST_GeomFromText('POINT(1 1)')" == Geometry::Point(point!( x: 1.0, y: 1.0 )), + )); + + test_type!(geometry_subtype_point>( + MySql, + "SELECT CAST({0} <=> ? AS SIGNED INTEGER), CAST({0} AS BINARY) as _2, ? as _3", + "ST_GeomFromText('POINT(3 4)')" == point!( x: 3.0, y: 4.0 ), )); test_type!(geometry_linestring>( MySql, "SELECT CAST({0} <=> ? AS SIGNED INTEGER), CAST({0} AS BINARY) as _2, ? as _3", - "ST_GeomFromText('LINESTRING(0 0, 1 1, 2 2)')" == Geometry::LineString(LineString(vec![ - Coord { x: 0.0, y: 0.0 }, - Coord { x: 1.0, y: 1.0 }, - Coord { x: 2.0, y: 2.0 }, - ])), + "ST_GeomFromText('LINESTRING(0 0, 1 1, 2 2)')" == Geometry::LineString(line_string![ + (x: 0.0, y: 0.0), + (x: 1.0, y: 1.0), + (x: 2.0, y: 2.0), + ]), + )); + + test_type!(geometry_subtype_linestring>( + MySql, + "SELECT CAST({0} <=> ? AS SIGNED INTEGER), CAST({0} AS BINARY) as _2, ? as _3", + "ST_GeomFromText('LINESTRING(6 5, 4 3, 2 1)')" == line_string![ + (x: 6.0, y: 5.0), + (x: 4.0, y: 3.0), + (x: 2.0, y: 1.0), + ], )); test_type!(geometry_polygon>( MySql, "SELECT CAST({0} <=> ? AS SIGNED INTEGER), CAST({0} AS BINARY) as _2, ? as _3", - "ST_GeomFromText('POLYGON((0 0, 1 1, 1 0, 0 0))')" == Geometry::Polygon(Polygon::new( - LineString(vec![ - Coord { x: 0.0, y: 0.0 }, - Coord { x: 1.0, y: 1.0 }, - Coord { x: 1.0, y: 0.0 }, - Coord { x: 0.0, y: 0.0 }, - ]), - vec![], - )), + "ST_GeomFromText('POLYGON((0 0, 1 1, 1 0, 0 0))')" == Geometry::Polygon(polygon![ + (x: 0.0, y: 0.0), + (x: 1.0, y: 1.0), + (x: 1.0, y: 0.0), + (x: 0.0, y: 0.0), + ]), + )); + + test_type!(geometry_subtype_polygon>( + MySql, + "SELECT CAST({0} <=> ? AS SIGNED INTEGER), CAST({0} AS BINARY) as _2, ? as _3", + "ST_GeomFromText('POLYGON((0 0, 2 2, 2 0, 0 0))')" == polygon![ + (x: 0.0, y: 0.0), + (x: 2.0, y: 2.0), + (x: 2.0, y: 0.0), + (x: 0.0, y: 0.0), + ], + )); + + test_type!(geometry_multipoint>( + MySql, + "SELECT CAST({0} <=> ? AS SIGNED INTEGER), CAST({0} AS BINARY) as _2, ? as _3", + "ST_GeomFromText('MULTIPOINT(0 0, 1 1, 2 2)')" == Geometry::MultiPoint(vec![ + point!(x: 0.0, y: 0.0), + point!(x: 1.0, y: 1.0), + point!(x: 2.0, y: 2.0), + ].into()), + )); + + test_type!(geometry_subtype_multipoint>( + MySql, + "SELECT CAST({0} <=> ? AS SIGNED INTEGER), CAST({0} AS BINARY) as _2, ? as _3", + "ST_GeomFromText('MULTIPOINT(0 0, 1 1, 2 2)')" == MultiPoint(vec![ + point!(x: 0.0, y: 0.0), + point!(x: 1.0, y: 1.0), + point!(x: 2.0, y: 2.0), + ]), )); test_type!(geometry_collection>( MySql, "SELECT CAST({0} <=> ? AS SIGNED INTEGER), CAST({0} AS BINARY) as _2, ? as _3", "ST_GeomFromText('GEOMETRYCOLLECTION(POINT(1 1),LINESTRING(0 0, 1 1, 2 2),POLYGON((0 0, 1 1, 1 0, 0 0)))')" == Geometry::GeometryCollection(GeometryCollection(vec![ - Geometry::Point(Point(Coord { x: 1.0, y: 1.0 })), - Geometry::LineString(LineString(vec![ - Coord { x: 0.0, y: 0.0 }, - Coord { x: 1.0, y: 1.0 }, - Coord { x: 2.0, y: 2.0 }, - ])), - Geometry::Polygon(Polygon::new( - LineString(vec![ - Coord { x: 0.0, y: 0.0 }, - Coord { x: 1.0, y: 1.0 }, - Coord { x: 1.0, y: 0.0 }, - Coord { x: 0.0, y: 0.0 }, - ]), - vec![], - )), + Geometry::Point(point!(x: 1.0, y: 1.0)), + Geometry::LineString(line_string![ + (x: 0.0, y: 0.0), + (x: 1.0, y: 1.0), + (x: 2.0, y: 2.0), + ]), + Geometry::Polygon(polygon![ + (x: 0.0, y: 0.0), + (x: 1.0, y: 1.0), + (x: 1.0, y: 0.0), + (x: 0.0, y: 0.0), + ]), ])), )); + test_type!(geometry_subtype_collection>( + MySql, + "SELECT CAST({0} <=> ? AS SIGNED INTEGER), CAST({0} AS BINARY) as _2, ? as _3", + "ST_GeomFromText('GEOMETRYCOLLECTION(POINT(8 7),LINESTRING(6 5, 4 3, 2 1),POLYGON((0 0, 1 1, 1 0, 0 0)))')" == GeometryCollection(vec![ + Geometry::Point(point!(x: 8.0, y: 7.0)), + Geometry::LineString(line_string![ + (x: 6.0, y: 5.0), + (x: 4.0, y: 3.0), + (x: 2.0, y: 1.0), + ]), + Geometry::Polygon(polygon![ + (x: 0.0, y: 0.0), + (x: 1.0, y: 1.0), + (x: 1.0, y: 0.0), + (x: 0.0, y: 0.0), + ]), + ]), + )); + test_type!(geometry_collection_empty>( MySql, "SELECT CAST({0} <=> ? AS SIGNED INTEGER), CAST({0} AS BINARY) as _2, ? as _3", "ST_GeomFromText('GEOMETRYCOLLECTION EMPTY')" == Geometry::::GeometryCollection(GeometryCollection(vec![])), )); + test_type!(geometry_subtype_collection_empty>( + MySql, + "SELECT CAST({0} <=> ? AS SIGNED INTEGER), CAST({0} AS BINARY) as _2, ? as _3", + "ST_GeomFromText('GEOMETRYCOLLECTION EMPTY')" == GeometryCollection::(vec![]), + )); + macro_rules! geo_table { (CREATE, $ty:literal) => { format!( @@ -374,17 +443,17 @@ CREATE TEMPORARY TABLE with_geometry ( /// geometry literals in selection. /// /// Because of the limitations of MySQL, we have to use the `Blob` type to represent - /// the `Geometry` type, so use case testing in actual tables make more sense with + /// the [`Geometry`] type, so use case testing in actual tables make more sense with /// the actual use of users. macro_rules! test_geo_table { - ($name:ident, $ty:literal, $($text:literal == $value:expr),+ $(,)?) => { + ($name:ident, $col:literal, $($text:literal == <$ty:ty>$value:expr),+ $(,)?) => { paste::item! { #[sqlx_macros::test] async fn [< test_geometry_table_ $name >] () -> anyhow::Result<()> { use sqlx::Connection; let mut conn = sqlx_test::new::().await?; - let tdl = geo_table!(CREATE, $ty); + let tdl = geo_table!(CREATE, $col); conn.execute(tdl.as_str()).await?; @@ -401,7 +470,7 @@ CREATE TEMPORARY TABLE with_geometry ( .bind(&expected) .fetch_one(&mut conn) .await?; - let geom: Geometry = row.try_get(0)?; + let geom: $ty = row.try_get(0)?; assert_eq!(geom, expected); @@ -411,7 +480,7 @@ CREATE TEMPORARY TABLE with_geometry ( let row = sqlx::query(&query) .fetch_one(&mut conn) .await?; - let geom: Geometry = row.try_get(0)?; + let geom: $ty = row.try_get(0)?; assert_eq!(geom, expected); conn.execute(geo_table!(TRUNCATE)).await?; @@ -428,62 +497,84 @@ CREATE TEMPORARY TABLE with_geometry ( test_geo_table!( point, "POINT", - "ST_GeomFromText('Point(0 0)')" == Geometry::Point(Point(Coord { x: 0.0, y: 0.0 })), - "ST_GeomFromText('Point(-2 -3)')" == Geometry::Point(Point(Coord { x: -2.0, y: -3.0 })), + "ST_GeomFromText('Point(0 0)')" == >Geometry::Point(point!(x: 0.0, y: 0.0)), + "ST_GeomFromText('Point(-2 -3)')" == >Geometry::Point(point!(x: -2.0, y: -3.0)), "ST_GeomFromText('Point(5.76814 12345)')" - == Geometry::Point(Point(Coord { - x: 5.76814, - y: 12345.0 - })), + == >Geometry::Point(point!(x: 5.76814, y: 12345.0)), + "ST_GeomFromText('Point(0 0)')" + == >point!(x: 0.0, y: 0.0), + "ST_GeomFromText('Point(-5.7 -4.3)')" == >point!(x: -5.7, y: -4.3), ); test_geo_table!( linestring, "LINESTRING", "ST_GeomFromText('LineString(0 0, 1 1, 2 2)')" - == Geometry::LineString(LineString(vec![ - Coord { x: 0.0, y: 0.0 }, - Coord { x: 1.0, y: 1.0 }, - Coord { x: 2.0, y: 2.0 }, - ])), + == >Geometry::LineString(line_string![ + (x: 0.0, y: 0.0), + (x: 1.0, y: 1.0), + (x: 2.0, y: 2.0), + ]), + "ST_GeomFromText('LineString(6 5, 4 3, 2 1)')" + == >line_string![ + (x: 6.0, y: 5.0), + (x: 4.0, y: 3.0), + (x: 2.0, y: 1.0), + ], ); test_geo_table!( polygon, "POLYGON", "ST_GeomFromText('Polygon((0 0, 1 1, 1 0, 0 0))')" - == Geometry::Polygon(Polygon::new( - LineString(vec![ - Coord { x: 0.0, y: 0.0 }, - Coord { x: 1.0, y: 1.0 }, - Coord { x: 1.0, y: 0.0 }, - Coord { x: 0.0, y: 0.0 }, - ]), - vec![], - )), + == >Geometry::Polygon(polygon![ + (x: 0.0, y: 0.0), + (x: 1.0, y: 1.0), + (x: 1.0, y: 0.0), + (x: 0.0, y: 0.0), + ]), + "ST_GeomFromText('Polygon((0 0, 2 2, 2 0, 0 0))')" + == >polygon![ + (x: 0.0, y: 0.0), + (x: 2.0, y: 2.0), + (x: 2.0, y: 0.0), + (x: 0.0, y: 0.0), + ], ); test_geo_table!( geometry_collection, "GEOMETRYCOLLECTION", "ST_GeomFromText('GeometryCollection(Point(1 2),LineString(3 4, 5 6, 7 8),Polygon((0 0, 1 1, 1 0, 0 0)))')" - == Geometry::GeometryCollection(GeometryCollection(vec![ - Geometry::Point(Point(Coord { x: 1.0, y: 2.0 })), - Geometry::LineString(LineString(vec![ - Coord { x: 3.0, y: 4.0 }, - Coord { x: 5.0, y: 6.0 }, - Coord { x: 7.0, y: 8.0 }, - ])), - Geometry::Polygon(Polygon::new( - LineString(vec![ - Coord { x: 0.0, y: 0.0 }, - Coord { x: 1.0, y: 1.0 }, - Coord { x: 1.0, y: 0.0 }, - Coord { x: 0.0, y: 0.0 }, - ]), - vec![], - )), + == >Geometry::GeometryCollection(GeometryCollection(vec![ + Geometry::Point(point!(x: 1.0, y: 2.0)), + Geometry::LineString(line_string![ + (x: 3.0, y: 4.0), + (x: 5.0, y: 6.0), + (x: 7.0, y: 8.0), + ]), + Geometry::Polygon(polygon![ + (x: 0.0, y: 0.0), + (x: 1.0, y: 1.0), + (x: 1.0, y: 0.0), + (x: 0.0, y: 0.0), + ]), ])), + "ST_GeomFromText('GeometryCollection(Point(8 7),LineString(6 5, 4 3, 2 1),Polygon((0 0, 1 1, 1 0, 0 0)))')" + == >GeometryCollection(vec![ + Geometry::Point(point!(x: 8.0, y: 7.0)), + Geometry::LineString(line_string![ + (x: 6.0, y: 5.0), + (x: 4.0, y: 3.0), + (x: 2.0, y: 1.0), + ]), + Geometry::Polygon(polygon![ + (x: 0.0, y: 0.0), + (x: 1.0, y: 1.0), + (x: 1.0, y: 0.0), + (x: 0.0, y: 0.0), + ]), + ]), ); }