kenro 0.3.0

SpatiaLite-style spatial SQL for SQLite in pure Rust — PostGIS-compatible ST_ functions, GeoPackage R-tree, CRS transform, H3, MVT. Use via rusqlite, loadable extension, or WASM
Documentation
//! Geometry constructors and output functions.

use crate::error::Result;
use crate::geom::{self, Geom};
use crate::gpb::{self, GpbHeader};

/// `ST_GeomFromText(wkt [, srid])` → canonical GPB.
pub fn st_geom_from_text(wkt: &str, srid: Option<i32>) -> Result<Vec<u8>> {
    let geom = geom::decode_wkt(wkt, srid.unwrap_or(0))?;
    geom::encode_canonical_gpb(&geom, "ST_GeomFromText")
}

/// `ST_GeomFromWKB(wkb [, srid])` → canonical GPB. Accepts ISO WKB and EWKB
/// (and, leniently, GPB); an explicit srid argument overrides an embedded one
/// (PostGIS behavior).
pub fn st_geom_from_wkb(bytes: &[u8], srid: Option<i32>) -> Result<Vec<u8>> {
    let mut geom = if gpb::is_gpb(bytes) {
        geom::decode_gpb(bytes)?.1
    } else {
        geom::decode_wkb(bytes, srid)?
    };
    if let Some(srid) = srid {
        geom.srid = srid;
    }
    geom::encode_canonical_gpb(&geom, "ST_GeomFromWKB")
}

/// `ST_GeomFromGPB(gpb)` → validated, normalized GPB (little-endian header,
/// envelope stripped). The WKB payload is passed through byte-for-byte after
/// being validated, so 3D payloads survive losslessly.
pub fn st_geom_from_gpb(bytes: &[u8]) -> Result<Vec<u8>> {
    // Validated by walking the encoding rather than decoding it. Both routes
    // check that every element count is backed by real bytes; only this one
    // also accepts a surface collection, which `decode_gpb` refuses — and a
    // byte-level normalizer has no business refusing it.
    crate::coords::map_coords(bytes, &mut |_| {})
}

/// `ST_AsText(geom)` → WKT.
pub fn st_as_text(bytes: &[u8]) -> Result<String> {
    decoded::st_as_text(&geom::decode_auto(bytes)?)
}

/// `ST_AsBinary(geom)` → ISO WKB, little-endian, SRID dropped (as in PostGIS).
pub fn st_as_binary(bytes: &[u8]) -> Result<Vec<u8>> {
    decoded::st_as_binary(&geom::decode_auto(bytes)?)
}

/// `ST_AsGPB(geom)` → storage-grade GPB (XY envelope for non-point
/// geometries), preserving srid.
pub fn st_as_gpb(bytes: &[u8]) -> Result<Vec<u8>> {
    // A surface collection cannot be decoded, but it can still be stored: the
    // envelope its R-tree needs is the 2D box over its patches, which
    // `functions::surface` reads straight from the encoding.
    if let Some(env) = crate::functions::surface::envelope(bytes)? {
        let (payload, srid) = if gpb::is_gpb(bytes) {
            let header = GpbHeader::parse(bytes)?;
            (&bytes[header.wkb_offset..], header.srid)
        } else {
            (bytes, geom::srid_of(bytes)?)
        };
        return Ok(gpb::write_gpb(
            payload,
            srid,
            Some(gpb::Envelope {
                min_x: env.0,
                min_y: env.1,
                max_x: env.2,
                max_y: env.3,
            }),
            false,
        ));
    }
    decoded::st_as_gpb(&geom::decode_auto(bytes)?)
}

/// Serialization for a geometry that is already decoded — see
/// [`crate::functions::predicates::decoded`] for why that exists. These are
/// one-liners over `geom::encode_*`; they exist so the function name in an
/// error message is the SQL one either way.
pub mod decoded {
    use super::*;

    pub fn st_as_text(geom: &Geom) -> Result<String> {
        geom::encode_wkt(geom, "ST_AsText")
    }

    pub fn st_as_binary(geom: &Geom) -> Result<Vec<u8>> {
        geom::encode_wkb(geom, "ST_AsBinary")
    }

    pub fn st_as_gpb(geom: &Geom) -> Result<Vec<u8>> {
        geom::encode_storage_gpb(geom, "ST_AsGPB")
    }
}

/// `ST_SetSRID(geom, srid)` — relabel only, coordinates unchanged. Operates
/// at the byte level so 3D payloads survive losslessly.
pub fn st_set_srid(bytes: &[u8], srid: i32) -> Result<Vec<u8>> {
    if gpb::is_gpb(bytes) {
        let header = GpbHeader::parse(bytes)?;
        Ok(gpb::write_gpb(
            &bytes[header.wkb_offset..],
            srid,
            None,
            header.empty,
        ))
    } else {
        // Structural validation without decoding, so relabelling a surface
        // collection works too. `threed.rs` promises that ST_SetSRID carries a
        // payload across byte-for-byte; decoding to validate broke that promise
        // for exactly the geometries the promise was written for.
        crate::coords::map_coords_relabelled(bytes, srid, &mut |_| {})
    }
}

/// `ST_SRID(geom)` — 0 means unknown, as in PostGIS/GeoPackage.
pub fn st_srid(bytes: &[u8]) -> Result<i32> {
    // From the header, not from a decoded value: a surface collection carries
    // an SRID like anything else, and asking for it should not require a
    // geometry model that cannot hold it.
    geom::srid_of(bytes)
}

/// `ST_MakePoint(x, y)` — POINT with unknown SRID. (The 3/4-arg z/m forms
/// are not shipped; kenro is 2D.)
pub fn st_make_point(x: f64, y: f64) -> Result<Vec<u8>> {
    st_point(x, y, None)
}

/// `ST_MakePoint(x, y, z)` — a 3D point, no SRID.
///
/// The smallest possible use of the XYZ writer: one coordinate, and the height
/// is an argument rather than something looked up. (PostGIS also has the
/// four-argument XYZM form; kenro cannot write an M, so it is not here.)
pub fn st_make_point_z(x: f64, y: f64, z: f64) -> Result<Vec<u8>> {
    const FUNC: &str = "ST_MakePoint";
    let point = geo_types::Geometry::Point(geo_types::Point::new(x, y));
    let wkb = crate::coords::write_wkb_z(&point, &crate::coords::ZIndex::constant(z), FUNC)?;
    Ok(gpb::write_gpb(&wkb, 0, None, false))
}

/// `ST_Point(x, y [, srid])` — like ST_MakePoint; the srid arity is
/// PostGIS ≥ 3.2.
pub fn st_point(x: f64, y: f64, srid: Option<i32>) -> Result<Vec<u8>> {
    geom::encode_canonical_gpb(
        &crate::geom::Geom {
            geometry: geo_types::Geometry::Point(geo_types::Point::new(x, y)),
            srid: srid.unwrap_or(0),
            has_zm: false,
        },
        "ST_Point",
    )
}

/// `ST_MakeEnvelope(xmin, ymin, xmax, ymax [, srid])` — rectangular
/// POLYGON in PostGIS vertex order; degenerate envelopes stay polygons
/// (unlike ST_Envelope's output collapsing).
pub fn st_make_envelope(
    xmin: f64,
    ymin: f64,
    xmax: f64,
    ymax: f64,
    srid: Option<i32>,
) -> Result<Vec<u8>> {
    use geo_types::{Geometry, LineString, Polygon, coord};
    let ring = LineString::new(vec![
        coord! { x: xmin, y: ymin },
        coord! { x: xmin, y: ymax },
        coord! { x: xmax, y: ymax },
        coord! { x: xmax, y: ymin },
        coord! { x: xmin, y: ymin },
    ]);
    geom::encode_canonical_gpb(
        &crate::geom::Geom {
            geometry: Geometry::Polygon(Polygon::new(ring, vec![])),
            srid: srid.unwrap_or(0),
            has_zm: false,
        },
        "ST_MakeEnvelope",
    )
}

/// Re-exported for binding layers that want header-only access.
pub use crate::gpb::is_gpb;

#[allow(unused)]
pub fn parse_gpb_header(bytes: &[u8]) -> Result<GpbHeader> {
    GpbHeader::parse(bytes)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn constructors_return_canonical_gpb() {
        let blob = st_geom_from_text("POINT(1 2)", Some(4326)).unwrap();
        let h = GpbHeader::parse(&blob).unwrap();
        assert_eq!(h.srid, 4326);
        assert_eq!(h.envelope, None);
        assert!(!h.empty);
        assert_eq!(st_as_text(&blob).unwrap(), "POINT(1 2)");
    }

    #[test]
    fn from_wkb_srid_override() {
        let wkb = st_as_binary(&st_geom_from_text("POINT(1 2)", Some(4326)).unwrap()).unwrap();
        let h = GpbHeader::parse(&st_geom_from_wkb(&wkb, None).unwrap()).unwrap();
        assert_eq!(h.srid, 0); // ISO WKB carries no srid
        let h = GpbHeader::parse(&st_geom_from_wkb(&wkb, Some(3857)).unwrap()).unwrap();
        assert_eq!(h.srid, 3857);
    }

    #[test]
    fn from_gpb_normalizes_and_strips_envelope() {
        let stored =
            st_as_gpb(&st_geom_from_text("POLYGON((0 0,4 0,4 4,0 4,0 0))", None).unwrap()).unwrap();
        assert!(GpbHeader::parse(&stored).unwrap().envelope.is_some());
        let normalized = st_geom_from_gpb(&stored).unwrap();
        assert_eq!(GpbHeader::parse(&normalized).unwrap().envelope, None);
        assert_eq!(
            st_as_text(&normalized).unwrap(),
            "POLYGON((0 0,4 0,4 4,0 4,0 0))"
        );
    }

    #[test]
    fn as_gpb_envelope_policy() {
        // Points: no envelope.
        let p = st_as_gpb(&st_geom_from_text("POINT(1 2)", None).unwrap()).unwrap();
        assert_eq!(GpbHeader::parse(&p).unwrap().envelope, None);
        // Non-point: XY envelope present and correct.
        let l = st_as_gpb(&st_geom_from_text("LINESTRING(0 0,10 5)", None).unwrap()).unwrap();
        let env = GpbHeader::parse(&l).unwrap().envelope.unwrap();
        assert_eq!(
            (env.min_x, env.max_x, env.min_y, env.max_y),
            (0.0, 10.0, 0.0, 5.0)
        );
        // Empty: empty flag, no envelope.
        let e = st_as_gpb(&st_geom_from_text("LINESTRING EMPTY", None).unwrap()).unwrap();
        let h = GpbHeader::parse(&e).unwrap();
        assert!(h.empty);
        assert_eq!(h.envelope, None);
    }

    #[test]
    fn empty_wkt_roundtrip() {
        for wkt in [
            "LINESTRING EMPTY",
            "POLYGON EMPTY",
            "MULTIPOINT EMPTY",
            "MULTILINESTRING EMPTY",
            "MULTIPOLYGON EMPTY",
            "GEOMETRYCOLLECTION EMPTY",
        ] {
            let blob = st_geom_from_text(wkt, None).unwrap();
            assert!(GpbHeader::parse(&blob).unwrap().empty, "{wkt}");
            assert_eq!(st_as_text(&blob).unwrap(), wkt);
        }
    }

    #[test]
    fn garbage_input_errors() {
        assert!(st_as_text(&[0xFF, 0x00, 0x01]).is_err());
        assert!(st_geom_from_gpb(&[0x01, 0x01, 0x00]).is_err()); // WKB is not GPB
        assert!(st_geom_from_text("POINT(a b)", None).is_err());
    }
}