fix(mysql): parse geometry WKB to WKT for display and editing

This commit is contained in:
t8y2 2026-06-17 00:49:33 +08:00
parent 3695042c8e
commit 63bb6a21ca
5 changed files with 362 additions and 308 deletions

View File

@ -801,6 +801,13 @@ pub fn format_grid_sql_literal(
if text.is_empty() {
return if database_type == Some(DatabaseType::SqlServer) { "N''" } else { "''" }.to_string();
}
// MySQL geometry columns: wrap WKT text with ST_GeomFromText()
if is_mysql_geometry_literal_database(database_type)
&& column_info.map(|column| is_geometry_column_type(&column.data_type)).unwrap_or(false)
{
let escaped = text.replace('\\', "\\\\").replace('\'', "''");
return format!("ST_GeomFromText('{}')", escaped);
}
let literal_text = if database_type == Some(DatabaseType::Tdengine) {
format_tdengine_timestamp_literal_text(&text)
} else if is_mysql_datetime_literal_database(database_type)
@ -832,6 +839,35 @@ fn is_bit_column_type(data_type: &str) -> bool {
lower.split(|ch: char| !ch.is_ascii_alphanumeric()).any(|token| token == "bit")
}
fn is_mysql_geometry_literal_database(database_type: Option<DatabaseType>) -> bool {
matches!(
database_type,
Some(
DatabaseType::Mysql
| DatabaseType::Doris
| DatabaseType::StarRocks
| DatabaseType::Goldendb
| DatabaseType::Sundb
)
)
}
fn is_geometry_column_type(data_type: &str) -> bool {
let lower = data_type.to_ascii_lowercase();
let base = lower.split('(').next().unwrap_or(&lower).trim();
matches!(
base,
"geometry"
| "point"
| "linestring"
| "polygon"
| "multipoint"
| "multilinestring"
| "multipolygon"
| "geometrycollection"
)
}
fn manticore_typed_attribute_value(text: &str, column_info: Option<&DataGridColumnInfo>) -> Option<Value> {
let data_type = column_info?.data_type.to_ascii_lowercase();
if is_boolean_type(&data_type) && text.eq_ignore_ascii_case("true") {

View File

@ -19,6 +19,7 @@ pub mod sqlserver;
pub mod ssh_tunnel;
pub mod transport_layer_tunnel;
pub mod turso_driver;
pub mod wkb;
use reqwest::ClientBuilder;
use std::future::Future;

View File

@ -284,7 +284,12 @@ fn mysql_value_to_json(row: &mysql_async::Row, idx: usize) -> serde_json::Value
return row_get::<Vec<u8>, _>(row, idx)
.map(|bytes| {
if matches!(column.column_type(), ColumnType::MYSQL_TYPE_GEOMETRY) {
mysql_blob_preview(&bytes, "GEOMETRY")
// MySQL prefixes geometry WKB with a 4-byte SRID.
// Strip it before passing to the WKB parser.
let wkb = if bytes.len() >= 4 { &bytes[4..] } else { &bytes };
super::wkb::wkb_to_wkt(wkb)
.map(serde_json::Value::String)
.unwrap_or_else(|| super::binary_value_to_json(&bytes))
} else {
mysql_bytes_to_json(bytes, column)
}

View File

@ -86,309 +86,6 @@ impl<'a> FromSql<'a> for PgRawBytes {
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct WkbDimensions {
has_z: bool,
has_m: bool,
}
impl WkbDimensions {
fn suffix(self) -> &'static str {
match (self.has_z, self.has_m) {
(false, false) => "",
(true, false) => " Z",
(false, true) => " M",
(true, true) => " ZM",
}
}
fn coordinate_len(self) -> usize {
2 + usize::from(self.has_z) + usize::from(self.has_m)
}
}
#[derive(Debug, Clone, PartialEq)]
enum WkbGeometry {
Point { dims: WkbDimensions, coords: Option<Vec<f64>> },
LineString { dims: WkbDimensions, points: Vec<Vec<f64>> },
Polygon { dims: WkbDimensions, rings: Vec<Vec<Vec<f64>>> },
MultiPoint { dims: WkbDimensions, points: Vec<Option<Vec<f64>>> },
MultiLineString { dims: WkbDimensions, lines: Vec<Vec<Vec<f64>>> },
MultiPolygon { dims: WkbDimensions, polygons: Vec<Vec<Vec<Vec<f64>>>> },
GeometryCollection { dims: WkbDimensions, geometries: Vec<WkbGeometry> },
}
impl WkbGeometry {
fn to_wkt(&self) -> String {
match self {
Self::Point { dims, coords } => match coords {
Some(coords) => format!("POINT{}({})", dims.suffix(), format_wkb_coordinate(coords)),
None => format!("POINT{} EMPTY", dims.suffix()),
},
Self::LineString { dims, points } => {
if points.is_empty() {
format!("LINESTRING{} EMPTY", dims.suffix())
} else {
format!("LINESTRING{}({})", dims.suffix(), format_wkb_coordinate_sequence(points))
}
}
Self::Polygon { dims, rings } => {
if rings.is_empty() {
format!("POLYGON{} EMPTY", dims.suffix())
} else {
format!(
"POLYGON{}({})",
dims.suffix(),
rings
.iter()
.map(|ring| format!("({})", format_wkb_coordinate_sequence(ring)))
.collect::<Vec<_>>()
.join(",")
)
}
}
Self::MultiPoint { dims, points } => {
if points.is_empty() {
format!("MULTIPOINT{} EMPTY", dims.suffix())
} else {
format!(
"MULTIPOINT{}({})",
dims.suffix(),
points
.iter()
.map(|point| match point {
Some(coords) => format!("({})", format_wkb_coordinate(coords)),
None => "EMPTY".to_string(),
})
.collect::<Vec<_>>()
.join(",")
)
}
}
Self::MultiLineString { dims, lines } => {
if lines.is_empty() {
format!("MULTILINESTRING{} EMPTY", dims.suffix())
} else {
format!(
"MULTILINESTRING{}({})",
dims.suffix(),
lines
.iter()
.map(|line| format!("({})", format_wkb_coordinate_sequence(line)))
.collect::<Vec<_>>()
.join(",")
)
}
}
Self::MultiPolygon { dims, polygons } => {
if polygons.is_empty() {
format!("MULTIPOLYGON{} EMPTY", dims.suffix())
} else {
format!(
"MULTIPOLYGON{}({})",
dims.suffix(),
polygons
.iter()
.map(|polygon| {
format!(
"({})",
polygon
.iter()
.map(|ring| format!("({})", format_wkb_coordinate_sequence(ring)))
.collect::<Vec<_>>()
.join(",")
)
})
.collect::<Vec<_>>()
.join(",")
)
}
}
Self::GeometryCollection { dims, geometries } => {
if geometries.is_empty() {
format!("GEOMETRYCOLLECTION{} EMPTY", dims.suffix())
} else {
format!(
"GEOMETRYCOLLECTION{}({})",
dims.suffix(),
geometries.iter().map(WkbGeometry::to_wkt).collect::<Vec<_>>().join(",")
)
}
}
}
}
}
fn format_wkb_coordinate(coords: &[f64]) -> String {
coords.iter().map(|value| value.to_string()).collect::<Vec<_>>().join(" ")
}
fn format_wkb_coordinate_sequence(points: &[Vec<f64>]) -> String {
points.iter().map(|point| format_wkb_coordinate(point)).collect::<Vec<_>>().join(",")
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum WkbEndian {
Big,
Little,
}
struct WkbReader<'a> {
raw: &'a [u8],
pos: usize,
}
impl<'a> WkbReader<'a> {
fn new(raw: &'a [u8]) -> Self {
Self { raw, pos: 0 }
}
fn read_u8(&mut self) -> Option<u8> {
let value = *self.raw.get(self.pos)?;
self.pos += 1;
Some(value)
}
fn read_array<const N: usize>(&mut self) -> Option<[u8; N]> {
let end = self.pos.checked_add(N)?;
let bytes: [u8; N] = self.raw.get(self.pos..end)?.try_into().ok()?;
self.pos = end;
Some(bytes)
}
fn read_u32(&mut self, endian: WkbEndian) -> Option<u32> {
let bytes = self.read_array::<4>()?;
Some(match endian {
WkbEndian::Big => u32::from_be_bytes(bytes),
WkbEndian::Little => u32::from_le_bytes(bytes),
})
}
fn read_f64(&mut self, endian: WkbEndian) -> Option<f64> {
let bytes = self.read_array::<8>()?;
Some(match endian {
WkbEndian::Big => f64::from_be_bytes(bytes),
WkbEndian::Little => f64::from_le_bytes(bytes),
})
}
}
fn parse_wkb_dimensions(type_word: u32) -> (u32, WkbDimensions, bool) {
let mut base_type = type_word & 0x1FFF_FFFF;
let mut dims = WkbDimensions { has_z: (type_word & 0x8000_0000) != 0, has_m: (type_word & 0x4000_0000) != 0 };
let has_srid = (type_word & 0x2000_0000) != 0;
if base_type >= 3000 {
dims.has_z = true;
dims.has_m = true;
base_type -= 3000;
} else if base_type >= 2000 {
dims.has_m = true;
base_type -= 2000;
} else if base_type >= 1000 {
dims.has_z = true;
base_type -= 1000;
}
(base_type, dims, has_srid)
}
fn read_wkb_point_coords(reader: &mut WkbReader<'_>, dims: WkbDimensions, allow_empty_point: bool) -> Option<Vec<f64>> {
let endian = match reader.read_u8()? {
0 => WkbEndian::Big,
1 => WkbEndian::Little,
_ => return None,
};
let type_word = reader.read_u32(endian)?;
let (base_type, parsed_dims, has_srid) = parse_wkb_dimensions(type_word);
if base_type != 1 || parsed_dims != dims {
return None;
}
if has_srid {
reader.read_u32(endian)?;
}
let coords = (0..parsed_dims.coordinate_len()).map(|_| reader.read_f64(endian)).collect::<Option<Vec<_>>>()?;
if allow_empty_point && coords.iter().all(|value| value.is_nan()) {
return None;
}
Some(coords)
}
fn parse_wkb_points(reader: &mut WkbReader<'_>, endian: WkbEndian, dims: WkbDimensions) -> Option<Vec<Vec<f64>>> {
let count = usize::try_from(reader.read_u32(endian)?).ok()?;
(0..count)
.map(|_| (0..dims.coordinate_len()).map(|_| reader.read_f64(endian)).collect::<Option<Vec<_>>>())
.collect::<Option<Vec<_>>>()
}
fn parse_wkb_geometry(reader: &mut WkbReader<'_>) -> Option<WkbGeometry> {
let endian = match reader.read_u8()? {
0 => WkbEndian::Big,
1 => WkbEndian::Little,
_ => return None,
};
let type_word = reader.read_u32(endian)?;
let (base_type, dims, has_srid) = parse_wkb_dimensions(type_word);
if has_srid {
reader.read_u32(endian)?;
}
match base_type {
1 => {
let coords = (0..dims.coordinate_len()).map(|_| reader.read_f64(endian)).collect::<Option<Vec<_>>>()?;
let coords = if coords.iter().all(|value| value.is_nan()) { None } else { Some(coords) };
Some(WkbGeometry::Point { dims, coords })
}
2 => Some(WkbGeometry::LineString { dims, points: parse_wkb_points(reader, endian, dims)? }),
3 => {
let ring_count = usize::try_from(reader.read_u32(endian)?).ok()?;
let rings = (0..ring_count).map(|_| parse_wkb_points(reader, endian, dims)).collect::<Option<Vec<_>>>()?;
Some(WkbGeometry::Polygon { dims, rings })
}
4 => {
let count = usize::try_from(reader.read_u32(endian)?).ok()?;
let points =
(0..count).map(|_| Some(read_wkb_point_coords(reader, dims, true))).collect::<Option<Vec<_>>>()?;
Some(WkbGeometry::MultiPoint { dims, points })
}
5 => {
let count = usize::try_from(reader.read_u32(endian)?).ok()?;
let lines = (0..count)
.map(|_| match parse_wkb_geometry(reader)? {
WkbGeometry::LineString { points, .. } => Some(points),
_ => None,
})
.collect::<Option<Vec<_>>>()?;
Some(WkbGeometry::MultiLineString { dims, lines })
}
6 => {
let count = usize::try_from(reader.read_u32(endian)?).ok()?;
let polygons = (0..count)
.map(|_| match parse_wkb_geometry(reader)? {
WkbGeometry::Polygon { rings, .. } => Some(rings),
_ => None,
})
.collect::<Option<Vec<_>>>()?;
Some(WkbGeometry::MultiPolygon { dims, polygons })
}
7 => {
let count = usize::try_from(reader.read_u32(endian)?).ok()?;
let geometries = (0..count).map(|_| parse_wkb_geometry(reader)).collect::<Option<Vec<_>>>()?;
Some(WkbGeometry::GeometryCollection { dims, geometries })
}
_ => None,
}
}
fn ewkb_to_wkt(raw: &[u8]) -> Option<String> {
let mut reader = WkbReader::new(raw);
let geometry = parse_wkb_geometry(&mut reader)?;
if reader.pos != raw.len() {
return None;
}
Some(geometry.to_wkt())
}
/// Decode pgvector binary format into a Vec<f32>.
///
/// pgvector binary layout (big-endian):
@ -575,7 +272,7 @@ fn pg_value_to_json(row: &Row, idx: usize, type_name: &str) -> serde_json::Value
if upper == "GEOMETRY" || upper == "GEOGRAPHY" {
if let Ok(PgRawBytes(raw)) = row.try_get::<_, PgRawBytes>(idx) {
return ewkb_to_wkt(&raw)
return super::wkb::wkb_to_wkt(&raw)
.map(serde_json::Value::String)
.unwrap_or_else(|| super::binary_value_to_json(&raw));
}
@ -2136,7 +1833,7 @@ mod tests {
#[test]
fn ewkb_point_with_srid_formats_as_wkt() {
let raw = decode_hex("0101000020E6100000C520B07268195D404E62105839F44340");
assert_eq!(ewkb_to_wkt(&raw), Some("POINT(116.397 39.908)".to_string()));
assert_eq!(super::super::wkb::wkb_to_wkt(&raw), Some("POINT(116.397 39.908)".to_string()));
}
#[test]
@ -2145,7 +1842,7 @@ mod tests {
"0106000020E610000002000000010300000001000000050000000000000000005D4000000000000044400000000000405D4000000000000044400000000000405D4000000000008044400000000000005D4000000000008044400000000000005D400000000000004440010300000001000000050000000000000000805D4000000000008043400000000000C05D4000000000008043400000000000C05D4000000000000044400000000000805D4000000000000044400000000000805D400000000000804340",
);
assert_eq!(
ewkb_to_wkt(&raw),
super::super::wkb::wkb_to_wkt(&raw),
Some(
"MULTIPOLYGON(((116 40,117 40,117 41,116 41,116 40)),((118 39,119 39,119 40,118 40,118 39)))"
.to_string()
@ -2158,7 +1855,10 @@ mod tests {
let raw = decode_hex(
"0107000020E61000000200000001010000000000000000005D4000000000000044400102000000020000000000000000405D4000000000008044400000000000805D400000000000004540",
);
assert_eq!(ewkb_to_wkt(&raw), Some("GEOMETRYCOLLECTION(POINT(116 40),LINESTRING(117 41,118 42))".to_string()));
assert_eq!(
super::super::wkb::wkb_to_wkt(&raw),
Some("GEOMETRYCOLLECTION(POINT(116 40),LINESTRING(117 41,118 42))".to_string())
);
}
#[test]

View File

@ -0,0 +1,312 @@
/// Shared WKB (Well-Known Binary) geometry parsing shared by PostgreSQL (EWKB)
/// and MySQL (standard WKB). Parses WKB bytes into WKT (Well-Known Text) strings.
///
/// Supports: POINT, LINESTRING, POLYGON, MULTIPOINT, MULTILINESTRING,
/// MULTIPOLYGON, GEOMETRYCOLLECTION, and their Z/M/ZM variants.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct WkbDimensions {
has_z: bool,
has_m: bool,
}
impl WkbDimensions {
fn suffix(self) -> &'static str {
match (self.has_z, self.has_m) {
(false, false) => "",
(true, false) => " Z",
(false, true) => " M",
(true, true) => " ZM",
}
}
fn coordinate_len(self) -> usize {
2 + usize::from(self.has_z) + usize::from(self.has_m)
}
}
#[derive(Debug, Clone, PartialEq)]
enum WkbGeometry {
Point { dims: WkbDimensions, coords: Option<Vec<f64>> },
LineString { dims: WkbDimensions, points: Vec<Vec<f64>> },
Polygon { dims: WkbDimensions, rings: Vec<Vec<Vec<f64>>> },
MultiPoint { dims: WkbDimensions, points: Vec<Option<Vec<f64>>> },
MultiLineString { dims: WkbDimensions, lines: Vec<Vec<Vec<f64>>> },
MultiPolygon { dims: WkbDimensions, polygons: Vec<Vec<Vec<Vec<f64>>>> },
GeometryCollection { dims: WkbDimensions, geometries: Vec<WkbGeometry> },
}
impl WkbGeometry {
fn to_wkt(&self) -> String {
match self {
Self::Point { dims, coords } => match coords {
Some(coords) => format!("POINT{}({})", dims.suffix(), format_wkb_coordinate(coords)),
None => format!("POINT{} EMPTY", dims.suffix()),
},
Self::LineString { dims, points } => {
if points.is_empty() {
format!("LINESTRING{} EMPTY", dims.suffix())
} else {
format!("LINESTRING{}({})", dims.suffix(), format_wkb_coordinate_sequence(points))
}
}
Self::Polygon { dims, rings } => {
if rings.is_empty() {
format!("POLYGON{} EMPTY", dims.suffix())
} else {
format!(
"POLYGON{}({})",
dims.suffix(),
rings
.iter()
.map(|ring| format!("({})", format_wkb_coordinate_sequence(ring)))
.collect::<Vec<_>>()
.join(",")
)
}
}
Self::MultiPoint { dims, points } => {
if points.is_empty() {
format!("MULTIPOINT{} EMPTY", dims.suffix())
} else {
format!(
"MULTIPOINT{}({})",
dims.suffix(),
points
.iter()
.map(|point| match point {
Some(coords) => format!("({})", format_wkb_coordinate(coords)),
None => "EMPTY".to_string(),
})
.collect::<Vec<_>>()
.join(",")
)
}
}
Self::MultiLineString { dims, lines } => {
if lines.is_empty() {
format!("MULTILINESTRING{} EMPTY", dims.suffix())
} else {
format!(
"MULTILINESTRING{}({})",
dims.suffix(),
lines
.iter()
.map(|line| format!("({})", format_wkb_coordinate_sequence(line)))
.collect::<Vec<_>>()
.join(",")
)
}
}
Self::MultiPolygon { dims, polygons } => {
if polygons.is_empty() {
format!("MULTIPOLYGON{} EMPTY", dims.suffix())
} else {
format!(
"MULTIPOLYGON{}({})",
dims.suffix(),
polygons
.iter()
.map(|polygon| {
format!(
"({})",
polygon
.iter()
.map(|ring| format!("({})", format_wkb_coordinate_sequence(ring)))
.collect::<Vec<_>>()
.join(",")
)
})
.collect::<Vec<_>>()
.join(",")
)
}
}
Self::GeometryCollection { dims, geometries } => {
if geometries.is_empty() {
format!("GEOMETRYCOLLECTION{} EMPTY", dims.suffix())
} else {
format!(
"GEOMETRYCOLLECTION{}({})",
dims.suffix(),
geometries.iter().map(WkbGeometry::to_wkt).collect::<Vec<_>>().join(",")
)
}
}
}
}
}
fn format_wkb_coordinate(coords: &[f64]) -> String {
coords.iter().map(|value| value.to_string()).collect::<Vec<_>>().join(" ")
}
fn format_wkb_coordinate_sequence(points: &[Vec<f64>]) -> String {
points.iter().map(|point| format_wkb_coordinate(point)).collect::<Vec<_>>().join(",")
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum WkbEndian {
Big,
Little,
}
struct WkbReader<'a> {
raw: &'a [u8],
pos: usize,
}
impl<'a> WkbReader<'a> {
fn new(raw: &'a [u8]) -> Self {
Self { raw, pos: 0 }
}
fn read_u8(&mut self) -> Option<u8> {
let value = *self.raw.get(self.pos)?;
self.pos += 1;
Some(value)
}
fn read_array<const N: usize>(&mut self) -> Option<[u8; N]> {
let end = self.pos.checked_add(N)?;
let bytes: [u8; N] = self.raw.get(self.pos..end)?.try_into().ok()?;
self.pos = end;
Some(bytes)
}
fn read_u32(&mut self, endian: WkbEndian) -> Option<u32> {
let bytes = self.read_array::<4>()?;
Some(match endian {
WkbEndian::Big => u32::from_be_bytes(bytes),
WkbEndian::Little => u32::from_le_bytes(bytes),
})
}
fn read_f64(&mut self, endian: WkbEndian) -> Option<f64> {
let bytes = self.read_array::<8>()?;
Some(match endian {
WkbEndian::Big => f64::from_be_bytes(bytes),
WkbEndian::Little => f64::from_le_bytes(bytes),
})
}
}
fn parse_wkb_dimensions(type_word: u32) -> (u32, WkbDimensions, bool) {
let mut base_type = type_word & 0x1FFF_FFFF;
let mut dims = WkbDimensions { has_z: (type_word & 0x8000_0000) != 0, has_m: (type_word & 0x4000_0000) != 0 };
let has_srid = (type_word & 0x2000_0000) != 0;
if base_type >= 3000 {
dims.has_z = true;
dims.has_m = true;
base_type -= 3000;
} else if base_type >= 2000 {
dims.has_m = true;
base_type -= 2000;
} else if base_type >= 1000 {
dims.has_z = true;
base_type -= 1000;
}
(base_type, dims, has_srid)
}
fn read_wkb_point_coords(reader: &mut WkbReader<'_>, dims: WkbDimensions, allow_empty_point: bool) -> Option<Vec<f64>> {
let endian = match reader.read_u8()? {
0 => WkbEndian::Big,
1 => WkbEndian::Little,
_ => return None,
};
let type_word = reader.read_u32(endian)?;
let (base_type, parsed_dims, has_srid) = parse_wkb_dimensions(type_word);
if base_type != 1 || parsed_dims != dims {
return None;
}
if has_srid {
reader.read_u32(endian)?;
}
let coords = (0..parsed_dims.coordinate_len()).map(|_| reader.read_f64(endian)).collect::<Option<Vec<_>>>()?;
if allow_empty_point && coords.iter().all(|value| value.is_nan()) {
return None;
}
Some(coords)
}
fn parse_wkb_points(reader: &mut WkbReader<'_>, endian: WkbEndian, dims: WkbDimensions) -> Option<Vec<Vec<f64>>> {
let count = usize::try_from(reader.read_u32(endian)?).ok()?;
(0..count)
.map(|_| (0..dims.coordinate_len()).map(|_| reader.read_f64(endian)).collect::<Option<Vec<_>>>())
.collect::<Option<Vec<_>>>()
}
fn parse_wkb_geometry(reader: &mut WkbReader<'_>) -> Option<WkbGeometry> {
let endian = match reader.read_u8()? {
0 => WkbEndian::Big,
1 => WkbEndian::Little,
_ => return None,
};
let type_word = reader.read_u32(endian)?;
let (base_type, dims, has_srid) = parse_wkb_dimensions(type_word);
if has_srid {
reader.read_u32(endian)?;
}
match base_type {
1 => {
let coords = (0..dims.coordinate_len()).map(|_| reader.read_f64(endian)).collect::<Option<Vec<_>>>()?;
let coords = if coords.iter().all(|value| value.is_nan()) { None } else { Some(coords) };
Some(WkbGeometry::Point { dims, coords })
}
2 => Some(WkbGeometry::LineString { dims, points: parse_wkb_points(reader, endian, dims)? }),
3 => {
let ring_count = usize::try_from(reader.read_u32(endian)?).ok()?;
let rings = (0..ring_count).map(|_| parse_wkb_points(reader, endian, dims)).collect::<Option<Vec<_>>>()?;
Some(WkbGeometry::Polygon { dims, rings })
}
4 => {
let count = usize::try_from(reader.read_u32(endian)?).ok()?;
let points =
(0..count).map(|_| Some(read_wkb_point_coords(reader, dims, true))).collect::<Option<Vec<_>>>()?;
Some(WkbGeometry::MultiPoint { dims, points })
}
5 => {
let count = usize::try_from(reader.read_u32(endian)?).ok()?;
let lines = (0..count)
.map(|_| match parse_wkb_geometry(reader)? {
WkbGeometry::LineString { points, .. } => Some(points),
_ => None,
})
.collect::<Option<Vec<_>>>()?;
Some(WkbGeometry::MultiLineString { dims, lines })
}
6 => {
let count = usize::try_from(reader.read_u32(endian)?).ok()?;
let polygons = (0..count)
.map(|_| match parse_wkb_geometry(reader)? {
WkbGeometry::Polygon { rings, .. } => Some(rings),
_ => None,
})
.collect::<Option<Vec<_>>>()?;
Some(WkbGeometry::MultiPolygon { dims, polygons })
}
7 => {
let count = usize::try_from(reader.read_u32(endian)?).ok()?;
let geometries = (0..count).map(|_| parse_wkb_geometry(reader)).collect::<Option<Vec<_>>>()?;
Some(WkbGeometry::GeometryCollection { dims, geometries })
}
_ => None,
}
}
/// Parse WKB/EWKB bytes into a WKT (Well-Known Text) string.
///
/// Supports both standard WKB (MySQL) and Extended WKB (PostgreSQL/PostGIS).
/// Returns `None` if the bytes cannot be parsed as valid WKB.
pub(crate) fn wkb_to_wkt(raw: &[u8]) -> Option<String> {
let mut reader = WkbReader::new(raw);
let geometry = parse_wkb_geometry(&mut reader)?;
if reader.pos != raw.len() {
return None;
}
Some(geometry.to_wkt())
}