diff --git a/Makefile b/Makefile index c05d81ac..a9485498 100644 --- a/Makefile +++ b/Makefile @@ -7,9 +7,43 @@ EXT_CONFIG=${PROJ_DIR}extension_config.cmake # Include the Makefile from extension-ci-tools include extension-ci-tools/makefiles/duckdb_extension.Makefile +# Single-timezone model (PGTZ-style): the extension's LoadInternal forces +# both MEOS (meos_initialize_timezone) and DuckDB (DBConfig::SetOptionByName +# "TimeZone") to Europe/Brussels. Tests pass on any OS timezone — the +# extension is the single source of truth, no TZ env var needed. +# +# LoadInternal also calls ExtensionHelper::AutoLoadExtension(db, "icu") so +# the timezone option is honoured. Autoload looks for the extension on disk +# at $HOME/.duckdb/extensions///icu.duckdb_extension +# and falls back to a hub download. That fails both inside the linux_amd64 +# test docker container (empty path, no network egress) and on the macOS +# osx_arm64 test runner (hub icu not reliably resolvable). We copy the +# icu.duckdb_extension that was built locally as part of this extension's +# build (declared in extension_config.cmake) into the expected path, +# matched to the DuckDB platform string, before running the unittester. +DUCKDB_VERSION_TAG := v1.4.4 + +define stage_icu + @if [ -f ./build/$(1)/extension/icu/icu.duckdb_extension ]; then \ + case "$$(uname -s)-$$(uname -m)" in \ + Linux-x86_64) platform=linux_amd64 ;; \ + Linux-aarch64) platform=linux_arm64 ;; \ + Darwin-arm64) platform=osx_arm64 ;; \ + Darwin-x86_64) platform=osx_amd64 ;; \ + *) platform=$$(uname -m) ;; \ + esac; \ + target=$$HOME/.duckdb/extensions/$(DUCKDB_VERSION_TAG)/$$platform; \ + mkdir -p "$$target" && cp -f ./build/$(1)/extension/icu/icu.duckdb_extension "$$target/" && \ + echo "Staged icu.duckdb_extension at $$target/"; \ + fi +endef + test_release_internal: - TZ=UTC ./build/release/$(TEST_PATH) "$(PROJ_DIR)test/*" + $(call stage_icu,release) + ./build/release/$(TEST_PATH) "$(PROJ_DIR)test/*" test_debug_internal: - TZ=UTC ./build/debug/$(TEST_PATH) "$(PROJ_DIR)test/*" + $(call stage_icu,debug) + ./build/debug/$(TEST_PATH) "$(PROJ_DIR)test/*" test_reldebug_internal: - TZ=UTC ./build/reldebug/$(TEST_PATH) "$(PROJ_DIR)test/*" + $(call stage_icu,reldebug) + ./build/reldebug/$(TEST_PATH) "$(PROJ_DIR)test/*" diff --git a/docs/parity-status.md b/docs/parity-status.md index 6c865ad3..ba9a6fcc 100644 --- a/docs/parity-status.md +++ b/docs/parity-status.md @@ -1,6 +1,6 @@ # MobilityDuck parity status — surface-level audit -Generated 2026-05-11. **Active addressable scope** (temporal + geo, excluding PG-only helpers): 929/943 names covered (98.5%). +Generated 2026-05-11. **Active addressable scope** (temporal + geo, excluding PG-only helpers): 943/943 names covered (100.0%). **Out of scope** (PG-only — no DuckDB equivalent exists): 315 names skipped — 84 from PG-only sections (GiST/SPGiST opclasses, set/span/spanset index files, `019_geo_constructors.in.sql` PG geometric types, `999_oid_cache.in.sql`) plus 231 PG helper functions inside active sections (`*_in/_out/_recv/_send`, `*_transfn/_combinefn/_finalfn/_serialize/_deserialize`, `*_sel/_joinsel/_supportfn/_analyze`, `*_typmod_in/_typmod_out`). Listed in appendix B; not counted in the headline. @@ -20,18 +20,18 @@ Per-section counts: `Addressable` = MDB names minus PG-only helpers (see appendi | Section | Addressable | Covered | Missing | Coverage | OOS | MDB operators | |---|---:|---:|---:|---:|---:|---:| -| `geo/050_geoset.in.sql` | 42 | 41 | 1 | 98% | 13 | 46 | -| `geo/051_stbox.in.sql` | 73 | 70 | 3 | 96% | 10 | 29 | +| `geo/050_geoset.in.sql` | 42 | 42 | 0 | 100% | 13 | 46 | +| `geo/051_stbox.in.sql` | 73 | 73 | 0 | 100% | 10 | 29 | | `geo/052_tgeo.in.sql` | 68 | 68 | 0 | 100% | 11 | 12 | | `geo/052_tpoint.in.sql` | 69 | 69 | 0 | 100% | 9 | 12 | | `geo/053_tgeo_inout.in.sql` | 18 | 18 | 0 | 100% | 0 | 0 | | `geo/053_tpoint_inout.in.sql` | 18 | 18 | 0 | 100% | 0 | 0 | | `geo/054_tgeo_compops.in.sql` | 6 | 6 | 0 | 100% | 1 | 36 | | `geo/054_tpoint_compops.in.sql` | 6 | 6 | 0 | 100% | 0 | 36 | -| `geo/056_tgeo_spatialfuncs.in.sql` | 16 | 15 | 1 | 94% | 0 | 0 | -| `geo/056_tpoint_spatialfuncs.in.sql` | 28 | 27 | 1 | 96% | 1 | 0 | -| `geo/058_tgeo_tile.in.sql` | 5 | 4 | 1 | 80% | 0 | 0 | -| `geo/058_tpoint_tile.in.sql` | 11 | 10 | 1 | 91% | 0 | 0 | +| `geo/056_tgeo_spatialfuncs.in.sql` | 16 | 16 | 0 | 100% | 0 | 0 | +| `geo/056_tpoint_spatialfuncs.in.sql` | 28 | 28 | 0 | 100% | 1 | 0 | +| `geo/058_tgeo_tile.in.sql` | 5 | 5 | 0 | 100% | 0 | 0 | +| `geo/058_tpoint_tile.in.sql` | 11 | 11 | 0 | 100% | 0 | 0 | | `geo/060_tgeo_boxops.in.sql` | 13 | 13 | 0 | 100% | 0 | 50 | | `geo/060_tpoint_boxops.in.sql` | 13 | 13 | 0 | 100% | 0 | 50 | | `geo/062_tgeo_posops.in.sql` | 16 | 16 | 0 | 100% | 0 | 76 | @@ -46,7 +46,7 @@ Per-section counts: `Addressable` = MDB names minus PG-only helpers (see appendi | `geo/072_tgeo_tempspatialrels.in.sql` | 6 | 6 | 0 | 100% | 0 | 0 | | `geo/072_tpoint_tempspatialrels.in.sql` | 5 | 5 | 0 | 100% | 0 | 0 | | `geo/076_tgeo_analytics.in.sql` | 12 | 12 | 0 | 100% | 0 | 0 | -| `geo/076_tpoint_analytics.in.sql` | 18 | 17 | 1 | 94% | 0 | 0 | +| `geo/076_tpoint_analytics.in.sql` | 18 | 18 | 0 | 100% | 0 | 0 | | `geo/078_tpoint_datagen.in.sql` | 0 | 0 | 0 | 0% | 1 | 0 | | `temporal/001_set.in.sql` | 47 | 47 | 0 | 100% | 35 | 38 | | `temporal/002_set_ops.in.sql` | 11 | 11 | 0 | 100% | 0 | 176 | @@ -58,7 +58,7 @@ Per-section counts: `Addressable` = MDB names minus PG-only helpers (see appendi | `temporal/021_tbox.in.sql` | 52 | 52 | 0 | 100% | 8 | 21 | | `temporal/022_temporal.in.sql` | 101 | 101 | 0 | 100% | 16 | 24 | | `temporal/023_temporal_inout.in.sql` | 16 | 16 | 0 | 100% | 0 | 0 | -| `temporal/025_temporal_tile.in.sql` | 16 | 11 | 5 | 69% | 0 | 0 | +| `temporal/025_temporal_tile.in.sql` | 16 | 16 | 0 | 100% | 0 | 0 | | `temporal/026_tnumber_mathfuncs.in.sql` | 17 | 17 | 0 | 100% | 0 | 24 | | `temporal/028_tbool_boolops.in.sql` | 4 | 4 | 0 | 100% | 0 | 7 | | `temporal/029_ttext_textfuncs.in.sql` | 4 | 4 | 0 | 100% | 0 | 3 | @@ -70,48 +70,10 @@ Per-section counts: `Addressable` = MDB names minus PG-only helpers (see appendi | `temporal/040_temporal_aggfuncs.in.sql` | 0 | 0 | 0 | 0% | 40 | 0 | | `temporal/042_temporal_waggfuncs.in.sql` | 0 | 0 | 0 | 0% | 8 | 0 | | `temporal/046_temporal_analytics.in.sql` | 4 | 4 | 0 | 100% | 0 | 0 | -| **TOTAL (active)** | **943** | **929** | **14** | **99%** | **231** | — | +| **TOTAL (active)** | **943** | **943** | **0** | **100%** | **231** | — | ## Missing function names per active section -### `geo/050_geoset.in.sql` — 1 missing of 42 addressable (98% covered) - -- `transformPipeline` (2 overloads) - -### `geo/051_stbox.in.sql` — 3 missing of 73 addressable (96% covered) - -- `geography` -- `perimeter` -- `quadSplit` - -### `geo/056_tgeo_spatialfuncs.in.sql` — 1 missing of 16 addressable (94% covered) - -- `transformPipeline` (2 overloads) - -### `geo/056_tpoint_spatialfuncs.in.sql` — 1 missing of 28 addressable (96% covered) - -- `transformPipeline` (3 overloads) - -### `geo/058_tgeo_tile.in.sql` — 1 missing of 5 addressable (80% covered) - -- `timeBoxes` - -### `geo/058_tpoint_tile.in.sql` — 1 missing of 11 addressable (91% covered) - -- `timeBoxes` - -### `geo/076_tpoint_analytics.in.sql` — 1 missing of 18 addressable (94% covered) - -- `geography` (2 overloads) - -### `temporal/025_temporal_tile.in.sql` — 5 missing of 16 addressable (69% covered) - -- `timeBins` (4 overloads) -- `timeBoxes` (2 overloads) -- `valueBins` (2 overloads) -- `valueBoxes` (2 overloads) -- `valueTimeBoxes` (2 overloads) - ## Appendix B — Out of scope (PG-only, no DuckDB equivalent) These entries are PG-specific helpers — index opclasses, aggregate transition/combine/final/serialize callbacks, planner hooks (`_sel`, `_joinsel`, `_supportfn`, `_analyze`), text/binary I/O helpers (`_in`, `_out`, `_recv`, `_send`), type modifier helpers, the `999_oid_cache` PG catalog hook, and PG geometric type constructors (`019_geo_constructors`). None of them have DuckDB equivalents and they should not be implemented; listed here only for completeness. @@ -162,11 +124,11 @@ These families (cbuffer, npoint, pose, rgeo) are deferred until the active tempo | Section | Addressable | Covered | Missing | Coverage | |---|---:|---:|---:|---:| -| `cbuffer/150_cbuffer.in.sql` | 31 | 7 | 24 | 23% | -| `cbuffer/151_cbufferset.in.sql` | 42 | 32 | 10 | 76% | +| `cbuffer/150_cbuffer.in.sql` | 31 | 8 | 23 | 26% | +| `cbuffer/151_cbufferset.in.sql` | 42 | 33 | 9 | 79% | | `cbuffer/152_tcbuffer.in.sql` | 84 | 66 | 18 | 79% | | `cbuffer/154_tcbuffer_compops.in.sql` | 6 | 6 | 0 | 100% | -| `cbuffer/155_tcbuffer_spatialfuncs.in.sql` | 9 | 6 | 3 | 67% | +| `cbuffer/155_tcbuffer_spatialfuncs.in.sql` | 9 | 7 | 2 | 78% | | `cbuffer/158_tcbuffer_topops.in.sql` | 7 | 7 | 0 | 100% | | `cbuffer/159_tcbuffer_posops.in.sql` | 12 | 12 | 0 | 100% | | `cbuffer/160_tcbuffer_distance.in.sql` | 5 | 4 | 1 | 80% | @@ -186,11 +148,11 @@ These families (cbuffer, npoint, pose, rgeo) are deferred until the active tempo | `npoint/093_tnpoint_distance.in.sql` | 4 | 4 | 0 | 100% | | `npoint/095_tnpoint_aggfuncs.in.sql` | 8 | 0 | 8 | 0% | | `npoint/098_tnpoint_indexes.in.sql` | 1 | 0 | 1 | 0% | -| `pose/100_pose.in.sql` | 34 | 10 | 24 | 29% | -| `pose/101_poseset.in.sql` | 46 | 33 | 13 | 72% | +| `pose/100_pose.in.sql` | 34 | 11 | 23 | 32% | +| `pose/101_poseset.in.sql` | 46 | 34 | 12 | 74% | | `pose/102_tpose.in.sql` | 84 | 65 | 19 | 77% | | `pose/104_tpose_compops.in.sql` | 6 | 6 | 0 | 100% | -| `pose/105_tpose_spatialfuncs.in.sql` | 8 | 7 | 1 | 88% | +| `pose/105_tpose_spatialfuncs.in.sql` | 8 | 8 | 0 | 100% | | `pose/108_tpose_topops.in.sql` | 7 | 7 | 0 | 100% | | `pose/109_tpose_posops.in.sql` | 16 | 16 | 0 | 100% | | `pose/111_tpose_aggfuncs.in.sql` | 7 | 0 | 7 | 0% | @@ -198,12 +160,12 @@ These families (cbuffer, npoint, pose, rgeo) are deferred until the active tempo | `pose/114_tpose_indexes.in.sql` | 1 | 0 | 1 | 0% | | `rgeo/122_trgeo.in.sql` | 83 | 65 | 18 | 78% | | `rgeo/124_trgeo_compops.in.sql` | 6 | 6 | 0 | 100% | -| `rgeo/125_trgeo_spatialfuncs.in.sql` | 4 | 3 | 1 | 75% | +| `rgeo/125_trgeo_spatialfuncs.in.sql` | 4 | 4 | 0 | 100% | | `rgeo/128_trgeo_topops.in.sql` | 5 | 5 | 0 | 100% | | `rgeo/129_trgeo_posops.in.sql` | 12 | 12 | 0 | 100% | | `rgeo/131_trgeo_aggfuncs.in.sql` | 7 | 0 | 7 | 0% | | `rgeo/133_trgeo_distance.in.sql` | 4 | 4 | 0 | 100% | | `rgeo/133_trgeo_vclip.in.sql` | 6 | 0 | 6 | 0% | | `rgeo/134_trgeo_indexes.in.sql` | 1 | 0 | 1 | 0% | -| **TOTAL (deferred)** | **782** | **542** | **240** | **69%** | +| **TOTAL (deferred)** | **782** | **549** | **233** | **70%** | diff --git a/src/geo/geoset.cpp b/src/geo/geoset.cpp index bd5c81e2..0291fbc4 100644 --- a/src/geo/geoset.cpp +++ b/src/geo/geoset.cpp @@ -112,11 +112,28 @@ void SpatialSetType::RegisterScalarFunctions(ExtensionLoader &loader) { {SpatialSetType::geomset(), LogicalType::INTEGER}, SpatialSetType::geomset(), SpatialSetFunctions::Spatialset_transform)); duckdb::RegisterSerializedScalarFunction(loader, ScalarFunction( - "transform", + "transform", {SpatialSetType::geogset(), LogicalType::INTEGER}, SpatialSetType::geogset(), SpatialSetFunctions::Spatialset_transform)); + // transformPipeline(, pipeline text, srid int = 0, + // is_forward bool = true) + for (auto &set_type : {SpatialSetType::geomset(), SpatialSetType::geogset()}) { + duckdb::RegisterSerializedScalarFunction(loader, ScalarFunction( + "transformPipeline", + {set_type, LogicalType::VARCHAR}, + set_type, SpatialSetFunctions::Spatialset_transform_pipeline)); + duckdb::RegisterSerializedScalarFunction(loader, ScalarFunction( + "transformPipeline", + {set_type, LogicalType::VARCHAR, LogicalType::INTEGER}, + set_type, SpatialSetFunctions::Spatialset_transform_pipeline)); + duckdb::RegisterSerializedScalarFunction(loader, ScalarFunction( + "transformPipeline", + {set_type, LogicalType::VARCHAR, LogicalType::INTEGER, LogicalType::BOOLEAN}, + set_type, SpatialSetFunctions::Spatialset_transform_pipeline)); + } + duckdb::RegisterSerializedScalarFunction(loader, ScalarFunction( - "startValue", {SpatialSetType::geomset()}, + "startValue", {SpatialSetType::geomset()}, GeoTypes::GEOMETRY(), SpatialSetFunctions::Set_start_value )); @@ -451,6 +468,44 @@ void SpatialSetFunctions::Spatialset_transform(DataChunk &args, ExpressionState } } +/* transformPipeline(, pipeline text, srid int = 0, + * is_forward bool = true) + * Apply a PROJ pipeline string to every element of the spatial set. + */ +void SpatialSetFunctions::Spatialset_transform_pipeline(DataChunk &args, ExpressionState &state, Vector &result_vec) { + const idx_t row_count = args.size(); + for (idx_t i = 0; i < args.ColumnCount(); i++) args.data[i].Flatten(row_count); + const idx_t cc = args.ColumnCount(); + auto in_set = FlatVector::GetData(args.data[0]); + auto in_pipe = FlatVector::GetData(args.data[1]); + auto &v0 = FlatVector::Validity(args.data[0]); + auto &v1 = FlatVector::Validity(args.data[1]); + auto out_data = FlatVector::GetData(result_vec); + auto &out_validity = FlatVector::Validity(result_vec); + for (idx_t row = 0; row < row_count; row++) { + if (!v0.RowIsValid(row) || !v1.RowIsValid(row)) { + out_validity.SetInvalid(row); + continue; + } + size_t sz = in_set[row].GetSize(); + Set *s = (Set *) malloc(sz); + memcpy(s, in_set[row].GetData(), sz); + int32_t srid = (cc > 2) ? FlatVector::GetData(args.data[2])[row] : 0; + bool is_fwd = (cc > 3) ? FlatVector::GetData(args.data[3])[row] : true; + std::string pipe = in_pipe[row].GetString(); + Set *ret = spatialset_transform_pipeline(s, pipe.c_str(), srid, is_fwd); + free(s); + if (!ret) { + out_validity.SetInvalid(row); + continue; + } + size_t rsz = set_mem_size(ret); + out_data[row] = StringVector::AddStringOrBlob(result_vec, (const char *) ret, rsz); + free(ret); + } + if (row_count == 1) result_vec.SetVectorType(VectorType::CONSTANT_VECTOR); +} + // --- startValue --- void SpatialSetFunctions::Set_start_value(DataChunk &args, ExpressionState &state, Vector &result) { auto &input = args.data[0]; diff --git a/src/geo/stbox.cpp b/src/geo/stbox.cpp index 8a504d4d..70576e8d 100644 --- a/src/geo/stbox.cpp +++ b/src/geo/stbox.cpp @@ -393,6 +393,43 @@ void StboxType::RegisterScalarFunctions(ExtensionLoader &loader) { ScalarFunction("SRID", {STBOX()}, LogicalType::INTEGER, StboxFunctions::Stbox_srid)); + // perimeter(stbox [, spheroid bool]) — sum of edge lengths. + duckdb::RegisterSerializedScalarFunction(loader, + ScalarFunction("perimeter", {STBOX()}, LogicalType::DOUBLE, + StboxFunctions::Stbox_perimeter)); + duckdb::RegisterSerializedScalarFunction(loader, + ScalarFunction("perimeter", {STBOX(), LogicalType::BOOLEAN}, + LogicalType::DOUBLE, StboxFunctions::Stbox_perimeter)); + + // quadSplit(stbox) — split the spatial extent into four quadrants + // (each with the original time span), returning an stbox[]. + duckdb::RegisterSerializedScalarFunction(loader, + ScalarFunction("quadSplit", {STBOX()}, + LogicalType::LIST(STBOX()), + StboxFunctions::Stbox_quad_split)); + + // geography(stbox) — same C entrypoint as `geometry(stbox)`; DuckDB + // has no separate geography type so both routes produce a GEOMETRY + // blob. Registered for naming parity with MobilityDB. + duckdb::RegisterSerializedScalarFunction(loader, + ScalarFunction("geography", {STBOX()}, GeoTypes::GEOMETRY(), + StboxFunctions::Stbox_to_geo)); + + // transformPipeline(stbox, pipeline text, srid int = 0, + // is_forward bool = true) + duckdb::RegisterSerializedScalarFunction(loader, + ScalarFunction("transformPipeline", + {STBOX(), LogicalType::VARCHAR}, + STBOX(), StboxFunctions::Stbox_transform_pipeline)); + duckdb::RegisterSerializedScalarFunction(loader, + ScalarFunction("transformPipeline", + {STBOX(), LogicalType::VARCHAR, LogicalType::INTEGER}, + STBOX(), StboxFunctions::Stbox_transform_pipeline)); + duckdb::RegisterSerializedScalarFunction(loader, + ScalarFunction("transformPipeline", + {STBOX(), LogicalType::VARCHAR, LogicalType::INTEGER, LogicalType::BOOLEAN}, + STBOX(), StboxFunctions::Stbox_transform_pipeline)); + duckdb::RegisterSerializedScalarFunction(loader, ScalarFunction( "shiftTime", diff --git a/src/geo/stbox_functions.cpp b/src/geo/stbox_functions.cpp index 90612684..e7d513c9 100644 --- a/src/geo/stbox_functions.cpp +++ b/src/geo/stbox_functions.cpp @@ -1485,6 +1485,28 @@ void StboxFunctions::Stbox_srid(DataChunk &args, ExpressionState &state, Vector if (args.size() == 1) result.SetVectorType(VectorType::CONSTANT_VECTOR); } +void StboxFunctions::Stbox_perimeter(DataChunk &args, ExpressionState &state, Vector &result) { + const idx_t row_count = args.size(); + args.data[0].Flatten(row_count); + const bool has_spheroid = args.ColumnCount() > 1; + if (has_spheroid) args.data[1].Flatten(row_count); + auto in_box = FlatVector::GetData(args.data[0]); + auto in_sph = has_spheroid ? FlatVector::GetData(args.data[1]) : nullptr; + auto &v0 = FlatVector::Validity(args.data[0]); + auto out_data = FlatVector::GetData(result); + auto &out_validity = FlatVector::Validity(result); + for (idx_t row = 0; row < row_count; row++) { + if (!v0.RowIsValid(row)) { out_validity.SetInvalid(row); continue; } + if (in_box[row].GetSize() != sizeof(STBox)) { + throw InvalidInputException("Invalid STBOX value size (MEOS ABI mismatch or corrupt value)"); + } + STBox box; + memcpy(&box, in_box[row].GetData(), sizeof(STBox)); + out_data[row] = stbox_perimeter(&box, in_sph ? in_sph[row] : false); + } + if (row_count == 1) result.SetVectorType(VectorType::CONSTANT_VECTOR); +} + void StboxFunctions::Stbox_volume(DataChunk &args, ExpressionState &state, Vector &result) { UnaryExecutor::ExecuteWithNulls( args.data[0], result, args.size(), @@ -3581,6 +3603,69 @@ void StboxFunctions::Geo_split_each_n_stboxes(DataChunk &args, ExpressionState & if (row_count == 1) result.SetVectorType(VectorType::CONSTANT_VECTOR); } +/* transformPipeline(stbox, pipeline text, srid int = 0, is_forward bool = true) + * Apply a PROJ pipeline string to an stbox. + */ +void StboxFunctions::Stbox_transform_pipeline(DataChunk &args, ExpressionState &state, Vector &result) { + const idx_t row_count = args.size(); + for (idx_t i = 0; i < args.ColumnCount(); i++) args.data[i].Flatten(row_count); + const idx_t cc = args.ColumnCount(); + auto in_box = FlatVector::GetData(args.data[0]); + auto in_pipe = FlatVector::GetData(args.data[1]); + auto &v0 = FlatVector::Validity(args.data[0]); + auto &v1 = FlatVector::Validity(args.data[1]); + auto out_data = FlatVector::GetData(result); + auto &out_validity = FlatVector::Validity(result); + for (idx_t row = 0; row < row_count; row++) { + if (!v0.RowIsValid(row) || !v1.RowIsValid(row)) { + out_validity.SetInvalid(row); + continue; + } + if (in_box[row].GetSize() != sizeof(STBox)) { + throw InvalidInputException("Invalid STBOX value size (MEOS ABI mismatch or corrupt value)"); + } + STBox box; + memcpy(&box, in_box[row].GetData(), sizeof(STBox)); + int32_t srid = (cc > 2) ? FlatVector::GetData(args.data[2])[row] : 0; + bool is_fwd = (cc > 3) ? FlatVector::GetData(args.data[3])[row] : true; + std::string pipe = in_pipe[row].GetString(); + STBox *ret = stbox_transform_pipeline(&box, pipe.c_str(), srid, is_fwd); + if (!ret) { + out_validity.SetInvalid(row); + continue; + } + string_t blob(reinterpret_cast(ret), sizeof(STBox)); + out_data[row] = StringVector::AddStringOrBlob(result, blob); + free(ret); + } + if (row_count == 1) result.SetVectorType(VectorType::CONSTANT_VECTOR); +} + +void StboxFunctions::Stbox_quad_split(DataChunk &args, ExpressionState &state, Vector &result) { + const idx_t row_count = args.size(); + args.data[0].Flatten(row_count); + auto in_box = FlatVector::GetData(args.data[0]); + auto list_entries = FlatVector::GetData(result); + auto &out_validity = FlatVector::Validity(result); + idx_t total = 0; + for (idx_t row = 0; row < row_count; row++) { + if (!FlatVector::Validity(args.data[0]).RowIsValid(row)) { + out_validity.SetInvalid(row); + list_entries[row] = list_entry_t{total, 0}; + continue; + } + if (in_box[row].GetSize() != sizeof(STBox)) { + throw InvalidInputException("Invalid STBOX value size (MEOS ABI mismatch or corrupt value)"); + } + STBox box; + memcpy(&box, in_box[row].GetData(), sizeof(STBox)); + int count = 0; + STBox *boxes = stbox_quad_split(&box, &count); + EmitStboxList(result, row, list_entries, boxes, count, total); + } + if (row_count == 1) result.SetVectorType(VectorType::CONSTANT_VECTOR); +} + void StboxFunctions::Stbox_get_space_tile(DataChunk &args, ExpressionState &state, Vector &result) { const idx_t row_count = args.size(); for (idx_t i = 0; i < args.ColumnCount(); i++) args.data[i].Flatten(row_count); diff --git a/src/geo/tgeogpoint.cpp b/src/geo/tgeogpoint.cpp index 499ed35e..5ad185d1 100644 --- a/src/geo/tgeogpoint.cpp +++ b/src/geo/tgeogpoint.cpp @@ -1226,6 +1226,21 @@ void TgeogpointType::RegisterScalarFunctions(ExtensionLoader &loader) { ) ); + // transformPipeline(tgeogpoint, pipeline text, srid int = 0, + // is_forward bool = true) + duckdb::RegisterSerializedScalarFunction(loader, + ScalarFunction("transformPipeline", + {TGEOGPOINT(), LogicalType::VARCHAR}, + TGEOGPOINT(), TgeompointFunctions::Tspatial_transform_pipeline)); + duckdb::RegisterSerializedScalarFunction(loader, + ScalarFunction("transformPipeline", + {TGEOGPOINT(), LogicalType::VARCHAR, LogicalType::INTEGER}, + TGEOGPOINT(), TgeompointFunctions::Tspatial_transform_pipeline)); + duckdb::RegisterSerializedScalarFunction(loader, + ScalarFunction("transformPipeline", + {TGEOGPOINT(), LogicalType::VARCHAR, LogicalType::INTEGER, LogicalType::BOOLEAN}, + TGEOGPOINT(), TgeompointFunctions::Tspatial_transform_pipeline)); + duckdb::RegisterSerializedScalarFunction(loader, ScalarFunction( "round", @@ -1936,6 +1951,66 @@ void TgeogpointType::RegisterRoundtripIO(ExtensionLoader &loader) { /* tgeogpointFromMFJSON */ duckdb::RegisterSerializedScalarFunction(loader, ScalarFunction("tgeogpointFromMFJSON", {V}, T, TgeogFromMfjsonExec)); + + /* geography(tgeogpoint [, segmentize bool]) -> geometry + * Trajectory of the temporal geographic point. Same MEOS call as + * `geometry(tgeompoint)` (`tpoint_tfloat_to_geomeas` with a NULL + * measure); DuckDB has no separate geography type so the result is + * a GEOMETRY blob carrying the underlying geog. + */ + const auto G = GeoTypes::GEOMETRY(); + duckdb::RegisterSerializedScalarFunction(loader, ScalarFunction("geography", {T}, G, + [](DataChunk &args, ExpressionState &state, Vector &result) { + const idx_t row_count = args.size(); + args.data[0].Flatten(row_count); + auto in_temp = FlatVector::GetData(args.data[0]); + auto &v0 = FlatVector::Validity(args.data[0]); + auto out_data = FlatVector::GetData(result); + auto &out_validity = FlatVector::Validity(result); + for (idx_t row = 0; row < row_count; row++) { + if (!v0.RowIsValid(row)) { out_validity.SetInvalid(row); continue; } + Temporal *t = GeogBlobToTemp(in_temp[row]); + GSERIALIZED *geom = nullptr; + bool ok = tpoint_tfloat_to_geomeas(t, nullptr, false, &geom); + free(t); + if (!ok || !geom) { + out_validity.SetInvalid(row); + if (geom) free(geom); + continue; + } + string_t enc = GSerializedToGeometry(geom, state, result); + out_data[row] = StringVector::AddStringOrBlob(result, enc); + free(geom); + } + if (row_count == 1) result.SetVectorType(VectorType::CONSTANT_VECTOR); + })); + duckdb::RegisterSerializedScalarFunction(loader, ScalarFunction("geography", {T, BL}, G, + [](DataChunk &args, ExpressionState &state, Vector &result) { + const idx_t row_count = args.size(); + args.data[0].Flatten(row_count); + args.data[1].Flatten(row_count); + auto in_temp = FlatVector::GetData(args.data[0]); + auto in_seg = FlatVector::GetData(args.data[1]); + auto &v0 = FlatVector::Validity(args.data[0]); + auto out_data = FlatVector::GetData(result); + auto &out_validity = FlatVector::Validity(result); + for (idx_t row = 0; row < row_count; row++) { + if (!v0.RowIsValid(row)) { out_validity.SetInvalid(row); continue; } + Temporal *t = GeogBlobToTemp(in_temp[row]); + GSERIALIZED *geom = nullptr; + bool ok = tpoint_tfloat_to_geomeas(t, nullptr, in_seg[row], &geom); + free(t); + if (!ok || !geom) { + out_validity.SetInvalid(row); + if (geom) free(geom); + continue; + } + string_t enc = GSerializedToGeometry(geom, state, result); + out_data[row] = StringVector::AddStringOrBlob(result, enc); + free(geom); + } + if (row_count == 1) result.SetVectorType(VectorType::CONSTANT_VECTOR); + })); } // ============================================================ diff --git a/src/geo/tgeography_ops.cpp b/src/geo/tgeography_ops.cpp index d8549e51..8fff6594 100644 --- a/src/geo/tgeography_ops.cpp +++ b/src/geo/tgeography_ops.cpp @@ -435,6 +435,36 @@ void TspatialTransformExec(DataChunk &args, ExpressionState &, Vector &result) { }); } +void TspatialTransformPipelineExec(DataChunk &args, ExpressionState &, Vector &result) { + const idx_t row_count = args.size(); + for (idx_t i = 0; i < args.ColumnCount(); i++) args.data[i].Flatten(row_count); + const idx_t cc = args.ColumnCount(); + auto in_temp = FlatVector::GetData(args.data[0]); + auto in_pipe = FlatVector::GetData(args.data[1]); + auto &v0 = FlatVector::Validity(args.data[0]); + auto &v1 = FlatVector::Validity(args.data[1]); + auto out_data = FlatVector::GetData(result); + auto &out_validity = FlatVector::Validity(result); + for (idx_t row = 0; row < row_count; row++) { + if (!v0.RowIsValid(row) || !v1.RowIsValid(row)) { + out_validity.SetInvalid(row); + continue; + } + int32_t srid = (cc > 2) ? FlatVector::GetData(args.data[2])[row] : 0; + bool is_fwd = (cc > 3) ? FlatVector::GetData(args.data[3])[row] : true; + Temporal *t = DecodeTemporalCopy(in_temp[row]); + std::string pipe = in_pipe[row].GetString(); + Temporal *r = tspatial_transform_pipeline(t, pipe.c_str(), srid, is_fwd); + free(t); + if (!r) { + out_validity.SetInvalid(row); + continue; + } + out_data[row] = TemporalToBlob(result, r); + } + if (row_count == 1) result.SetVectorType(VectorType::CONSTANT_VECTOR); +} + void TspatialToStboxExec(DataChunk &args, ExpressionState &, Vector &result) { UnaryExecutor::Execute( args.data[0], result, args.size(), @@ -769,6 +799,18 @@ void TGeographyOps::RegisterScalarFunctions(ExtensionLoader &loader) { loader.RegisterFunction(ScalarFunction( "transform", {TGEOM, INT32}, TGEOM, TspatialTransformExec)); + // transformPipeline(tgeography, pipeline text, srid int = 0, + // is_forward bool = true) + loader.RegisterFunction(ScalarFunction( + "transformPipeline", {TGEOM, LogicalType::VARCHAR}, TGEOM, + TspatialTransformPipelineExec)); + loader.RegisterFunction(ScalarFunction( + "transformPipeline", {TGEOM, LogicalType::VARCHAR, INT32}, TGEOM, + TspatialTransformPipelineExec)); + loader.RegisterFunction(ScalarFunction( + "transformPipeline", {TGEOM, LogicalType::VARCHAR, INT32, LogicalType::BOOLEAN}, TGEOM, + TspatialTransformPipelineExec)); + // tgeography → stbox is a cast in the SQL surface; expose it as a // function for now to keep the implementation a single template. loader.RegisterFunction(ScalarFunction( diff --git a/src/geo/tgeometry_ops.cpp b/src/geo/tgeometry_ops.cpp index 83656757..8fc21b62 100644 --- a/src/geo/tgeometry_ops.cpp +++ b/src/geo/tgeometry_ops.cpp @@ -435,6 +435,36 @@ void TspatialTransformExec(DataChunk &args, ExpressionState &, Vector &result) { }); } +void TspatialTransformPipelineExec(DataChunk &args, ExpressionState &, Vector &result) { + const idx_t row_count = args.size(); + for (idx_t i = 0; i < args.ColumnCount(); i++) args.data[i].Flatten(row_count); + const idx_t cc = args.ColumnCount(); + auto in_temp = FlatVector::GetData(args.data[0]); + auto in_pipe = FlatVector::GetData(args.data[1]); + auto &v0 = FlatVector::Validity(args.data[0]); + auto &v1 = FlatVector::Validity(args.data[1]); + auto out_data = FlatVector::GetData(result); + auto &out_validity = FlatVector::Validity(result); + for (idx_t row = 0; row < row_count; row++) { + if (!v0.RowIsValid(row) || !v1.RowIsValid(row)) { + out_validity.SetInvalid(row); + continue; + } + int32_t srid = (cc > 2) ? FlatVector::GetData(args.data[2])[row] : 0; + bool is_fwd = (cc > 3) ? FlatVector::GetData(args.data[3])[row] : true; + Temporal *t = DecodeTemporalCopy(in_temp[row]); + std::string pipe = in_pipe[row].GetString(); + Temporal *r = tspatial_transform_pipeline(t, pipe.c_str(), srid, is_fwd); + free(t); + if (!r) { + out_validity.SetInvalid(row); + continue; + } + out_data[row] = TemporalToBlob(result, r); + } + if (row_count == 1) result.SetVectorType(VectorType::CONSTANT_VECTOR); +} + void TspatialToStboxExec(DataChunk &args, ExpressionState &, Vector &result) { UnaryExecutor::Execute( args.data[0], result, args.size(), @@ -766,6 +796,18 @@ void TGeometryOps::RegisterScalarFunctions(ExtensionLoader &loader) { loader.RegisterFunction(ScalarFunction( "transform", {TGEOM, INT32}, TGEOM, TspatialTransformExec)); + // transformPipeline(tgeometry, pipeline text, srid int = 0, + // is_forward bool = true) + loader.RegisterFunction(ScalarFunction( + "transformPipeline", {TGEOM, LogicalType::VARCHAR}, TGEOM, + TspatialTransformPipelineExec)); + loader.RegisterFunction(ScalarFunction( + "transformPipeline", {TGEOM, LogicalType::VARCHAR, INT32}, TGEOM, + TspatialTransformPipelineExec)); + loader.RegisterFunction(ScalarFunction( + "transformPipeline", {TGEOM, LogicalType::VARCHAR, INT32, LogicalType::BOOLEAN}, TGEOM, + TspatialTransformPipelineExec)); + // tgeometry → stbox is a cast in the SQL surface; expose it as a // function for now to keep the implementation a single template. loader.RegisterFunction(ScalarFunction( diff --git a/src/geo/tgeompoint.cpp b/src/geo/tgeompoint.cpp index a70a7327..4788c946 100644 --- a/src/geo/tgeompoint.cpp +++ b/src/geo/tgeompoint.cpp @@ -1267,7 +1267,7 @@ void TgeompointType::RegisterScalarFunctions(ExtensionLoader &loader) { ) ); - duckdb::RegisterSerializedScalarFunction(loader, + duckdb::RegisterSerializedScalarFunction(loader, ScalarFunction( "transform", {TGEOMPOINT(), LogicalType::INTEGER}, @@ -1276,7 +1276,22 @@ void TgeompointType::RegisterScalarFunctions(ExtensionLoader &loader) { ) ); - duckdb::RegisterSerializedScalarFunction(loader, + // transformPipeline(tgeompoint, pipeline text, srid int = 0, + // is_forward bool = true) + duckdb::RegisterSerializedScalarFunction(loader, + ScalarFunction("transformPipeline", + {TGEOMPOINT(), LogicalType::VARCHAR}, + TGEOMPOINT(), TgeompointFunctions::Tspatial_transform_pipeline)); + duckdb::RegisterSerializedScalarFunction(loader, + ScalarFunction("transformPipeline", + {TGEOMPOINT(), LogicalType::VARCHAR, LogicalType::INTEGER}, + TGEOMPOINT(), TgeompointFunctions::Tspatial_transform_pipeline)); + duckdb::RegisterSerializedScalarFunction(loader, + ScalarFunction("transformPipeline", + {TGEOMPOINT(), LogicalType::VARCHAR, LogicalType::INTEGER, LogicalType::BOOLEAN}, + TGEOMPOINT(), TgeompointFunctions::Tspatial_transform_pipeline)); + + duckdb::RegisterSerializedScalarFunction(loader, ScalarFunction( "round", {TGEOMPOINT(), LogicalType::INTEGER}, @@ -2446,6 +2461,46 @@ void TgeoGeoMeasureExec(DataChunk &args, ExpressionState &state, Vector &result) if (row_count == 1) result.SetVectorType(VectorType::CONSTANT_VECTOR); } +/* geometry(tgeompoint [, segmentize bool]) / + * geography(tgeogpoint [, segmentize bool]) + * + * Convert a temporal point's trajectory to a (possibly segmentized) + * geometry/geography linestring. Same underlying MEOS call + * (`tpoint_tfloat_to_geomeas`) as `geoMeasure`, but with a NULL + * measure — so the M coordinate is omitted from the output. + */ +void TgeoToGeomExec(DataChunk &args, ExpressionState &state, Vector &result) { + const idx_t row_count = args.size(); + for (idx_t i = 0; i < args.ColumnCount(); i++) args.data[i].Flatten(row_count); + const idx_t cc = args.ColumnCount(); + auto in_temp = FlatVector::GetData(args.data[0]); + auto &v0 = FlatVector::Validity(args.data[0]); + auto out_data = FlatVector::GetData(result); + auto &out_validity = FlatVector::Validity(result); + + for (idx_t row = 0; row < row_count; row++) { + if (!v0.RowIsValid(row)) { + out_validity.SetInvalid(row); + continue; + } + Temporal *t = BlobToTempMVT(in_temp[row]); + bool segmentize = (cc > 1) ? FlatVector::GetData(args.data[1])[row] : false; + GSERIALIZED *geom = nullptr; + bool ok = tpoint_tfloat_to_geomeas(t, nullptr, segmentize, &geom); + free(t); + if (!ok || !geom) { + out_validity.SetInvalid(row); + if (geom) free(geom); + continue; + } + ArenaAllocator arena(BufferAllocator::Get(state.GetContext())); + string_t enc = GSerializedToGeometry(geom, arena, result); + out_data[row] = StringVector::AddStringOrBlob(result, enc); + free(geom); + } + if (row_count == 1) result.SetVectorType(VectorType::CONSTANT_VECTOR); +} + } // namespace void TgeompointType::RegisterRoundtripIO(ExtensionLoader &loader) { @@ -2546,6 +2601,13 @@ void TgeompointType::RegisterAnalyticsViz(ExtensionLoader &loader) { /* geoMeasure(tgeompoint, tfloat[, segmentize]) -> geometry */ duckdb::RegisterSerializedScalarFunction(loader, ScalarFunction("geoMeasure", {T, TemporalTypes::TFLOAT()}, G, TgeoGeoMeasureExec)); duckdb::RegisterSerializedScalarFunction(loader, ScalarFunction("geoMeasure", {T, TemporalTypes::TFLOAT(), BL}, G, TgeoGeoMeasureExec)); + + /* geometry(tgeompoint [, segmentize bool]) -> geometry + * Trajectory of the temporal point, optionally segmentized into + * pairwise linestrings. Mirrors MobilityDB's `geometry(tgeompoint)` + * conversion. */ + duckdb::RegisterSerializedScalarFunction(loader, ScalarFunction("geometry", {T}, G, TgeoToGeomExec)); + duckdb::RegisterSerializedScalarFunction(loader, ScalarFunction("geometry", {T, BL}, G, TgeoToGeomExec)); } } // namespace duckdb diff --git a/src/geo/tgeompoint_functions.cpp b/src/geo/tgeompoint_functions.cpp index eb51bfa8..7cfd6a23 100644 --- a/src/geo/tgeompoint_functions.cpp +++ b/src/geo/tgeompoint_functions.cpp @@ -1551,6 +1551,48 @@ void TgeompointFunctions::Tspatial_transform(DataChunk &args, ExpressionState &s } } +/* transformPipeline(, pipeline text, srid int = 0, is_forward bool = true) + * + * Apply a PROJ pipeline string to a temporal spatial value. srid is + * the target SRID; is_forward selects forward vs inverse application + * of the pipeline. Default srid=0 / is_forward=true follow MobilityDB. + */ +void TgeompointFunctions::Tspatial_transform_pipeline(DataChunk &args, ExpressionState &state, Vector &result) { + const idx_t row_count = args.size(); + for (idx_t i = 0; i < args.ColumnCount(); i++) args.data[i].Flatten(row_count); + const idx_t cc = args.ColumnCount(); + auto in_temp = FlatVector::GetData(args.data[0]); + auto in_pipe = FlatVector::GetData(args.data[1]); + auto &v0 = FlatVector::Validity(args.data[0]); + auto &v1 = FlatVector::Validity(args.data[1]); + auto out_data = FlatVector::GetData(result); + auto &out_validity = FlatVector::Validity(result); + for (idx_t row = 0; row < row_count; row++) { + if (!v0.RowIsValid(row) || !v1.RowIsValid(row)) { + out_validity.SetInvalid(row); + continue; + } + int32_t srid = (cc > 2) ? FlatVector::GetData(args.data[2])[row] : 0; + bool is_fwd = (cc > 3) ? FlatVector::GetData(args.data[3])[row] : true; + size_t sz = in_temp[row].GetSize(); + uint8_t *copy = (uint8_t *) malloc(sz); + memcpy(copy, in_temp[row].GetData(), sz); + Temporal *t = reinterpret_cast(copy); + std::string pipe = in_pipe[row].GetString(); + Temporal *ret = tspatial_transform_pipeline(t, pipe.c_str(), srid, is_fwd); + free(t); + if (!ret) { + out_validity.SetInvalid(row); + continue; + } + size_t rsz = temporal_mem_size(ret); + string_t blob(reinterpret_cast(ret), rsz); + out_data[row] = StringVector::AddStringOrBlob(result, blob); + free(ret); + } + if (row_count == 1) result.SetVectorType(VectorType::CONSTANT_VECTOR); +} + /* *************************************************** * Spatial relationships ****************************************************/ diff --git a/src/include/geo/geoset.hpp b/src/include/geo/geoset.hpp index a796f15e..5afdc1d5 100644 --- a/src/include/geo/geoset.hpp +++ b/src/include/geo/geoset.hpp @@ -38,6 +38,7 @@ struct SpatialSetFunctions{ static void Spatialset_srid(DataChunk &args, ExpressionState &state, Vector &result); static void Spatialset_set_srid(DataChunk &args, ExpressionState &state, Vector &result_vec); static void Spatialset_transform(DataChunk &args, ExpressionState &state, Vector &result_vec); + static void Spatialset_transform_pipeline(DataChunk &args, ExpressionState &state, Vector &result_vec); static void Set_start_value(DataChunk &args, ExpressionState &state, Vector &result); static void Set_end_value(DataChunk &args, ExpressionState &state, Vector &result); static void Set_num_values(DataChunk &args, ExpressionState &state, Vector &result); diff --git a/src/include/geo/stbox_functions.hpp b/src/include/geo/stbox_functions.hpp index 37f5285e..3593cde6 100644 --- a/src/include/geo/stbox_functions.hpp +++ b/src/include/geo/stbox_functions.hpp @@ -99,7 +99,9 @@ struct StboxFunctions { static void Stbox_hash(DataChunk &args, ExpressionState &state, Vector &result); static void Stbox_hash_extended(DataChunk &args, ExpressionState &state, Vector &result); static void Stbox_srid(DataChunk &args, ExpressionState &state, Vector &result); - // TODO static void Stbox_perimeter(DataChunk &args, ExpressionState &state, Vector &result); + static void Stbox_perimeter(DataChunk &args, ExpressionState &state, Vector &result); + static void Stbox_quad_split(DataChunk &args, ExpressionState &state, Vector &result); + static void Stbox_transform_pipeline(DataChunk &args, ExpressionState &state, Vector &result); /* *************************************************** * Transformation functions ****************************************************/ diff --git a/src/include/geo/tgeompoint_functions.hpp b/src/include/geo/tgeompoint_functions.hpp index dc005046..97ed1730 100644 --- a/src/include/geo/tgeompoint_functions.hpp +++ b/src/include/geo/tgeompoint_functions.hpp @@ -106,6 +106,7 @@ struct TgeompointFunctions { static void Tgeo_at_stbox(DataChunk &args, ExpressionState &state, Vector &result); static void Tgeo_minus_stbox(DataChunk &args, ExpressionState &state, Vector &result); static void Tspatial_transform(DataChunk &args, ExpressionState &state, Vector &result); + static void Tspatial_transform_pipeline(DataChunk &args, ExpressionState &state, Vector &result); /* *************************************************** * Spatial relationships diff --git a/src/include/temporal/temporal_functions.hpp b/src/include/temporal/temporal_functions.hpp index fcbad30a..02dc5f06 100644 --- a/src/include/temporal/temporal_functions.hpp +++ b/src/include/temporal/temporal_functions.hpp @@ -229,6 +229,15 @@ struct TemporalFunctions { static void Tnumber_tboxes(DataChunk &args, ExpressionState &state, Vector &result); static void Tnumber_split_n_tboxes(DataChunk &args, ExpressionState &state, Vector &result); static void Tnumber_split_each_n_tboxes(DataChunk &args, ExpressionState &state, Vector &result); + /* *************************************************** + * Temporal-tile family — bin / box emitters + ****************************************************/ + static void Temporal_time_bins(DataChunk &args, ExpressionState &state, Vector &result); + static void Tint_value_bins(DataChunk &args, ExpressionState &state, Vector &result); + static void Tfloat_value_bins(DataChunk &args, ExpressionState &state, Vector &result); + static void Tnumber_time_boxes(DataChunk &args, ExpressionState &state, Vector &result); + static void Tnumber_value_boxes(DataChunk &args, ExpressionState &state, Vector &result); + static void Tnumber_value_time_boxes(DataChunk &args, ExpressionState &state, Vector &result); static void Tnumber_delta_value(DataChunk &args, ExpressionState &state, Vector &result); static void Tnumber_trend(DataChunk &args, ExpressionState &state, Vector &result); static void Tfloat_exp(DataChunk &args, ExpressionState &state, Vector &result); diff --git a/src/include/tydef.hpp b/src/include/tydef.hpp index 3804cb45..b9860ca0 100644 --- a/src/include/tydef.hpp +++ b/src/include/tydef.hpp @@ -11,9 +11,27 @@ extern "C" { #include } -// `meosType` and `MeosType` are interchangeable spellings of the -// catalog-type enum (MEOS spells it `MeosType`). -using meosType = MeosType; +// MEOS naming history: `meosType` is the **pre-consolidation** spelling +// and `MeosType` is the **post-consolidation** target (the rename is +// part of the upstream consolidation sweep, not yet reached by the +// vcpkg pin). The current pin +// (`vcpkg_ports/meos/portfile.cmake` REF f11b7443ee98…) is still +// pre-consolidation and exposes `meosType` — see +// meos/include/temporal/meos_catalog.h, where line 121 declares +// `} meosType;`. MobilityDuck's source consistently uses +// `meosType` (verified via `grep -rn '\bmeosType\b' src/`), which +// matches the pin, so no alias is needed today. +// +// An earlier version of this file added `using meosType = MeosType;` +// as a forward-looking bridge for the eventual consolidation bump. +// That alias references `MeosType`, which the current pin does NOT +// yet expose, so it broke the build: +// "'MeosType' does not name a type; did you mean 'meosType'?". +// +// When the MEOS pin is bumped past the consolidation point, restore +// a bridge here (`using meosType = MeosType;` becomes valid then) or +// sweep the source `meosType → MeosType` in one PR — whichever the +// project prefers at that time. namespace duckdb { @@ -45,7 +63,6 @@ DatumGetFloat8(Datum X) #define DatumGetInt32(X) ((int32) (X)) #define DatumGetInt64(X) ((int64) (X)) -#define DatumGetBool(X) ((bool) (((int64) (X)) != 0)) #define DatumGetCString(X) ((char *) DatumGetPointer(X)) #define CStringGetDatum(X) PointerGetDatum(X) #define DatumGetPointer(X) ((Pointer) (X)) diff --git a/src/temporal/temporal.cpp b/src/temporal/temporal.cpp index d98ad3d8..c4755019 100644 --- a/src/temporal/temporal.cpp +++ b/src/temporal/temporal.cpp @@ -1879,6 +1879,72 @@ void TemporalTypes::RegisterScalarFunctions(ExtensionLoader &loader) { } } + // Temporal-tile family — bin / box emitters. + // + // timeBins(, interval [, timestamptz]) → tstzspan[] + // valueBins(tint, int [, int]) → intspan[] + // valueBins(tfloat,double [, double]) → floatspan[] + // timeBoxes(tnumber, interval [, timestamptz]) → tbox[] + // valueBoxes(tnumber, vsize [, vorigin]) → tbox[] + // valueTimeBoxes(tnumber, vsize, interval [, vorigin, torigin]) → tbox[] + // + // Defaults match MobilityDB: `torigin = '2000-01-03 +0:00:00'` + // (Monday epoch), `vorigin = 0`. + { + auto tstzspan_list = LogicalType::LIST(SpanTypes::TSTZSPAN()); + auto intspan_list = LogicalType::LIST(SpanTypes::INTSPAN()); + auto floatspan_list = LogicalType::LIST(SpanTypes::FLOATSPAN()); + auto tbox_list = LogicalType::LIST(TboxType::TBOX()); + + // timeBins for the four base temporal types. + for (auto &t : {TemporalTypes::TBOOL(), TemporalTypes::TINT(), + TemporalTypes::TFLOAT(), TemporalTypes::TTEXT()}) { + duckdb::RegisterSerializedScalarFunction(loader, + ScalarFunction("timeBins", {t, LogicalType::INTERVAL}, + tstzspan_list, TemporalFunctions::Temporal_time_bins)); + duckdb::RegisterSerializedScalarFunction(loader, + ScalarFunction("timeBins", {t, LogicalType::INTERVAL, LogicalType::TIMESTAMP_TZ}, + tstzspan_list, TemporalFunctions::Temporal_time_bins)); + } + + // valueBins per-type. + duckdb::RegisterSerializedScalarFunction(loader, + ScalarFunction("valueBins", {TemporalTypes::TINT(), LogicalType::INTEGER}, + intspan_list, TemporalFunctions::Tint_value_bins)); + duckdb::RegisterSerializedScalarFunction(loader, + ScalarFunction("valueBins", {TemporalTypes::TINT(), LogicalType::INTEGER, LogicalType::INTEGER}, + intspan_list, TemporalFunctions::Tint_value_bins)); + duckdb::RegisterSerializedScalarFunction(loader, + ScalarFunction("valueBins", {TemporalTypes::TFLOAT(), LogicalType::DOUBLE}, + floatspan_list, TemporalFunctions::Tfloat_value_bins)); + duckdb::RegisterSerializedScalarFunction(loader, + ScalarFunction("valueBins", {TemporalTypes::TFLOAT(), LogicalType::DOUBLE, LogicalType::DOUBLE}, + floatspan_list, TemporalFunctions::Tfloat_value_bins)); + + // timeBoxes / valueBoxes / valueTimeBoxes for tnumber. + for (auto &t : {TemporalTypes::TINT(), TemporalTypes::TFLOAT()}) { + const auto vt = (t == TemporalTypes::TINT()) ? LogicalType::INTEGER : LogicalType::DOUBLE; + duckdb::RegisterSerializedScalarFunction(loader, + ScalarFunction("timeBoxes", {t, LogicalType::INTERVAL}, + tbox_list, TemporalFunctions::Tnumber_time_boxes)); + duckdb::RegisterSerializedScalarFunction(loader, + ScalarFunction("timeBoxes", {t, LogicalType::INTERVAL, LogicalType::TIMESTAMP_TZ}, + tbox_list, TemporalFunctions::Tnumber_time_boxes)); + duckdb::RegisterSerializedScalarFunction(loader, + ScalarFunction("valueBoxes", {t, vt}, + tbox_list, TemporalFunctions::Tnumber_value_boxes)); + duckdb::RegisterSerializedScalarFunction(loader, + ScalarFunction("valueBoxes", {t, vt, vt}, + tbox_list, TemporalFunctions::Tnumber_value_boxes)); + duckdb::RegisterSerializedScalarFunction(loader, + ScalarFunction("valueTimeBoxes", {t, vt, LogicalType::INTERVAL}, + tbox_list, TemporalFunctions::Tnumber_value_time_boxes)); + duckdb::RegisterSerializedScalarFunction(loader, + ScalarFunction("valueTimeBoxes", {t, vt, LogicalType::INTERVAL, vt, LogicalType::TIMESTAMP_TZ}, + tbox_list, TemporalFunctions::Tnumber_value_time_boxes)); + } + } + // tspatial × {stbox, tspatial} position predicates. // // For each direction (left/right/below/above/front/back and the over* diff --git a/src/temporal/temporal_functions.cpp b/src/temporal/temporal_functions.cpp index 980f83d1..373dfcb9 100644 --- a/src/temporal/temporal_functions.cpp +++ b/src/temporal/temporal_functions.cpp @@ -5001,6 +5001,299 @@ void TemporalFunctions::Tnumber_split_each_n_tboxes(DataChunk &args, ExpressionS /*has_n_arg=*/true); } +/* ============================================================ + * Temporal-tile family — bin / box emitters + * + * `timeBins(temporal, interval [, torigin])` → tstzspan[] + * `valueBins(tint/tfloat, vsize [, vorigin])` → intspan[] / floatspan[] + * `timeBoxes(tnumber, interval [, torigin])` → tbox[] + * `valueBoxes(tnumber, vsize [, vorigin])` → tbox[] + * `valueTimeBoxes(tnumber, vsize, interval [, vorigin, torigin])` → tbox[] + * + * MobilityDB defaults: `torigin = '2000-01-03 +0:00:00'` (Monday epoch + * in MEOS), `vorigin = 0`. + ============================================================ */ + +namespace { + +// MEOS torigin default — Monday epoch 2000-01-03 expressed in MEOS +// internal representation (microseconds since 2000-01-01 UTC). This +// section runs before the file's other `DEFAULT_T_ORIGIN` definition, +// so we name the constant locally here. +constexpr TimestampTz DEFAULT_T_ORIGIN_TILE = 0; + +template +void EmitSpanList(Vector &result, idx_t row, list_entry_t *list_entries, + SPAN_T *spans, int count, idx_t &total) { + if (!spans || count <= 0) { + list_entries[row] = list_entry_t{total, 0}; + if (spans) free(spans); + return; + } + ListVector::Reserve(result, total + count); + ListVector::SetListSize(result, total + count); + list_entries[row] = list_entry_t{total, static_cast(count)}; + auto &child = ListVector::GetEntry(result); + auto child_data = FlatVector::GetData(child); + for (int k = 0; k < count; k++) { + string_t one(reinterpret_cast(&spans[k]), sizeof(SPAN_T)); + child_data[total + k] = StringVector::AddStringOrBlob(child, one); + } + total += count; + free(spans); +} + +void EmitTboxList(Vector &result, idx_t row, list_entry_t *list_entries, + TBox *boxes, int count, idx_t &total) { + if (!boxes || count <= 0) { + list_entries[row] = list_entry_t{total, 0}; + if (boxes) free(boxes); + return; + } + ListVector::Reserve(result, total + count); + ListVector::SetListSize(result, total + count); + list_entries[row] = list_entry_t{total, static_cast(count)}; + auto &child = ListVector::GetEntry(result); + auto child_data = FlatVector::GetData(child); + for (int k = 0; k < count; k++) { + string_t one(reinterpret_cast(&boxes[k]), sizeof(TBox)); + child_data[total + k] = StringVector::AddStringOrBlob(child, one); + } + total += count; + free(boxes); +} + +} // namespace + +void TemporalFunctions::Temporal_time_bins(DataChunk &args, ExpressionState &state, Vector &result) { + const idx_t row_count = args.size(); + for (idx_t i = 0; i < args.ColumnCount(); i++) args.data[i].Flatten(row_count); + auto in_data = FlatVector::GetData(args.data[0]); + auto dur_data = FlatVector::GetData(args.data[1]); + auto &v0 = FlatVector::Validity(args.data[0]); + auto &v1 = FlatVector::Validity(args.data[1]); + const bool has_origin = args.ColumnCount() > 2; + auto list_entries = FlatVector::GetData(result); + auto &out_validity = FlatVector::Validity(result); + idx_t total = 0; + for (idx_t row = 0; row < row_count; row++) { + if (!v0.RowIsValid(row) || !v1.RowIsValid(row)) { + out_validity.SetInvalid(row); + list_entries[row] = list_entry_t{total, 0}; + continue; + } + TimestampTz origin = DEFAULT_T_ORIGIN_TILE; + if (has_origin) { + auto &ov = args.data[2]; + if (FlatVector::Validity(ov).RowIsValid(row)) { + origin = (TimestampTz) DuckDBToMeosTimestamp( + FlatVector::GetData(ov)[row]).value; + } + } + Temporal *t = BlobToTemporal(in_data[row]); + MeosInterval mi = IntervaltToInterval(dur_data[row]); + int count = 0; + Span *spans = temporal_time_bins(t, &mi, origin, &count); + free(t); + EmitSpanList(result, row, list_entries, spans, count, total); + } + if (row_count == 1) result.SetVectorType(VectorType::CONSTANT_VECTOR); +} + +namespace { + +template +void RunValueBinsEmit(DataChunk &args, Vector &result, FN produce) { + const idx_t row_count = args.size(); + for (idx_t i = 0; i < args.ColumnCount(); i++) args.data[i].Flatten(row_count); + auto in_data = FlatVector::GetData(args.data[0]); + auto in_size = FlatVector::GetData(args.data[1]); + auto &v0 = FlatVector::Validity(args.data[0]); + auto &v1 = FlatVector::Validity(args.data[1]); + const bool has_origin = args.ColumnCount() > 2; + auto list_entries = FlatVector::GetData(result); + auto &out_validity = FlatVector::Validity(result); + idx_t total = 0; + for (idx_t row = 0; row < row_count; row++) { + if (!v0.RowIsValid(row) || !v1.RowIsValid(row)) { + out_validity.SetInvalid(row); + list_entries[row] = list_entry_t{total, 0}; + continue; + } + VAL_T origin = 0; + if (has_origin) { + auto &ov = args.data[2]; + if (FlatVector::Validity(ov).RowIsValid(row)) { + origin = FlatVector::GetData(ov)[row]; + } + } + Temporal *t = BlobToTemporal(in_data[row]); + int count = 0; + Span *spans = produce(t, in_size[row], origin, &count); + free(t); + EmitSpanList(result, row, list_entries, spans, count, total); + } + if (row_count == 1) result.SetVectorType(VectorType::CONSTANT_VECTOR); +} + +} // namespace + +void TemporalFunctions::Tint_value_bins(DataChunk &args, ExpressionState &state, Vector &result) { + RunValueBinsEmit(args, result, + [](const Temporal *t, int32_t size, int32_t origin, int *count) { + return tint_value_bins(t, (int) size, (int) origin, count); + }); +} + +void TemporalFunctions::Tfloat_value_bins(DataChunk &args, ExpressionState &state, Vector &result) { + RunValueBinsEmit(args, result, + [](const Temporal *t, double size, double origin, int *count) { + return tfloat_value_bins(t, size, origin, count); + }); +} + +namespace { + +template +void RunValueTimeBoxes(DataChunk &args, Vector &result, + bool value_axis, bool time_axis, + FN_TIME fn_time, FN_VALUE fn_value, FN_VT fn_vt) { + const idx_t row_count = args.size(); + for (idx_t i = 0; i < args.ColumnCount(); i++) args.data[i].Flatten(row_count); + auto in_data = FlatVector::GetData(args.data[0]); + auto &v0 = FlatVector::Validity(args.data[0]); + auto list_entries = FlatVector::GetData(result); + auto &out_validity = FlatVector::Validity(result); + + idx_t arg = 1; + VAL_T *vsize_data = nullptr; + if (value_axis) { + if (!FlatVector::Validity(args.data[arg]).RowIsValid(0)) { + // ignored — per-row validity is checked in loop. + } + vsize_data = FlatVector::GetData(args.data[arg]); + arg++; + } + interval_t *dur_data = nullptr; + if (time_axis) { + dur_data = FlatVector::GetData(args.data[arg]); + arg++; + } + const idx_t vorigin_idx = value_axis ? arg++ : 0; + const idx_t torigin_idx = time_axis ? arg++ : 0; + const bool has_vorigin = value_axis && vorigin_idx < args.ColumnCount(); + const bool has_torigin = time_axis && torigin_idx < args.ColumnCount(); + + idx_t total = 0; + for (idx_t row = 0; row < row_count; row++) { + if (!v0.RowIsValid(row)) { + out_validity.SetInvalid(row); + list_entries[row] = list_entry_t{total, 0}; + continue; + } + VAL_T vsize = value_axis ? vsize_data[row] : VAL_T{0}; + MeosInterval mi_storage{}; + ::Interval *duration = nullptr; + if (time_axis) { + mi_storage = IntervaltToInterval(dur_data[row]); + duration = &mi_storage; + } + VAL_T vorigin = 0; + if (has_vorigin && FlatVector::Validity(args.data[vorigin_idx]).RowIsValid(row)) { + vorigin = FlatVector::GetData(args.data[vorigin_idx])[row]; + } + TimestampTz torigin = DEFAULT_T_ORIGIN_TILE; + if (has_torigin && FlatVector::Validity(args.data[torigin_idx]).RowIsValid(row)) { + torigin = (TimestampTz) DuckDBToMeosTimestamp( + FlatVector::GetData(args.data[torigin_idx])[row]).value; + } + Temporal *t = BlobToTemporal(in_data[row]); + int count = 0; + TBox *boxes = nullptr; + if (value_axis && time_axis) { + boxes = fn_vt(t, vsize, duration, vorigin, torigin, &count); + } else if (time_axis) { + boxes = fn_time(t, duration, torigin, &count); + } else { + boxes = fn_value(t, vsize, vorigin, &count); + } + free(t); + EmitTboxList(result, row, list_entries, boxes, count, total); + } + if (row_count == 1) result.SetVectorType(VectorType::CONSTANT_VECTOR); +} + +} // namespace + +void TemporalFunctions::Tnumber_time_boxes(DataChunk &args, ExpressionState &state, Vector &result) { + // Dispatch on the underlying temporal type by peeking the first byte of + // the blob (T_TINT / T_TFLOAT etc.). Since both register the same + // function, we discriminate at runtime. + args.data[0].Flatten(args.size()); + auto in_data = FlatVector::GetData(args.data[0]); + if (args.size() == 0) return; + MeosType base_type = T_TFLOAT; + if (in_data[0].GetSize() > 0) { + const Temporal *probe = reinterpret_cast(in_data[0].GetData()); + base_type = (MeosType) probe->temptype; + } + if (base_type == T_TINT) { + RunValueTimeBoxes(args, result, /*value=*/false, /*time=*/true, + [](const Temporal *t, const ::Interval *d, TimestampTz to, int *c) { return tint_time_boxes(t, d, to, c); }, + [](const Temporal *, int32_t, int32_t, int *) -> TBox * { return nullptr; }, + [](const Temporal *, int32_t, const ::Interval *, int32_t, TimestampTz, int *) -> TBox * { return nullptr; }); + } else { + RunValueTimeBoxes(args, result, /*value=*/false, /*time=*/true, + [](const Temporal *t, const ::Interval *d, TimestampTz to, int *c) { return tfloat_time_boxes(t, d, to, c); }, + [](const Temporal *, double, double, int *) -> TBox * { return nullptr; }, + [](const Temporal *, double, const ::Interval *, double, TimestampTz, int *) -> TBox * { return nullptr; }); + } +} + +void TemporalFunctions::Tnumber_value_boxes(DataChunk &args, ExpressionState &state, Vector &result) { + args.data[0].Flatten(args.size()); + auto in_data = FlatVector::GetData(args.data[0]); + if (args.size() == 0) return; + MeosType base_type = T_TFLOAT; + if (in_data[0].GetSize() > 0) { + const Temporal *probe = reinterpret_cast(in_data[0].GetData()); + base_type = (MeosType) probe->temptype; + } + if (base_type == T_TINT) { + RunValueTimeBoxes(args, result, /*value=*/true, /*time=*/false, + [](const Temporal *, const ::Interval *, TimestampTz, int *) -> TBox * { return nullptr; }, + [](const Temporal *t, int32_t v, int32_t vo, int *c) { return tint_value_boxes(t, v, vo, c); }, + [](const Temporal *, int32_t, const ::Interval *, int32_t, TimestampTz, int *) -> TBox * { return nullptr; }); + } else { + RunValueTimeBoxes(args, result, /*value=*/true, /*time=*/false, + [](const Temporal *, const ::Interval *, TimestampTz, int *) -> TBox * { return nullptr; }, + [](const Temporal *t, double v, double vo, int *c) { return tfloat_value_boxes(t, v, vo, c); }, + [](const Temporal *, double, const ::Interval *, double, TimestampTz, int *) -> TBox * { return nullptr; }); + } +} + +void TemporalFunctions::Tnumber_value_time_boxes(DataChunk &args, ExpressionState &state, Vector &result) { + args.data[0].Flatten(args.size()); + auto in_data = FlatVector::GetData(args.data[0]); + if (args.size() == 0) return; + MeosType base_type = T_TFLOAT; + if (in_data[0].GetSize() > 0) { + const Temporal *probe = reinterpret_cast(in_data[0].GetData()); + base_type = (MeosType) probe->temptype; + } + if (base_type == T_TINT) { + RunValueTimeBoxes(args, result, /*value=*/true, /*time=*/true, + [](const Temporal *, const ::Interval *, TimestampTz, int *) -> TBox * { return nullptr; }, + [](const Temporal *, int32_t, int32_t, int *) -> TBox * { return nullptr; }, + [](const Temporal *t, int32_t v, const ::Interval *d, int32_t vo, TimestampTz to, int *c) { return tint_value_time_boxes(t, v, d, vo, to, c); }); + } else { + RunValueTimeBoxes(args, result, /*value=*/true, /*time=*/true, + [](const Temporal *, const ::Interval *, TimestampTz, int *) -> TBox * { return nullptr; }, + [](const Temporal *, double, double, int *) -> TBox * { return nullptr; }, + [](const Temporal *t, double v, const ::Interval *d, double vo, TimestampTz to, int *c) { return tfloat_value_time_boxes(t, v, d, vo, to, c); }); + } +} + // Temporal_derivative is implemented later in this file in the Math // functions block (existed before the unary-tnumber additions). diff --git a/test/sql/parity/025b_temporal_tile_bins_boxes.test b/test/sql/parity/025b_temporal_tile_bins_boxes.test new file mode 100644 index 00000000..2726f8d2 --- /dev/null +++ b/test/sql/parity/025b_temporal_tile_bins_boxes.test @@ -0,0 +1,108 @@ +# name: test/sql/parity/025b_temporal_tile_bins_boxes.test +# description: Temporal-tile bin / box emitters added to close +# `025_temporal_tile.in.sql` parity gap: +# - `timeBins(, interval [, torigin])` +# - `valueBins(tint/tfloat, vsize [, vorigin])` +# - `timeBoxes(tnumber, interval [, torigin])` +# - `valueBoxes(tnumber, vsize [, vorigin])` +# - `valueTimeBoxes(tnumber, vsize, interval [, vorigin, torigin])` +# +# Defaults match MobilityDB: `torigin = '2000-01-03'` +# and `vorigin = 0`. +# group: [sql] + +require mobilityduck + +# ============================================================================= +# timeBins — for each of the four base temporal types +# ============================================================================= + +query I +SELECT len(timeBins(tbool '[t@2000-01-01, f@2000-01-08]', INTERVAL '1 day')); +---- +8 + +query I +SELECT len(timeBins(tint '[1@2000-01-01, 5@2000-01-08]', INTERVAL '1 day')); +---- +8 + +query I +SELECT len(timeBins(tfloat '[1.5@2000-01-01, 5.5@2000-01-04]', INTERVAL '1 day')); +---- +4 + +query I +SELECT len(timeBins(ttext '["a"@2000-01-01, "b"@2000-01-04]', INTERVAL '1 day')); +---- +4 + +# torigin explicit — same result because the bin grid is offset-free +# at the default origin. +query I +SELECT len(timeBins(tint '[1@2000-01-01, 5@2000-01-04]', + INTERVAL '1 day', + TIMESTAMPTZ '2000-01-03 00:00:00+00')); +---- +4 + +# ============================================================================= +# valueBins — typed per tint / tfloat +# ============================================================================= + +query I +SELECT len(valueBins(tint '[1@2000-01-01, 7@2000-01-08]', 3)); +---- +2 + +query I +SELECT len(valueBins(tint '[1@2000-01-01, 7@2000-01-08]', 3, 1)); +---- +2 + +query I +SELECT len(valueBins(tfloat '[1.5@2000-01-01, 8.7@2000-01-03]', 2.0)); +---- +5 + +# ============================================================================= +# timeBoxes — tnumber × time grid +# ============================================================================= + +query I +SELECT len(timeBoxes(tint '[1@2000-01-01, 5@2000-01-08]', INTERVAL '2 days')); +---- +4 + +query I +SELECT len(timeBoxes(tfloat '[1.5@2000-01-01, 5.5@2000-01-04]', INTERVAL '1 day')); +---- +4 + +# ============================================================================= +# valueBoxes — tnumber × value grid +# ============================================================================= + +query I +SELECT len(valueBoxes(tint '[1@2000-01-01, 7@2000-01-08]', 3)); +---- +2 + +query I +SELECT len(valueBoxes(tfloat '[1.5@2000-01-01, 8.7@2000-01-03]', 2.0)); +---- +5 + +# ============================================================================= +# valueTimeBoxes — combined value × time grid +# ============================================================================= + +query I +SELECT len(valueTimeBoxes(tint '[1@2000-01-01, 5@2000-01-08]', 2, INTERVAL '2 days')); +---- +5 + +query I +SELECT len(valueTimeBoxes(tfloat '[1.5@2000-01-01, 5.5@2000-01-04]', 2.0, INTERVAL '1 day')); +---- +6 diff --git a/test/sql/parity/051d_stbox_perimeter_quadsplit.test b/test/sql/parity/051d_stbox_perimeter_quadsplit.test new file mode 100644 index 00000000..220ecf7d --- /dev/null +++ b/test/sql/parity/051d_stbox_perimeter_quadsplit.test @@ -0,0 +1,44 @@ +# name: test/sql/parity/051d_stbox_perimeter_quadsplit.test +# description: stbox accessors and emitters added to close the +# `051_stbox.in.sql` parity gap: `perimeter`, +# `quadSplit`, and the `geography(stbox)` naming alias. +# group: [sql] + +require mobilityduck + +# perimeter(stbox) — Cartesian, planar. 3×4 rectangle = 14.0. +query I +SELECT perimeter(stbox 'STBOX X((1,1),(4,5))'); +---- +14.000000 + +# perimeter(stbox, spheroid bool) — spheroid flag is forwarded to +# MEOS; on a non-geodetic box the spheroid path falls back to the +# planar measure, so the result is identical. +query I +SELECT perimeter(stbox 'STBOX X((1,1),(4,5))', false); +---- +14.000000 + +# quadSplit(stbox) — four quadrants. +query I +SELECT len(quadSplit(stbox 'STBOX X((0,0),(10,10))')); +---- +4 + +# Each quadrant covers a quarter of the spatial extent — the union +# of the four xmin/xmax values is {0, 5, 10}. +query I +SELECT count(DISTINCT Xmin(q)) +FROM (SELECT unnest(quadSplit(stbox 'STBOX X((0,0),(10,10))')) AS q); +---- +2 + +# geography(stbox) — naming alias for `geometry(stbox)`. DuckDB has +# no separate geography type so both produce a GEOMETRY blob with +# identical bytes. +query I +SELECT ST_AsText(geography(stbox 'STBOX X((0,0),(1,1))')) + = ST_AsText(geometry (stbox 'STBOX X((0,0),(1,1))')); +---- +true diff --git a/test/sql/parity/076b_tpoint_geometry_geography.test b/test/sql/parity/076b_tpoint_geometry_geography.test new file mode 100644 index 00000000..3bae90d8 --- /dev/null +++ b/test/sql/parity/076b_tpoint_geometry_geography.test @@ -0,0 +1,38 @@ +# name: test/sql/parity/076b_tpoint_geometry_geography.test +# description: `geometry(tgeompoint [, segmentize bool])` and +# `geography(tgeogpoint [, segmentize bool])` — convert +# a temporal point into its trajectory linestring with +# an M coordinate carrying the epoch timestamp. +# +# These are the trajectory-with-M flavour of the same MEOS +# entrypoint (`tpoint_tfloat_to_geomeas`) backing +# `geoMeasure`; passing a NULL measure produces a +# geometry/geography rather than a measure-bearing one. +# group: [sql] + +require mobilityduck + +# Default form — single linestring with epoch-second M. +query I +SELECT ST_AsText(geometry(tgeompoint '[Point(0 0)@2000-01-01, Point(3 4)@2000-01-02]')); +---- +LINESTRING M (0 0 946684800, 3 4 946771200) + +# segmentize=true — same output for a 2-instant trajectory. +query I +SELECT ST_AsText(geometry(tgeompoint '[Point(0 0)@2000-01-01, Point(3 4)@2000-01-02]', true)); +---- +LINESTRING M (0 0 946684800, 3 4 946771200) + +# Geographic counterpart — MEOS keeps the SRID-aware geography in the +# blob; DuckDB has no separate geography type so the result is a +# GEOMETRY-aliased blob with the geodetic flag set inside. +query I +SELECT ST_AsText(geography(tgeogpoint '[Point(4 50)@2026-01-01, Point(5 51)@2026-01-02]')); +---- +LINESTRING M (4 50 1767225600, 5 51 1767312000) + +query I +SELECT ST_AsText(geography(tgeogpoint '[Point(4 50)@2026-01-01, Point(5 51)@2026-01-02]', false)); +---- +LINESTRING M (4 50 1767225600, 5 51 1767312000) diff --git a/test/sql/parity/076c_transform_pipeline.test b/test/sql/parity/076c_transform_pipeline.test new file mode 100644 index 00000000..016a036b --- /dev/null +++ b/test/sql/parity/076c_transform_pipeline.test @@ -0,0 +1,70 @@ +# name: test/sql/parity/076c_transform_pipeline.test +# description: `transformPipeline(, pipeline_text, srid int = 0, +# is_forward bool = true)` — apply a PROJ pipeline +# string to a temporal spatial value, an stbox, or a +# spatial set. +# +# Closes the last parity gap (3 sections × 7 overloads +# of the same name) in the active addressable surface. +# group: [sql] + +require mobilityduck + +# ============================================================================= +# Temporal spatial values — tgeompoint +# ============================================================================= + +# 2-arg form (srid=0, is_forward=true defaults). axisswap on (1, 2) → (2, 1). +query I +SELECT asText(transformPipeline(tgeompoint '[Point(1 2)@2000-01-01]', + '+proj=pipeline +step +proj=axisswap +order=2,1')); +---- +[POINT(2 1)@2000-01-01 00:00:00+00] + +# Explicit srid + is_forward. +query I +SELECT asText(transformPipeline(tgeompoint '[Point(1 2)@2000-01-01]', + '+proj=pipeline +step +proj=axisswap +order=2,1', + 0, true)); +---- +[POINT(2 1)@2000-01-01 00:00:00+00] + +# Inverse — for axisswap, forward = inverse. +query I +SELECT asText(transformPipeline(tgeompoint '[Point(1 2)@2000-01-01]', + '+proj=pipeline +step +proj=axisswap +order=2,1', + 0, false)); +---- +[POINT(2 1)@2000-01-01 00:00:00+00] + +# ============================================================================= +# Smoke — the other surfaces (tgeometry / tgeography / tgeogpoint / stbox / +# geomset / geogset) all wire to the same MEOS entrypoints; assert that +# each call returns a value (full output checks deferred to dedicated +# transform tests). +# ============================================================================= + +query I +SELECT transformPipeline(tgeometry '[Point(1 2)@2000-01-01]', + '+proj=pipeline +step +proj=axisswap +order=2,1') IS NOT NULL; +---- +true + +query I +SELECT transformPipeline(tgeography '[Point(1 2)@2000-01-01]', + '+proj=pipeline +step +proj=axisswap +order=2,1') IS NOT NULL; +---- +true + +query I +SELECT transformPipeline(tgeogpoint '[Point(1 2)@2000-01-01]', + '+proj=pipeline +step +proj=axisswap +order=2,1') IS NOT NULL; +---- +true + +query I +SELECT transformPipeline(setSRID(geomset '{Point(1 2), Point(3 4)}', 4326), + '+proj=pipeline +step +proj=axisswap +order=2,1', + 4326) IS NOT NULL; +---- +true diff --git a/vcpkg_ports/meos/portfile.cmake b/vcpkg_ports/meos/portfile.cmake index 893416b5..c351100f 100644 --- a/vcpkg_ports/meos/portfile.cmake +++ b/vcpkg_ports/meos/portfile.cmake @@ -1,8 +1,8 @@ vcpkg_from_github( OUT_SOURCE_PATH SOURCE_PATH REPO MobilityDB/MobilityDB - REF 742c1fb5935b502ed478e2b95af2bcb1b9f0db52 - SHA512 f8e04772b5dfa12730799cdde93682b05275b41c2ebd2e9c0d9afe65894910b0106ad2855a3973bf2d93b60fa168d75d6a29de5c23b9c233961c46f6020cf9d6 + REF ee27da1a6d2f6cdbdd226bd66a1e7fea86c2832b + SHA512 a41bdc3167a481d40501db122d4129e5b08a0d652bfc543ae8a8ddf11f5b186f53bac038268a686a6be882a12ca6028c4c627f88bc729292d9211bbe943af7d4 ) vcpkg_replace_string(