kenro 0.4.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
//! Golden vectors for `ST_QuantizeCoordinates`, compared **bit-exactly**.
//!
//! `tests/golden/quantize.jsonl` is generated by `scripts/golden/quantize.sql`
//! against the reference PostGIS. Both the operand and the expectation are
//! hex EWKB, because the whole function is "zero these mantissa bits" — a
//! tolerance comparison would pass an implementation that got the bit count
//! wrong by one, which is exactly the failure mode `docs/scope.md` used to
//! cite as the reason not to ship this at all.
//!
//! The rule is PostGIS's `trim_preserve_decimal_digits`, reproduced in
//! `functions::edit`. It was confirmed here, not assumed: the sweep is 31
//! sentinel values (subnormals, ±0, `f64::MIN_POSITIVE`, `f64::MAX`,
//! powers of ten from 1e-320 to 1e300) crossed with `prec` −30…40, which is
//! well past the clamps at both ends, plus the per-ordinate arities and the
//! structural cases.
//!
//! The one deliberate divergence is a geometry carrying M: PostGIS quantizes
//! it at `prec_x`, kenro raises rather than returning a geometry whose M is
//! silently un-quantized. That vector carries `kenro_expected`.

mod common;

use kenro::functions::{edit, io};

/// PostGIS hex EWKB → the bytes kenro's functions take.
fn from_hex(hex: &str) -> Vec<u8> {
    (0..hex.len())
        .step_by(2)
        .map(|i| u8::from_str_radix(&hex[i..i + 2], 16).unwrap())
        .collect()
}

/// Every ordinate of an encoded geometry, as raw `u64` bit patterns, in
/// walk order.
///
/// This is the comparison, and it has to be at this level for two reasons.
/// It is *bit*-exact — quantization is a mask on the mantissa, so anything
/// that goes through a float printer or a tolerance would pass a
/// one-bit-wrong implementation, which is precisely what `docs/scope.md`
/// used to cite as the reason not to ship the function. And it compares the
/// two sides on the only thing they can agree about: kenro answers a
/// canonical GeoPackage blob while PostGIS answers EWKB, and `ST_AsEWKB`
/// refuses a geometry with a Z, so there is no common serialization to diff.
/// `coords` reads both encodings, so the ordinates line up regardless.
fn ordinate_bits(bytes: &[u8]) -> Vec<u64> {
    let mut out = Vec::new();
    kenro::coords::for_each_coord(bytes, &mut |c| {
        out.push(c.x.to_bits());
        out.push(c.y.to_bits());
        if let Some(z) = c.z {
            out.push(z.to_bits());
        }
    })
    .unwrap();
    out
}

#[test]
fn golden_quantize() {
    let (mut sweep, mut xy, mut xyz, mut errors) = (0usize, 0usize, 0usize, 0usize);
    for v in common::load("quantize") {
        let operand = from_hex(v.a.as_deref().unwrap_or_else(|| panic!("{}: no `a`", v.id)));
        let args = v
            .args
            .as_ref()
            .unwrap_or_else(|| panic!("{}: no args", v.id));
        let p = |i: usize| args[i] as i32;
        let got = match v.func.as_str() {
            "quantize" => {
                sweep += 1;
                edit::st_quantize_coordinates(&operand, p(0), None, None)
            }
            "quantize_xy" => {
                xy += 1;
                edit::st_quantize_coordinates(&operand, p(0), Some(p(1)), None)
            }
            "quantize_xyz" => {
                xyz += 1;
                edit::st_quantize_coordinates(&operand, p(0), Some(p(1)), Some(p(2)))
            }
            other => panic!("unknown fn {other} in the quantize suite"),
        };
        let expected = v.effective();
        if expected.get("error").is_some() {
            errors += 1;
            assert!(got.is_err(), "{}: expected an error, got {got:?}", v.id);
            continue;
        }
        let want = from_hex(expected.as_str().unwrap_or_else(|| panic!("{}", v.id)));
        let got = got.unwrap_or_else(|e| panic!("{}: {e}", v.id));
        assert_eq!(
            ordinate_bits(&got),
            ordinate_bits(&want),
            "{}: ordinate bits differ",
            v.id
        );
    }
    assert!(sweep > 2000, "only {sweep} sweep vectors ran");
    assert!(xy == 3 && xyz == 3, "{xy} xy, {xyz} xyz");
    assert_eq!(errors, 1, "the M divergence vector did not run");
}

/// The property that makes the function useful, stated independently of the
/// reference: quantizing never moves an ordinate further than the precision
/// it was asked to preserve.
#[test]
fn every_ordinate_stays_within_its_stated_precision() {
    for x in [
        1.0f64,
        0.1,
        std::f64::consts::PI,
        123.456789,
        -98765.4321,
        1e-5,
        6.02214076e23,
    ] {
        for prec in 0..=17i32 {
            let g = io::st_geom_from_text(&format!("POINT({x} 0)"), None).unwrap();
            let q = edit::st_quantize_coordinates(&g, prec, None, None).unwrap();
            let out: f64 = io::st_as_text(&q)
                .unwrap()
                .trim_start_matches("POINT(")
                .split(' ')
                .next()
                .unwrap()
                .parse()
                .unwrap();
            let tolerance = 10f64.powi(-prec) * x.abs().max(1.0);
            assert!(
                (out - x).abs() <= tolerance,
                "x={x} prec={prec}: {out} is further than {tolerance} away"
            );
        }
    }
}

/// Quantizing twice at the same precision changes nothing the second time —
/// the mask is idempotent — and quantizing at a wider precision after a
/// narrower one cannot bring bits back.
#[test]
fn quantization_is_idempotent_and_monotone() {
    let g = io::st_geom_from_text(
        "LINESTRING(3.14159265 2.71828182,1.41421356 1.73205080)",
        None,
    )
    .unwrap();
    let once = edit::st_quantize_coordinates(&g, 4, None, None).unwrap();
    let twice = edit::st_quantize_coordinates(&once, 4, None, None).unwrap();
    assert_eq!(ordinate_bits(&once), ordinate_bits(&twice));
    let widened = edit::st_quantize_coordinates(&once, 12, None, None).unwrap();
    assert_eq!(ordinate_bits(&once), ordinate_bits(&widened));
}

/// Through SQL, on every arity, with the SRID surviving.
#[test]
fn the_sql_surface() {
    let conn = rusqlite::Connection::open_in_memory().unwrap();
    kenro::register(&conn).unwrap();
    let wkt: String = conn
        .query_row(
            "SELECT ST_AsText(ST_QuantizeCoordinates(
                ST_GeomFromText('POINT(1.23456789 9.87654321)'), 2))",
            [],
            |r| r.get(0),
        )
        .unwrap();
    assert_eq!(wkt, "POINT(1.234375 9.875)");

    // prec_y given: only x moves.
    let wkt: String = conn
        .query_row(
            "SELECT ST_AsText(ST_QuantizeCoordinates(
                ST_GeomFromText('POINT(1.23456789 9.87654321)'), 2, 15))",
            [],
            |r| r.get(0),
        )
        .unwrap();
    assert_eq!(wkt, "POINT(1.234375 9.87654321)");

    let srid: i32 = conn
        .query_row(
            "SELECT ST_SRID(ST_QuantizeCoordinates(
                ST_GeomFromText('POINT(139.7654321 35.6812345)', 4326), 5, 5, 5))",
            [],
            |r| r.get(0),
        )
        .unwrap();
    assert_eq!(srid, 4326);

    // NULL in, NULL out on every argument.
    for sql in [
        "ST_QuantizeCoordinates(NULL, 3)",
        "ST_QuantizeCoordinates(ST_GeomFromText('POINT(1 2)'), NULL)",
        "ST_QuantizeCoordinates(ST_GeomFromText('POINT(1 2)'), 3, NULL)",
    ] {
        let v: Option<Vec<u8>> = conn
            .query_row(&format!("SELECT {sql}"), [], |r| r.get(0))
            .unwrap();
        assert_eq!(v, None, "{sql}");
    }
}