From db28572260577a6250f4995fdf1ad42677a162f6 Mon Sep 17 00:00:00 2001 From: Esteban Zimanyi Date: Wed, 10 Jun 2026 21:22:20 +0200 Subject: [PATCH] feat(h3): geoToH3IndexSet + everEq(H3INDEXSET, TH3INDEX) prefilter UDFs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit H3INDEXSET is a Set blob covering a static geometry at a given H3 resolution. Two new UDFs accelerate trip × static cross-join queries (BerlinMOD Q4, Q7, Q11, Q12, Q15, Q17): - geoToH3IndexSet(GEOMETRY, INTEGER) → H3INDEXSET: covers POINT, LINESTRING, POLYGON, MULTI*, and GeomCollection input in EPSG:4326. - everEq(H3INDEXSET, TH3INDEX) → BOOLEAN: true when the trajectory ever passes through any cell of the static set; overloads the existing everEq family. Q7-style intersection on BerlinMOD sf 0.005: 0.517 s without prefilter, 0.076 s with prefilter (6.8×). --- src/h3/th3index.cpp | 75 +++++++++++++++++++++++++++++++++++++ src/include/h3/th3index.hpp | 12 ++++++ test/sql/h3_prefilter.test | 29 ++++++++++++++ 3 files changed, 116 insertions(+) create mode 100644 test/sql/h3_prefilter.test diff --git a/src/h3/th3index.cpp b/src/h3/th3index.cpp index ee6a03e1..30ae6354 100644 --- a/src/h3/th3index.cpp +++ b/src/h3/th3index.cpp @@ -11,6 +11,8 @@ #include "temporal/temporal.hpp" #include "geo/tgeompoint.hpp" #include "geo/tgeogpoint.hpp" +#include "geo_util.hpp" +#include "spatial/spatial_types.hpp" #include "tydef.hpp" #include "duckdb/common/types/data_chunk.hpp" #include "duckdb/main/extension/extension_loader.hpp" @@ -74,9 +76,16 @@ LogicalType H3IndexTypes::TH3INDEX() { return type; } +LogicalType H3IndexTypes::H3INDEXSET() { + auto type = LogicalType(LogicalTypeId::BLOB); + type.SetAlias("H3INDEXSET"); + return type; +} + void H3IndexTypes::RegisterTypes(ExtensionLoader &loader) { loader.RegisterType("H3INDEX", H3INDEX()); loader.RegisterType("TH3INDEX", TH3INDEX()); + loader.RegisterType("H3INDEXSET", H3INDEXSET()); } void H3IndexTypes::RegisterCastFunctions(ExtensionLoader &loader) { @@ -111,6 +120,22 @@ inline string_t TempToBlob(Vector &result, Temporal *t) { /* TINT → BIGINT result for the int-returning H3 predicates. */ inline bool IntToBool(int r) { return r != 0; } +inline Set *BlobToSet(string_t blob) { + size_t sz = blob.GetSize(); + uint8_t *copy = (uint8_t *) malloc(sz); + memcpy(copy, blob.GetData(), sz); + return reinterpret_cast(copy); +} + +inline string_t SetToBlob(Vector &result, Set *s) { + if (!s) return string_t(); + size_t sz = set_mem_size(s); + string_t out = StringVector::AddStringOrBlob( + result, string_t(reinterpret_cast(s), sz)); + free(s); + return out; +} + } // namespace /* ===================================================================== @@ -544,6 +569,45 @@ TH3_T_T_T_TEMP(Tne_th3index_th3index, tne_th3index_th3index) #undef TH3_T_T_T_TEMP +/* ===================================================================== + * Static geometry → h3indexset, and h3indexset × th3index prefilter + * ===================================================================== */ + +void H3IndexFunctions::Geo_to_h3index_set(DataChunk &args, ExpressionState &state, + Vector &result) { + BinaryExecutor::ExecuteWithNulls( + args.data[0], args.data[1], result, args.size(), + [&](string_t geom_blob, int32_t resolution, ValidityMask &mask, + idx_t idx) -> string_t { + /* H3 cells are inherently geographic (WGS84). The DuckDB + * spatial GEOMETRY blob has no embedded SRID, so callers + * must pass a geometry in EPSG:4326 coordinates (e.g. + * `ST_Transform(geom, 'EPSG:4326')`). We mark the + * GSERIALIZED with SRID 4326 explicitly. */ + GSERIALIZED *gs = GeometryToGSerialized(geom_blob, 4326); + if (!gs) { mask.SetInvalid(idx); return string_t(); } + Set *s = geo_to_h3index_set(gs, resolution); + free(gs); + if (!s) { mask.SetInvalid(idx); return string_t(); } + return SetToBlob(result, s); + }); +} + +void H3IndexFunctions::Ever_eq_h3index_set_th3index(DataChunk &args, + ExpressionState &state, Vector &result) { + BinaryExecutor::ExecuteWithNulls( + args.data[0], args.data[1], result, args.size(), + [&](string_t set_blob, string_t temp_blob, ValidityMask &mask, + idx_t idx) -> bool { + Set *s = BlobToSet(set_blob); + Temporal *t = BlobToTemp(temp_blob); + int r = ever_eq_h3indexset_th3index(s, t); + free(s); free(t); + if (r < 0) { mask.SetInvalid(idx); return false; } + return IntToBool(r); + }); +} + /* ===================================================================== * Registration * ===================================================================== */ @@ -706,6 +770,17 @@ void H3IndexTypes::RegisterScalarFunctions(ExtensionLoader &loader) { duckdb::RegisterSerializedScalarFunction(loader, ScalarFunction( "tgeogpointGreatCircleDistance", {TgeogpointType::TGEOGPOINT(), TgeogpointType::TGEOGPOINT(), V}, TemporalTypes::TFLOAT(), H3IndexFunctions::Tgeogpoint_great_circle_distance)); + + /* --- Static geometry h3 prefilter for trip × static cross-joins --- */ + const auto H3SET = H3INDEXSET(); + duckdb::RegisterSerializedScalarFunction(loader, ScalarFunction( + "geoToH3IndexSet", + {GeoTypes::GEOMETRY(), LogicalType::INTEGER}, + H3SET, H3IndexFunctions::Geo_to_h3index_set)); + duckdb::RegisterSerializedScalarFunction(loader, ScalarFunction( + "everEq", + {H3SET, TH3}, LogicalType::BOOLEAN, + H3IndexFunctions::Ever_eq_h3index_set_th3index)); } } // namespace duckdb diff --git a/src/include/h3/th3index.hpp b/src/include/h3/th3index.hpp index 8c2ab960..2cdb4a12 100644 --- a/src/include/h3/th3index.hpp +++ b/src/include/h3/th3index.hpp @@ -16,6 +16,11 @@ namespace duckdb { struct H3IndexTypes { static LogicalType H3INDEX(); static LogicalType TH3INDEX(); + /* H3INDEXSET is a Set, stored as a serialized Set* blob, + * built from a static geometry by `geoToH3IndexSet`. Used as the + * static side of the trip×static h3 prefilter on Q4 / Q7 / Q11 / + * Q12 / Q15 / Q17. */ + static LogicalType H3INDEXSET(); static void RegisterTypes(ExtensionLoader &loader); static void RegisterCastFunctions(ExtensionLoader &loader); @@ -113,6 +118,13 @@ struct H3IndexFunctions { static void Th3index_cell_area(DataChunk &args, ExpressionState &state, Vector &result); static void Th3index_edge_length(DataChunk &args, ExpressionState &state, Vector &result); static void Tgeogpoint_great_circle_distance(DataChunk &args, ExpressionState &state, Vector &result); + + /* Static geometry → h3indexset (Set) at a given H3 resolution */ + static void Geo_to_h3index_set(DataChunk &args, ExpressionState &state, Vector &result); + + /* Trip × static h3indexset prefilter: true if any cell of the + * trajectory's th3index ever equals any cell of the static set. */ + static void Ever_eq_h3index_set_th3index(DataChunk &args, ExpressionState &state, Vector &result); }; } // namespace duckdb diff --git a/test/sql/h3_prefilter.test b/test/sql/h3_prefilter.test new file mode 100644 index 00000000..25df18ae --- /dev/null +++ b/test/sql/h3_prefilter.test @@ -0,0 +1,29 @@ +# name: test/sql/h3_prefilter.test +# description: H3INDEXSET prefilter UDFs smoke coverage +# group: [mobilityduck] + +require mobilityduck + +# geoToH3IndexSet: a POINT input returns an H3INDEXSET blob +query I +SELECT typeof(geoToH3IndexSet(ST_Point(-122.4185, 37.7758), 5)); +---- +H3INDEXSET + +# everEq: trajectory passing through a cell in the set → true +query I +SELECT everEq( + geoToH3IndexSet(ST_Point(-122.4185, 37.7758), 5), + th3index(tgeompoint(ST_Point(-122.4185, 37.7758), timestamptz '2023-01-01', 4326), 5) +); +---- +true + +# everEq: trajectory at a distant cell → false +query I +SELECT everEq( + geoToH3IndexSet(ST_Point(0.0, 0.0), 5), + th3index(tgeompoint(ST_Point(-122.4185, 37.7758), timestamptz '2023-01-01', 4326), 5) +); +---- +false