kenro 0.2.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

kenro

CI crates.io docs.rs npm Go Reference license

SpatiaLite-style spatial SQL for SQLite, in pure Rust — PostGIS-compatible ST_ functions that work with rusqlite, as a loadable extension (Python / Node / Bun / Deno / Go / Ruby / C / sqlite3 CLI), in pure Go with no cgo (modernc.org/sqlite + wazero), in containers and serverless (Cloud Run / Lambda / Cloudflare Workers, D1 and Durable Objects), and in the browser (sql.js / wa-sqlite / official SQLite WASM).

If you searched for rusqlite spatial, SQLite spatial functions without SpatiaLite, SpatiaLite alternative in Rust, GeoPackage in pure Rust, or spatial queries on Cloudflare D1 / Durable Objects: this is that crate.

kenro is a spatial SQL engine for SQLite covering the PostGIS function surface you actually use — predicates, overlay, repair, buffering, reprojection, vector tiles, spatial aggregates, ~205 functions — in pure Rust, golden-tested against PostGIS itself, with zero C dependencies and one-call registration:

  • Geometry I/O — WKT, WKB, GeoJSON, GML 2/3 and GeoPackage blobs in and out, MVT vector tiles, KML and SVG out; 3D and POLYHEDRALSURFACE/TIN columns are read, measured and flattened rather than silently lost — all first-class citizens
  • Predicates — the full DE-9IM family: ST_Intersects / ST_Contains / ST_Within / ST_Touches / ST_Crosses / ST_Overlaps / ST_Equals / ST_Covers / ST_Relate, plus ST_Distance / ST_DWithin (via georust/geo)
  • Overlay & repair (full feature) — ST_Intersection / ST_Union (scalar and aggregate) / ST_Difference / ST_SymDifference / ST_Buffer / ST_MakeValid / ST_Split in pure Rust, with the differences vs GEOS quantified by golden tests
  • GeoPackage support — the exact function set the spec's R-tree maintenance triggers require, plus the helper the (since-withdrawn) geometry-type triggers call, because files carrying them are still out there
  • CRS transform — pure-Rust proj4rs: WGS84, Web Mercator and every UTM zone built in, the full EPSG registry behind a feature flag, with measured accuracy
  • H3 cells — mesh aggregation in GROUP BY (h3-pg naming)
  • Vector tilesST_AsMVTGeom + the ST_AsMVT aggregate with a hand-rolled, dependency-free encoder
  • Accessors, measures, processing — area, length, centroid, convex hull, line interpolation, simplification, affine transforms, …
  • Tiny — the loadable extension is a single ~2 MB file with zero dependencies, EPSG registry included, where mod_spatialite's GEOS/PROJ/proj.db chain is ~25 MB across 9 files (~12× smaller, measured); the wasm build starts at 560 KB (216 KB wire), and the everything-included tier is 2.1 MB (640 KB wire) against DuckDB-WASM spatial's ~23.5 MB. Two honest reasons: a deliberately narrower scope (no topology, no XML machinery beyond geometry encodings, no spreadsheet import, no datum grids) and a statically-linked binary that only carries what you enable — a dynamic-library chain ships everything to everyone

The headline: with kenro registered, a plain SQLite build maintains a GeoPackage spatial index correctly. No SpatiaLite, no GDAL, no C toolchain.

kenro = 間縄 (kennawa) × rope — two words for the same cord, twisted into one. Named after the measuring rope of historical Japanese land surveys: the tool for turning land into ledgers.

Quickstart (Rust / rusqlite)

[dependencies]
kenro = { version = "0.2", features = ["rusqlite"] }  # add "full" for overlay/repair
let conn = rusqlite::Connection::open("parks.gpkg")?;
kenro::register(&conn)?;

// Query a GeoPackage with its R-tree index + a precise predicate refine:
let n: i64 = conn.query_row(
    "SELECT count(*) FROM parks p
     JOIN rtree_parks_geom r ON p.fid = r.id
     WHERE r.minx <= ?1 AND r.maxx >= ?2 AND r.miny <= ?3 AND r.maxy >= ?4
       AND ST_Within(ST_GeomFromGPB(p.geom), ST_GeomFromText(?5, 4326))",
    rusqlite::params![max_x, min_x, max_y, min_y, window_wkt],
    |r| r.get(0),
)?;

Inserts, updates and deletes through SQL keep the spatial index in sync via the standard GeoPackage triggers — including files written by GDAL/QGIS.

Quickstart (every other platform — loadable extension)

kenro ships as a standard SQLite loadable extension — one prebuilt binary per OS on the releases page (Linux x86_64/arm64, macOS universal, Windows), loadable from any language whose SQLite driver exposes extension loading. (Not applicable in the browser, on Cloudflare, or with pure-Go SQLite — see the three quickstarts below.)

curl -fsSL https://github.com/reearth/kenro/releases/latest/download/kenro-ext-x86_64-unknown-linux-gnu.tar.gz | tar xz
# → libkenro_ext.so  (or build from source: cargo build -p kenro-ext --release)
import sqlite3

con = sqlite3.connect("parks.gpkg")
con.enable_load_extension(True)
con.load_extension("./libkenro_ext")

print(con.execute(
    "SELECT ST_AsText(ST_GeomFromGPB(geom)) FROM parks LIMIT 1").fetchone())

Copy-paste guides for every platform live in docs/quickstart.md: Python, Node.js, Bun, Deno, Go, Ruby, C/C++, the sqlite3 CLI, containers (Cloud Run / Fly.io / ECS) and AWS Lambda, with the per-platform gotchas (macOS system SQLite, cross-compilation, …). JavaScript hosts — browser and Cloudflare — are docs/wasm.md, where no extension binary is involved.

Quickstart (browser — kenro-wasm)

Browser SQLite builds can't load native extensions, but they all accept JS-level user-defined functions — so kenro's SQLite-free core compiles to wasm — 560–2105 KB (216–640 KB wire) depending on the feature tier (sizes) — with one adapter per host:

import sqlite3InitModule from "@sqlite.org/sqlite-wasm";
import initKenro, * as kenroWasm from "kenro-wasm";
import { registerKenro } from "kenro-wasm/sqlite-wasm";

await initKenro();
const sqlite3 = await sqlite3InitModule();
const db = new sqlite3.oo1.DB(":memory:");
registerKenro(db, kenroWasm, sqlite3);

db.selectValue("SELECT ST_AsText(ST_GeomFromText('POINT(1 2)'))"); // POINT(1 2)

Adapters for sql.js and wa-sqlite ship alongside; host support matrix, measured sizes, and per-host limitations are in docs/wasm.md.

Live demo: https://reearth.github.io/kenro/ — drag a GeoPackage in and query it with spatial SQL, entirely client-side (source in crates/kenro-wasm/demo/).

Quickstart (Cloudflare — Workers, D1, Durable Objects)

Workers can't load a native extension, and neither D1 nor Durable Object SQLite accepts user-defined functions — no ST_ call can appear in their SQL, and there is no R-tree module either. What works is a split: kenro derives the bounding box and a tile cell at write time so plain SQL can index them, then runs the exact predicate in JS on the survivors.

import wasmModule from "kenro-wasm/pkg/kenro_wasm_bg.wasm";  // Workers hand you the Module
import * as kenro from "kenro-wasm";
import { cellsForQuery } from "kenro-wasm/tiles";            // the R-tree stand-in

kenro.initSync({ module: wasmModule });                      // once per isolate

A complete Worker doing this on both D1 and Durable Objects — schema, migrations, and tests that run in workerd — is in crates/kenro-wasm/cloudflare/.

Quickstart (pure Go — no cgo)

modernc.org/sqlite is SQLite transpiled to Go, so it can't load a native extension — but it can register Go functions. kenro's core therefore runs as wasm inside wazero, and both halves stay cgo-free: spatial SQL in a CGO_ENABLED=0 static binary.

go get github.com/reearth/kenro/go
import (
    "database/sql"

    kenro "github.com/reearth/kenro/go"
    _ "modernc.org/sqlite"
)

kenro.Register() // once at start-up, before opening connections

db, _ := sql.Open("sqlite", "parks.gpkg")
db.QueryRow(`SELECT ST_AsText(ST_GeomFromGPB(geom)) FROM parks LIMIT 1`).Scan(&wkt)

The wasm module is committed to the repository, so go get needs no Rust toolchain. GeoPackage R-tree triggers are maintained correctly here too (modernc is built with SQLITE_ENABLE_RTREE).

go/README.md has the rest: runnable examples (storing and querying geometries, the R-tree filter-then-refine query, dissolve with the ST_Union aggregate, reprojection), the two modernc.org/sqlite limitations worth knowing about, measured per-call costs, and how to embed a smaller feature tier.

Function reference

203 SQL functions across geometry I/O (WKT/WKB/GeoJSON/GML/KML/SVG/GeoPackage), the full DE-9IM predicate family (ST_Relate included), measures, overlay & buffer, splitting and merging, processing & affine transforms, hulls and triangulation, accessors, constructors, GeoPackage trigger helpers, 3D and surface-collection pass-through, H3, and MVT vector tiles — plus three aggregates (ST_Union(geom), ST_AsMVT(…), ST_Extent(geom)).

The full table — every function with its PostGIS / DuckDB Spatial / SpatiaLite comparison and documented behavior differences — lives in docs/functions.md.

All implemented functions are deterministic and pure (no I/O, no clock, no randomness) and NULL-strict (NULL in → NULL out; aggregates skip NULL rows, following PostGIS aggregate semantics). Malformed input raises an explicit error prefixed kenro: — never a silent NULL. You also never get SQLite's bare no such function: anything outside the build's feature set registers as a stub naming the missing cargo feature (a default build stubs the six overlay-family functions; full has none), and the one deliberate exclusion, ST_Collect, errors with a pointer to the ST_Union aggregate.

Semantics: PostGIS is the reference

Function names, signatures, and semantics follow PostGIS (SQL/MM ST_ prefix), validated against PostGIS-generated golden vectors committed in this repo (tests/golden/*.jsonl — 700+ vectors across nine suites; H3 vectors come from the reference C library, MVT tiles are cross-decoded by two independent decoders). Where kenro deviates, it does so loudly and documentedly — never a silently different result. The full list of divergences (empty-geometry handling, GeometryCollection, SRID rules, the overlay engine's areal-only results, …) is in docs/functions.md.

Choosing kenro vs PostGIS vs DuckDB Spatial

Structural differences that matter more than any single function:

  • SRID model — PostGIS geometries and kenro's GeoPackage blobs both carry their SRID; DuckDB's GEOMETRY does not, so CRS bookkeeping is the user's job there (and always_xy axis-order care is needed for EPSG:4326).
  • Where it runs / weight — kenro lives inside SQLite: pure Rust, a single ~2 MB extension (SpatiaLite's GEOS/PROJ/proj.db chain is ~25 MB across 9 files), no C toolchain, deterministic. PostGIS is a server-side PostgreSQL extension. DuckDB spatial bundles GEOS + PROJ + GDAL (its WASM build is ~23.5 MB uncompressed, ~6.3 MB over the wire).
  • Division of labor — kenro covers spatial SQL end-to-end inside your app's SQLite file: predicates, overlay/repair/buffer, R-tree maintenance, CRS transforms, MVT generation, aggregates. Reach for PostGIS or DuckDB spatial when you need what kenro deliberately leaves out — raster, topology/networks, file-format conversion, GeometryCollection-producing operations, datum-grid transforms. They compose rather than compete.

Supported CRS

ST_Transform ships a built-in EPSG table (proj4rs carries no EPSG database). kenro is region-neutral: the built-in codes are exactly the globally-defined, algorithmically-derivable systems —

Codes System
4326 WGS84 geographic
3857 Web Mercator
32601–32660 WGS84 UTM zones 1N–60N
32701–32760 WGS84 UTM zones 1S–60S

Every national and regional system is served the same way: crs-full adds the whole crs-definitions registry as a fallback, and it is part of full — so a full build transforms to EPSG:6677 or 27700 without a rebuild (EPSG codes ≤ 65535 only). The default/standard tiers leave it out and raise an error naming the code and the feature, because a build that only touches 4326 and 3857 should not carry the tables. Accuracy against PROJ is measured and documented in docs/accuracy.md — TL;DR: nanometer-level projection math, but no datum grids: national datum modernizations and earthquake-displacement models are not applied; use full PROJ for survey-grade work.

Cargo features

The default (standard) set — transform (proj4rs), h3 (h3o), geojson, mvt — covers most use cases: I/O, the whole predicate family, GeoPackage triggers, measures/processing/affine, CRS transform, H3, GeoJSON, and MVT vector tiles (tile clipping uses dedicated rectangle algorithms, so MVT costs almost nothing).

full adds the features excluded from the default for size: overlay (ST_Intersection/ST_Union/ST_Difference/ST_SymDifference/ ST_Buffer/ST_MakeValid/ST_Split — pulls the i_overlay mesh, the largest single contributor to binary size) and spheroid (ST_DistanceSpheroid/ST_LengthSpheroid — pulls geographiclib for a 0.1% refinement over the always-available spherical ST_DistanceSphere), plus concave-hull (+41 KB) and delaunay (+81 KB, pulling spade) — the two functions whose algorithms cost more than any other single entry — gml (GML 2/3 I/O, +13 KB for quick-xml), text-encodings (ST_AsKML/ST_AsSVG; no XML library, but KML reprojects to WGS84 and so needs transform), and crs-full, the EPSG registry (+155 KB gzipped). With overlay present, ST_AsMVTGeom also upgrades to PostGIS-grade validity repair (invalid input and snap-induced self-intersections are made valid before tiling). In wasm terms: standard 755 KB (296 KB gzip) vs full 2105 KB (640 KB gzip, the EPSG registry being most of the difference); --no-default-features gives a 560 KB (216 KB gzip) minimal build.

Building with any feature off keeps the corresponding SQL functions registered as stubs that explain which feature is missing. rusqlite (off by default) enables kenro::register. The prebuilt loadable extension (kenro-ext) always ships full.

Requirements & caveats

  • SQLite ≥ 3.31 (for SQLITE_INNOCUOUS, which lets kenro functions run inside the GeoPackage R-tree triggers under PRAGMA trusted_schema=off); the loadable-extension path needs SQLite ≥ 3.34 (it fails with a clear version-mismatch message on older hosts).
  • Loading kenro and SpatiaLite into the same connection: both register ST_ names and SQLite keeps the last registration. Don't mix them (a registration-filter feature flag can be added if needed).
  • ST_Distance is 2D cartesian in the geometry's coordinate system. For meters over lon/lat, reproject to the local UTM zone first (e.g. ST_Transform(geom, 32654) around Tokyo, 32633 around Berlin).

Roadmap

  1. ✅ Core: GeoPackage blobs, WKB/WKT, predicates, R-tree functions, rusqlite registration, PostGIS golden tests
  2. ST_Transform (proj4rs; accuracy measured and documented), H3 cell IDs, GeoJSON, accessors
  3. kenro-ext: loadable extension (.so/.dylib/.dll) for Python / Node / sqlite3 CLI
  4. kenro-wasm: browser builds (official SQLite WASM / sql.js / wa-sqlite, details) + drag-and-drop GeoPackage demo
  5. ✅ Full predicate family + ST_Relate, measures/processing/affine, pure-Rust overlay & ST_Buffer, SQL aggregates (ST_Union), MVT (ST_AsMVTGeom + ST_AsMVT), GPKG_IsAssignable
  6. ✅ Release pipeline: prebuilt extension binaries (Linux x86_64/arm64, macOS universal, Windows) + wasm bundle on every v* tag
  7. ✅ Public repository + live demo on GitHub Pages
  8. ✅ v0.1.0 on crates.io + npm as kenro-wasm
  9. ✅ v0.2.0: pure-Go binding (no cgo), Cloudflare D1/Durable Objects support, GML 2/3 I/O, KML and SVG output, 3D and POLYHEDRALSURFACE/TIN pass-through, ST_Split / ST_LineMerge / ST_IsSimple / ST_TriangulatePolygon, and the concave-hull, Delaunay and full-EPSG tiers

License

MIT OR Apache-2.0, at your option.