geopackage
A Rust implementation of the OGC GeoPackage 1.4 format: vector features, attribute tables, spatial indexing, and columnar I/O through Apache Arrow. Pre-1.0: the API will change without notice.
Install
[]
= "0.1"
= "0.7" # any geo-traits implementation works; this is the common one
Columnar read and write through Apache Arrow is behind an off-by-default
feature, which adds the arrow-array and arrow-schema dependencies:
= { = "0.1", = ["arrow"] }
SQLite is bundled and built from source, so a C compiler is required and there is no system SQLite dependency. The minimum supported Rust version is 1.95.
Example
Create a file, declare a point layer, write features, and query by bounding box.
use Point;
use ;
use ;
let gpkg = create?;
gpkg.create_layer?;
// `create_layer` builds a spatial index; decline it with
// `.spatial_index(false)` on the builder.
let layer = gpkg.layer?;
layer.write_all?;
// Uses the RTree index when one is present, a full scan otherwise.
for feature in layer.features_in?
Columnar read and write
With the arrow feature, Layer::read_arrow reads a layer as Arrow record
batches, on min(4, available parallelism) threads by default, and
Layer::write_arrow writes batches back through the same path as write_all.
Geometry is a GeoArrow WKB column whose metadata stores the CRS as PROJJSON.
Layer::arrow_schema and TableSchemaBuilder::from_arrow_schema give the two
directions of the type mapping, so a layer can be copied without restating its
schema.
More examples
Runnable programs in geopackage/examples:
| Example | What it shows |
|---|---|
quickstart |
The example above. |
inspect |
Layers, schemas, SRS, feature counts and spatial-index health for a file, like ogrinfo -al -so. Uses open_lenient, so it reports problems rather than refusing to open. |
bulk_load |
Loading a large point layer with write_all, which engages the bulk index build. |
bbox_query |
features_in bounding-box queries and the select WHERE passthrough, with lazy geometry parsing. |
repair_index |
Detecting Legacy and Stale spatial indexes and repairing them. |
Configuration
The defaults produce a single-file GeoPackage with an indexed feature layer. What is settable:
| Setting | Where | Default |
|---|---|---|
Journal mode and synchronous level |
OpenOptions |
unset: an existing file keeps its mode, a new file is DELETE, synchronous is SQLite's default; WAL is opt-in |
| Whether a new layer is indexed | TableSchemaBuilder::spatial_index |
true |
| Primary-key and geometry column names | TableSchemaBuilder, GeometrySpec |
fid, geom |
| Rows sharing a write transaction | the batch_size argument of write_all / write_arrow |
caller-supplied; 0 writes all rows in one transaction |
| Bulk index build: row threshold, structural check, RTree node fill | BulkIndexOptions |
10,000 rows, RtreeOnly, 1.0 |
DATETIME parsing, and whether a value its declared type does not strictly permit is read or rejected |
ConversionOptions |
strict, lenient |
| Rows per Arrow batch, and threads the columnar read uses | ArrowReadOptions |
65,536 rows, min(4, available parallelism) |
| Geometry bytes per Arrow batch; a batch that would cross it is emitted with fewer rows | ArrowReadOptions::max_batch_bytes |
min(INT32_MAX, RAM / 4); the column's Arrow offsets are 32-bit, so 2 GB is a hard ceiling |
Each setting is documented in full on its type in the
crate documentation. Anything not covered is
reachable as SQL through GeoPackage::connection().
Workspace
| Crate | Purpose |
|---|---|
geopackage-core |
Format primitives, no IO or SQLite: GeoPackage Binary (GPB) header codec, normative table DDL, version-aware RTree trigger SQL, identifier quoting, application_id/user_version handling. |
geopackage |
The container: create/open over rusqlite with bundled SQLite, the feature and attribute read and write paths, columnar read and write through Apache Arrow (feature arrow), the RTree spatial-index lifecycle, and the ST_* SQL functions the index triggers require. |
geopackage-core/fuzz |
cargo-fuzz targets for the GPB parser. |
Design
- Synchronous API over rusqlite.
GeoPackage::connection()andfrom_connection()expose the underlying connection; SQLite is the query engine, so anything the API does not cover is a query away. - WAL is opt-in, and a handle that opted into it checkpoints and resets the
file to
DELETEon close, so a handed-over.gpkgis a single file with no sidecars. - New indexes get the GeoPackage 1.4 trigger set. Older and mixed trigger
generations, a known source of file corruption, are detected and repaired
with
repair_spatial_indexrather than silently mixed. - CRS definitions are stored, never transformed: there is no PROJ dependency
and no coordinate transformation.
add_epsg_srswrites WKT1 where the code has one, and WKT2 through thegpkg_crs_wkt_1_1extension otherwise (for codes with no WKT1 form, such as the geographic 3D EPSG:4979), matching GDAL.
Performance
Measured over three public datasets. Apple M2 Pro, 12 cores, 16 GB, release build, warm page cache, medians over repeated runs.
buildings |
rivers |
admin |
|
|---|---|---|---|
| source | Microsoft Building Footprints, California | HydroRIVERS v1.0, global | GADM 4.1, global |
| rows | 11,542,912 | 8,477,883 | 356,508 |
| geometry | Polygon | LineString | MultiPolygon |
columns, including fid and geometry |
4 | 16 | 54 |
| file | 2.37 GB | 2.03 GB | 2.74 GB |
| operation | buildings |
rivers |
admin |
|---|---|---|---|
columnar read, read_arrow |
2.1 s | 1.9 s | ~2.9 s |
scalar read, cursor |
4.3 s | 6.9 s | 2.4 s |
| write from Arrow batches | 12.7 s | 12.9 s | 8.4 s |
| the same write, index built as it goes | 31.0 s | 21.7 s | 8.9 s |
create_spatial_index afterwards instead |
26.0 s | 14.6 s | 8.5 s |
| bounding-box query, indexed | 80 ms | 199 ms | 178 ms |
| the same query with no index | 1.7 s | 2.2 s | 1.3 s |
| features that query returned | 70,130 | 180,544 | 36,556 |
Reading is bound by bytes, not rows: the columnar path varies between 0.95 to 1.13 GB/s
across three layers whose rows differ by a factor of 37 in size. The index is
the other way round, tracking row count at 42,000 to 581,000 rows/s built and
about 40 bytes per row stored, whatever the geometry. Building it during the
write rather than afterwards between 20% and 47%, because the bulk path reuses the
envelopes it computed while encoding; that is why create_layer leaves an empty
index in place for write_all to fill.
The admin columnar read is given as approximate because it is: 7.7 kB rows
produce six batches of roughly 450 MB, and with the default four reader threads
holding one each, the figure follows host memory rather than the read path
(1.7 s to 5.1 s observed here). ArrowReadOptions::with_batch_size and
with_max_batch_bytes bound what is in flight.
Method, per-dataset detail and the rest of the caveats are in
the benchmark write-up;
scripts/bench_datasets.sh
fetches the datasets and reproduces the table.
Conformance
Files written by this crate are checked against OGC
ets-gpkg12: 40 passed, 1
failed, where the failing test's regex hard-codes the GeoPackage 1.2 trigger
set and rejects a correct 1.4 one (no 1.3/1.4 ETS exists). The
PDOK validator reports two
advisory findings, both on deliberate test-file choices. A GDAL round-trip
byte-compares geometry WKB and attribute values after an ogr2ogr copy, and
the test corpus includes GDAL-written, QGIS-written and raw-SQLite files.
Known limitations
- Untrusted files can trigger a large allocation. The
wkbreader pre-allocates from element counts read out of the geometry blob without bounding them against the buffer, so a malformed 17-byte GPB blob declaring a 0xFFFFFFFF-member collection drives a multi-gigabyte allocation. The fix belongs upstream in georust/wkb; do not parse untrusted GeoPackage files. Tracked in #3. - Non-linear curve types (
CIRCULARSTRING,COMPOUNDCURVE, ...) cannot have their envelopes computed and so cannot be inserted into an indexed table. Tracked in #5. - Tile pyramids are not implemented. A file's tile tables are visible
through
contents()but there is no read or write path for them.
License
Licensed under either of Apache License, Version 2.0 or MIT license at your option.