fix(mysql): correct geometry export lon/lat order via WKB round-trip
This commit is contained in:
1 parent
00bbfad055
commit
c835deb3f9
5 files changed
+553
-90
No files matched your search
@@ -505,18 +505,59 @@ pub(crate) fn mysql_value_to_json(row: &mysql_async::Row, idx: usize) -> serde_j
|
||||
.unwrap_or(serde_json::Value::Null)
|
||||
}
|
||||
|
||||
fn decode_mysql_geometry(bytes: &[u8]) -> Option<super::wkb::DecodedGeometry> {
|
||||
fn bytes_to_upper_hex(bytes: &[u8]) -> String {
|
||||
const HEX: &[u8; 16] = b"0123456789ABCDEF";
|
||||
let mut output = String::with_capacity(bytes.len() * 2);
|
||||
for byte in bytes {
|
||||
output.push(HEX[(byte >> 4) as usize] as char);
|
||||
output.push(HEX[(byte & 0x0F) as usize] as char);
|
||||
}
|
||||
output
|
||||
}
|
||||
|
||||
// Native MySQL geometry values normally begin with a four-byte little-endian
|
||||
// SRID prefix. A bare WKB value can have 0 or 1 at byte four as well, so only
|
||||
// treat that prefix as present when the remaining bytes parse as WKB.
|
||||
fn mysql_geometry_wkb_parts(bytes: &[u8]) -> Option<(&[u8], u32)> {
|
||||
if bytes.len() >= 5 && matches!(bytes[4], 0 | 1) {
|
||||
let prefix: [u8; 4] = bytes[..4].try_into().ok()?;
|
||||
if let Some(mut geometry) = super::wkb::decode_wkb_geometry(&bytes[4..]) {
|
||||
if geometry.srid.is_none() {
|
||||
let srid = u32::from_le_bytes(prefix);
|
||||
geometry.srid = (srid != 0).then_some(srid);
|
||||
}
|
||||
return Some(geometry);
|
||||
if super::wkb::decode_wkb_geometry(&bytes[4..]).is_some() {
|
||||
return Some((&bytes[4..], u32::from_le_bytes(prefix)));
|
||||
}
|
||||
}
|
||||
super::wkb::decode_wkb_geometry(bytes)
|
||||
super::wkb::decode_wkb_geometry(bytes).map(|_| (bytes, 0))
|
||||
}
|
||||
|
||||
fn mysql_geometry_to_export_marker(bytes: &[u8]) -> Option<String> {
|
||||
let (wkb, srid) = mysql_geometry_wkb_parts(bytes)?;
|
||||
Some(format!("DBX_WKB:{srid}:{}", bytes_to_upper_hex(wkb)))
|
||||
}
|
||||
|
||||
fn mysql_value_to_json_for_export(
|
||||
row: &mysql_async::Row,
|
||||
idx: usize,
|
||||
spatial_as_wkb: bool,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
if !spatial_as_wkb
|
||||
|| !row.columns_ref().get(idx).is_some_and(|column| column.column_type() == ColumnType::MYSQL_TYPE_GEOMETRY)
|
||||
{
|
||||
return Ok(mysql_value_to_json(row, idx));
|
||||
}
|
||||
let Some(bytes) = row_get::<Vec<u8>, _>(row, idx) else {
|
||||
return Ok(mysql_value_to_json(row, idx));
|
||||
};
|
||||
mysql_geometry_to_export_marker(&bytes)
|
||||
.map(serde_json::Value::String)
|
||||
.ok_or_else(|| "Cannot export MySQL geometry value as WKB".to_string())
|
||||
}
|
||||
|
||||
fn decode_mysql_geometry(bytes: &[u8]) -> Option<super::wkb::DecodedGeometry> {
|
||||
let (wkb, srid) = mysql_geometry_wkb_parts(bytes)?;
|
||||
let mut geometry = super::wkb::decode_wkb_geometry(wkb)?;
|
||||
if geometry.srid.is_none() {
|
||||
geometry.srid = (srid != 0).then_some(srid);
|
||||
}
|
||||
Some(geometry)
|
||||
}
|
||||
|
||||
fn mysql_spatial_column_builder(columns: &[mysql_async::Column]) -> SpatialColumnBuilder {
|
||||
@@ -4011,7 +4052,7 @@ pub async fn stream_query_rows(
|
||||
mut on_row: impl FnMut(&[serde_json::Value]) -> Result<(), String>,
|
||||
) -> Result<u64, String> {
|
||||
let mut conn = get_conn_with_health_check(pool).await?;
|
||||
stream_query_result_on_conn(&mut conn, sql, bare, max_rows, dialect, cancelled, |item| {
|
||||
stream_query_result_on_conn(&mut conn, sql, bare, max_rows, dialect, cancelled, false, |item| {
|
||||
if let MySqlQueryStreamItem::Row(row) = item {
|
||||
on_row(&row)?;
|
||||
}
|
||||
@@ -4027,17 +4068,18 @@ pub async fn stream_query_result_on_conn(
|
||||
max_rows: Option<usize>,
|
||||
dialect: MySqlQueryDialect,
|
||||
cancelled: &AtomicBool,
|
||||
spatial_as_wkb: bool,
|
||||
mut on_item: impl FnMut(MySqlQueryStreamItem) -> Result<(), String>,
|
||||
) -> Result<u64, String> {
|
||||
let row_limit = max_rows.unwrap_or(usize::MAX);
|
||||
|
||||
if bare || prefers_text_protocol_query(sql, dialect) {
|
||||
stream_query_result_text(conn, sql, row_limit, cancelled, &mut on_item).await
|
||||
stream_query_result_text(conn, sql, row_limit, cancelled, spatial_as_wkb, &mut on_item).await
|
||||
} else {
|
||||
match stream_query_result_prepared(conn, sql, row_limit, cancelled, &mut on_item).await {
|
||||
match stream_query_result_prepared(conn, sql, row_limit, cancelled, spatial_as_wkb, &mut on_item).await {
|
||||
Ok(rows) => Ok(rows),
|
||||
Err(err) if mysql_error_should_retry_with_text_protocol(&err) => {
|
||||
stream_query_result_text(conn, sql, row_limit, cancelled, &mut on_item).await
|
||||
stream_query_result_text(conn, sql, row_limit, cancelled, spatial_as_wkb, &mut on_item).await
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
@@ -4049,6 +4091,7 @@ async fn stream_query_result_text(
|
||||
sql: &str,
|
||||
row_limit: usize,
|
||||
cancelled: &AtomicBool,
|
||||
spatial_as_wkb: bool,
|
||||
on_item: &mut impl FnMut(MySqlQueryStreamItem) -> Result<(), String>,
|
||||
) -> Result<u64, String> {
|
||||
let mut result = conn.query_iter(sql).await.map_err(|e| e.to_string())?;
|
||||
@@ -4074,7 +4117,9 @@ async fn stream_query_result_text(
|
||||
break;
|
||||
}
|
||||
let row = row.map_err(|e| e.to_string())?;
|
||||
let values: Vec<serde_json::Value> = (0..row.len()).map(|i| mysql_value_to_json(&row, i)).collect();
|
||||
let values: Vec<serde_json::Value> = (0..row.len())
|
||||
.map(|i| mysql_value_to_json_for_export(&row, i, spatial_as_wkb))
|
||||
.collect::<Result<_, _>>()?;
|
||||
on_item(MySqlQueryStreamItem::Row(values))?;
|
||||
rows_exported += 1;
|
||||
}
|
||||
@@ -4087,6 +4132,7 @@ async fn stream_query_result_prepared(
|
||||
sql: &str,
|
||||
row_limit: usize,
|
||||
cancelled: &AtomicBool,
|
||||
spatial_as_wkb: bool,
|
||||
on_item: &mut impl FnMut(MySqlQueryStreamItem) -> Result<(), String>,
|
||||
) -> Result<u64, String> {
|
||||
let mut result = conn.exec_iter(sql, ()).await.map_err(|e| e.to_string())?;
|
||||
@@ -4112,7 +4158,9 @@ async fn stream_query_result_prepared(
|
||||
break;
|
||||
}
|
||||
let row = row.map_err(|e| e.to_string())?;
|
||||
let values: Vec<serde_json::Value> = (0..row.len()).map(|i| mysql_value_to_json(&row, i)).collect();
|
||||
let values: Vec<serde_json::Value> = (0..row.len())
|
||||
.map(|i| mysql_value_to_json_for_export(&row, i, spatial_as_wkb))
|
||||
.collect::<Result<_, _>>()?;
|
||||
on_item(MySqlQueryStreamItem::Row(values))?;
|
||||
rows_exported += 1;
|
||||
}
|
||||
@@ -5038,6 +5086,23 @@ mod tests {
|
||||
let decoded = decode_mysql_geometry(&raw).unwrap();
|
||||
assert_eq!(decoded.wkt, "POINT(1 2)");
|
||||
assert_eq!(decoded.srid, None);
|
||||
assert_eq!(
|
||||
mysql_geometry_to_export_marker(&raw).as_deref(),
|
||||
Some("DBX_WKB:0:0101000000000000000000F03F0000000000000040")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mysql_geometry_export_marker_strips_internal_srid_prefix() {
|
||||
let mut raw = 4326_u32.to_le_bytes().to_vec();
|
||||
raw.extend_from_slice(&[
|
||||
0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x40,
|
||||
]);
|
||||
assert_eq!(
|
||||
mysql_geometry_to_export_marker(&raw).as_deref(),
|
||||
Some("DBX_WKB:4326:0101000000000000000000F03F0000000000000040")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in new issue
Block a user