From 2fa7ccd5a5bb7dc7f459ae37e273ec55e3b46f66 Mon Sep 17 00:00:00 2001 From: Mirro Mutth Date: Wed, 21 Feb 2024 19:50:10 +0900 Subject: [PATCH 1/3] feat(mysql): add GEOMETRY support --- Cargo.lock | 55 +++++++++ Cargo.toml | 4 + README.md | 2 + sqlx-mysql/Cargo.toml | 3 + sqlx-mysql/src/types/geometry.rs | 50 ++++++++ sqlx-mysql/src/types/mod.rs | 3 + tests/mysql/types.rs | 200 +++++++++++++++++++++++++++++++ 7 files changed, 317 insertions(+) create mode 100644 sqlx-mysql/src/types/geometry.rs diff --git a/Cargo.lock b/Cargo.lock index 56cde771a9..4f1dbf13d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -140,6 +140,15 @@ version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + [[package]] name = "argon2" version = "0.4.1" @@ -1474,6 +1483,31 @@ dependencies = [ "version_check", ] +[[package]] +name = "geo-types" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567495020b114f1ce9bed679b29975aa0bfae06ac22beacd5cfde5dabe7b05d6" +dependencies = [ + "approx", + "num-traits", + "serde", +] + +[[package]] +name = "geozero" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61d25cc15c7e5b86cd8dadea56bb78c46f346d4fb09022e7cbba0839c890d0a1" +dependencies = [ + "geo-types", + "log", + "scroll", + "serde_json", + "thiserror", + "wkt", +] + [[package]] name = "getrandom" version = "0.2.11" @@ -2888,6 +2922,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "scroll" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04c565b551bafbef4157586fa379538366e4385d42082f255bfd96e4fe8519da" + [[package]] name = "sct" version = "0.7.1" @@ -3156,6 +3196,7 @@ dependencies = [ "dotenvy", "env_logger", "futures", + "geo-types", "hex", "libsqlite3-sys", "paste", @@ -3443,6 +3484,8 @@ dependencies = [ "futures-io", "futures-util", "generic-array", + "geo-types", + "geozero", "hex", "hkdf", "hmac", @@ -4463,6 +4506,18 @@ dependencies = [ "memchr", ] +[[package]] +name = "wkt" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3c2252781f8927974e8ba6a67c965a759a2b88ea2b1825f6862426bbb1c8f41" +dependencies = [ + "geo-types", + "log", + "num-traits", + "thiserror", +] + [[package]] name = "wyz" version = "0.5.1" diff --git a/Cargo.toml b/Cargo.toml index 9a4dfdf5fe..6cc8626bea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -110,6 +110,7 @@ mac_address = ["sqlx-core/mac_address", "sqlx-macros?/mac_address", "sqlx-postgr rust_decimal = ["sqlx-core/rust_decimal", "sqlx-macros?/rust_decimal", "sqlx-mysql?/rust_decimal", "sqlx-postgres?/rust_decimal"] time = ["sqlx-core/time", "sqlx-macros?/time", "sqlx-mysql?/time", "sqlx-postgres?/time", "sqlx-sqlite?/time"] uuid = ["sqlx-core/uuid", "sqlx-macros?/uuid", "sqlx-mysql?/uuid", "sqlx-postgres?/uuid", "sqlx-sqlite?/uuid"] +geometry = ["sqlx-mysql?/geometry"] regexp = ["sqlx-sqlite?/regexp"] [workspace.dependencies] @@ -136,6 +137,8 @@ mac_address = "1.1.5" rust_decimal = "1.26.1" time = { version = "0.3.14", features = ["formatting", "parsing", "macros"] } uuid = "1.1.2" +geozero = { version = "0.12.0", default-features = false } +geo-types = "0.7.12" # Common utility crates dotenvy = { version = "0.15.0", default-features = false } @@ -170,6 +173,7 @@ sqlx-test = { path = "./sqlx-test" } paste = "1.0.6" serde = { version = "1.0.132", features = ["derive"] } serde_json = "1.0.73" +geo-types = { workspace = true } url = "2.2.2" rand = "0.8.4" rand_xoshiro = "0.6.0" diff --git a/README.md b/README.md index ecbf0137cc..3a4a67259b 100644 --- a/README.md +++ b/README.md @@ -195,6 +195,8 @@ be removed in the future. - `json`: Add support for `JSON` and `JSONB` (in postgres) using the `serde_json` crate. +- `geometry`: Add support for `GEOMETRY` using the `geozero` crate, currently only available for MySQL. + - Offline mode is now always enabled. See [sqlx-cli/README.md][readme-offline]. [readme-offline]: sqlx-cli/README.md#enable-building-in-offline-mode-with-query diff --git a/sqlx-mysql/Cargo.toml b/sqlx-mysql/Cargo.toml index d56a550f47..8031514f34 100644 --- a/sqlx-mysql/Cargo.toml +++ b/sqlx-mysql/Cargo.toml @@ -14,6 +14,7 @@ json = ["sqlx-core/json", "serde"] any = ["sqlx-core/any"] offline = ["sqlx-core/offline", "serde/derive"] migrate = ["sqlx-core/migrate"] +geometry = ["geozero", "geo-types"] [dependencies] sqlx-core = { workspace = true } @@ -41,6 +42,8 @@ chrono = { workspace = true, optional = true } rust_decimal = { workspace = true, optional = true } time = { workspace = true, optional = true } uuid = { workspace = true, optional = true } +geozero = { workspace = true, features = ["with-geo", "with-wkb"], optional = true } +geo-types = { workspace = true, optional = true } # Misc atoi = "2.0" diff --git a/sqlx-mysql/src/types/geometry.rs b/sqlx-mysql/src/types/geometry.rs new file mode 100644 index 0000000000..0851fa19e1 --- /dev/null +++ b/sqlx-mysql/src/types/geometry.rs @@ -0,0 +1,50 @@ +use geo_types::Geometry; +use geozero::wkb::{FromWkb, WkbDialect}; +use geozero::{GeozeroGeometry, ToWkb}; + +use crate::decode::Decode; +use crate::encode::{Encode, IsNull}; +use crate::error::BoxDynError; +use crate::io::MySqlBufMutExt; +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) + } + + fn compatible(ty: &MySqlTypeInfo) -> bool { + ty.r#type == ColumnType::Geometry || <&[u8] as Type>::compatible(ty) + } +} + +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 { + fn encode_by_ref(&self, buf: &mut Vec) -> IsNull { + // Encoding is supposed to be infallible, so we don't have much choice but to panic here. + // However, in most cases, a geometry being unable to serialize to WKB is most likely due to user error. + let bytes = self.to_mysql_wkb(self.srid()).expect(ENCODE_ERR); + + buf.put_bytes_lenenc(bytes.as_ref()); + + IsNull::No + } +} + +impl Decode<'_, MySql> for Geometry { + fn decode(value: MySqlValueRef<'_>) -> Result { + let mut bytes = value.as_bytes()?; + + Ok(FromWkb::from_wkb(&mut bytes, WkbDialect::MySQL)?) + } +} diff --git a/sqlx-mysql/src/types/mod.rs b/sqlx-mysql/src/types/mod.rs index 9b7ef29fc7..25c466dc4b 100644 --- a/sqlx-mysql/src/types/mod.rs +++ b/sqlx-mysql/src/types/mod.rs @@ -128,3 +128,6 @@ mod time; #[cfg(feature = "uuid")] mod uuid; + +#[cfg(feature = "geometry")] +mod geometry; diff --git a/tests/mysql/types.rs b/tests/mysql/types.rs index fad95b36a4..0d67a9f0d1 100644 --- a/tests/mysql/types.rs +++ b/tests/mysql/types.rs @@ -287,6 +287,206 @@ mod json_tests { )); } +#[cfg(feature = "geometry")] +mod geometry_tests { + use geo_types::{Coord, Geometry, GeometryCollection, LineString, Point, Polygon}; + use sqlx_test::test_type; + + use super::*; + + 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 })), + )); + + 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 }, + ])), + )); + + 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![], + )), + )); + + 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![], + )), + ])), + )); + + 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![])), + )); + + macro_rules! geo_table { + (CREATE, $ty:literal) => { + format!( + r#" +CREATE TEMPORARY TABLE with_geometry ( + id INT PRIMARY KEY AUTO_INCREMENT, + geom {} NOT NULL +);"#, + $ty + ) + }; + (TRUNCATE) => { + "TRUNCATE TABLE with_geometry" + }; + } + + /// Test with a table that has a column which type is a subtype of `GEOMETRY` + /// + /// It tests that we can insert and select values from the table, including using + /// 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 actual use of users. + macro_rules! test_geo_table { + ($name:ident, $ty:literal, $($text:literal == $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); + + conn.execute(tdl.as_str()).await?; + + $( + let expected = $value; + + println!("Insert with select {:?}", expected); + sqlx::query("INSERT INTO with_geometry (geom) VALUES (?)") + .bind(&expected) + .execute(&mut conn) + .await?; + + let row = sqlx::query("SELECT geom FROM with_geometry WHERE geom = ?") + .bind(&expected) + .fetch_one(&mut conn) + .await?; + let geom: Geometry = row.try_get(0)?; + + assert_eq!(geom, expected); + + let query = format!("SELECT geom FROM with_geometry WHERE geom = {}", $text); + println!("{query}"); + + let row = sqlx::query(&query) + .fetch_one(&mut conn) + .await?; + let geom: Geometry = row.try_get(0)?; + + assert_eq!(geom, expected); + conn.execute(geo_table!(TRUNCATE)).await?; + )+ + + conn.close().await?; + + Ok(()) + } + } + }; + } + + 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(5.76814 12345)')" + == Geometry::Point(Point(Coord { + x: 5.76814, + y: 12345.0 + })), + ); + + 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 }, + ])), + ); + + 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![], + )), + ); + + 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![], + )), + ])), + ); +} + #[sqlx_macros::test] async fn test_bits() -> anyhow::Result<()> { let mut conn = new::().await?; From 60e424cb317b422f55c1a3fb9fe9e0a57bc22fce Mon Sep 17 00:00:00 2001 From: Mirro Mutth Date: Thu, 22 Feb 2024 19:39:28 +0900 Subject: [PATCH 2/3] 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), + ]), + ]), ); } From 2e144625e5a7c034e2299b6c14e85a578215122b Mon Sep 17 00:00:00 2001 From: Mirro Mutth Date: Tue, 27 Feb 2024 12:23:48 +0900 Subject: [PATCH 3/3] feat(mysql): add nullability tests for geometry --- tests/mysql/types.rs | 49 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 46 insertions(+), 3 deletions(-) diff --git a/tests/mysql/types.rs b/tests/mysql/types.rs index b4e1117ca2..4fe8fc7757 100644 --- a/tests/mysql/types.rs +++ b/tests/mysql/types.rs @@ -290,8 +290,8 @@ mod json_tests { #[cfg(feature = "geometry")] mod geometry_tests { use geo_types::{ - line_string, point, polygon, Geometry, GeometryCollection, LineString, MultiPoint, Point, - Polygon, + line_string, point, polygon, Geometry, GeometryCollection, LineString, MultiPoint, + MultiPolygon, Point, Polygon, }; use sqlx_test::test_type; @@ -427,7 +427,7 @@ mod geometry_tests { r#" CREATE TEMPORARY TABLE with_geometry ( id INT PRIMARY KEY AUTO_INCREMENT, - geom {} NOT NULL + geom {} NULL );"#, $ty ) @@ -494,6 +494,49 @@ CREATE TEMPORARY TABLE with_geometry ( }; } + /// Nullability tests for a subtype of the `GEOMETRY` + macro_rules! test_geo_table_null { + ($name:ident, $col:literal, $ty:ty $(,)?) => { + paste::item! { + #[sqlx_macros::test] + async fn [< test_geometry_table_null_ $name >] () -> anyhow::Result<()> { + use sqlx::Connection; + + let mut conn = sqlx_test::new::().await?; + let tdl = geo_table!(CREATE, $col); + + conn.execute(tdl.as_str()).await?; + + + println!("Insert with select NULL of {}", std::any::type_name::<$ty>()); + conn.execute("INSERT INTO with_geometry (geom) VALUES (NULL)").await?; + + let row = sqlx::query("SELECT geom FROM with_geometry WHERE geom IS NULL") + .fetch_one(&mut conn) + .await?; + let geom: Option<$ty> = row.try_get(0)?; + + conn.close().await?; + assert_eq!(geom, None); + + Ok(()) + } + } + }; + } + + test_geo_table_null!(geometry, "GEOMETRY", Geometry); + test_geo_table_null!(point, "POINT", Point); + test_geo_table_null!(linestring, "LINESTRING", LineString); + test_geo_table_null!(polygon, "POLYGON", Polygon); + test_geo_table_null!(multipoint, "MULTIPOINT", MultiPoint); + test_geo_table_null!(multipolygon, "MULTIPOLYGON", MultiPolygon); + test_geo_table_null!( + geometry_collection, + "GEOMETRYCOLLECTION", + GeometryCollection + ); + test_geo_table!( point, "POINT",