From 91bf7ed5f7d6f04aa8c4562f53d3b7385d43ad83 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 26 Jun 2025 02:13:59 +0000 Subject: [PATCH 1/3] Implement MySQL Spatial Types --- Cargo.lock | 70 +++++++- Cargo.toml | 2 + sqlx-core/Cargo.toml | 3 + sqlx-mysql/Cargo.toml | 7 +- sqlx-mysql/src/types/geo/geometry.rs | 153 ++++++++++++++++++ .../src/types/geo/geometry_collection.rs | 145 +++++++++++++++++ sqlx-mysql/src/types/geo/linestring.rs | 136 ++++++++++++++++ sqlx-mysql/src/types/geo/mod.rs | 8 + sqlx-mysql/src/types/geo/multi_line_string.rs | 140 ++++++++++++++++ sqlx-mysql/src/types/geo/multi_point.rs | 136 ++++++++++++++++ sqlx-mysql/src/types/geo/multi_polygon.rs | 144 +++++++++++++++++ sqlx-mysql/src/types/geo/point.rs | 135 ++++++++++++++++ sqlx-mysql/src/types/geo/polygon.rs | 139 ++++++++++++++++ sqlx-mysql/src/types/mod.rs | 3 + 14 files changed, 1219 insertions(+), 2 deletions(-) create mode 100644 sqlx-mysql/src/types/geo/geometry.rs create mode 100644 sqlx-mysql/src/types/geo/geometry_collection.rs create mode 100644 sqlx-mysql/src/types/geo/linestring.rs create mode 100644 sqlx-mysql/src/types/geo/mod.rs create mode 100644 sqlx-mysql/src/types/geo/multi_line_string.rs create mode 100644 sqlx-mysql/src/types/geo/multi_point.rs create mode 100644 sqlx-mysql/src/types/geo/multi_polygon.rs create mode 100644 sqlx-mysql/src/types/geo/point.rs create mode 100644 sqlx-mysql/src/types/geo/polygon.rs diff --git a/Cargo.lock b/Cargo.lock index 4b490a4a4c..6509cc9586 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -119,6 +119,15 @@ version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" +[[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" @@ -1513,6 +1522,26 @@ dependencies = [ "version_check", ] +[[package]] +name = "geo-traits" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e7c353d12a704ccfab1ba8bfb1a7fe6cb18b665bf89d37f4f7890edcd260206" +dependencies = [ + "geo-types", +] + +[[package]] +name = "geo-types" +version = "0.7.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62ddb1950450d67efee2bbc5e429c68d052a822de3aad010d28b351fbb705224" +dependencies = [ + "approx", + "num-traits", + "serde", +] + [[package]] name = "getrandom" version = "0.2.15" @@ -2364,6 +2393,27 @@ dependencies = [ "libm", ] +[[package]] +name = "num_enum" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.96", +] + [[package]] name = "object" version = "0.36.7" @@ -3028,7 +3078,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.9.4", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3400,6 +3450,7 @@ dependencies = [ "dotenvy", "env_logger", "futures-util", + "geo-types", "hex", "libsqlite3-sys", "paste", @@ -3464,6 +3515,7 @@ dependencies = [ "futures-intrusive", "futures-io", "futures-util", + "geo-types", "hashbrown 0.15.2", "hashlink", "indexmap 2.7.0", @@ -3649,6 +3701,7 @@ dependencies = [ name = "sqlx-mysql" version = "0.9.0-alpha.1" dependencies = [ + "anyhow", "atoi", "base64 0.22.1", "bigdecimal", @@ -3665,6 +3718,8 @@ dependencies = [ "futures-io", "futures-util", "generic-array", + "geo-traits", + "geo-types", "hex", "hkdf", "hmac", @@ -3688,6 +3743,7 @@ dependencies = [ "tracing", "uuid", "whoami", + "wkb", ] [[package]] @@ -4840,6 +4896,18 @@ dependencies = [ "memchr", ] +[[package]] +name = "wkb" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff9eff6aebac4c64f9c7c057a68f6359284e2a80acf102dffe041fe219b3a082" +dependencies = [ + "byteorder", + "geo-traits", + "num_enum", + "thiserror 1.0.69", +] + [[package]] name = "write16" version = "1.0.0" diff --git a/Cargo.toml b/Cargo.toml index f206eadd26..651a82b438 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -137,6 +137,7 @@ sqlx = { version = "=0.9.0-alpha.1", path = ".", default-features = false } # Common type integrations shared by multiple driver crates. # These are optional unless enabled in a workspace crate. bigdecimal = "0.4.0" +geo-types = { version = "0.7.16", default-features = false } bit-vec = "0.6.3" chrono = { version = "0.4.34", default-features = false, features = ["std", "clock"] } ipnet = "2.3.0" @@ -174,6 +175,7 @@ env_logger = "0.11" async-std = { workspace = true, features = ["attributes"] } tokio = { version = "1.15.0", features = ["full"] } dotenvy = "0.15.0" +geo-types = { workspace = true } # Added for tests trybuild = "1.0.53" sqlx-test = { path = "./sqlx-test" } paste = "1.0.6" diff --git a/sqlx-core/Cargo.toml b/sqlx-core/Cargo.toml index 1bba2d65bb..9d9fcf69d6 100644 --- a/sqlx-core/Cargo.toml +++ b/sqlx-core/Cargo.toml @@ -32,6 +32,8 @@ _tls-none = [] # support offline/decoupled building (enables serialization of `Describe`) offline = ["serde", "either/serde"] +geo-types = ["dep:geo-types"] + [dependencies] # Runtimes async-std = { workspace = true, optional = true } @@ -47,6 +49,7 @@ rustls-native-certs = { version = "0.8.0", optional = true } # Type Integrations bit-vec = { workspace = true, optional = true } bigdecimal = { workspace = true, optional = true } +geo-types = { workspace = true, optional = true } rust_decimal = { workspace = true, optional = true } time = { workspace = true, optional = true } ipnet = { workspace = true, optional = true } diff --git a/sqlx-mysql/Cargo.toml b/sqlx-mysql/Cargo.toml index 52717c4207..9609e13e0b 100644 --- a/sqlx-mysql/Cargo.toml +++ b/sqlx-mysql/Cargo.toml @@ -19,6 +19,7 @@ migrate = ["sqlx-core/migrate"] bigdecimal = ["dep:bigdecimal", "sqlx-core/bigdecimal"] chrono = ["dep:chrono", "sqlx-core/chrono"] rust_decimal = ["dep:rust_decimal", "rust_decimal/maths", "sqlx-core/rust_decimal"] +spatial-data-types = ["sqlx-core/geo-types", "dep:geo-types", "dep:wkb", "dep:geo-traits"] time = ["dep:time", "sqlx-core/time"] uuid = ["dep:uuid", "sqlx-core/uuid"] @@ -45,9 +46,12 @@ sha2 = { version = "0.10.0", default-features = false } # Type Integrations (versions inherited from `[workspace.dependencies]`) bigdecimal = { workspace = true, optional = true } chrono = { workspace = true, optional = true } +geo-types = { workspace = true, optional = true } +geo-traits = { version = "0.3.0", optional = true } rust_decimal = { workspace = true, optional = true } time = { workspace = true, optional = true } uuid = { workspace = true, optional = true } +wkb = { version = "0.9.0", optional = true } # Misc atoi = "2.0" @@ -72,7 +76,8 @@ whoami = { version = "1.2.1", default-features = false } serde = { version = "1.0.144", optional = true } [dev-dependencies] -sqlx = { workspace = true, features = ["mysql"] } +sqlx = { workspace = true, features = ["mysql", "macros", "migrate"] } +anyhow = "1.0" # For test results [lints] workspace = true diff --git a/sqlx-mysql/src/types/geo/geometry.rs b/sqlx-mysql/src/types/geo/geometry.rs new file mode 100644 index 0000000000..e87cc4230a --- /dev/null +++ b/sqlx-mysql/src/types/geo/geometry.rs @@ -0,0 +1,153 @@ +use sqlx_core::decode::Decode; +use sqlx_core::encode::{Encode, IsNull}; +use sqlx_core::error::BoxDynError; +use sqlx_core::types::Type; + +use crate::io::MySqlBufMutExt; +use crate::{MySql, MySqlTypeInfo, MySqlValueRef}; +use crate::protocol::text::{ColumnFlags, ColumnType}; + + +use geo_types::Geometry; +use geo_traits::to_geo::ToGeoGeometry; +use wkb::reader; +use wkb::writer; + +impl Type for Geometry { + fn type_info() -> MySqlTypeInfo { + MySqlTypeInfo::binary(ColumnType::Geometry) + } + + fn compatible(ty: &MySqlTypeInfo) -> bool { + matches!( + ty.r#type, + ColumnType::Geometry + | ColumnType::Blob + | ColumnType::MediumBlob + | ColumnType::LongBlob + | ColumnType::TinyBlob + | ColumnType::VarString if ty.flags.contains(ColumnFlags::BINARY) + ) + } +} + +impl Encode<'_, MySql> for Geometry { + fn encode_by_ref(&self, buf: &mut Vec) -> Result { + let mut wkb_buffer = Vec::new(); + wkb_buffer.extend_from_slice(&0u32.to_le_bytes()); // SRID = 0, Little Endian + writer::write_geometry(&mut wkb_buffer, self, &writer::WriteOptions::default())?; + buf.put_bytes_lenenc(&wkb_buffer); + Ok(IsNull::No) + } +} + +impl<'r> Decode<'r, MySql> for Geometry { + fn decode(value: MySqlValueRef<'r>) -> Result { + let bytes = value.as_bytes()?; + if bytes.len() < 4 { + return Err(format!( + "Invalid GEOMETRY data for Geometry: received {} bytes, expected at least 4 for SRID prefix.", + bytes.len() + ) + .into()); + } + let wkb_data = &bytes[4..]; // Skip 4-byte SRID + + let wkb_reader_geom = reader::Wkb::try_new(wkb_data) + .map_err(|e| BoxDynError::from(format!("WKB parsing error for Geometry: {}", e)))?; + + Ok(wkb_reader_geom + .to_geometry()) + } +} + +#[cfg(test)] +mod tests { + use sqlx::mysql::{MySqlPool, MySqlRow}; + use sqlx::{Executor, Row}; + use geo_types::{Geometry as TestableGeoType, Point, LineString, Polygon, coord}; + + #[sqlx::test] + async fn test_encode_decode_geometry_enum(pool: MySqlPool) -> anyhow::Result<()> { + let table_name = format!("test_geo_geometry_enum_table"); + pool.execute(format!("DROP TABLE IF EXISTS {}", table_name).as_str()).await?; + pool.execute( + format!( + "CREATE TABLE {} (id INT, geom_val GEOMETRY, geom_null GEOMETRY NULL)", + table_name + ) + .as_str(), + ) + .await?; + + let p1_geom = Point::new(1.0, 2.0); + let geom_enum1 = TestableGeoType::Point(p1_geom); + + let ls1_geom = LineString::new(vec![coord!{x: 3., y: 4.}, coord!{x: 5., y: 6.}]); + let geom_enum2 = TestableGeoType::LineString(ls1_geom); + + let ext_poly_geom = LineString::new(vec![coord!{x:0.,y:0.}, coord!{x:1.,y:1.}, coord!{x:1.,y:0.}, coord!{x:0.,y:0.}]); + let poly1_geom = Polygon::new(ext_poly_geom, vec![]); + let geom_enum3 = TestableGeoType::Polygon(poly1_geom); + + // Test non-nullable Point variant + sqlx::query(&format!("INSERT INTO {} (id, geom_val) VALUES (1, ?)", table_name)) + .bind(geom_enum1.clone()) + .execute(&pool) + .await?; + let row1: MySqlRow = sqlx::query(&format!("SELECT geom_val FROM {} WHERE id = 1", table_name)) + .fetch_one(&pool) + .await?; + let decoded_val1: TestableGeoType = row1.try_get("geom_val")?; + assert_eq!(decoded_val1, geom_enum1); + + // Test non-nullable LineString variant + sqlx::query(&format!("INSERT INTO {} (id, geom_val) VALUES (2, ?)", table_name)) + .bind(geom_enum2.clone()) + .execute(&pool) + .await?; + let row2: MySqlRow = sqlx::query(&format!("SELECT geom_val FROM {} WHERE id = 2", table_name)) + .fetch_one(&pool) + .await?; + let decoded_val2: TestableGeoType = row2.try_get("geom_val")?; + assert_eq!(decoded_val2, geom_enum2); + + // Test non-nullable Polygon variant + sqlx::query(&format!("INSERT INTO {} (id, geom_val) VALUES (3, ?)", table_name)) + .bind(geom_enum3.clone()) + .execute(&pool) + .await?; + let row3: MySqlRow = sqlx::query(&format!("SELECT geom_val FROM {} WHERE id = 3", table_name)) + .fetch_one(&pool) + .await?; + let decoded_val3: TestableGeoType = row3.try_get("geom_val")?; + assert_eq!(decoded_val3, geom_enum3); + + // Test nullable Some(value) - using Point variant + let some_val: Option = Some(geom_enum1.clone()); + sqlx::query(&format!("INSERT INTO {} (id, geom_null) VALUES (4, ?)", table_name)) + .bind(some_val.clone()) + .execute(&pool) + .await?; + let row_some: MySqlRow = sqlx::query(&format!("SELECT geom_null FROM {} WHERE id = 4", table_name)) + .fetch_one(&pool) + .await?; + let decoded_some: Option = row_some.try_get("geom_null")?; + assert_eq!(decoded_some, some_val); + + // Test nullable None + let none_val: Option = None; + sqlx::query(&format!("INSERT INTO {} (id, geom_null) VALUES (5, ?)", table_name)) + .bind(none_val.clone()) + .execute(&pool) + .await?; + let row_none: MySqlRow = sqlx::query(&format!("SELECT geom_null FROM {} WHERE id = 5", table_name)) + .fetch_one(&pool) + .await?; + let decoded_none: Option = row_none.try_get("geom_null")?; + assert_eq!(decoded_none, none_val); + + pool.execute(format!("DROP TABLE IF EXISTS {}", table_name).as_str()).await?; + Ok(()) + } +} diff --git a/sqlx-mysql/src/types/geo/geometry_collection.rs b/sqlx-mysql/src/types/geo/geometry_collection.rs new file mode 100644 index 0000000000..a53a6f94a8 --- /dev/null +++ b/sqlx-mysql/src/types/geo/geometry_collection.rs @@ -0,0 +1,145 @@ +use sqlx_core::decode::Decode; +use sqlx_core::encode::{Encode, IsNull}; +use sqlx_core::error::BoxDynError; +use sqlx_core::types::Type; + +use crate::io::MySqlBufMutExt; +use crate::{MySql, MySqlTypeInfo, MySqlValueRef}; +use crate::protocol::text::{ColumnFlags, ColumnType}; + +use std::convert::TryFrom; + +use geo_types::GeometryCollection; +use geo_traits::to_geo::ToGeoGeometry; +use wkb::reader; +use wkb::writer; + +impl Type for GeometryCollection { + fn type_info() -> MySqlTypeInfo { + MySqlTypeInfo::binary(ColumnType::Geometry) + } + + fn compatible(ty: &MySqlTypeInfo) -> bool { + matches!( + ty.r#type, + ColumnType::Geometry + | ColumnType::Blob + | ColumnType::MediumBlob + | ColumnType::LongBlob + | ColumnType::TinyBlob + | ColumnType::VarString if ty.flags.contains(ColumnFlags::BINARY) + ) + } +} + +impl Encode<'_, MySql> for GeometryCollection { + fn encode_by_ref(&self, buf: &mut Vec) -> Result { + let mut wkb_buffer = Vec::new(); + wkb_buffer.extend_from_slice(&0u32.to_le_bytes()); // SRID = 0, Little Endian + writer::write_geometry_collection(&mut wkb_buffer, self, &writer::WriteOptions::default())?; + buf.put_bytes_lenenc(&wkb_buffer); + Ok(IsNull::No) + } +} + +impl<'r> Decode<'r, MySql> for GeometryCollection { + fn decode(value: MySqlValueRef<'r>) -> Result { + let bytes = value.as_bytes()?; + if bytes.len() < 4 { + return Err(format!( + "Invalid GEOMETRY data for GeometryCollection: received {} bytes, expected at least 4 for SRID prefix.", + bytes.len() + ) + .into()); + } + let wkb_data = &bytes[4..]; // Skip 4-byte SRID + + let wkb_reader_geom = reader::Wkb::try_new(wkb_data) + .map_err(|e| BoxDynError::from(format!("WKB parsing error for GeometryCollection: {}", e)))?; + + let geo_geom: geo_types::Geometry = wkb_reader_geom + .to_geometry(); + + GeometryCollection::try_from(geo_geom).map_err(|e| { + BoxDynError::from(format!( + "Failed to convert geo_types::Geometry to GeometryCollection: {:?}", + e + )) + }) + } +} + +#[cfg(test)] +mod tests { + use sqlx::mysql::{MySqlPool, MySqlRow}; + use sqlx::{Executor, Row}; + use geo_types::{GeometryCollection as TestableGeoType, Point, LineString, Geometry, coord}; + + #[sqlx::test] + async fn test_encode_decode_geometry_collection(pool: MySqlPool) -> anyhow::Result<()> { + let table_name = format!("test_geo_geometry_collection_table"); + pool.execute(format!("DROP TABLE IF EXISTS {}", table_name).as_str()).await?; + pool.execute( + format!( + "CREATE TABLE {} (id INT, geom GEOMETRY, geom_null GEOMETRY NULL)", + table_name + ) + .as_str(), + ) + .await?; + + let p1 = Point::new(0., 0.); + let ls1 = LineString::new(vec![coord!{x: 1., y: 1.}, coord!{x: 2., y: 2.}]); + let gc1 = TestableGeoType::new_from(vec![ // Changed to new_from + Geometry::Point(p1.clone()), + Geometry::LineString(ls1.clone()), + ]); + + let p2 = Point::new(10., 10.); + let gc2 = TestableGeoType::new_from(vec![ // Changed to new_from + Geometry::Point(p2.clone()), + ]); + + // Test non-nullable + sqlx::query(&format!("INSERT INTO {} (id, geom) VALUES (1, ?)", table_name)) + .bind(gc1.clone()) + .execute(&pool) + .await?; + + let row: MySqlRow = sqlx::query(&format!("SELECT geom FROM {} WHERE id = 1", table_name)) + .fetch_one(&pool) + .await?; + let decoded_val: TestableGeoType = row.try_get("geom")?; + assert_eq!(decoded_val.len(), gc1.len()); + assert_eq!(decoded_val, gc1); + + // Test nullable Some(value) + let some_val: Option = Some(gc2.clone()); + sqlx::query(&format!("INSERT INTO {} (id, geom_null) VALUES (2, ?)", table_name)) + .bind(some_val.clone()) + .execute(&pool) + .await?; + + let row_some: MySqlRow = sqlx::query(&format!("SELECT geom_null FROM {} WHERE id = 2", table_name)) + .fetch_one(&pool) + .await?; + let decoded_some: Option = row_some.try_get("geom_null")?; + assert_eq!(decoded_some, some_val); + + // Test nullable None + let none_val: Option = None; + sqlx::query(&format!("INSERT INTO {} (id, geom_null) VALUES (3, ?)", table_name)) + .bind(none_val.clone()) + .execute(&pool) + .await?; + + let row_none: MySqlRow = sqlx::query(&format!("SELECT geom_null FROM {} WHERE id = 3", table_name)) + .fetch_one(&pool) + .await?; + let decoded_none: Option = row_none.try_get("geom_null")?; + assert_eq!(decoded_none, none_val); + + pool.execute(format!("DROP TABLE IF EXISTS {}", table_name).as_str()).await?; + Ok(()) + } +} diff --git a/sqlx-mysql/src/types/geo/linestring.rs b/sqlx-mysql/src/types/geo/linestring.rs new file mode 100644 index 0000000000..92fdbcce3d --- /dev/null +++ b/sqlx-mysql/src/types/geo/linestring.rs @@ -0,0 +1,136 @@ +use sqlx_core::decode::Decode; +use sqlx_core::encode::{Encode, IsNull}; +use sqlx_core::error::BoxDynError; +use sqlx_core::types::Type; + +use crate::io::MySqlBufMutExt; +use crate::{MySql, MySqlTypeInfo, MySqlValueRef}; +use crate::protocol::text::{ColumnFlags, ColumnType}; + +use std::convert::TryFrom; + +use geo_types::LineString; +use geo_traits::to_geo::ToGeoGeometry; +use wkb::reader; +use wkb::writer; + +impl Type for LineString { + fn type_info() -> MySqlTypeInfo { + MySqlTypeInfo::binary(ColumnType::Geometry) + } + + fn compatible(ty: &MySqlTypeInfo) -> bool { + matches!( + ty.r#type, + ColumnType::Geometry + | ColumnType::Blob + | ColumnType::MediumBlob + | ColumnType::LongBlob + | ColumnType::TinyBlob + | ColumnType::VarString if ty.flags.contains(ColumnFlags::BINARY) + ) + } +} + +impl Encode<'_, MySql> for LineString { + fn encode_by_ref(&self, buf: &mut Vec) -> Result { + let mut wkb_buffer = Vec::new(); + wkb_buffer.extend_from_slice(&0u32.to_le_bytes()); // SRID = 0, Little Endian + writer::write_line_string(&mut wkb_buffer, self, &writer::WriteOptions::default())?; + buf.put_bytes_lenenc(&wkb_buffer); + Ok(IsNull::No) + } +} + +impl<'r> Decode<'r, MySql> for LineString { + fn decode(value: MySqlValueRef<'r>) -> Result { + let bytes = value.as_bytes()?; + if bytes.len() < 4 { + return Err(format!( + "Invalid GEOMETRY data for LineString: received {} bytes, expected at least 4 for SRID prefix.", + bytes.len() + ) + .into()); + } + let wkb_data = &bytes[4..]; // Skip 4-byte SRID + + let wkb_reader_geom = reader::Wkb::try_new(wkb_data) + .map_err(|e| BoxDynError::from(format!("WKB parsing error for LineString: {}", e)))?; + + let geo_geom: geo_types::Geometry = wkb_reader_geom + .to_geometry(); + + LineString::try_from(geo_geom).map_err(|e| { + BoxDynError::from(format!( + "Failed to convert geo_types::Geometry to LineString: {:?}", + e + )) + }) + } +} + +#[cfg(test)] +mod tests { + use sqlx::mysql::{MySqlPool, MySqlRow}; + use sqlx::{Executor, Row}; + use geo_types::LineString as TestableGeoType; + use geo_types::coord; // For CONSTRUCTION_CODE + + #[sqlx::test] + async fn test_encode_decode_linestring(pool: MySqlPool) -> anyhow::Result<()> { + let table_name = format!("test_geo_linestring_table"); + pool.execute(format!("DROP TABLE IF EXISTS {}", table_name).as_str()).await?; + pool.execute( + format!( + "CREATE TABLE {} (id INT, geom GEOMETRY, geom_null GEOMETRY NULL)", + table_name + ) + .as_str(), + ) + .await?; + + let ls1 = TestableGeoType::new(vec![coord! { x: 0., y: 0. }, coord! { x: 1., y: 1. }, coord! { x: 2., y: 0. }]); + let ls2 = TestableGeoType::new(vec![coord! { x: 10., y: 10. }, coord! { x: 20., y: 20. }]); + + // Test non-nullable + sqlx::query(&format!("INSERT INTO {} (id, geom) VALUES (1, ?)", table_name)) + .bind(ls1.clone()) + .execute(&pool) + .await?; + + let row: MySqlRow = sqlx::query(&format!("SELECT geom FROM {} WHERE id = 1", table_name)) + .fetch_one(&pool) + .await?; + let decoded_val: TestableGeoType = row.try_get("geom")?; + assert_eq!(decoded_val, ls1); + + // Test nullable Some(value) + let some_val: Option = Some(ls2.clone()); + sqlx::query(&format!("INSERT INTO {} (id, geom_null) VALUES (2, ?)", table_name)) + .bind(some_val.clone()) + .execute(&pool) + .await?; + + let row_some: MySqlRow = sqlx::query(&format!("SELECT geom_null FROM {} WHERE id = 2", table_name)) + .fetch_one(&pool) + .await?; + let decoded_some: Option = row_some.try_get("geom_null")?; + assert_eq!(decoded_some, some_val); + + // Test nullable None + let none_val: Option = None; + sqlx::query(&format!("INSERT INTO {} (id, geom_null) VALUES (3, ?)", table_name)) + .bind(none_val.clone()) + .execute(&pool) + .await?; + + let row_none: MySqlRow = sqlx::query(&format!("SELECT geom_null FROM {} WHERE id = 3", table_name)) + .fetch_one(&pool) + .await?; + let decoded_none: Option = row_none.try_get("geom_null")?; + assert_eq!(decoded_none, none_val); + + pool.execute(format!("DROP TABLE IF EXISTS {}", table_name).as_str()).await?; + Ok(()) + } +} diff --git a/sqlx-mysql/src/types/geo/mod.rs b/sqlx-mysql/src/types/geo/mod.rs new file mode 100644 index 0000000000..accadafc90 --- /dev/null +++ b/sqlx-mysql/src/types/geo/mod.rs @@ -0,0 +1,8 @@ +pub mod point; +pub mod linestring; +pub mod polygon; +pub mod multi_point; +pub mod multi_line_string; +pub mod multi_polygon; +pub mod geometry_collection; +pub mod geometry; diff --git a/sqlx-mysql/src/types/geo/multi_line_string.rs b/sqlx-mysql/src/types/geo/multi_line_string.rs new file mode 100644 index 0000000000..18f4f74668 --- /dev/null +++ b/sqlx-mysql/src/types/geo/multi_line_string.rs @@ -0,0 +1,140 @@ +use sqlx_core::decode::Decode; +use sqlx_core::encode::{Encode, IsNull}; +use sqlx_core::error::BoxDynError; +use sqlx_core::types::Type; + +use crate::io::MySqlBufMutExt; +use crate::{MySql, MySqlTypeInfo, MySqlValueRef}; +use crate::protocol::text::{ColumnFlags, ColumnType}; + +use std::convert::TryFrom; + +use geo_types::MultiLineString; +use geo_traits::to_geo::ToGeoGeometry; +use wkb::reader; +use wkb::writer; + +impl Type for MultiLineString { + fn type_info() -> MySqlTypeInfo { + MySqlTypeInfo::binary(ColumnType::Geometry) + } + + fn compatible(ty: &MySqlTypeInfo) -> bool { + matches!( + ty.r#type, + ColumnType::Geometry + | ColumnType::Blob + | ColumnType::MediumBlob + | ColumnType::LongBlob + | ColumnType::TinyBlob + | ColumnType::VarString if ty.flags.contains(ColumnFlags::BINARY) + ) + } +} + +impl Encode<'_, MySql> for MultiLineString { + fn encode_by_ref(&self, buf: &mut Vec) -> Result { + let mut wkb_buffer = Vec::new(); + wkb_buffer.extend_from_slice(&0u32.to_le_bytes()); // SRID = 0, Little Endian + writer::write_multi_line_string(&mut wkb_buffer, self, &writer::WriteOptions::default())?; + buf.put_bytes_lenenc(&wkb_buffer); + Ok(IsNull::No) + } +} + +impl<'r> Decode<'r, MySql> for MultiLineString { + fn decode(value: MySqlValueRef<'r>) -> Result { + let bytes = value.as_bytes()?; + if bytes.len() < 4 { + return Err(format!( + "Invalid GEOMETRY data for MultiLineString: received {} bytes, expected at least 4 for SRID prefix.", + bytes.len() + ) + .into()); + } + let wkb_data = &bytes[4..]; // Skip 4-byte SRID + + let wkb_reader_geom = reader::Wkb::try_new(wkb_data) + .map_err(|e| BoxDynError::from(format!("WKB parsing error for MultiLineString: {}", e)))?; + + let geo_geom: geo_types::Geometry = wkb_reader_geom + .to_geometry(); + + MultiLineString::try_from(geo_geom).map_err(|e| { + BoxDynError::from(format!( + "Failed to convert geo_types::Geometry to MultiLineString: {:?}", + e + )) + }) + } +} + +#[cfg(test)] +mod tests { + use sqlx::mysql::{MySqlPool, MySqlRow}; + use sqlx::{Executor, Row}; + use geo_types::MultiLineString as TestableGeoType; + use geo_types::LineString; + use geo_types::coord; + + #[sqlx::test] + async fn test_encode_decode_multilinestring(pool: MySqlPool) -> anyhow::Result<()> { + let table_name = format!("test_geo_multilinestring_table"); + pool.execute(format!("DROP TABLE IF EXISTS {}", table_name).as_str()).await?; + pool.execute( + format!( + "CREATE TABLE {} (id INT, geom GEOMETRY, geom_null GEOMETRY NULL)", + table_name + ) + .as_str(), + ) + .await?; + + let ls1_in = LineString::new(vec![coord!{x:0.,y:0.}, coord!{x:1.,y:1.}]); + let ls2_in = LineString::new(vec![coord!{x:2.,y:2.}, coord!{x:3.,y:3.}]); + let mls1 = TestableGeoType::new(vec![ls1_in, ls2_in]); + let ls3_in = LineString::new(vec![coord!{x:10.,y:10.}, coord!{x:20.,y:20.}]); + let mls2 = TestableGeoType::new(vec![ls3_in]); + + // Test non-nullable + sqlx::query(&format!("INSERT INTO {} (id, geom) VALUES (1, ?)", table_name)) + .bind(mls1.clone()) + .execute(&pool) + .await?; + + let row: MySqlRow = sqlx::query(&format!("SELECT geom FROM {} WHERE id = 1", table_name)) + .fetch_one(&pool) + .await?; + let decoded_val: TestableGeoType = row.try_get("geom")?; + assert_eq!(decoded_val, mls1); + + // Test nullable Some(value) + let some_val: Option = Some(mls2.clone()); + sqlx::query(&format!("INSERT INTO {} (id, geom_null) VALUES (2, ?)", table_name)) + .bind(some_val.clone()) + .execute(&pool) + .await?; + + let row_some: MySqlRow = sqlx::query(&format!("SELECT geom_null FROM {} WHERE id = 2", table_name)) + .fetch_one(&pool) + .await?; + let decoded_some: Option = row_some.try_get("geom_null")?; + assert_eq!(decoded_some, some_val); + + // Test nullable None + let none_val: Option = None; + sqlx::query(&format!("INSERT INTO {} (id, geom_null) VALUES (3, ?)", table_name)) + .bind(none_val.clone()) + .execute(&pool) + .await?; + + let row_none: MySqlRow = sqlx::query(&format!("SELECT geom_null FROM {} WHERE id = 3", table_name)) + .fetch_one(&pool) + .await?; + let decoded_none: Option = row_none.try_get("geom_null")?; + assert_eq!(decoded_none, none_val); + + pool.execute(format!("DROP TABLE IF EXISTS {}", table_name).as_str()).await?; + Ok(()) + } +} diff --git a/sqlx-mysql/src/types/geo/multi_point.rs b/sqlx-mysql/src/types/geo/multi_point.rs new file mode 100644 index 0000000000..5f43afd0ae --- /dev/null +++ b/sqlx-mysql/src/types/geo/multi_point.rs @@ -0,0 +1,136 @@ +use sqlx_core::decode::Decode; +use sqlx_core::encode::{Encode, IsNull}; +use sqlx_core::error::BoxDynError; +use sqlx_core::types::Type; + +use crate::io::MySqlBufMutExt; +use crate::{MySql, MySqlTypeInfo, MySqlValueRef}; +use crate::protocol::text::{ColumnFlags, ColumnType}; + +use std::convert::TryFrom; + +use geo_types::MultiPoint; +use geo_traits::to_geo::ToGeoGeometry; +use wkb::reader; +use wkb::writer; + +impl Type for MultiPoint { + fn type_info() -> MySqlTypeInfo { + MySqlTypeInfo::binary(ColumnType::Geometry) + } + + fn compatible(ty: &MySqlTypeInfo) -> bool { + matches!( + ty.r#type, + ColumnType::Geometry + | ColumnType::Blob + | ColumnType::MediumBlob + | ColumnType::LongBlob + | ColumnType::TinyBlob + | ColumnType::VarString if ty.flags.contains(ColumnFlags::BINARY) + ) + } +} + +impl Encode<'_, MySql> for MultiPoint { + fn encode_by_ref(&self, buf: &mut Vec) -> Result { + let mut wkb_buffer = Vec::new(); + wkb_buffer.extend_from_slice(&0u32.to_le_bytes()); // SRID = 0, Little Endian + writer::write_multi_point(&mut wkb_buffer, self, &writer::WriteOptions::default())?; + buf.put_bytes_lenenc(&wkb_buffer); + Ok(IsNull::No) + } +} + +impl<'r> Decode<'r, MySql> for MultiPoint { + fn decode(value: MySqlValueRef<'r>) -> Result { + let bytes = value.as_bytes()?; + if bytes.len() < 4 { + return Err(format!( + "Invalid GEOMETRY data for MultiPoint: received {} bytes, expected at least 4 for SRID prefix.", + bytes.len() + ) + .into()); + } + let wkb_data = &bytes[4..]; // Skip 4-byte SRID + + let wkb_reader_geom = reader::Wkb::try_new(wkb_data) + .map_err(|e| BoxDynError::from(format!("WKB parsing error for MultiPoint: {}", e)))?; + + let geo_geom: geo_types::Geometry = wkb_reader_geom + .to_geometry(); + + MultiPoint::try_from(geo_geom).map_err(|e| { + BoxDynError::from(format!( + "Failed to convert geo_types::Geometry to MultiPoint: {:?}", + e + )) + }) + } +} + +#[cfg(test)] +mod tests { + use sqlx::mysql::{MySqlPool, MySqlRow}; + use sqlx::{Executor, Row}; + use geo_types::MultiPoint as TestableGeoType; + use geo_types::Point; + + #[sqlx::test] + async fn test_encode_decode_multipoint(pool: MySqlPool) -> anyhow::Result<()> { + let table_name = format!("test_geo_multipoint_table"); + pool.execute(format!("DROP TABLE IF EXISTS {}", table_name).as_str()).await?; + pool.execute( + format!( + "CREATE TABLE {} (id INT, geom GEOMETRY, geom_null GEOMETRY NULL)", + table_name + ) + .as_str(), + ) + .await?; + + let mp1 = TestableGeoType::new(vec![Point::new(0.,0.), Point::new(1.,1.)]); + let mp2 = TestableGeoType::new(vec![Point::new(10.,10.), Point::new(20.,20.)]); + + // Test non-nullable + sqlx::query(&format!("INSERT INTO {} (id, geom) VALUES (1, ?)", table_name)) + .bind(mp1.clone()) + .execute(&pool) + .await?; + + let row: MySqlRow = sqlx::query(&format!("SELECT geom FROM {} WHERE id = 1", table_name)) + .fetch_one(&pool) + .await?; + let decoded_val: TestableGeoType = row.try_get("geom")?; + assert_eq!(decoded_val, mp1); + + // Test nullable Some(value) + let some_val: Option = Some(mp2.clone()); + sqlx::query(&format!("INSERT INTO {} (id, geom_null) VALUES (2, ?)", table_name)) + .bind(some_val.clone()) + .execute(&pool) + .await?; + + let row_some: MySqlRow = sqlx::query(&format!("SELECT geom_null FROM {} WHERE id = 2", table_name)) + .fetch_one(&pool) + .await?; + let decoded_some: Option = row_some.try_get("geom_null")?; + assert_eq!(decoded_some, some_val); + + // Test nullable None + let none_val: Option = None; + sqlx::query(&format!("INSERT INTO {} (id, geom_null) VALUES (3, ?)", table_name)) + .bind(none_val.clone()) + .execute(&pool) + .await?; + + let row_none: MySqlRow = sqlx::query(&format!("SELECT geom_null FROM {} WHERE id = 3", table_name)) + .fetch_one(&pool) + .await?; + let decoded_none: Option = row_none.try_get("geom_null")?; + assert_eq!(decoded_none, none_val); + + pool.execute(format!("DROP TABLE IF EXISTS {}", table_name).as_str()).await?; + Ok(()) + } +} diff --git a/sqlx-mysql/src/types/geo/multi_polygon.rs b/sqlx-mysql/src/types/geo/multi_polygon.rs new file mode 100644 index 0000000000..e5a1fd2456 --- /dev/null +++ b/sqlx-mysql/src/types/geo/multi_polygon.rs @@ -0,0 +1,144 @@ +use sqlx_core::decode::Decode; +use sqlx_core::encode::{Encode, IsNull}; +use sqlx_core::error::BoxDynError; +use sqlx_core::types::Type; + +use crate::io::MySqlBufMutExt; +use crate::{MySql, MySqlTypeInfo, MySqlValueRef}; +use crate::protocol::text::{ColumnFlags, ColumnType}; + +use std::convert::TryFrom; + +use geo_types::MultiPolygon; +use geo_traits::to_geo::ToGeoGeometry; +use wkb::reader; +use wkb::writer; + +impl Type for MultiPolygon { + fn type_info() -> MySqlTypeInfo { + MySqlTypeInfo::binary(ColumnType::Geometry) + } + + fn compatible(ty: &MySqlTypeInfo) -> bool { + matches!( + ty.r#type, + ColumnType::Geometry + | ColumnType::Blob + | ColumnType::MediumBlob + | ColumnType::LongBlob + | ColumnType::TinyBlob + | ColumnType::VarString if ty.flags.contains(ColumnFlags::BINARY) + ) + } +} + +impl Encode<'_, MySql> for MultiPolygon { + fn encode_by_ref(&self, buf: &mut Vec) -> Result { + let mut wkb_buffer = Vec::new(); + wkb_buffer.extend_from_slice(&0u32.to_le_bytes()); // SRID = 0, Little Endian + writer::write_multi_polygon(&mut wkb_buffer, self, &writer::WriteOptions::default())?; + buf.put_bytes_lenenc(&wkb_buffer); + Ok(IsNull::No) + } +} + +impl<'r> Decode<'r, MySql> for MultiPolygon { + fn decode(value: MySqlValueRef<'r>) -> Result { + let bytes = value.as_bytes()?; + if bytes.len() < 4 { + return Err(format!( + "Invalid GEOMETRY data for MultiPolygon: received {} bytes, expected at least 4 for SRID prefix.", + bytes.len() + ) + .into()); + } + let wkb_data = &bytes[4..]; // Skip 4-byte SRID + + let wkb_reader_geom = reader::Wkb::try_new(wkb_data) + .map_err(|e| BoxDynError::from(format!("WKB parsing error for MultiPolygon: {}", e)))?; + + let geo_geom: geo_types::Geometry = wkb_reader_geom + .to_geometry(); + + MultiPolygon::try_from(geo_geom).map_err(|e| { + BoxDynError::from(format!( + "Failed to convert geo_types::Geometry to MultiPolygon: {:?}", + e + )) + }) + } +} + +#[cfg(test)] +mod tests { + use sqlx::mysql::{MySqlPool, MySqlRow}; + use sqlx::{Executor, Row}; + use geo_types::MultiPolygon as TestableGeoType; + use geo_types::LineString; + use geo_types::Polygon; + use geo_types::coord; + + #[sqlx::test] + async fn test_encode_decode_multipolygon(pool: MySqlPool) -> anyhow::Result<()> { + let table_name = format!("test_geo_multipolygon_table"); + pool.execute(format!("DROP TABLE IF EXISTS {}", table_name).as_str()).await?; + pool.execute( + format!( + "CREATE TABLE {} (id INT, geom GEOMETRY, geom_null GEOMETRY NULL)", + table_name + ) + .as_str(), + ) + .await?; + + let ext1 = LineString::new(vec![coord!{x:0.,y:0.}, coord!{x:1.,y:1.}, coord!{x:1.,y:0.}, coord!{x:0.,y:0.}]); + let poly1 = Polygon::new(ext1, vec![]); + let ext2 = LineString::new(vec![coord!{x:10.,y:10.}, coord!{x:11.,y:11.}, coord!{x:11.,y:10.}, coord!{x:10.,y:10.}]); + let poly2 = Polygon::new(ext2, vec![]); + let mpoly1 = TestableGeoType::new(vec![poly1, poly2]); + let ext3 = LineString::new(vec![coord!{x:20.,y:20.}, coord!{x:21.,y:21.}, coord!{x:21.,y:20.}, coord!{x:20.,y:20.}]); + let poly3 = Polygon::new(ext3, vec![]); + let mpoly2 = TestableGeoType::new(vec![poly3]); + + // Test non-nullable + sqlx::query(&format!("INSERT INTO {} (id, geom) VALUES (1, ?)", table_name)) + .bind(mpoly1.clone()) + .execute(&pool) + .await?; + + let row: MySqlRow = sqlx::query(&format!("SELECT geom FROM {} WHERE id = 1", table_name)) + .fetch_one(&pool) + .await?; + let decoded_val: TestableGeoType = row.try_get("geom")?; + assert_eq!(decoded_val, mpoly1); + + // Test nullable Some(value) + let some_val: Option = Some(mpoly2.clone()); + sqlx::query(&format!("INSERT INTO {} (id, geom_null) VALUES (2, ?)", table_name)) + .bind(some_val.clone()) + .execute(&pool) + .await?; + + let row_some: MySqlRow = sqlx::query(&format!("SELECT geom_null FROM {} WHERE id = 2", table_name)) + .fetch_one(&pool) + .await?; + let decoded_some: Option = row_some.try_get("geom_null")?; + assert_eq!(decoded_some, some_val); + + // Test nullable None + let none_val: Option = None; + sqlx::query(&format!("INSERT INTO {} (id, geom_null) VALUES (3, ?)", table_name)) + .bind(none_val.clone()) + .execute(&pool) + .await?; + + let row_none: MySqlRow = sqlx::query(&format!("SELECT geom_null FROM {} WHERE id = 3", table_name)) + .fetch_one(&pool) + .await?; + let decoded_none: Option = row_none.try_get("geom_null")?; + assert_eq!(decoded_none, none_val); + + pool.execute(format!("DROP TABLE IF EXISTS {}", table_name).as_str()).await?; + Ok(()) + } +} diff --git a/sqlx-mysql/src/types/geo/point.rs b/sqlx-mysql/src/types/geo/point.rs new file mode 100644 index 0000000000..030fb7de55 --- /dev/null +++ b/sqlx-mysql/src/types/geo/point.rs @@ -0,0 +1,135 @@ +use sqlx_core::decode::Decode; +use sqlx_core::encode::{Encode, IsNull}; +use sqlx_core::error::BoxDynError; +use sqlx_core::types::Type; + +use crate::io::MySqlBufMutExt; +use crate::{MySql, MySqlTypeInfo, MySqlValueRef}; +use crate::protocol::text::{ColumnFlags, ColumnType}; + +use std::convert::TryFrom; + +use geo_types::Point; +use geo_traits::to_geo::ToGeoGeometry; +use wkb::reader; +use wkb::writer; + +impl Type for Point { + fn type_info() -> MySqlTypeInfo { + MySqlTypeInfo::binary(ColumnType::Geometry) + } + + fn compatible(ty: &MySqlTypeInfo) -> bool { + matches!( + ty.r#type, + ColumnType::Geometry + | ColumnType::Blob + | ColumnType::MediumBlob + | ColumnType::LongBlob + | ColumnType::TinyBlob + | ColumnType::VarString if ty.flags.contains(ColumnFlags::BINARY) + ) + } +} + +impl Encode<'_, MySql> for Point { + fn encode_by_ref(&self, buf: &mut Vec) -> Result { + let mut wkb_buffer = Vec::new(); + wkb_buffer.extend_from_slice(&0u32.to_le_bytes()); // SRID = 0, Little Endian + writer::write_point(&mut wkb_buffer, self, &writer::WriteOptions::default())?; + buf.put_bytes_lenenc(&wkb_buffer); + Ok(IsNull::No) + } +} + +impl<'r> Decode<'r, MySql> for Point { + fn decode(value: MySqlValueRef<'r>) -> Result { + let bytes = value.as_bytes()?; + if bytes.len() < 4 { + return Err(format!( + "Invalid GEOMETRY data for Point: received {} bytes, expected at least 4 for SRID prefix.", + bytes.len() + ) + .into()); + } + let wkb_data = &bytes[4..]; // Skip 4-byte SRID + + let wkb_reader_geom = reader::Wkb::try_new(wkb_data) + .map_err(|e| BoxDynError::from(format!("WKB parsing error for Point: {}", e)))?; + + let geo_geom: geo_types::Geometry = wkb_reader_geom + .to_geometry(); + + Point::try_from(geo_geom).map_err(|e| { + BoxDynError::from(format!( + "Failed to convert geo_types::Geometry to Point: {:?}", + e + )) + }) + } +} + +#[cfg(test)] +mod tests { + use sqlx::mysql::{MySqlPool, MySqlRow}; + use sqlx::{Executor, Row}; + use geo_types::Point as TestableGeoType; + + #[sqlx::test] + async fn test_encode_decode_point(pool: MySqlPool) -> anyhow::Result<()> { + let table_name = format!("test_geo_point_table"); + pool.execute(format!("DROP TABLE IF EXISTS {}", table_name).as_str()).await?; + pool.execute( + format!( + "CREATE TABLE {} (id INT, geom GEOMETRY, geom_null GEOMETRY NULL)", + table_name + ) + .as_str(), + ) + .await?; + + let p1 = TestableGeoType::new(10.0, 20.5); + let p2 = TestableGeoType::new(30.0, 40.0); + + // Test non-nullable + sqlx::query(&format!("INSERT INTO {} (id, geom) VALUES (1, ?)", table_name)) + .bind(p1.clone()) + .execute(&pool) + .await?; + + let row: MySqlRow = sqlx::query(&format!("SELECT geom FROM {} WHERE id = 1", table_name)) + .fetch_one(&pool) + .await?; + let decoded_val: TestableGeoType = row.try_get("geom")?; + assert_eq!(decoded_val, p1); + + // Test nullable Some(value) + let some_val: Option = Some(p2.clone()); + sqlx::query(&format!("INSERT INTO {} (id, geom_null) VALUES (2, ?)", table_name)) + .bind(some_val.clone()) + .execute(&pool) + .await?; + + let row_some: MySqlRow = sqlx::query(&format!("SELECT geom_null FROM {} WHERE id = 2", table_name)) + .fetch_one(&pool) + .await?; + let decoded_some: Option = row_some.try_get("geom_null")?; + assert_eq!(decoded_some, some_val); + + // Test nullable None + let none_val: Option = None; + sqlx::query(&format!("INSERT INTO {} (id, geom_null) VALUES (3, ?)", table_name)) + .bind(none_val.clone()) + .execute(&pool) + .await?; + + let row_none: MySqlRow = sqlx::query(&format!("SELECT geom_null FROM {} WHERE id = 3", table_name)) + .fetch_one(&pool) + .await?; + let decoded_none: Option = row_none.try_get("geom_null")?; + assert_eq!(decoded_none, none_val); + + pool.execute(format!("DROP TABLE IF EXISTS {}", table_name).as_str()).await?; + Ok(()) + } +} diff --git a/sqlx-mysql/src/types/geo/polygon.rs b/sqlx-mysql/src/types/geo/polygon.rs new file mode 100644 index 0000000000..3a263b5494 --- /dev/null +++ b/sqlx-mysql/src/types/geo/polygon.rs @@ -0,0 +1,139 @@ +use sqlx_core::decode::Decode; +use sqlx_core::encode::{Encode, IsNull}; +use sqlx_core::error::BoxDynError; +use sqlx_core::types::Type; + +use crate::io::MySqlBufMutExt; +use crate::{MySql, MySqlTypeInfo, MySqlValueRef}; +use crate::protocol::text::{ColumnFlags, ColumnType}; + +use std::convert::TryFrom; + +use geo_types::Polygon; +use geo_traits::to_geo::ToGeoGeometry; +use wkb::reader; +use wkb::writer; + +impl Type for Polygon { + fn type_info() -> MySqlTypeInfo { + MySqlTypeInfo::binary(ColumnType::Geometry) + } + + fn compatible(ty: &MySqlTypeInfo) -> bool { + matches!( + ty.r#type, + ColumnType::Geometry + | ColumnType::Blob + | ColumnType::MediumBlob + | ColumnType::LongBlob + | ColumnType::TinyBlob + | ColumnType::VarString if ty.flags.contains(ColumnFlags::BINARY) + ) + } +} + +impl Encode<'_, MySql> for Polygon { + fn encode_by_ref(&self, buf: &mut Vec) -> Result { + let mut wkb_buffer = Vec::new(); + wkb_buffer.extend_from_slice(&0u32.to_le_bytes()); // SRID = 0, Little Endian + writer::write_polygon(&mut wkb_buffer, self, &writer::WriteOptions::default())?; + buf.put_bytes_lenenc(&wkb_buffer); + Ok(IsNull::No) + } +} + +impl<'r> Decode<'r, MySql> for Polygon { + fn decode(value: MySqlValueRef<'r>) -> Result { + let bytes = value.as_bytes()?; + if bytes.len() < 4 { + return Err(format!( + "Invalid GEOMETRY data for Polygon: received {} bytes, expected at least 4 for SRID prefix.", + bytes.len() + ) + .into()); + } + let wkb_data = &bytes[4..]; // Skip 4-byte SRID + + let wkb_reader_geom = reader::Wkb::try_new(wkb_data) + .map_err(|e| BoxDynError::from(format!("WKB parsing error for Polygon: {}", e)))?; + + let geo_geom: geo_types::Geometry = wkb_reader_geom + .to_geometry(); + + Polygon::try_from(geo_geom).map_err(|e| { + BoxDynError::from(format!( + "Failed to convert geo_types::Geometry to Polygon: {:?}", + e + )) + }) + } +} + +#[cfg(test)] +mod tests { + use sqlx::mysql::{MySqlPool, MySqlRow}; + use sqlx::{Executor, Row}; + use geo_types::Polygon as TestableGeoType; + use geo_types::LineString; + use geo_types::coord; + + #[sqlx::test] + async fn test_encode_decode_polygon(pool: MySqlPool) -> anyhow::Result<()> { + let table_name = format!("test_geo_polygon_table"); + pool.execute(format!("DROP TABLE IF EXISTS {}", table_name).as_str()).await?; + pool.execute( + format!( + "CREATE TABLE {} (id INT, geom GEOMETRY, geom_null GEOMETRY NULL)", + table_name + ) + .as_str(), + ) + .await?; + + let exterior1 = LineString::new(vec![coord!{x: 0., y: 0.}, coord!{x: 1., y: 1.}, coord!{x: 1., y: 0.}, coord!{x: 0., y: 0.}]); + let poly1 = TestableGeoType::new(exterior1, vec![]); + let exterior2 = LineString::new(vec![coord!{x: 10., y: 10.}, coord!{x: 20., y: 20.}, coord!{x: 20., y: 10.}, coord!{x: 10., y: 10.}]); + let poly2 = TestableGeoType::new(exterior2, vec![]); + + // Test non-nullable + sqlx::query(&format!("INSERT INTO {} (id, geom) VALUES (1, ?)", table_name)) + .bind(poly1.clone()) + .execute(&pool) + .await?; + + let row: MySqlRow = sqlx::query(&format!("SELECT geom FROM {} WHERE id = 1", table_name)) + .fetch_one(&pool) + .await?; + let decoded_val: TestableGeoType = row.try_get("geom")?; + assert_eq!(decoded_val, poly1); + + // Test nullable Some(value) + let some_val: Option = Some(poly2.clone()); + sqlx::query(&format!("INSERT INTO {} (id, geom_null) VALUES (2, ?)", table_name)) + .bind(some_val.clone()) + .execute(&pool) + .await?; + + let row_some: MySqlRow = sqlx::query(&format!("SELECT geom_null FROM {} WHERE id = 2", table_name)) + .fetch_one(&pool) + .await?; + let decoded_some: Option = row_some.try_get("geom_null")?; + assert_eq!(decoded_some, some_val); + + // Test nullable None + let none_val: Option = None; + sqlx::query(&format!("INSERT INTO {} (id, geom_null) VALUES (3, ?)", table_name)) + .bind(none_val.clone()) + .execute(&pool) + .await?; + + let row_none: MySqlRow = sqlx::query(&format!("SELECT geom_null FROM {} WHERE id = 3", table_name)) + .fetch_one(&pool) + .await?; + let decoded_none: Option = row_none.try_get("geom_null")?; + assert_eq!(decoded_none, none_val); + + pool.execute(format!("DROP TABLE IF EXISTS {}", table_name).as_str()).await?; + Ok(()) + } +} diff --git a/sqlx-mysql/src/types/mod.rs b/sqlx-mysql/src/types/mod.rs index dc5105fa2f..8d29480560 100644 --- a/sqlx-mysql/src/types/mod.rs +++ b/sqlx-mysql/src/types/mod.rs @@ -189,3 +189,6 @@ mod time; #[cfg(feature = "uuid")] mod uuid; + +#[cfg(feature = "spatial-data-types")] +mod geo; From 9ea105434c0914293862ede441786f49bbeb00b0 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 26 Jun 2025 02:56:51 +0000 Subject: [PATCH 2/3] format code --- sqlx-mysql/src/types/geo/geometry.rs | 119 +++++++++++------- .../src/types/geo/geometry_collection.rs | 79 +++++++----- sqlx-mysql/src/types/geo/linestring.rs | 76 ++++++----- sqlx-mysql/src/types/geo/mod.rs | 10 +- sqlx-mysql/src/types/geo/multi_line_string.rs | 81 +++++++----- sqlx-mysql/src/types/geo/multi_point.rs | 72 ++++++----- sqlx-mysql/src/types/geo/multi_polygon.rs | 93 +++++++++----- sqlx-mysql/src/types/geo/point.rs | 66 ++++++---- sqlx-mysql/src/types/geo/polygon.rs | 84 ++++++++----- 9 files changed, 427 insertions(+), 253 deletions(-) diff --git a/sqlx-mysql/src/types/geo/geometry.rs b/sqlx-mysql/src/types/geo/geometry.rs index e87cc4230a..1027971ff6 100644 --- a/sqlx-mysql/src/types/geo/geometry.rs +++ b/sqlx-mysql/src/types/geo/geometry.rs @@ -4,12 +4,11 @@ use sqlx_core::error::BoxDynError; use sqlx_core::types::Type; use crate::io::MySqlBufMutExt; -use crate::{MySql, MySqlTypeInfo, MySqlValueRef}; use crate::protocol::text::{ColumnFlags, ColumnType}; +use crate::{MySql, MySqlTypeInfo, MySqlValueRef}; - -use geo_types::Geometry; use geo_traits::to_geo::ToGeoGeometry; +use geo_types::Geometry; use wkb::reader; use wkb::writer; @@ -56,21 +55,21 @@ impl<'r> Decode<'r, MySql> for Geometry { let wkb_reader_geom = reader::Wkb::try_new(wkb_data) .map_err(|e| BoxDynError::from(format!("WKB parsing error for Geometry: {}", e)))?; - Ok(wkb_reader_geom - .to_geometry()) + Ok(wkb_reader_geom.to_geometry()) } } #[cfg(test)] mod tests { + use geo_types::{coord, Geometry as TestableGeoType, LineString, Point, Polygon}; use sqlx::mysql::{MySqlPool, MySqlRow}; use sqlx::{Executor, Row}; - use geo_types::{Geometry as TestableGeoType, Point, LineString, Polygon, coord}; #[sqlx::test] async fn test_encode_decode_geometry_enum(pool: MySqlPool) -> anyhow::Result<()> { let table_name = format!("test_geo_geometry_enum_table"); - pool.execute(format!("DROP TABLE IF EXISTS {}", table_name).as_str()).await?; + pool.execute(format!("DROP TABLE IF EXISTS {}", table_name).as_str()) + .await?; pool.execute( format!( "CREATE TABLE {} (id INT, geom_val GEOMETRY, geom_null GEOMETRY NULL)", @@ -83,71 +82,101 @@ mod tests { let p1_geom = Point::new(1.0, 2.0); let geom_enum1 = TestableGeoType::Point(p1_geom); - let ls1_geom = LineString::new(vec![coord!{x: 3., y: 4.}, coord!{x: 5., y: 6.}]); + let ls1_geom = LineString::new(vec![coord! {x: 3., y: 4.}, coord! {x: 5., y: 6.}]); let geom_enum2 = TestableGeoType::LineString(ls1_geom); - let ext_poly_geom = LineString::new(vec![coord!{x:0.,y:0.}, coord!{x:1.,y:1.}, coord!{x:1.,y:0.}, coord!{x:0.,y:0.}]); + let ext_poly_geom = LineString::new(vec![ + coord! {x:0.,y:0.}, + coord! {x:1.,y:1.}, + coord! {x:1.,y:0.}, + coord! {x:0.,y:0.}, + ]); let poly1_geom = Polygon::new(ext_poly_geom, vec![]); let geom_enum3 = TestableGeoType::Polygon(poly1_geom); // Test non-nullable Point variant - sqlx::query(&format!("INSERT INTO {} (id, geom_val) VALUES (1, ?)", table_name)) - .bind(geom_enum1.clone()) - .execute(&pool) - .await?; - let row1: MySqlRow = sqlx::query(&format!("SELECT geom_val FROM {} WHERE id = 1", table_name)) - .fetch_one(&pool) - .await?; + sqlx::query(&format!( + "INSERT INTO {} (id, geom_val) VALUES (1, ?)", + table_name + )) + .bind(geom_enum1.clone()) + .execute(&pool) + .await?; + let row1: MySqlRow = + sqlx::query(&format!("SELECT geom_val FROM {} WHERE id = 1", table_name)) + .fetch_one(&pool) + .await?; let decoded_val1: TestableGeoType = row1.try_get("geom_val")?; assert_eq!(decoded_val1, geom_enum1); // Test non-nullable LineString variant - sqlx::query(&format!("INSERT INTO {} (id, geom_val) VALUES (2, ?)", table_name)) - .bind(geom_enum2.clone()) - .execute(&pool) - .await?; - let row2: MySqlRow = sqlx::query(&format!("SELECT geom_val FROM {} WHERE id = 2", table_name)) - .fetch_one(&pool) - .await?; + sqlx::query(&format!( + "INSERT INTO {} (id, geom_val) VALUES (2, ?)", + table_name + )) + .bind(geom_enum2.clone()) + .execute(&pool) + .await?; + let row2: MySqlRow = + sqlx::query(&format!("SELECT geom_val FROM {} WHERE id = 2", table_name)) + .fetch_one(&pool) + .await?; let decoded_val2: TestableGeoType = row2.try_get("geom_val")?; assert_eq!(decoded_val2, geom_enum2); // Test non-nullable Polygon variant - sqlx::query(&format!("INSERT INTO {} (id, geom_val) VALUES (3, ?)", table_name)) - .bind(geom_enum3.clone()) - .execute(&pool) - .await?; - let row3: MySqlRow = sqlx::query(&format!("SELECT geom_val FROM {} WHERE id = 3", table_name)) - .fetch_one(&pool) - .await?; + sqlx::query(&format!( + "INSERT INTO {} (id, geom_val) VALUES (3, ?)", + table_name + )) + .bind(geom_enum3.clone()) + .execute(&pool) + .await?; + let row3: MySqlRow = + sqlx::query(&format!("SELECT geom_val FROM {} WHERE id = 3", table_name)) + .fetch_one(&pool) + .await?; let decoded_val3: TestableGeoType = row3.try_get("geom_val")?; assert_eq!(decoded_val3, geom_enum3); // Test nullable Some(value) - using Point variant let some_val: Option = Some(geom_enum1.clone()); - sqlx::query(&format!("INSERT INTO {} (id, geom_null) VALUES (4, ?)", table_name)) - .bind(some_val.clone()) - .execute(&pool) - .await?; - let row_some: MySqlRow = sqlx::query(&format!("SELECT geom_null FROM {} WHERE id = 4", table_name)) - .fetch_one(&pool) - .await?; + sqlx::query(&format!( + "INSERT INTO {} (id, geom_null) VALUES (4, ?)", + table_name + )) + .bind(some_val.clone()) + .execute(&pool) + .await?; + let row_some: MySqlRow = sqlx::query(&format!( + "SELECT geom_null FROM {} WHERE id = 4", + table_name + )) + .fetch_one(&pool) + .await?; let decoded_some: Option = row_some.try_get("geom_null")?; assert_eq!(decoded_some, some_val); // Test nullable None let none_val: Option = None; - sqlx::query(&format!("INSERT INTO {} (id, geom_null) VALUES (5, ?)", table_name)) - .bind(none_val.clone()) - .execute(&pool) - .await?; - let row_none: MySqlRow = sqlx::query(&format!("SELECT geom_null FROM {} WHERE id = 5", table_name)) - .fetch_one(&pool) - .await?; + sqlx::query(&format!( + "INSERT INTO {} (id, geom_null) VALUES (5, ?)", + table_name + )) + .bind(none_val.clone()) + .execute(&pool) + .await?; + let row_none: MySqlRow = sqlx::query(&format!( + "SELECT geom_null FROM {} WHERE id = 5", + table_name + )) + .fetch_one(&pool) + .await?; let decoded_none: Option = row_none.try_get("geom_null")?; assert_eq!(decoded_none, none_val); - pool.execute(format!("DROP TABLE IF EXISTS {}", table_name).as_str()).await?; + pool.execute(format!("DROP TABLE IF EXISTS {}", table_name).as_str()) + .await?; Ok(()) } } diff --git a/sqlx-mysql/src/types/geo/geometry_collection.rs b/sqlx-mysql/src/types/geo/geometry_collection.rs index a53a6f94a8..e0f50c7697 100644 --- a/sqlx-mysql/src/types/geo/geometry_collection.rs +++ b/sqlx-mysql/src/types/geo/geometry_collection.rs @@ -4,13 +4,13 @@ use sqlx_core::error::BoxDynError; use sqlx_core::types::Type; use crate::io::MySqlBufMutExt; -use crate::{MySql, MySqlTypeInfo, MySqlValueRef}; use crate::protocol::text::{ColumnFlags, ColumnType}; +use crate::{MySql, MySqlTypeInfo, MySqlValueRef}; use std::convert::TryFrom; -use geo_types::GeometryCollection; use geo_traits::to_geo::ToGeoGeometry; +use geo_types::GeometryCollection; use wkb::reader; use wkb::writer; @@ -54,11 +54,11 @@ impl<'r> Decode<'r, MySql> for GeometryCollection { } let wkb_data = &bytes[4..]; // Skip 4-byte SRID - let wkb_reader_geom = reader::Wkb::try_new(wkb_data) - .map_err(|e| BoxDynError::from(format!("WKB parsing error for GeometryCollection: {}", e)))?; + let wkb_reader_geom = reader::Wkb::try_new(wkb_data).map_err(|e| { + BoxDynError::from(format!("WKB parsing error for GeometryCollection: {}", e)) + })?; - let geo_geom: geo_types::Geometry = wkb_reader_geom - .to_geometry(); + let geo_geom: geo_types::Geometry = wkb_reader_geom.to_geometry(); GeometryCollection::try_from(geo_geom).map_err(|e| { BoxDynError::from(format!( @@ -71,14 +71,15 @@ impl<'r> Decode<'r, MySql> for GeometryCollection { #[cfg(test)] mod tests { + use geo_types::{coord, Geometry, GeometryCollection as TestableGeoType, LineString, Point}; use sqlx::mysql::{MySqlPool, MySqlRow}; use sqlx::{Executor, Row}; - use geo_types::{GeometryCollection as TestableGeoType, Point, LineString, Geometry, coord}; #[sqlx::test] async fn test_encode_decode_geometry_collection(pool: MySqlPool) -> anyhow::Result<()> { let table_name = format!("test_geo_geometry_collection_table"); - pool.execute(format!("DROP TABLE IF EXISTS {}", table_name).as_str()).await?; + pool.execute(format!("DROP TABLE IF EXISTS {}", table_name).as_str()) + .await?; pool.execute( format!( "CREATE TABLE {} (id INT, geom GEOMETRY, geom_null GEOMETRY NULL)", @@ -89,22 +90,27 @@ mod tests { .await?; let p1 = Point::new(0., 0.); - let ls1 = LineString::new(vec![coord!{x: 1., y: 1.}, coord!{x: 2., y: 2.}]); - let gc1 = TestableGeoType::new_from(vec![ // Changed to new_from + let ls1 = LineString::new(vec![coord! {x: 1., y: 1.}, coord! {x: 2., y: 2.}]); + let gc1 = TestableGeoType::new_from(vec![ + // Changed to new_from Geometry::Point(p1.clone()), Geometry::LineString(ls1.clone()), ]); let p2 = Point::new(10., 10.); - let gc2 = TestableGeoType::new_from(vec![ // Changed to new_from + let gc2 = TestableGeoType::new_from(vec![ + // Changed to new_from Geometry::Point(p2.clone()), ]); // Test non-nullable - sqlx::query(&format!("INSERT INTO {} (id, geom) VALUES (1, ?)", table_name)) - .bind(gc1.clone()) - .execute(&pool) - .await?; + sqlx::query(&format!( + "INSERT INTO {} (id, geom) VALUES (1, ?)", + table_name + )) + .bind(gc1.clone()) + .execute(&pool) + .await?; let row: MySqlRow = sqlx::query(&format!("SELECT geom FROM {} WHERE id = 1", table_name)) .fetch_one(&pool) @@ -115,31 +121,44 @@ mod tests { // Test nullable Some(value) let some_val: Option = Some(gc2.clone()); - sqlx::query(&format!("INSERT INTO {} (id, geom_null) VALUES (2, ?)", table_name)) - .bind(some_val.clone()) - .execute(&pool) - .await?; + sqlx::query(&format!( + "INSERT INTO {} (id, geom_null) VALUES (2, ?)", + table_name + )) + .bind(some_val.clone()) + .execute(&pool) + .await?; - let row_some: MySqlRow = sqlx::query(&format!("SELECT geom_null FROM {} WHERE id = 2", table_name)) - .fetch_one(&pool) - .await?; + let row_some: MySqlRow = sqlx::query(&format!( + "SELECT geom_null FROM {} WHERE id = 2", + table_name + )) + .fetch_one(&pool) + .await?; let decoded_some: Option = row_some.try_get("geom_null")?; assert_eq!(decoded_some, some_val); // Test nullable None let none_val: Option = None; - sqlx::query(&format!("INSERT INTO {} (id, geom_null) VALUES (3, ?)", table_name)) - .bind(none_val.clone()) - .execute(&pool) - .await?; + sqlx::query(&format!( + "INSERT INTO {} (id, geom_null) VALUES (3, ?)", + table_name + )) + .bind(none_val.clone()) + .execute(&pool) + .await?; - let row_none: MySqlRow = sqlx::query(&format!("SELECT geom_null FROM {} WHERE id = 3", table_name)) - .fetch_one(&pool) - .await?; + let row_none: MySqlRow = sqlx::query(&format!( + "SELECT geom_null FROM {} WHERE id = 3", + table_name + )) + .fetch_one(&pool) + .await?; let decoded_none: Option = row_none.try_get("geom_null")?; assert_eq!(decoded_none, none_val); - pool.execute(format!("DROP TABLE IF EXISTS {}", table_name).as_str()).await?; + pool.execute(format!("DROP TABLE IF EXISTS {}", table_name).as_str()) + .await?; Ok(()) } } diff --git a/sqlx-mysql/src/types/geo/linestring.rs b/sqlx-mysql/src/types/geo/linestring.rs index 92fdbcce3d..fb8a679e80 100644 --- a/sqlx-mysql/src/types/geo/linestring.rs +++ b/sqlx-mysql/src/types/geo/linestring.rs @@ -4,13 +4,13 @@ use sqlx_core::error::BoxDynError; use sqlx_core::types::Type; use crate::io::MySqlBufMutExt; -use crate::{MySql, MySqlTypeInfo, MySqlValueRef}; use crate::protocol::text::{ColumnFlags, ColumnType}; +use crate::{MySql, MySqlTypeInfo, MySqlValueRef}; use std::convert::TryFrom; -use geo_types::LineString; use geo_traits::to_geo::ToGeoGeometry; +use geo_types::LineString; use wkb::reader; use wkb::writer; @@ -57,8 +57,7 @@ impl<'r> Decode<'r, MySql> for LineString { let wkb_reader_geom = reader::Wkb::try_new(wkb_data) .map_err(|e| BoxDynError::from(format!("WKB parsing error for LineString: {}", e)))?; - let geo_geom: geo_types::Geometry = wkb_reader_geom - .to_geometry(); + let geo_geom: geo_types::Geometry = wkb_reader_geom.to_geometry(); LineString::try_from(geo_geom).map_err(|e| { BoxDynError::from(format!( @@ -71,15 +70,16 @@ impl<'r> Decode<'r, MySql> for LineString { #[cfg(test)] mod tests { - use sqlx::mysql::{MySqlPool, MySqlRow}; - use sqlx::{Executor, Row}; + use geo_types::coord; use geo_types::LineString as TestableGeoType; - use geo_types::coord; // For CONSTRUCTION_CODE + use sqlx::mysql::{MySqlPool, MySqlRow}; + use sqlx::{Executor, Row}; // For CONSTRUCTION_CODE #[sqlx::test] async fn test_encode_decode_linestring(pool: MySqlPool) -> anyhow::Result<()> { let table_name = format!("test_geo_linestring_table"); - pool.execute(format!("DROP TABLE IF EXISTS {}", table_name).as_str()).await?; + pool.execute(format!("DROP TABLE IF EXISTS {}", table_name).as_str()) + .await?; pool.execute( format!( "CREATE TABLE {} (id INT, geom GEOMETRY, geom_null GEOMETRY NULL)", @@ -89,14 +89,21 @@ mod tests { ) .await?; - let ls1 = TestableGeoType::new(vec![coord! { x: 0., y: 0. }, coord! { x: 1., y: 1. }, coord! { x: 2., y: 0. }]); + let ls1 = TestableGeoType::new(vec![ + coord! { x: 0., y: 0. }, + coord! { x: 1., y: 1. }, + coord! { x: 2., y: 0. }, + ]); let ls2 = TestableGeoType::new(vec![coord! { x: 10., y: 10. }, coord! { x: 20., y: 20. }]); // Test non-nullable - sqlx::query(&format!("INSERT INTO {} (id, geom) VALUES (1, ?)", table_name)) - .bind(ls1.clone()) - .execute(&pool) - .await?; + sqlx::query(&format!( + "INSERT INTO {} (id, geom) VALUES (1, ?)", + table_name + )) + .bind(ls1.clone()) + .execute(&pool) + .await?; let row: MySqlRow = sqlx::query(&format!("SELECT geom FROM {} WHERE id = 1", table_name)) .fetch_one(&pool) @@ -106,31 +113,44 @@ mod tests { // Test nullable Some(value) let some_val: Option = Some(ls2.clone()); - sqlx::query(&format!("INSERT INTO {} (id, geom_null) VALUES (2, ?)", table_name)) - .bind(some_val.clone()) - .execute(&pool) - .await?; + sqlx::query(&format!( + "INSERT INTO {} (id, geom_null) VALUES (2, ?)", + table_name + )) + .bind(some_val.clone()) + .execute(&pool) + .await?; - let row_some: MySqlRow = sqlx::query(&format!("SELECT geom_null FROM {} WHERE id = 2", table_name)) - .fetch_one(&pool) - .await?; + let row_some: MySqlRow = sqlx::query(&format!( + "SELECT geom_null FROM {} WHERE id = 2", + table_name + )) + .fetch_one(&pool) + .await?; let decoded_some: Option = row_some.try_get("geom_null")?; assert_eq!(decoded_some, some_val); // Test nullable None let none_val: Option = None; - sqlx::query(&format!("INSERT INTO {} (id, geom_null) VALUES (3, ?)", table_name)) - .bind(none_val.clone()) - .execute(&pool) - .await?; + sqlx::query(&format!( + "INSERT INTO {} (id, geom_null) VALUES (3, ?)", + table_name + )) + .bind(none_val.clone()) + .execute(&pool) + .await?; - let row_none: MySqlRow = sqlx::query(&format!("SELECT geom_null FROM {} WHERE id = 3", table_name)) - .fetch_one(&pool) - .await?; + let row_none: MySqlRow = sqlx::query(&format!( + "SELECT geom_null FROM {} WHERE id = 3", + table_name + )) + .fetch_one(&pool) + .await?; let decoded_none: Option = row_none.try_get("geom_null")?; assert_eq!(decoded_none, none_val); - pool.execute(format!("DROP TABLE IF EXISTS {}", table_name).as_str()).await?; + pool.execute(format!("DROP TABLE IF EXISTS {}", table_name).as_str()) + .await?; Ok(()) } } diff --git a/sqlx-mysql/src/types/geo/mod.rs b/sqlx-mysql/src/types/geo/mod.rs index accadafc90..472a04cdae 100644 --- a/sqlx-mysql/src/types/geo/mod.rs +++ b/sqlx-mysql/src/types/geo/mod.rs @@ -1,8 +1,8 @@ -pub mod point; +pub mod geometry; +pub mod geometry_collection; pub mod linestring; -pub mod polygon; -pub mod multi_point; pub mod multi_line_string; +pub mod multi_point; pub mod multi_polygon; -pub mod geometry_collection; -pub mod geometry; +pub mod point; +pub mod polygon; diff --git a/sqlx-mysql/src/types/geo/multi_line_string.rs b/sqlx-mysql/src/types/geo/multi_line_string.rs index 18f4f74668..3305e518ec 100644 --- a/sqlx-mysql/src/types/geo/multi_line_string.rs +++ b/sqlx-mysql/src/types/geo/multi_line_string.rs @@ -4,13 +4,13 @@ use sqlx_core::error::BoxDynError; use sqlx_core::types::Type; use crate::io::MySqlBufMutExt; -use crate::{MySql, MySqlTypeInfo, MySqlValueRef}; use crate::protocol::text::{ColumnFlags, ColumnType}; +use crate::{MySql, MySqlTypeInfo, MySqlValueRef}; use std::convert::TryFrom; -use geo_types::MultiLineString; use geo_traits::to_geo::ToGeoGeometry; +use geo_types::MultiLineString; use wkb::reader; use wkb::writer; @@ -54,11 +54,11 @@ impl<'r> Decode<'r, MySql> for MultiLineString { } let wkb_data = &bytes[4..]; // Skip 4-byte SRID - let wkb_reader_geom = reader::Wkb::try_new(wkb_data) - .map_err(|e| BoxDynError::from(format!("WKB parsing error for MultiLineString: {}", e)))?; + let wkb_reader_geom = reader::Wkb::try_new(wkb_data).map_err(|e| { + BoxDynError::from(format!("WKB parsing error for MultiLineString: {}", e)) + })?; - let geo_geom: geo_types::Geometry = wkb_reader_geom - .to_geometry(); + let geo_geom: geo_types::Geometry = wkb_reader_geom.to_geometry(); MultiLineString::try_from(geo_geom).map_err(|e| { BoxDynError::from(format!( @@ -71,16 +71,17 @@ impl<'r> Decode<'r, MySql> for MultiLineString { #[cfg(test)] mod tests { + use geo_types::coord; + use geo_types::LineString; + use geo_types::MultiLineString as TestableGeoType; use sqlx::mysql::{MySqlPool, MySqlRow}; use sqlx::{Executor, Row}; - use geo_types::MultiLineString as TestableGeoType; - use geo_types::LineString; - use geo_types::coord; #[sqlx::test] async fn test_encode_decode_multilinestring(pool: MySqlPool) -> anyhow::Result<()> { let table_name = format!("test_geo_multilinestring_table"); - pool.execute(format!("DROP TABLE IF EXISTS {}", table_name).as_str()).await?; + pool.execute(format!("DROP TABLE IF EXISTS {}", table_name).as_str()) + .await?; pool.execute( format!( "CREATE TABLE {} (id INT, geom GEOMETRY, geom_null GEOMETRY NULL)", @@ -90,17 +91,20 @@ mod tests { ) .await?; - let ls1_in = LineString::new(vec![coord!{x:0.,y:0.}, coord!{x:1.,y:1.}]); - let ls2_in = LineString::new(vec![coord!{x:2.,y:2.}, coord!{x:3.,y:3.}]); + let ls1_in = LineString::new(vec![coord! {x:0.,y:0.}, coord! {x:1.,y:1.}]); + let ls2_in = LineString::new(vec![coord! {x:2.,y:2.}, coord! {x:3.,y:3.}]); let mls1 = TestableGeoType::new(vec![ls1_in, ls2_in]); - let ls3_in = LineString::new(vec![coord!{x:10.,y:10.}, coord!{x:20.,y:20.}]); + let ls3_in = LineString::new(vec![coord! {x:10.,y:10.}, coord! {x:20.,y:20.}]); let mls2 = TestableGeoType::new(vec![ls3_in]); // Test non-nullable - sqlx::query(&format!("INSERT INTO {} (id, geom) VALUES (1, ?)", table_name)) - .bind(mls1.clone()) - .execute(&pool) - .await?; + sqlx::query(&format!( + "INSERT INTO {} (id, geom) VALUES (1, ?)", + table_name + )) + .bind(mls1.clone()) + .execute(&pool) + .await?; let row: MySqlRow = sqlx::query(&format!("SELECT geom FROM {} WHERE id = 1", table_name)) .fetch_one(&pool) @@ -110,31 +114,44 @@ mod tests { // Test nullable Some(value) let some_val: Option = Some(mls2.clone()); - sqlx::query(&format!("INSERT INTO {} (id, geom_null) VALUES (2, ?)", table_name)) - .bind(some_val.clone()) - .execute(&pool) - .await?; + sqlx::query(&format!( + "INSERT INTO {} (id, geom_null) VALUES (2, ?)", + table_name + )) + .bind(some_val.clone()) + .execute(&pool) + .await?; - let row_some: MySqlRow = sqlx::query(&format!("SELECT geom_null FROM {} WHERE id = 2", table_name)) - .fetch_one(&pool) - .await?; + let row_some: MySqlRow = sqlx::query(&format!( + "SELECT geom_null FROM {} WHERE id = 2", + table_name + )) + .fetch_one(&pool) + .await?; let decoded_some: Option = row_some.try_get("geom_null")?; assert_eq!(decoded_some, some_val); // Test nullable None let none_val: Option = None; - sqlx::query(&format!("INSERT INTO {} (id, geom_null) VALUES (3, ?)", table_name)) - .bind(none_val.clone()) - .execute(&pool) - .await?; + sqlx::query(&format!( + "INSERT INTO {} (id, geom_null) VALUES (3, ?)", + table_name + )) + .bind(none_val.clone()) + .execute(&pool) + .await?; - let row_none: MySqlRow = sqlx::query(&format!("SELECT geom_null FROM {} WHERE id = 3", table_name)) - .fetch_one(&pool) - .await?; + let row_none: MySqlRow = sqlx::query(&format!( + "SELECT geom_null FROM {} WHERE id = 3", + table_name + )) + .fetch_one(&pool) + .await?; let decoded_none: Option = row_none.try_get("geom_null")?; assert_eq!(decoded_none, none_val); - pool.execute(format!("DROP TABLE IF EXISTS {}", table_name).as_str()).await?; + pool.execute(format!("DROP TABLE IF EXISTS {}", table_name).as_str()) + .await?; Ok(()) } } diff --git a/sqlx-mysql/src/types/geo/multi_point.rs b/sqlx-mysql/src/types/geo/multi_point.rs index 5f43afd0ae..7969e0f2ee 100644 --- a/sqlx-mysql/src/types/geo/multi_point.rs +++ b/sqlx-mysql/src/types/geo/multi_point.rs @@ -4,13 +4,13 @@ use sqlx_core::error::BoxDynError; use sqlx_core::types::Type; use crate::io::MySqlBufMutExt; -use crate::{MySql, MySqlTypeInfo, MySqlValueRef}; use crate::protocol::text::{ColumnFlags, ColumnType}; +use crate::{MySql, MySqlTypeInfo, MySqlValueRef}; use std::convert::TryFrom; -use geo_types::MultiPoint; use geo_traits::to_geo::ToGeoGeometry; +use geo_types::MultiPoint; use wkb::reader; use wkb::writer; @@ -57,8 +57,7 @@ impl<'r> Decode<'r, MySql> for MultiPoint { let wkb_reader_geom = reader::Wkb::try_new(wkb_data) .map_err(|e| BoxDynError::from(format!("WKB parsing error for MultiPoint: {}", e)))?; - let geo_geom: geo_types::Geometry = wkb_reader_geom - .to_geometry(); + let geo_geom: geo_types::Geometry = wkb_reader_geom.to_geometry(); MultiPoint::try_from(geo_geom).map_err(|e| { BoxDynError::from(format!( @@ -71,15 +70,16 @@ impl<'r> Decode<'r, MySql> for MultiPoint { #[cfg(test)] mod tests { - use sqlx::mysql::{MySqlPool, MySqlRow}; - use sqlx::{Executor, Row}; use geo_types::MultiPoint as TestableGeoType; use geo_types::Point; + use sqlx::mysql::{MySqlPool, MySqlRow}; + use sqlx::{Executor, Row}; #[sqlx::test] async fn test_encode_decode_multipoint(pool: MySqlPool) -> anyhow::Result<()> { let table_name = format!("test_geo_multipoint_table"); - pool.execute(format!("DROP TABLE IF EXISTS {}", table_name).as_str()).await?; + pool.execute(format!("DROP TABLE IF EXISTS {}", table_name).as_str()) + .await?; pool.execute( format!( "CREATE TABLE {} (id INT, geom GEOMETRY, geom_null GEOMETRY NULL)", @@ -89,14 +89,17 @@ mod tests { ) .await?; - let mp1 = TestableGeoType::new(vec![Point::new(0.,0.), Point::new(1.,1.)]); - let mp2 = TestableGeoType::new(vec![Point::new(10.,10.), Point::new(20.,20.)]); + let mp1 = TestableGeoType::new(vec![Point::new(0., 0.), Point::new(1., 1.)]); + let mp2 = TestableGeoType::new(vec![Point::new(10., 10.), Point::new(20., 20.)]); // Test non-nullable - sqlx::query(&format!("INSERT INTO {} (id, geom) VALUES (1, ?)", table_name)) - .bind(mp1.clone()) - .execute(&pool) - .await?; + sqlx::query(&format!( + "INSERT INTO {} (id, geom) VALUES (1, ?)", + table_name + )) + .bind(mp1.clone()) + .execute(&pool) + .await?; let row: MySqlRow = sqlx::query(&format!("SELECT geom FROM {} WHERE id = 1", table_name)) .fetch_one(&pool) @@ -106,31 +109,44 @@ mod tests { // Test nullable Some(value) let some_val: Option = Some(mp2.clone()); - sqlx::query(&format!("INSERT INTO {} (id, geom_null) VALUES (2, ?)", table_name)) - .bind(some_val.clone()) - .execute(&pool) - .await?; + sqlx::query(&format!( + "INSERT INTO {} (id, geom_null) VALUES (2, ?)", + table_name + )) + .bind(some_val.clone()) + .execute(&pool) + .await?; - let row_some: MySqlRow = sqlx::query(&format!("SELECT geom_null FROM {} WHERE id = 2", table_name)) - .fetch_one(&pool) - .await?; + let row_some: MySqlRow = sqlx::query(&format!( + "SELECT geom_null FROM {} WHERE id = 2", + table_name + )) + .fetch_one(&pool) + .await?; let decoded_some: Option = row_some.try_get("geom_null")?; assert_eq!(decoded_some, some_val); // Test nullable None let none_val: Option = None; - sqlx::query(&format!("INSERT INTO {} (id, geom_null) VALUES (3, ?)", table_name)) - .bind(none_val.clone()) - .execute(&pool) - .await?; + sqlx::query(&format!( + "INSERT INTO {} (id, geom_null) VALUES (3, ?)", + table_name + )) + .bind(none_val.clone()) + .execute(&pool) + .await?; - let row_none: MySqlRow = sqlx::query(&format!("SELECT geom_null FROM {} WHERE id = 3", table_name)) - .fetch_one(&pool) - .await?; + let row_none: MySqlRow = sqlx::query(&format!( + "SELECT geom_null FROM {} WHERE id = 3", + table_name + )) + .fetch_one(&pool) + .await?; let decoded_none: Option = row_none.try_get("geom_null")?; assert_eq!(decoded_none, none_val); - pool.execute(format!("DROP TABLE IF EXISTS {}", table_name).as_str()).await?; + pool.execute(format!("DROP TABLE IF EXISTS {}", table_name).as_str()) + .await?; Ok(()) } } diff --git a/sqlx-mysql/src/types/geo/multi_polygon.rs b/sqlx-mysql/src/types/geo/multi_polygon.rs index e5a1fd2456..9d4f42cbfa 100644 --- a/sqlx-mysql/src/types/geo/multi_polygon.rs +++ b/sqlx-mysql/src/types/geo/multi_polygon.rs @@ -4,13 +4,13 @@ use sqlx_core::error::BoxDynError; use sqlx_core::types::Type; use crate::io::MySqlBufMutExt; -use crate::{MySql, MySqlTypeInfo, MySqlValueRef}; use crate::protocol::text::{ColumnFlags, ColumnType}; +use crate::{MySql, MySqlTypeInfo, MySqlValueRef}; use std::convert::TryFrom; -use geo_types::MultiPolygon; use geo_traits::to_geo::ToGeoGeometry; +use geo_types::MultiPolygon; use wkb::reader; use wkb::writer; @@ -57,8 +57,7 @@ impl<'r> Decode<'r, MySql> for MultiPolygon { let wkb_reader_geom = reader::Wkb::try_new(wkb_data) .map_err(|e| BoxDynError::from(format!("WKB parsing error for MultiPolygon: {}", e)))?; - let geo_geom: geo_types::Geometry = wkb_reader_geom - .to_geometry(); + let geo_geom: geo_types::Geometry = wkb_reader_geom.to_geometry(); MultiPolygon::try_from(geo_geom).map_err(|e| { BoxDynError::from(format!( @@ -71,17 +70,18 @@ impl<'r> Decode<'r, MySql> for MultiPolygon { #[cfg(test)] mod tests { - use sqlx::mysql::{MySqlPool, MySqlRow}; - use sqlx::{Executor, Row}; - use geo_types::MultiPolygon as TestableGeoType; + use geo_types::coord; use geo_types::LineString; + use geo_types::MultiPolygon as TestableGeoType; use geo_types::Polygon; - use geo_types::coord; + use sqlx::mysql::{MySqlPool, MySqlRow}; + use sqlx::{Executor, Row}; #[sqlx::test] async fn test_encode_decode_multipolygon(pool: MySqlPool) -> anyhow::Result<()> { let table_name = format!("test_geo_multipolygon_table"); - pool.execute(format!("DROP TABLE IF EXISTS {}", table_name).as_str()).await?; + pool.execute(format!("DROP TABLE IF EXISTS {}", table_name).as_str()) + .await?; pool.execute( format!( "CREATE TABLE {} (id INT, geom GEOMETRY, geom_null GEOMETRY NULL)", @@ -91,20 +91,38 @@ mod tests { ) .await?; - let ext1 = LineString::new(vec![coord!{x:0.,y:0.}, coord!{x:1.,y:1.}, coord!{x:1.,y:0.}, coord!{x:0.,y:0.}]); + let ext1 = LineString::new(vec![ + coord! {x:0.,y:0.}, + coord! {x:1.,y:1.}, + coord! {x:1.,y:0.}, + coord! {x:0.,y:0.}, + ]); let poly1 = Polygon::new(ext1, vec![]); - let ext2 = LineString::new(vec![coord!{x:10.,y:10.}, coord!{x:11.,y:11.}, coord!{x:11.,y:10.}, coord!{x:10.,y:10.}]); + let ext2 = LineString::new(vec![ + coord! {x:10.,y:10.}, + coord! {x:11.,y:11.}, + coord! {x:11.,y:10.}, + coord! {x:10.,y:10.}, + ]); let poly2 = Polygon::new(ext2, vec![]); let mpoly1 = TestableGeoType::new(vec![poly1, poly2]); - let ext3 = LineString::new(vec![coord!{x:20.,y:20.}, coord!{x:21.,y:21.}, coord!{x:21.,y:20.}, coord!{x:20.,y:20.}]); + let ext3 = LineString::new(vec![ + coord! {x:20.,y:20.}, + coord! {x:21.,y:21.}, + coord! {x:21.,y:20.}, + coord! {x:20.,y:20.}, + ]); let poly3 = Polygon::new(ext3, vec![]); let mpoly2 = TestableGeoType::new(vec![poly3]); // Test non-nullable - sqlx::query(&format!("INSERT INTO {} (id, geom) VALUES (1, ?)", table_name)) - .bind(mpoly1.clone()) - .execute(&pool) - .await?; + sqlx::query(&format!( + "INSERT INTO {} (id, geom) VALUES (1, ?)", + table_name + )) + .bind(mpoly1.clone()) + .execute(&pool) + .await?; let row: MySqlRow = sqlx::query(&format!("SELECT geom FROM {} WHERE id = 1", table_name)) .fetch_one(&pool) @@ -114,31 +132,44 @@ mod tests { // Test nullable Some(value) let some_val: Option = Some(mpoly2.clone()); - sqlx::query(&format!("INSERT INTO {} (id, geom_null) VALUES (2, ?)", table_name)) - .bind(some_val.clone()) - .execute(&pool) - .await?; + sqlx::query(&format!( + "INSERT INTO {} (id, geom_null) VALUES (2, ?)", + table_name + )) + .bind(some_val.clone()) + .execute(&pool) + .await?; - let row_some: MySqlRow = sqlx::query(&format!("SELECT geom_null FROM {} WHERE id = 2", table_name)) - .fetch_one(&pool) - .await?; + let row_some: MySqlRow = sqlx::query(&format!( + "SELECT geom_null FROM {} WHERE id = 2", + table_name + )) + .fetch_one(&pool) + .await?; let decoded_some: Option = row_some.try_get("geom_null")?; assert_eq!(decoded_some, some_val); // Test nullable None let none_val: Option = None; - sqlx::query(&format!("INSERT INTO {} (id, geom_null) VALUES (3, ?)", table_name)) - .bind(none_val.clone()) - .execute(&pool) - .await?; + sqlx::query(&format!( + "INSERT INTO {} (id, geom_null) VALUES (3, ?)", + table_name + )) + .bind(none_val.clone()) + .execute(&pool) + .await?; - let row_none: MySqlRow = sqlx::query(&format!("SELECT geom_null FROM {} WHERE id = 3", table_name)) - .fetch_one(&pool) - .await?; + let row_none: MySqlRow = sqlx::query(&format!( + "SELECT geom_null FROM {} WHERE id = 3", + table_name + )) + .fetch_one(&pool) + .await?; let decoded_none: Option = row_none.try_get("geom_null")?; assert_eq!(decoded_none, none_val); - pool.execute(format!("DROP TABLE IF EXISTS {}", table_name).as_str()).await?; + pool.execute(format!("DROP TABLE IF EXISTS {}", table_name).as_str()) + .await?; Ok(()) } } diff --git a/sqlx-mysql/src/types/geo/point.rs b/sqlx-mysql/src/types/geo/point.rs index 030fb7de55..52ba2e5902 100644 --- a/sqlx-mysql/src/types/geo/point.rs +++ b/sqlx-mysql/src/types/geo/point.rs @@ -4,13 +4,13 @@ use sqlx_core::error::BoxDynError; use sqlx_core::types::Type; use crate::io::MySqlBufMutExt; -use crate::{MySql, MySqlTypeInfo, MySqlValueRef}; use crate::protocol::text::{ColumnFlags, ColumnType}; +use crate::{MySql, MySqlTypeInfo, MySqlValueRef}; use std::convert::TryFrom; -use geo_types::Point; use geo_traits::to_geo::ToGeoGeometry; +use geo_types::Point; use wkb::reader; use wkb::writer; @@ -57,8 +57,7 @@ impl<'r> Decode<'r, MySql> for Point { let wkb_reader_geom = reader::Wkb::try_new(wkb_data) .map_err(|e| BoxDynError::from(format!("WKB parsing error for Point: {}", e)))?; - let geo_geom: geo_types::Geometry = wkb_reader_geom - .to_geometry(); + let geo_geom: geo_types::Geometry = wkb_reader_geom.to_geometry(); Point::try_from(geo_geom).map_err(|e| { BoxDynError::from(format!( @@ -71,14 +70,15 @@ impl<'r> Decode<'r, MySql> for Point { #[cfg(test)] mod tests { + use geo_types::Point as TestableGeoType; use sqlx::mysql::{MySqlPool, MySqlRow}; use sqlx::{Executor, Row}; - use geo_types::Point as TestableGeoType; #[sqlx::test] async fn test_encode_decode_point(pool: MySqlPool) -> anyhow::Result<()> { let table_name = format!("test_geo_point_table"); - pool.execute(format!("DROP TABLE IF EXISTS {}", table_name).as_str()).await?; + pool.execute(format!("DROP TABLE IF EXISTS {}", table_name).as_str()) + .await?; pool.execute( format!( "CREATE TABLE {} (id INT, geom GEOMETRY, geom_null GEOMETRY NULL)", @@ -92,10 +92,13 @@ mod tests { let p2 = TestableGeoType::new(30.0, 40.0); // Test non-nullable - sqlx::query(&format!("INSERT INTO {} (id, geom) VALUES (1, ?)", table_name)) - .bind(p1.clone()) - .execute(&pool) - .await?; + sqlx::query(&format!( + "INSERT INTO {} (id, geom) VALUES (1, ?)", + table_name + )) + .bind(p1.clone()) + .execute(&pool) + .await?; let row: MySqlRow = sqlx::query(&format!("SELECT geom FROM {} WHERE id = 1", table_name)) .fetch_one(&pool) @@ -105,31 +108,44 @@ mod tests { // Test nullable Some(value) let some_val: Option = Some(p2.clone()); - sqlx::query(&format!("INSERT INTO {} (id, geom_null) VALUES (2, ?)", table_name)) - .bind(some_val.clone()) - .execute(&pool) - .await?; + sqlx::query(&format!( + "INSERT INTO {} (id, geom_null) VALUES (2, ?)", + table_name + )) + .bind(some_val.clone()) + .execute(&pool) + .await?; - let row_some: MySqlRow = sqlx::query(&format!("SELECT geom_null FROM {} WHERE id = 2", table_name)) - .fetch_one(&pool) - .await?; + let row_some: MySqlRow = sqlx::query(&format!( + "SELECT geom_null FROM {} WHERE id = 2", + table_name + )) + .fetch_one(&pool) + .await?; let decoded_some: Option = row_some.try_get("geom_null")?; assert_eq!(decoded_some, some_val); // Test nullable None let none_val: Option = None; - sqlx::query(&format!("INSERT INTO {} (id, geom_null) VALUES (3, ?)", table_name)) - .bind(none_val.clone()) - .execute(&pool) - .await?; + sqlx::query(&format!( + "INSERT INTO {} (id, geom_null) VALUES (3, ?)", + table_name + )) + .bind(none_val.clone()) + .execute(&pool) + .await?; - let row_none: MySqlRow = sqlx::query(&format!("SELECT geom_null FROM {} WHERE id = 3", table_name)) - .fetch_one(&pool) - .await?; + let row_none: MySqlRow = sqlx::query(&format!( + "SELECT geom_null FROM {} WHERE id = 3", + table_name + )) + .fetch_one(&pool) + .await?; let decoded_none: Option = row_none.try_get("geom_null")?; assert_eq!(decoded_none, none_val); - pool.execute(format!("DROP TABLE IF EXISTS {}", table_name).as_str()).await?; + pool.execute(format!("DROP TABLE IF EXISTS {}", table_name).as_str()) + .await?; Ok(()) } } diff --git a/sqlx-mysql/src/types/geo/polygon.rs b/sqlx-mysql/src/types/geo/polygon.rs index 3a263b5494..07eb72aa99 100644 --- a/sqlx-mysql/src/types/geo/polygon.rs +++ b/sqlx-mysql/src/types/geo/polygon.rs @@ -4,13 +4,13 @@ use sqlx_core::error::BoxDynError; use sqlx_core::types::Type; use crate::io::MySqlBufMutExt; -use crate::{MySql, MySqlTypeInfo, MySqlValueRef}; use crate::protocol::text::{ColumnFlags, ColumnType}; +use crate::{MySql, MySqlTypeInfo, MySqlValueRef}; use std::convert::TryFrom; -use geo_types::Polygon; use geo_traits::to_geo::ToGeoGeometry; +use geo_types::Polygon; use wkb::reader; use wkb::writer; @@ -57,8 +57,7 @@ impl<'r> Decode<'r, MySql> for Polygon { let wkb_reader_geom = reader::Wkb::try_new(wkb_data) .map_err(|e| BoxDynError::from(format!("WKB parsing error for Polygon: {}", e)))?; - let geo_geom: geo_types::Geometry = wkb_reader_geom - .to_geometry(); + let geo_geom: geo_types::Geometry = wkb_reader_geom.to_geometry(); Polygon::try_from(geo_geom).map_err(|e| { BoxDynError::from(format!( @@ -71,16 +70,17 @@ impl<'r> Decode<'r, MySql> for Polygon { #[cfg(test)] mod tests { + use geo_types::coord; + use geo_types::LineString; + use geo_types::Polygon as TestableGeoType; use sqlx::mysql::{MySqlPool, MySqlRow}; use sqlx::{Executor, Row}; - use geo_types::Polygon as TestableGeoType; - use geo_types::LineString; - use geo_types::coord; #[sqlx::test] async fn test_encode_decode_polygon(pool: MySqlPool) -> anyhow::Result<()> { let table_name = format!("test_geo_polygon_table"); - pool.execute(format!("DROP TABLE IF EXISTS {}", table_name).as_str()).await?; + pool.execute(format!("DROP TABLE IF EXISTS {}", table_name).as_str()) + .await?; pool.execute( format!( "CREATE TABLE {} (id INT, geom GEOMETRY, geom_null GEOMETRY NULL)", @@ -90,16 +90,29 @@ mod tests { ) .await?; - let exterior1 = LineString::new(vec![coord!{x: 0., y: 0.}, coord!{x: 1., y: 1.}, coord!{x: 1., y: 0.}, coord!{x: 0., y: 0.}]); + let exterior1 = LineString::new(vec![ + coord! {x: 0., y: 0.}, + coord! {x: 1., y: 1.}, + coord! {x: 1., y: 0.}, + coord! {x: 0., y: 0.}, + ]); let poly1 = TestableGeoType::new(exterior1, vec![]); - let exterior2 = LineString::new(vec![coord!{x: 10., y: 10.}, coord!{x: 20., y: 20.}, coord!{x: 20., y: 10.}, coord!{x: 10., y: 10.}]); + let exterior2 = LineString::new(vec![ + coord! {x: 10., y: 10.}, + coord! {x: 20., y: 20.}, + coord! {x: 20., y: 10.}, + coord! {x: 10., y: 10.}, + ]); let poly2 = TestableGeoType::new(exterior2, vec![]); // Test non-nullable - sqlx::query(&format!("INSERT INTO {} (id, geom) VALUES (1, ?)", table_name)) - .bind(poly1.clone()) - .execute(&pool) - .await?; + sqlx::query(&format!( + "INSERT INTO {} (id, geom) VALUES (1, ?)", + table_name + )) + .bind(poly1.clone()) + .execute(&pool) + .await?; let row: MySqlRow = sqlx::query(&format!("SELECT geom FROM {} WHERE id = 1", table_name)) .fetch_one(&pool) @@ -109,31 +122,44 @@ mod tests { // Test nullable Some(value) let some_val: Option = Some(poly2.clone()); - sqlx::query(&format!("INSERT INTO {} (id, geom_null) VALUES (2, ?)", table_name)) - .bind(some_val.clone()) - .execute(&pool) - .await?; + sqlx::query(&format!( + "INSERT INTO {} (id, geom_null) VALUES (2, ?)", + table_name + )) + .bind(some_val.clone()) + .execute(&pool) + .await?; - let row_some: MySqlRow = sqlx::query(&format!("SELECT geom_null FROM {} WHERE id = 2", table_name)) - .fetch_one(&pool) - .await?; + let row_some: MySqlRow = sqlx::query(&format!( + "SELECT geom_null FROM {} WHERE id = 2", + table_name + )) + .fetch_one(&pool) + .await?; let decoded_some: Option = row_some.try_get("geom_null")?; assert_eq!(decoded_some, some_val); // Test nullable None let none_val: Option = None; - sqlx::query(&format!("INSERT INTO {} (id, geom_null) VALUES (3, ?)", table_name)) - .bind(none_val.clone()) - .execute(&pool) - .await?; + sqlx::query(&format!( + "INSERT INTO {} (id, geom_null) VALUES (3, ?)", + table_name + )) + .bind(none_val.clone()) + .execute(&pool) + .await?; - let row_none: MySqlRow = sqlx::query(&format!("SELECT geom_null FROM {} WHERE id = 3", table_name)) - .fetch_one(&pool) - .await?; + let row_none: MySqlRow = sqlx::query(&format!( + "SELECT geom_null FROM {} WHERE id = 3", + table_name + )) + .fetch_one(&pool) + .await?; let decoded_none: Option = row_none.try_get("geom_null")?; assert_eq!(decoded_none, none_val); - pool.execute(format!("DROP TABLE IF EXISTS {}", table_name).as_str()).await?; + pool.execute(format!("DROP TABLE IF EXISTS {}", table_name).as_str()) + .await?; Ok(()) } } From 50f71d6ce1438a212cba5ee074789b4e08e6082a Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 26 Jun 2025 03:12:53 +0000 Subject: [PATCH 3/3] fix: Add runtime feature to sqlx dev-dependency for mysql tests --- sqlx-mysql/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sqlx-mysql/Cargo.toml b/sqlx-mysql/Cargo.toml index 9609e13e0b..b1bf6d51df 100644 --- a/sqlx-mysql/Cargo.toml +++ b/sqlx-mysql/Cargo.toml @@ -76,7 +76,7 @@ whoami = { version = "1.2.1", default-features = false } serde = { version = "1.0.144", optional = true } [dev-dependencies] -sqlx = { workspace = true, features = ["mysql", "macros", "migrate"] } +sqlx = { workspace = true, features = ["mysql", "macros", "migrate", "runtime-tokio"] } anyhow = "1.0" # For test results [lints]