Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat(mysql): add GEOMETRY support #3069

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


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

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

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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 }
Expand Down Expand Up @@ -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"
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions sqlx-mysql/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down Expand Up @@ -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"
Expand Down
149 changes: 149 additions & 0 deletions sqlx-mysql/src/types/geometry.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
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};
use crate::error::BoxDynError;
use crate::io::MySqlBufMutExt;
use crate::protocol::text::ColumnType;
use crate::types::Type;
use crate::{MySql, MySqlTypeInfo, MySqlValueRef};

macro_rules! impl_mysql_type {
($name:ident) => {
impl Type<MySql> for $name<f64> {
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<MySql>>::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<f64> {
fn encode_by_ref(&self, buf: &mut Vec<u8>) -> 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<f64> {
fn decode(value: MySqlValueRef<'_>) -> Result<Self, BoxDynError> {
let mut bytes = value.as_bytes()?;

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<f64> {
fn encode(self, buf: &mut Vec<u8>) -> IsNull {
Geometry::<f64>::$name(self).encode(buf)
}

fn encode_by_ref(&self, buf: &mut Vec<u8>) -> IsNull {
Geometry::<f64>::$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<Geometry<f64>>`] 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<f64> {
fn decode(value: MySqlValueRef<'_>) -> Result<Self, BoxDynError> {
Ok(<Geometry<f64> 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<f64> {
fn decode(value: MySqlValueRef<'_>) -> Result<Self, BoxDynError> {
let $n = <Geometry<f64> 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::<GeometryCollection<f64>>(),
found: type_name::<geo_types::geometry::$name<f64>>(),
}
.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),
});
3 changes: 3 additions & 0 deletions sqlx-mysql/src/types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,3 +128,6 @@ mod time;

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

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