This document provides guidance to coding agents working in the DuckDB CityJSON Extension repository.
This is a C++ DuckDB extension that registers SQL table functions for reading CityJSON and CityJSONSeq files. It is not Rust — the entire extension is written in C++17.
Key entry points:
src/cityjson_extension.cpp— extension loader (LoadInternal), registers all functionssrc/cityjson/— all implementation filessrc/include/cityjson/— all headerstest/sql/— SQL-based teststest/data/— sample.city.jsonand.city.jsonlfiles
| Function | File | Description |
|---|---|---|
read_cityjson(path) |
bind_function.cpp, scan_function.cpp |
Read CityJSON (.city.json) |
read_cityjsonseq(path) |
bind_function.cpp, scan_function.cpp |
Read CityJSONSeq (.city.jsonl) |
cityjson_metadata(path) |
metadata_table_function.cpp |
Metadata for CityJSON |
cityjsonseq_metadata(path) |
metadata_table_function.cpp |
Metadata for CityJSONSeq |
| File | Purpose |
|---|---|
cityjson_types.hpp/cpp |
Core data types: CityJSON, CityJSONFeature, CityObject, Geometry, Transform |
reader.hpp |
Abstract CityJSONReader interface |
reader_factory.cpp |
OpenAnyCityJSONFile() — auto-detects format by extension |
local_cityjsonreader.cpp |
Reads .city.json (full CityJSON) |
local_cityjsonseq_reader.cpp |
Reads .city.jsonl (CityJSONSeq, line-delimited) |
bind_function.cpp |
CityJSONBind / CityJSONSeqBind — schema inference, chunk loading |
scan_function.cpp |
CityJSONScan — iterates CityObjects, writes to DuckDB vectors |
city_object_utils.cpp |
Attribute extraction, geometry encoding, schema inference |
lod_table.cpp |
LOD-based schema inference for lod= mode |
wkb_encoder.cpp |
WKB geometry encoding |
metadata_table_function.cpp |
cityjson_metadata and cityjsonseq_metadata implementations |
- Line 1: CityJSON metadata header (
"type": "CityJSON") — used by*_metadatafunctions - Line 2+:
CityJSONFeaturerecords ("type": "CityJSONFeature") — each has its own local"vertices"array - Important: per-feature
"vertices"are local to that feature; geometry boundary indices reference them, not the global header vertices
When lod='X' is passed:
- Schema switches to
geometry(BLOB/WKB) +geometry_properties(VARCHAR/JSON) - Per-feature vertex pool is used for CityJSONSeq; global metadata vertices used for regular CityJSON
GetGeometryAtLOD()finds the geometry matching the requested LOD stringlodfield in geometry objects is optional (not all features declare it)
# Initial setup (once)
GEN=ninja make
# Incremental rebuild of extension + duckdb binary
cmake --build build/release --target cityjson_extension cityjson_loadable_extension duckdb
# Or full rebuild
make releaseThe duckdb binary is statically linked with the extension. Always rebuild it after code changes to test interactively:
./build/release/duckdb -c "SELECT COUNT(*) FROM read_cityjson('test/data/minimal.city.json');"Whenever you make code changes, you MUST run the tests before considering the task done.
# Run all SQL tests
cmake --build build/release --target cityjson_extension cityjson_loadable_extension duckdb
./build/release/duckdb -c "SELECT * FROM read_cityjson('test/data/minimal.city.json');"
./build/release/duckdb -c "SELECT COUNT(*) FROM read_cityjsonseq('test/data/sample.city.jsonl');"Or run the full test suite:
make testTests live in test/sql/*.test. Always verify:
read_cityjsonstill works on.city.jsonfilesread_cityjsonseqworks on.city.jsonlfileslod=option works for bothcityjson_metadata/cityjsonseq_metadatareturn correct rows
- Add to
func.named_parameters["param_name"] = LogicalType::...intable_function_registration.cpp - Parse it in
CityJSONBind/CityJSONSeqBindinbind_function.cpp - Store on
CityJSONBindData - Use in
CityJSONScaninscan_function.cpp
- Add to
GetDefinedColumns()incolumn_types.cpp - Handle in
CityObjectUtils::GetAttributeValue()incity_object_utils.cpp - Add
IsPredefinedColumn()check if needed
Geometry::FromJson in cityjson_types.cpp:
typeandboundariesare requiredlodis optional (default"")semantics,material,textureare optional
In CityJSONSeq, each feature line has its own "vertices" array. These are parsed into CityJSONFeature::vertices and used during WKB encoding. The scan resolves the correct vertex pool per-feature:
// In scan_function.cpp
const std::vector<std::array<double, 3>> *vertex_pool = nullptr;
if (!feature.vertices.empty()) {
vertex_pool = &feature.vertices; // CityJSONSeq: per-feature
} else if (bind_data.metadata.vertices.has_value()) {
vertex_pool = &bind_data.metadata.vertices.value(); // CityJSON: global
}- Filter Pushdown — push WHERE clauses down to readers for better performance
- Column Statistics — implement column min/max statistics for query optimization
- Spatial Indexing — integrate with DuckDB spatial extension for spatial queries
- Streaming — support very large files without loading all data into memory during bind
- Compression — support compressed CityJSON files (.gz, .bz2)
- CityJSON specification: https://www.cityjson.org/specs/2.0.1/
- CityJSONSeq specification: https://www.cityjson.org/cityjsonseq/
- DuckDB C++ API: https://duckdb.org/docs/stable/clients/c/api
- DuckDB Extension development: https://duckdb.org/docs/stable/dev/extensions