duckdb-spatial は(まだ)複数ファイルを読み込めないので Rust でDuckDB 拡張をつくりました
duckdb-spatial の ST_Read
は、単一のファイルしか読めません。メンテナの人によれば、DuckDB は複数のデータソースを扱うための仕組みがあり、duckdb-spatial でもそれを使うようにできるはず、とのことです。なので、将来的には複数ファイルを読めるようになりそうです。
ただ、それは少し先の話です。それまでのつなぎとして、ちょっとした DuckDB 拡張を作りました。以下のように glob パターンでパスを指定してで複数の GeoJSON ファイル、もしくは GeoPackage ファイルを読み込めます。(REPLACE
でごにょごにょやってる部分はなんやねん?、という話はあとでします...)
SELECT * REPLACE (ST_GeomFromWkb(geometry) as geometry)
FROM ST_Read_Multi('test/data/*.geojson');
┌─────────────────┬────────┬─────────┬───────────────────────────┐
│ geometry │ val1 │ val2 │ filename │
│ geometry │ double │ varchar │ varchar │
├─────────────────┼────────┼─────────┼───────────────────────────┤
│ POINT (1 2) │ 1.0 │ a │ test/data/points.geojson │
│ POINT (10 20) │ 2.0 │ b │ test/data/points.geojson │
│ POINT (100 200) │ 5.0 │ c │ test/data/points2.geojson │
│ POINT (111 222) │ 6.0 │ d │ test/data/points2.geojson │
└─────────────────┴────────┴─────────┴───────────────────────────┘
インストール
Community extension に入れたので、以下のコマンドでインストールできます。
INSTALL st_read_multi FROM community;
LOAD st_read_multi;
制限
この DuckDB 拡張は、上に書いたようにあくまでつなぎなので、かなり割り切ったつくりになっています。
- 書き込みはない。読むだけ。
- 効率的なデータアクセス(空間インデックスとか)は諦める。でかいデータならあらかじめ GDAL CLI とかで加工してね、ということで。
- メタデータ(CRS とか BBox とか)は諦める。中身のデータが読めればそれでいい。
また、GEOMETRY
型ではなく BLOB
型のデータを返します。上では REPLACE
でごまかしてたんですが、ST_GeomFromWkb()
で明示的に変換しないと、ただの BLOB
なのでこう表示されます。これは、別の拡張で定義されたエイリアス型は、たとえ中身が同じでもほかの拡張から使うことはできないっぽく、諦めました。C++ 拡張ならマクロとかでなんとかなるのかもしれませんが、Rust で書いたのでそういうことはできないぽいです。
┌────────────────────────────────────────────────────────────────────────────────┬────────┬─────────┬───────────────────────────┐
│ geometry │ val1 │ val2 │ filename │
│ blob │ double │ varchar │ varchar │
├────────────────────────────────────────────────────────────────────────────────┼────────┼─────────┼───────────────────────────┤
│ \x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\xF0?\x00\x00\x00\x00\x00\x00\x00@ │ 1.0 │ a │ test/data/points.geojson │
│ \x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00$@\x00\x00\x00\x00\x00\x004@ │ 2.0 │ b │ test/data/points.geojson │
│ \x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00Y@\x00\x00\x00\x00\x00\x00i@ │ 5.0 │ c │ test/data/points2.geojson │
│ \x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\xC0[@\x00\x00\x00\x00\x00\xC0k@ │ 6.0 │ d │ test/data/points2.geojson │
└────────────────────────────────────────────────────────────────────────────────┴────────┴─────────┴───────────────────────────┘
ちなみに、最近いろんなデータ関連のプロダクトで GEOMETRY
型や GEOGRAPHY
型のサポートが進んでいますが、DuckDB も将来的には拡張ではなく本体に GEOMETRY
型や GEOGRAPHY
型が入るっぽいです。待ち遠しいですね。
使い方
GeoJSON
上に書いたように、glob パターンで GeoJSON を読み込めます。どのファイルから読み込まれたかの情報は filename
カラムに入ります。
なお、スキーマが違うファイルが混ざっているとエラーになります。このあたりも、いい感じにマージするとかできそうですが、考え出すとキリがないのでやめました。
FROM ST_Read_Multi('test/data/different_schema/*.geojson');
Binder Error:
Schema mismatch in test/data/different_schema/points2.geojson: column 1 has name 'val3', expected 'val2'
GeoPackage
GeoPackage ファイルは複数のレイヤーを持てますが、ST_Read_Multi()
はすべてのレイヤーを読み込みます。filename
カラムに加えて layer
カラムも表示されます。
-- load all layers
SELECT * REPLACE (ST_GeomFromWkb(geom) as geom)
FROM ST_Read_Multi('test/data/*.gpkg');
┌─────────────────┬───────┬─────────┬─────────────────────────────┬───────────────┐
│ geom │ val1 │ val2 │ filename │ layer │
│ geometry │ int32 │ varchar │ varchar │ varchar │
├─────────────────┼───────┼─────────┼─────────────────────────────┼───────────────┤
│ POINT (100 200) │ 5 │ c │ test/data/multi_layers.gpkg │ points2_point │
│ POINT (111 222) │ 6 │ d │ test/data/multi_layers.gpkg │ points2_point │
│ POINT (1 2) │ 1 │ a │ test/data/multi_layers.gpkg │ points_point │
│ POINT (10 20) │ 2 │ b │ test/data/multi_layers.gpkg │ points_point │
│ POINT (1 2) │ 1 │ a │ test/data/points.gpkg │ points │
│ POINT (10 20) │ 2 │ b │ test/data/points.gpkg │ points │
│ POINT (100 200) │ 5 │ c │ test/data/points2.gpkg │ points │
│ POINT (111 222) │ 6 │ d │ test/data/points2.gpkg │ points │
└─────────────────┴───────┴─────────┴─────────────────────────────┴───────────────┘
もし、特定のレイヤーだけに絞り込みたい場合は、 layer
引数にレイヤー名を指定すれば絞り込んでくれます。
-- load specific layers
SELECT * REPLACE (ST_GeomFromWkb(geom) as geom)
FROM ST_Read_Multi('test/data/*.gpkg', layer='points');
[WARN] No such layer 'points' in test/data/multi_layers.gpkg
┌─────────────────┬───────┬─────────┬────────────────────────┬─────────┐
│ geom │ val1 │ val2 │ filename │ layer │
│ geometry │ int32 │ varchar │ varchar │ varchar │
├─────────────────┼───────┼─────────┼────────────────────────┼─────────┤
│ POINT (1 2) │ 1 │ a │ test/data/points.gpkg │ points │
│ POINT (10 20) │ 2 │ b │ test/data/points.gpkg │ points │
│ POINT (100 200) │ 5 │ c │ test/data/points2.gpkg │ points │
│ POINT (111 222) │ 6 │ d │ test/data/points2.gpkg │ points │
└─────────────────┴───────┴─────────┴────────────────────────┴─────────┘
実装
DuckDB 拡張をつくるのに興味がある人も読むかもしれないので、実装についても少しだけ。
DuckDB の Rust 拡張全般の話
DuckDB 拡張を Rust で書く際の仕組みについては以前記事を書いたのでこれを読んでください。というか、私も、すべてを忘れて途方に暮れていたところ、これを読み返してやり方をなんとなく思い出しました。ありがとう過去の自分...
GeoJSON
GeoJSON は、geojson クレートを使いました。
geojson::Feature
は GeoRust エコシステムの中にあるので、geo-types を介して変換することができます。WKB への変換は定義されてるんですが、.to_wkb()
みたいな手ごろなメソッドは生えてないので、こんな感じの struct をつくって変換してました。
pub struct WkbConverter {
buffer: Vec<u8>,
}
impl WkbConverter {
pub fn new() -> Self {
Self { buffer: Vec::new() }
}
pub fn convert(&mut self, feature: &Feature) -> Result<&[u8], Box<dyn std::error::Error>> {
self.buffer.clear();
match &feature.geometry {
Some(geojson_geom) => {
let geometry: geo_types::Geometry = geojson_geom.try_into()?;
wkb::writer::write_geometry(&mut self.buffer, &geometry, &Default::default())
.unwrap();
}
None => panic!("Geometry should exist!"),
}
Ok(&self.buffer)
}
}
GeoPackage
これも過去の自分に助けられた案件ですが、GeoPackage というフォーマットは SQLite です。「SQLite を拡張している」のではなく、「SQLite さえあれば読み書きできる」ように作られています。なので、GDAL みたいな GIS に特化したライブラリがなくても、SQLite 用のライブラリさえあれば事足ります。
今回は、Rust ということで、SQLite を Rust でゼロから実装しなおそうという試み・limbo を使おうとしたんですが、なんか謎のエラーでうまくいきませんでした。コンパイルは通っても読み込むとクラッシュするという感じで、非同期周りが怪しそうな感じするんですが、深追いはしてません。
代わりに、rusqlite というクレートを使いました。こっちは pure Rust ではなく SQLite へのバインディングということで、windows_amd64_gnu
ではコンパイルエラーになるのですが(GNU じゃなくて MSVC は大丈夫)、まあそれ以外は特に問題なく動きました。
読むだけできればいいので、pure Rust でもっと軽量な実装ないのかな...、と思うのですが、うまく見つけられませんでした。
最後に
まだ実際のデータでテストできていない experimental な 拡張ですが、よかったら使ってください。何か変な挙動に遭遇したり、要望などあれば、GitHub の issue にお知らせいただけると助かります。
Discussion