From d40dc91e8156d6a614ed1d7281af5c41ef092bf6 Mon Sep 17 00:00:00 2001 From: Esteban Zimanyi Date: Sun, 17 May 2026 17:30:50 +0200 Subject: [PATCH 1/3] Add the core trgeometry type port Port the temporal rigid geometry type into MobilityDuck by cloning the core tpose port and substituting trgeometry. A trgeometry value is a moving rigid geometry: a reference geometry rigidly rotated and translated over time by an embedded temporal pose, written as 'Geometry;Pose@timestamp'. The renamed trgeometry_in, trgeometry_from_mfjson and trgeometry_out symbols are exported from meos_rgeo.h after the cross-type C-API uniformization, so text, EWKT and MF-JSON I/O route through the type-specific entry points the canonical MobilityDB SQL binds, making this a clean clone of the template rather than a tspatial_parse work-around. meos_rgeo.h is not included because its trgeometry_append_tinstant prototype takes a MEOS Interval that collides with duckdb::Interval; the few trgeometry_* symbols are declared locally instead. The rigid value model adds the geometry/pose/timestamptz and geometry/tpose constructors, the geometry accessor that surfaces the reference geometry via trgeometry_geom, and startValue/endValue that surface the moved rigid-geometry snapshot via trgeometry_start_value/trgeometry_end_value, while getValue surfaces the underlying pose. The cloned instant-array helper is suffixed temparr_extract_rg to avoid an ODR clash with the tpose, tnpoint and tcbuffer siblings. Generic subtype-agnostic accessors, time-domain restrictions, modifiers and comparison reuse the shared temporal handlers. Core scope only, no operators layer, mirroring the tcbuffer, tnpoint and tpose core ports. The provisional MEOS pin is moved to the verified integration branch that exposes the renamed trgeometry_* MEOS surface. Stacks on the core tpose type port #151 and targets its branch as base. --- CMakeLists.txt | 2 + src/include/rgeo/trgeometry.hpp | 29 + src/mobilityduck_extension.cpp | 6 + src/rgeo/trgeometry.cpp | 1477 +++++++++++++++++++++++++++++++ src/rgeo/trgeometry_in_out.cpp | 325 +++++++ test/sql/trgeometry.test | 199 +++++ vcpkg_ports/meos/portfile.cmake | 45 +- 7 files changed, 2061 insertions(+), 22 deletions(-) create mode 100644 src/include/rgeo/trgeometry.hpp create mode 100644 src/rgeo/trgeometry.cpp create mode 100644 src/rgeo/trgeometry_in_out.cpp create mode 100644 test/sql/trgeometry.test diff --git a/CMakeLists.txt b/CMakeLists.txt index ad30a13..2c9b2ec 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -93,6 +93,8 @@ set(EXTENSION_SOURCES src/geo/tnpoint_in_out.cpp src/geo/tpose.cpp src/geo/tpose_in_out.cpp + src/rgeo/trgeometry.cpp + src/rgeo/trgeometry_in_out.cpp src/geo/tgeogpoint.cpp src/geo/tgeogpoint_in_out.cpp src/geo/tgeogpoint_ops.cpp diff --git a/src/include/rgeo/trgeometry.hpp b/src/include/rgeo/trgeometry.hpp new file mode 100644 index 0000000..125c41e --- /dev/null +++ b/src/include/rgeo/trgeometry.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include +#include "duckdb/common/exception.hpp" +#include "duckdb/common/string_util.hpp" +#include "duckdb/function/scalar_function.hpp" +#include "duckdb/main/extension/extension_loader.hpp" +#include + +namespace duckdb { + + +struct TRGeometryTypes { + static LogicalType TRGEOMETRY(); + static LogicalType GEOMETRY(); + static void RegisterTypes(ExtensionLoader &loader); + static void RegisterScalarFunctions(ExtensionLoader &loader); + static void RegisterCastFunctions(ExtensionLoader &loader); + static void RegisterScalarInOutFunctions(ExtensionLoader &loader); +}; + +struct TrgeometryFunctions { + static bool StringToTrgeometry(Vector &source, Vector &result, idx_t count, CastParameters ¶meters); + static bool TrgeometryToString(Vector &source, Vector &result, idx_t count, CastParameters ¶meters); + static bool WkbBlobToGeometry(Vector &source, Vector &result, idx_t count, CastParameters ¶meters); +}; + + +} // namespace duckdb diff --git a/src/mobilityduck_extension.cpp b/src/mobilityduck_extension.cpp index ba74b65..caabf65 100644 --- a/src/mobilityduck_extension.cpp +++ b/src/mobilityduck_extension.cpp @@ -18,6 +18,7 @@ #include "geo/tcbuffer.hpp" #include "geo/tnpoint.hpp" #include "geo/tpose.hpp" +#include "rgeo/trgeometry.hpp" #include "geo/tgeogpoint.hpp" #include "geo/tgeogpoint_ops.hpp" #include "temporal/span.hpp" @@ -365,6 +366,11 @@ static void LoadInternal(ExtensionLoader &loader) { TPoseTypes::RegisterCastFunctions(loader); TPoseTypes::RegisterScalarInOutFunctions(loader); + TRGeometryTypes::RegisterScalarFunctions(loader); + TRGeometryTypes::RegisterTypes(loader); + TRGeometryTypes::RegisterCastFunctions(loader); + TRGeometryTypes::RegisterScalarInOutFunctions(loader); + TGeogpointType::RegisterScalarFunctions(loader); TGeogpointType::RegisterCastFunctions(loader); TGeogpointType::RegisterScalarInOutFunctions(loader); diff --git a/src/rgeo/trgeometry.cpp b/src/rgeo/trgeometry.cpp new file mode 100644 index 0000000..62d3384 --- /dev/null +++ b/src/rgeo/trgeometry.cpp @@ -0,0 +1,1477 @@ +#include "rgeo/trgeometry.hpp" +#include "geo/tgeompoint_functions.hpp" +#include "duckdb/main/extension/extension_loader.hpp" +#include "duckdb/common/extension_type_info.hpp" +#include +#include +#include +#include "temporal/spanset.hpp" +#include "temporal/set.hpp" +#include "temporal/temporal_functions.hpp" +#include "geo/stbox.hpp" +#include "geo/geoset.hpp" +#include +#include "geo_util.hpp" +#include "spatial/spatial_types.hpp" +#include "mobilityduck/meos_exec_serial.hpp" + +extern "C" { + #include + #include + #include + #include + #include +} + +// The temporal rigid geometry module header meos_rgeo.h is deliberately +// NOT included. It is the only extended-type module header that declares +// an Interval-typed prototype (trgeometry_append_tinstant), and pulling +// the MEOS `struct Interval` into a scope that also has `duckdb::Interval` +// (from tydef.hpp) makes the unqualified `Interval` ambiguous and breaks +// the build. The few trgeometry_* symbols this port needs are declared +// locally instead (the same technique the sibling ports use for +// subtype-specific parsers); GSERIALIZED / Pose / MeosType stay reachable +// via meos_geo.h / meos_internal.h / meos_pose.h. Every name below is +// the post-uniformization trgeometry_* name exported from meos_rgeo.h. +extern "C" { + extern Temporal *trgeometry_in(const char *str); + extern TInstant *trgeometryinst_make(const GSERIALIZED *geom, + const Pose *pose, TimestampTz t); + extern Temporal *geo_tpose_to_trgeometry(const GSERIALIZED *gs, + const Temporal *temp); + extern GSERIALIZED *trgeometry_geom(const Temporal *temp); + extern GSERIALIZED *trgeometry_start_value(const Temporal *temp); + extern GSERIALIZED *trgeometry_end_value(const Temporal *temp); +} + + +namespace duckdb { + +LogicalType TRGeometryTypes::TRGEOMETRY() { + auto type = LogicalType(LogicalTypeId::BLOB); + type.SetAlias("TRGEOMETRY"); + return type; +} + +/* + * Constructors +*/ + +inline void Trgeometry_constructor(DataChunk &args, ExpressionState &state, Vector &result) { + auto count = args.size(); + auto &input_geom_vec = args.data[0]; + + UnaryExecutor::Execute( + input_geom_vec, result, count, + [&](string_t input_geom_str) -> string_t { + std::string input = input_geom_str.GetString(); + + // The temporal rigid geometry text form is a reference + // geometry, a ';' delimiter and a temporal pose, e.g. + // 'Polygon((1 1,2 2,3 1,1 1));Pose(Point(1 1),0.5)@2000-01-01'. + // The renamed trgeometry_in symbol is exported from + // meos_rgeo.h and is itself the canonical parser the + // MobilityDB SQL binds trgeometry_in / trgeometryFromText to. + Temporal *tinst = trgeometry_in(input.c_str()); + if (!tinst) { + throw InvalidInputException("Invalid TRGEOMETRY input: " + input); + } + + size_t data_size = temporal_mem_size(tinst); + + uint8_t *data_buffer = (uint8_t*)malloc(data_size); + if (!data_buffer) { + free(tinst); + throw InvalidInputException("Failed to allocate memory for TRGEOMETRY data"); + } + + memcpy(data_buffer, tinst, data_size); + + string_t data_string_t(reinterpret_cast(data_buffer), data_size); + string_t stored_data = StringVector::AddStringOrBlob(result, data_string_t); + + free(data_buffer); + free(tinst); + + return stored_data; + }); + + if (count == 1) { + result.SetVectorType(VectorType::CONSTANT_VECTOR); + } +} + +inline void Trgeometryinst_constructor(DataChunk &args, ExpressionState &state, Vector &result) { + auto count = args.size(); + auto &geom_vec = args.data[0]; + auto &pose_vec = args.data[1]; + auto &t_vec = args.data[2]; + + TernaryExecutor::Execute( + geom_vec, pose_vec, t_vec, result, count, + [&](string_t geom_blob, string_t pose_str, timestamp_tz_t t) -> string_t { + // The reference geometry arrives as a GEOMETRY blob. + GSERIALIZED *gs = GeometryToGSerialized(geom_blob, 0); + if (gs == NULL) { + throw InvalidInputException("Invalid geometry for TRGEOMETRY"); + } + + // The pose value type is a Pose (position + orientation): + // a 2D pose is a point plus a rotation angle, a 3D pose a + // point plus an orientation quaternion. It is parsed from + // its canonical text form, e.g. 'Pose(Point(1 1), 0.5)'. + std::string pose_value = pose_str.GetString(); + Pose *po = pose_in(pose_value.c_str()); + if (po == NULL) { + free(gs); + throw InvalidInputException("Invalid pose format: " + pose_value); + } + + timestamp_tz_t meos_timestamp = DuckDBToMeosTimestamp(t); + // The rigid-geometry instant constructor pairs the reference + // geometry with the moving pose at the given timestamp. + TInstant *inst = trgeometryinst_make(gs, po, + static_cast(meos_timestamp.value)); + + if (inst == NULL) { + free(po); + free(gs); + throw InvalidInputException("Failed to create TInstant"); + } + + size_t data_size = temporal_mem_size((Temporal*)inst); + + uint8_t *data_buffer = (uint8_t *)malloc(data_size); + + if (!data_buffer){ + free(inst); + free(po); + free(gs); + throw InvalidInputException("Failed to allocate memory to TRGEOMETRY data"); + } + memcpy(data_buffer, inst, data_size); + + string_t data_string_t(reinterpret_cast(data_buffer),data_size); + string_t stored_data = StringVector::AddStringOrBlob(result, data_string_t); + + free(data_buffer); + free(inst); + free(po); + free(gs); + + return stored_data; + + }); + + if (count == 1) { + result.SetVectorType(VectorType::CONSTANT_VECTOR); + } +} + + +inline void Geo_tpose_to_trgeometry(DataChunk &args, ExpressionState &state, Vector &result) { + auto count = args.size(); + auto &geom_vec = args.data[0]; + auto &tpose_vec = args.data[1]; + + BinaryExecutor::Execute( + geom_vec, tpose_vec, result, count, + [&](string_t geom_blob, string_t tpose_blob)-> string_t{ + // The reference geometry arrives as a GEOMETRY blob and the + // motion as a serialized temporal pose; the rigid geometry + // is the geometry moved by that temporal pose. + GSERIALIZED *gs = GeometryToGSerialized(geom_blob, 0); + if (gs == NULL) { + throw InvalidInputException("Invalid geometry for TRGEOMETRY"); + } + + std::string tpose_input = tpose_blob.GetString(); + Temporal *tpose = reinterpret_cast(const_cast(tpose_input.c_str())); + if (!tpose) { + free(gs); + throw InvalidInputException("Invalid TPOSE data: null pointer"); + } + + Temporal *temp = geo_tpose_to_trgeometry(gs, tpose); + if (temp == NULL) { + free(gs); + throw InvalidInputException("Failed to create TRGEOMETRY"); + } + + size_t temp_size = temporal_mem_size(temp); + + uint8_t *temp_buffer = (uint8_t *)malloc(temp_size); + if (!temp_buffer) { + free(temp); + free(gs); + throw InvalidInputException("Failed to allocate memory for TRGEOMETRY data"); + } + + memcpy(temp_buffer, temp, temp_size); + + string_t temp_string_t((char*) temp_buffer, temp_size); + string_t stored_data = StringVector::AddStringOrBlob(result, temp_string_t); + + free(temp_buffer); + free(temp); + free(gs); + + return stored_data; + + }); + + if (count == 1){ + result.SetVectorType(VectorType::CONSTANT_VECTOR); + } +} + +TInstant **temparr_extract_rg(Vector &trgeom_arr_vec, list_entry_t list_entry, int *count) { + auto &child_vector = ListVector::GetEntry(trgeom_arr_vec); + auto list_size = list_entry.length; + auto list_offset = list_entry.offset; + + if (list_size == 0) { + *count = 0; + return nullptr; + } + + *count = list_size; + + TInstant **instants = (TInstant**)malloc(sizeof(TInstant*) * list_size); + if (!instants) { + *count = 0; + return nullptr; + } + + for (idx_t i = 0; i < list_size; i++) { + auto element_idx = list_offset + i; + string_t tgeom_blob = FlatVector::GetData(child_vector)[element_idx]; + + const uint8_t *data = reinterpret_cast(tgeom_blob.GetData()); + size_t data_size = tgeom_blob.GetSize(); + + if (data_size < sizeof(void*)) { + for (idx_t j = 0; j < i; j++) { + if (instants[j]) free(instants[j]); + } + free(instants); + *count = 0; + return nullptr; + } + + uint8_t *data_copy = (uint8_t*)malloc(data_size); + if (!data_copy) { + for (idx_t j = 0; j < i; j++) { + if (instants[j]) free(instants[j]); + } + free(instants); + *count = 0; + return nullptr; + } + memcpy(data_copy, data, data_size); + + Temporal *temp = reinterpret_cast(data_copy); + if (!temp) { + free(data_copy); + for (idx_t j = 0; j < i; j++) { + if (instants[j]) free(instants[j]); + } + free(instants); + *count = 0; + return nullptr; + } + + instants[i] = (TInstant*)temp; + } + + return instants; +} + +inline void Trgeometry_sequence_constructor(DataChunk &args, ExpressionState &state, Vector &result) { + // Default values + const char* default_interp = "linear"; + bool default_lower_inc = true; + bool default_upper_inc = true; + + auto count = args.size(); + auto arg_count = args.ColumnCount(); + + + auto &trgeom_arr_vec = args.data[0]; + trgeom_arr_vec.Flatten(count); + + Vector *interp_vec = nullptr; + Vector *lower_vec = nullptr; + Vector *upper_vec = nullptr; + + if (arg_count > 1) { + interp_vec = &args.data[1]; + interp_vec->Flatten(count); + } + if (arg_count > 2) { + lower_vec = &args.data[2]; + lower_vec->Flatten(count); + } + if (arg_count > 3) { + upper_vec = &args.data[3]; + upper_vec->Flatten(count); + } + + result.Flatten(count); + + auto trgeom_data = FlatVector::GetData(trgeom_arr_vec); + auto result_data = FlatVector::GetData(result); + + // Get validity masks + auto &trgeom_validity = FlatVector::Validity(trgeom_arr_vec); + auto &result_validity = FlatVector::Validity(result); + + for (idx_t i = 0; i < count; i++) { + if (!trgeom_validity.RowIsValid(i)) { + result_validity.SetInvalid(i); + continue; + } + + try { + list_entry_t list_entry = trgeom_data[i]; + + // Handle interp parameter with default + std::string interp_str = default_interp; + if (interp_vec) { + auto interp_data = FlatVector::GetData(*interp_vec); + auto &interp_validity = FlatVector::Validity(*interp_vec); + if (interp_validity.RowIsValid(i)) { + interp_str = interp_data[i].GetString(); + } + } + interpType interp = interptype_from_string(interp_str.c_str()); + + bool lower_inc = default_lower_inc; + bool upper_inc = default_upper_inc; + + if (lower_vec) { + auto lower_data = FlatVector::GetData(*lower_vec); + auto &lower_validity = FlatVector::Validity(*lower_vec); + if (lower_validity.RowIsValid(i)) { + lower_inc = lower_data[i]; + } + } + + if (upper_vec) { + auto upper_data = FlatVector::GetData(*upper_vec); + auto &upper_validity = FlatVector::Validity(*upper_vec); + if (upper_validity.RowIsValid(i)) { + upper_inc = upper_data[i]; + } + } + + // Extract array elements + int element_count; + TInstant **instants = temparr_extract_rg(trgeom_arr_vec, list_entry, &element_count); + + if (!instants || element_count == 0) { + result_validity.SetInvalid(i); + continue; + } + + TSequence *sequence_result = tsequence_make((TInstant **) instants, element_count, + lower_inc, upper_inc, interp, true); + + if (!sequence_result) { + for (int j = 0; j < element_count; j++) { + if (instants[j]) { + free(instants[j]); + } + } + free(instants); + result_validity.SetInvalid(i); + continue; + } + + size_t data_size = temporal_mem_size(reinterpret_cast(sequence_result)); + uint8_t *data_buffer = (uint8_t*)malloc(data_size); + if (!data_buffer) { + free(sequence_result); + for (int j = 0; j < element_count; j++) { + if (instants[j]) { + free(instants[j]); + } + } + free(instants); + result_validity.SetInvalid(i); + continue; + } + + memcpy(data_buffer, sequence_result, data_size); + + string_t data_string_t(reinterpret_cast(data_buffer), data_size); + result_data[i] = StringVector::AddStringOrBlob(result, data_string_t); + + free(data_buffer); + free(sequence_result); + for (int j = 0; j < element_count; j++) { + if (instants[j]) { + free(instants[j]); + } + } + free(instants); + + } catch (const std::exception& e) { + result_validity.SetInvalid(i); + } + } + + if (count == 1) { + result.SetVectorType(VectorType::CONSTANT_VECTOR); + } +} + + + + +/* + * Conversions +*/ + +inline void Temporal_to_tstzspan(DataChunk &args, ExpressionState &state, Vector &result) { + auto count = args.size(); + auto &input_geom_vec = args.data[0]; + + UnaryExecutor::Execute( + input_geom_vec, result, count, + [&](string_t input_str) -> string_t { + std::string input = input_str.GetString(); + + Temporal *temp = reinterpret_cast(const_cast(input.c_str())); + + if (!temp) { + throw InvalidInputException("Invalid TRGEOMETRY data: null pointer"); + } + + Span *timespan = temporal_to_tstzspan(temp); + + if (!timespan) { + throw InvalidInputException("Failed to extract timespan from TRGEOMETRY"); + } + + size_t span_size = sizeof(Span); + + uint8_t *span_buffer = (uint8_t*)malloc(span_size); + if (!span_buffer) { + free(timespan); + throw InvalidInputException("Failed to allocate memory for timespan data"); + } + + memcpy(span_buffer, timespan, span_size); + + string_t span_string_t(reinterpret_cast(span_buffer), span_size); + string_t stored_data = StringVector::AddStringOrBlob(result, span_string_t); + + free(span_buffer); + free(timespan); + + return stored_data; + } + ); + + if (count == 1) { + result.SetVectorType(VectorType::CONSTANT_VECTOR); + } +} + +/* + * Transformations +*/ + +inline void Temporal_to_tinstant(DataChunk &args, ExpressionState &state, Vector &result) { + auto count = args.size(); + auto &input_geom_vec = args.data[0]; + + UnaryExecutor::Execute( + input_geom_vec, result, count, + [&](string_t input_str) -> string_t { + std::string input = input_str.GetString(); + + Temporal *temp = reinterpret_cast(const_cast(input.c_str())); + + if (!temp) { + throw InvalidInputException("Invalid TRGEOMETRY data: null pointer"); + } + + TInstant *inst = temporal_to_tinstant(temp); + if (!inst) { + throw InvalidInputException("Failed to convert TRGEOMETRY to TInstant"); + } + + size_t inst_size = temporal_mem_size((Temporal*)inst); + + uint8_t *inst_buffer = (uint8_t*)malloc(inst_size); + if (!inst_buffer) { + free(inst); + throw InvalidInputException("Failed to allocate memory for TInstant data"); + } + + memcpy(inst_buffer, inst, inst_size); + + string_t inst_string_t(reinterpret_cast(inst_buffer), inst_size); + string_t stored_data = StringVector::AddStringOrBlob(result, inst_string_t); + + free(inst_buffer); + free(inst); + + return stored_data; + } + ); + + if (count == 1) { + result.SetVectorType(VectorType::CONSTANT_VECTOR); + } +} + + +inline void Temporal_set_interp(DataChunk &args, ExpressionState &state, Vector &result) { + auto count = args.size(); + auto &tgeom_vec = args.data[0]; + auto &interp_vec = args.data[1]; + + tgeom_vec.Flatten(count); + interp_vec.Flatten(count); + + BinaryExecutor::Execute( + tgeom_vec, interp_vec, result, count, + [&](string_t tgeom_str_t, string_t interp_str_t) -> string_t { + + std::string input = tgeom_str_t.GetString(); + + Temporal *temp = reinterpret_cast(const_cast(input.c_str())); + if (!temp) { + throw InvalidInputException("Invalid TRGEOMETRY data: null pointer"); + } + + std::string interp_str = interp_str_t.GetString(); + interpType new_interp = interptype_from_string(interp_str.c_str()); + + Temporal *result_temp = temporal_set_interp(temp, new_interp); + if (!result_temp) { + throw InvalidInputException("Failed to set interpolation"); + } + + // Serialize result back to binary + size_t result_size = temporal_mem_size(result_temp); + uint8_t *result_buffer = (uint8_t*)malloc(result_size); + if (!result_buffer) { + free(result_temp); + throw InvalidInputException("Failed to allocate memory for result"); + } + + memcpy(result_buffer, result_temp, result_size); + string_t result_string_t(reinterpret_cast(result_buffer), result_size); + string_t stored_data = StringVector::AddStringOrBlob(result, result_string_t); + + free(result_buffer); + free(result_temp); + + return stored_data; + }); + + if (count == 1) { + result.SetVectorType(VectorType::CONSTANT_VECTOR); + } +} + + +inline void Temporal_merge(DataChunk &args, ExpressionState &state, Vector &result) { + auto count = args.size(); + auto &tgeom1_vec = args.data[0]; + auto &tgeom2_vec = args.data[1]; + + tgeom1_vec.Flatten(count); + tgeom2_vec.Flatten(count); + + BinaryExecutor::Execute( + tgeom1_vec, tgeom2_vec, result, count, + [&](string_t tgeom1_str_t, string_t tgeom2_str_t) -> string_t { + std::string tgeom1 = tgeom1_str_t.GetString(); + + Temporal *temp1 = reinterpret_cast(const_cast(tgeom1.c_str())); + if (!temp1) { + throw InvalidInputException("Invalid TRGEOMETRY data: null pointer"); + } + + std::string tgeom2 = tgeom2_str_t.GetString(); + + Temporal *temp2 = reinterpret_cast(const_cast(tgeom2.c_str())); + if (!temp2) { + throw InvalidInputException("Invalid TRGEOMETRY data: null pointer"); + } + + Temporal *result_temp = temporal_merge(temp1, temp2); + if (!result_temp) { + throw InvalidInputException("Failed to merge temporal rigid geometries"); + } + + // Serialize result back to binary + size_t result_size = temporal_mem_size(result_temp); + uint8_t *result_buffer = (uint8_t*)malloc(result_size); + if (!result_buffer) { + free(result_temp); + throw InvalidInputException("Failed to allocate memory for result"); + } + + memcpy(result_buffer, result_temp, result_size); + string_t result_string_t(reinterpret_cast(result_buffer), result_size); + string_t stored_data = StringVector::AddStringOrBlob(result, result_string_t); + + free(result_buffer); + free(result_temp); + + return stored_data; + }); + + if (count == 1) { + result.SetVectorType(VectorType::CONSTANT_VECTOR); + } +} + + +/* + * Accessor Functions +*/ + +inline void Temporal_subtype(DataChunk &args, ExpressionState &state, Vector &result) { + auto count = args.size(); + auto &tgeom_vec = args.data[0]; + + tgeom_vec.Flatten(count); + + UnaryExecutor::Execute( + tgeom_vec, result, count, + [&](string_t tgeom_str_t) -> string_t { + std::string input = tgeom_str_t.GetString(); + + Temporal *temp = reinterpret_cast(const_cast(input.c_str())); + if (!temp) { + throw InvalidInputException("Invalid TRGEOMETRY data: null pointer"); + } + + const char *subtype_str = temporal_subtype(temp); + if (!subtype_str) { + throw InvalidInputException("Failed to get temporal subtype"); + } + + std::string result_str(subtype_str); + string_t stored_result = StringVector::AddString(result, result_str); + + return stored_result; + }); + + if (count == 1) { + result.SetVectorType(VectorType::CONSTANT_VECTOR); + } +} + + + + +inline void Temporal_interp(DataChunk &args, ExpressionState &state, Vector &result) { + auto count = args.size(); + auto &tgeom_vec = args.data[0]; + + tgeom_vec.Flatten(count); + + UnaryExecutor::Execute( + tgeom_vec, result, count, + [&](string_t tgeom_str_t) -> string_t { + + std::string input = tgeom_str_t.GetString(); + + Temporal *temp = reinterpret_cast(const_cast(input.c_str())); + if (!temp) { + throw InvalidInputException("Invalid TRGEOMETRY data: null pointer"); + } + + + const char *interp_str = temporal_interp(temp); + if (!interp_str) { + throw InvalidInputException("Failed to get temporal interpolation"); + } + + std::string result_str(interp_str); + string_t stored_result = StringVector::AddString(result, result_str); + + return stored_result; + }); + + if (count == 1) { + result.SetVectorType(VectorType::CONSTANT_VECTOR); + } +} + +inline void Temporal_mem_size(DataChunk &args, ExpressionState &state, Vector &result) { + auto count = args.size(); + auto &tgeom_vec = args.data[0]; + + tgeom_vec.Flatten(count); + + UnaryExecutor::Execute( + tgeom_vec, result, count, + [&](string_t tgeom_str_t) -> int32_t { + std::string input = tgeom_str_t.GetString(); + + Temporal *temp = reinterpret_cast(const_cast(input.c_str())); + if (!temp) { + throw InvalidInputException("Invalid TRGEOMETRY data: null pointer"); + } + + size_t mem_size = temporal_mem_size(temp); + + + return static_cast(mem_size); + }); + + if (count == 1) { + result.SetVectorType(VectorType::CONSTANT_VECTOR); + } +} + +// ---- Rigid-geometry value accessors ---- +// A temporal rigid geometry is a reference geometry rigidly moved over +// time by an embedded temporal pose, so its instant value type is a Pose +// (not a registered DuckDB type). getValue surfaces that pose in its +// canonical text form (`Pose(POINT(x y),theta)`), mirroring the asText +// output, exactly as the tpose port does. The reference geometry is +// surfaced by geometry(trgeometry) via trgeometry_geom, and the moved +// rigid-geometry snapshot at the bounding instants is surfaced by +// startValue / endValue via trgeometry_start_value / trgeometry_end_value +// (each a freshly allocated GSERIALIZED the caller owns), per the +// canonical MobilityDB SQL signatures. + +inline Pose *pose_from_instant_value(const TInstant *inst) { + Datum d = tinstant_value(inst); + return reinterpret_cast(d); +} + +inline void Tinstant_value(DataChunk &args, ExpressionState &state, Vector &result) { + auto count = args.size(); + auto &input_vec = args.data[0]; + + UnaryExecutor::Execute( + input_vec, result, count, + [&](string_t input_str) -> string_t { + std::string input = input_str.GetString(); + + TInstant *tinst = reinterpret_cast(const_cast(input.c_str())); + + // tinstant_value returns a freshly allocated copy of the + // Pose value (datum_copy), which the caller owns. + Pose *po = pose_from_instant_value(tinst); + + char *str = pose_out(po, 15); + if (!str) { + free(po); + throw InvalidInputException("Failed to convert pose value to text"); + } + std::string output(str); + string_t stored_result = StringVector::AddString(result, output); + + free(str); + free(po); + + return stored_result; + }); + + if (count == 1) { + result.SetVectorType(VectorType::CONSTANT_VECTOR); + } +} + + + +inline void Temporal_start_value(DataChunk &args, ExpressionState &state, Vector &result) { + auto count = args.size(); + auto &input_vec = args.data[0]; + + UnaryExecutor::Execute( + input_vec, result, count, + [&](string_t input_str) -> string_t { + std::string input = input_str.GetString(); + + Temporal *temp = reinterpret_cast(const_cast(input.c_str())); + + // trgeometry_start_value returns a freshly allocated + // GSERIALIZED of the rigid-geometry snapshot at the first + // instant (the reference geometry moved by the start pose). + GSERIALIZED *gs = trgeometry_start_value(temp); + if (!gs) { + throw InvalidInputException("Failed to extract start value from TRGEOMETRY"); + } + + string_t geometry_blob = GSerializedToGeometry(gs, state, result); + string_t stored_result = StringVector::AddStringOrBlob(result, geometry_blob); + + free(gs); + + return stored_result; + }); + + if (count == 1) { + result.SetVectorType(VectorType::CONSTANT_VECTOR); + } +} + + +inline void Temporal_end_value(DataChunk &args, ExpressionState &state, Vector &result) { + auto count = args.size(); + auto &input_vec = args.data[0]; + + UnaryExecutor::Execute( + input_vec, result, count, + [&](string_t input_str) -> string_t { + std::string input = input_str.GetString(); + + Temporal *temp = reinterpret_cast(const_cast(input.c_str())); + + // trgeometry_end_value returns a freshly allocated + // GSERIALIZED of the rigid-geometry snapshot at the last + // instant (the reference geometry moved by the end pose). + GSERIALIZED *gs = trgeometry_end_value(temp); + if (!gs) { + throw InvalidInputException("Failed to extract end value from TRGEOMETRY"); + } + + string_t geometry_blob = GSerializedToGeometry(gs, state, result); + string_t stored_result = StringVector::AddStringOrBlob(result, geometry_blob); + + free(gs); + + return stored_result; + }); + + if (count == 1) { + result.SetVectorType(VectorType::CONSTANT_VECTOR); + } +} + +inline void Trgeometry_geom(DataChunk &args, ExpressionState &state, Vector &result) { + auto count = args.size(); + auto &input_vec = args.data[0]; + + UnaryExecutor::Execute( + input_vec, result, count, + [&](string_t input_str) -> string_t { + std::string input = input_str.GetString(); + + Temporal *temp = reinterpret_cast(const_cast(input.c_str())); + + // trgeometry_geom returns a freshly allocated GSERIALIZED + // copy of the reference geometry that is rigidly moved. + GSERIALIZED *gs = trgeometry_geom(temp); + if (!gs) { + throw InvalidInputException("Failed to extract reference geometry from TRGEOMETRY"); + } + + string_t geometry_blob = GSerializedToGeometry(gs, state, result); + string_t stored_result = StringVector::AddStringOrBlob(result, geometry_blob); + + free(gs); + + return stored_result; + }); + + if (count == 1) { + result.SetVectorType(VectorType::CONSTANT_VECTOR); + } +} + + +inline void Temporal_lower_inc(DataChunk &args, ExpressionState &state, Vector &result) { + auto count = args.size(); + auto &input_vec = args.data[0]; + + UnaryExecutor::Execute( + input_vec, result, count, + [&](string_t input_str) -> string_t { + std::string input = input_str.GetString(); + + Temporal* temp = reinterpret_cast(const_cast(input.c_str())); + + bool lower_inc = temporal_lower_inc(temp); + + std::string result_str = lower_inc ? "true" : "false"; + string_t stored_result = StringVector::AddString(result, result_str); + return stored_result; + }); + + if (count == 1) { + result.SetVectorType(VectorType::CONSTANT_VECTOR); + } +} + +inline void Temporal_upper_inc(DataChunk &args, ExpressionState &state, Vector &result) { + auto count = args.size(); + auto &input_vec = args.data[0]; + + UnaryExecutor::Execute( + input_vec, result, count, + [&](string_t input_str) -> string_t { + std::string input = input_str.GetString(); + + Temporal* temp = reinterpret_cast(const_cast(input.c_str())); + + bool upper_inc = temporal_upper_inc(temp); + + std::string result_str = upper_inc ? "true" : "false"; + string_t stored_result = StringVector::AddString(result, result_str); + return stored_result; + }); + + if (count == 1) { + result.SetVectorType(VectorType::CONSTANT_VECTOR); + } +} + +inline void Temporal_start_instant(DataChunk &args, ExpressionState &state, Vector &result) { + auto count = args.size(); + auto &input_vec = args.data[0]; + + UnaryExecutor::Execute( + input_vec, result, count, + [&](string_t input_str) -> string_t { + std::string input = input_str.GetString(); + + Temporal *temp = reinterpret_cast(const_cast(input.c_str())); + + TInstant *start_inst = temporal_start_instant(temp); + + if (!start_inst) { + throw InvalidInputException("Failed to get start_inst from temporal object"); + } + + size_t result_size = temporal_mem_size((Temporal*)start_inst); + if (result_size == 0) { + throw InvalidInputException("Invalid result size from temporal object"); + } + + uint8_t *result_buffer = (uint8_t*)malloc(result_size); + if (!result_buffer) { + free(start_inst); + throw InvalidInputException("Failed to allocate memory for result"); + } + + memcpy(result_buffer, start_inst, result_size); + string_t result_string_t(reinterpret_cast(result_buffer), result_size); + string_t stored_result = StringVector::AddStringOrBlob(result, result_string_t); + + free(result_buffer); + free(start_inst); + return stored_result; + }); + + if (count == 1) { + result.SetVectorType(VectorType::CONSTANT_VECTOR); + } +} + +inline void Temporal_end_instant(DataChunk &args, ExpressionState &state, Vector &result) { + auto count = args.size(); + auto &input_vec = args.data[0]; + + UnaryExecutor::Execute( + input_vec, result, count, + [&](string_t input_str) -> string_t { + std::string input = input_str.GetString(); + + Temporal *temp = reinterpret_cast(const_cast(input.c_str())); + + TInstant *end_inst = temporal_end_instant(temp); + + if (!end_inst) { + throw InvalidInputException("Failed to get end_inst from temporal object"); + } + + size_t result_size = temporal_mem_size((Temporal*)end_inst); + if (result_size == 0) { + throw InvalidInputException("Invalid result size from temporal object"); + } + + uint8_t *result_buffer = (uint8_t*)malloc(result_size); + if (!result_buffer) { + free(end_inst); + throw InvalidInputException("Failed to allocate memory for result"); + } + + memcpy(result_buffer, end_inst, result_size); + string_t result_string_t(reinterpret_cast(result_buffer), result_size); + string_t stored_result = StringVector::AddStringOrBlob(result, result_string_t); + + free(result_buffer); + free(end_inst); + return stored_result; + }); + + if (count == 1) { + result.SetVectorType(VectorType::CONSTANT_VECTOR); + } +} + + + + +inline void Temporal_instant_n(DataChunk &args, ExpressionState &state, Vector &result) { + auto count = args.size(); + auto &tgeom_vec = args.data[0]; + auto &n_vec = args.data[1]; + + BinaryExecutor::Execute( + tgeom_vec, n_vec, result, count, + [&](string_t tgeom_str, int32_t n) -> string_t { + std::string tgeom = tgeom_str.GetString(); + + Temporal *temp = reinterpret_cast(const_cast(tgeom.c_str())); + + TInstant *inst_n = temporal_instant_n(temp, n); + if (!inst_n) { + throw InvalidInputException("Failed to get instant n from temporal object"); + } + + size_t result_size = temporal_mem_size((Temporal*)inst_n); + if (result_size == 0) { + throw InvalidInputException("Invalid result size from temporal object"); + } + + uint8_t *result_buffer = (uint8_t*)malloc(result_size); + if (!result_buffer) { + free(inst_n); + throw InvalidInputException("Failed to allocate memory for result"); + } + + memcpy(result_buffer, inst_n, result_size); + string_t result_string_t(reinterpret_cast(result_buffer), result_size); + string_t stored_result = StringVector::AddStringOrBlob(result, result_string_t); + + free(result_buffer); + free(inst_n); + return stored_result; + }); + + if (count == 1) { + result.SetVectorType(VectorType::CONSTANT_VECTOR); + } +} + + +inline void Tinstant_timestamptz(DataChunk &args, ExpressionState &state, Vector &result) { + auto count = args.size(); + auto &input_geom_vec = args.data[0]; + + UnaryExecutor::Execute( + input_geom_vec, result, count, + [&](string_t input_geom_str) -> timestamp_tz_t { + const uint8_t *data = reinterpret_cast(input_geom_str.GetData()); + size_t data_size = input_geom_str.GetSize(); + + if (data_size < sizeof(void*)) { + throw InvalidInputException("Invalid TRGEOMETRY data: insufficient size"); + } + + uint8_t *data_copy = (uint8_t*)malloc(data_size); + if (!data_copy) { + throw InvalidInputException("Failed to allocate memory for TRGEOMETRY deserialization"); + } + memcpy(data_copy, data, data_size); + + TInstant *temp = reinterpret_cast(data_copy); + + if (!temp) { + free(data_copy); + throw InvalidInputException("Invalid TRGEOMETRY data: null pointer"); + } + + TimestampTz meos_t = temp->t; + + timestamp_tz_t meos_timestamp{meos_t}; + timestamp_tz_t duckdb_t = MeosToDuckDBTimestamp(meos_timestamp); + + free(data_copy); + + return duckdb_t; + } + ); + + if (count == 1) { + result.SetVectorType(VectorType::CONSTANT_VECTOR); + } +} + + +void TRGeometryTypes::RegisterScalarFunctions(ExtensionLoader &loader) { + + auto trgeometry_function = ScalarFunction( + "TRGEOMETRY", + {LogicalType::VARCHAR}, + TRGeometryTypes::TRGEOMETRY(), + Trgeometry_constructor + ); + duckdb::RegisterSerializedScalarFunction(loader, trgeometry_function); + + auto trgeometry_inst_function = ScalarFunction( + "TRGEOMETRY", + {GeoTypes::GEOMETRY(), LogicalType::VARCHAR, LogicalType::TIMESTAMP_TZ}, + TRGeometryTypes::TRGEOMETRY(), + Trgeometryinst_constructor); + duckdb::RegisterSerializedScalarFunction(loader, trgeometry_inst_function); + + auto trgeometry_from_tpose_function = ScalarFunction( + "TRGEOMETRY", + {GeoTypes::GEOMETRY(), LogicalType::VARCHAR}, + TRGeometryTypes::TRGEOMETRY(), + Geo_tpose_to_trgeometry + ); + duckdb::RegisterSerializedScalarFunction(loader, trgeometry_from_tpose_function); + + auto trgeometryseqarr_1param= ScalarFunction( + "trgeometrySeq", + {LogicalType::LIST(TRGeometryTypes::TRGEOMETRY())}, + TRGeometryTypes::TRGEOMETRY(), + Trgeometry_sequence_constructor + ); + duckdb::RegisterSerializedScalarFunction(loader, trgeometryseqarr_1param); + + auto trgeometryseqarr_2params = ScalarFunction( + "trgeometrySeq", + {LogicalType::LIST(TRGeometryTypes::TRGEOMETRY()), LogicalType::VARCHAR}, + TRGeometryTypes::TRGEOMETRY(), + Trgeometry_sequence_constructor + ); + duckdb::RegisterSerializedScalarFunction(loader, trgeometryseqarr_2params); + + auto trgeometryseqarr_3params = ScalarFunction( + "trgeometrySeq", + {LogicalType::LIST(TRGeometryTypes::TRGEOMETRY()), LogicalType::VARCHAR, LogicalType::BOOLEAN}, + TRGeometryTypes::TRGEOMETRY(), + Trgeometry_sequence_constructor + ); + duckdb::RegisterSerializedScalarFunction(loader, trgeometryseqarr_3params); + + auto trgeometryseqarr_4params = ScalarFunction( + "trgeometrySeq", + {LogicalType::LIST(TRGeometryTypes::TRGEOMETRY()), LogicalType::VARCHAR, LogicalType::BOOLEAN, LogicalType::BOOLEAN}, + TRGeometryTypes::TRGEOMETRY(), + Trgeometry_sequence_constructor + ); + duckdb::RegisterSerializedScalarFunction(loader, trgeometryseqarr_4params); + + auto trgeometry_to_timespan_function = ScalarFunction( + "timeSpan", + {TRGeometryTypes::TRGEOMETRY()}, + SpanTypes::TSTZSPAN(), + Temporal_to_tstzspan); + duckdb::RegisterSerializedScalarFunction(loader, trgeometry_to_timespan_function); + + auto trgeometry_to_tinstant_function = ScalarFunction( + "trgeometryInst", + {TRGeometryTypes::TRGEOMETRY()}, + TRGeometryTypes::TRGEOMETRY(), + Temporal_to_tinstant); + duckdb::RegisterSerializedScalarFunction(loader, trgeometry_to_tinstant_function); + + + auto setInterp_function = ScalarFunction( + "setInterp", + {TRGeometryTypes::TRGEOMETRY(), LogicalType::VARCHAR}, + TRGeometryTypes::TRGEOMETRY(), + Temporal_set_interp + ); + duckdb::RegisterSerializedScalarFunction(loader, setInterp_function); + + + auto merge_function = ScalarFunction( + "merge", + {TRGeometryTypes::TRGEOMETRY(), TRGeometryTypes::TRGEOMETRY()}, + TRGeometryTypes::TRGEOMETRY(), + Temporal_merge + ); + duckdb::RegisterSerializedScalarFunction(loader, merge_function); + + auto tempSubtype_function = ScalarFunction( + "tempSubtype", + {TRGeometryTypes::TRGEOMETRY()}, + LogicalType::VARCHAR, + Temporal_subtype + ); + duckdb::RegisterSerializedScalarFunction(loader, tempSubtype_function); + + auto interp_function = ScalarFunction( + "interp", + {TRGeometryTypes::TRGEOMETRY()}, + LogicalType::VARCHAR, + Temporal_interp + ); + duckdb::RegisterSerializedScalarFunction(loader, interp_function); + + auto memSize_function = ScalarFunction( + "memSize", + {TRGeometryTypes::TRGEOMETRY()}, + LogicalType::INTEGER, + Temporal_mem_size + ); + duckdb::RegisterSerializedScalarFunction(loader, memSize_function); + + auto getValue_function = ScalarFunction( + "getValue", + {TRGeometryTypes::TRGEOMETRY()}, + LogicalType::VARCHAR, + Tinstant_value + ); + duckdb::RegisterSerializedScalarFunction(loader, getValue_function); + + + auto trgeometry_start_value_function = ScalarFunction( + "startValue", + {TRGeometryTypes::TRGEOMETRY()}, + GeoTypes::GEOMETRY(), + Temporal_start_value + ); + duckdb::RegisterSerializedScalarFunction(loader, trgeometry_start_value_function); + + auto trgeometry_end_value_function = ScalarFunction( + "endValue", + {TRGeometryTypes::TRGEOMETRY()}, + GeoTypes::GEOMETRY(), + Temporal_end_value + ); + duckdb::RegisterSerializedScalarFunction(loader, trgeometry_end_value_function); + + auto trgeometry_geom_function = ScalarFunction( + "geometry", + {TRGeometryTypes::TRGEOMETRY()}, + GeoTypes::GEOMETRY(), + Trgeometry_geom + ); + duckdb::RegisterSerializedScalarFunction(loader, trgeometry_geom_function); + + auto startInstant_function = ScalarFunction( + "startInstant", + {TRGeometryTypes::TRGEOMETRY()}, + TRGeometryTypes::TRGEOMETRY(), + Temporal_start_instant + ); + duckdb::RegisterSerializedScalarFunction(loader, startInstant_function); + + auto endInstant_function = ScalarFunction( + "endInstant", + {TRGeometryTypes::TRGEOMETRY()}, + TRGeometryTypes::TRGEOMETRY(), + Temporal_end_instant + ); + duckdb::RegisterSerializedScalarFunction(loader, endInstant_function); + + auto instantN_function = ScalarFunction( + "instantN", + {TRGeometryTypes::TRGEOMETRY(), LogicalType::INTEGER}, + TRGeometryTypes::TRGEOMETRY(), + Temporal_instant_n + ); + duckdb::RegisterSerializedScalarFunction(loader, instantN_function); + + + auto trgeometry_gettimestamptz_function = ScalarFunction( + "getTimestamp", + {TRGeometryTypes::TRGEOMETRY()}, + LogicalType::TIMESTAMP_TZ, + Tinstant_timestamptz); + duckdb::RegisterSerializedScalarFunction(loader, trgeometry_gettimestamptz_function); + + + // =================================================================== + // Foundational trgeometry surface — accessors, time/value-restrict, + // modifiers, and comparison. The MEOS C functions delegated to here + // are subtype-agnostic (they take Temporal *), so we reuse the same + // generic handlers wired for tgeompoint in temporal_functions.cpp. + // =================================================================== + + const LogicalType TGEOM = TRGeometryTypes::TRGEOMETRY(); + const LogicalType TSTZ = LogicalType::TIMESTAMP_TZ; + const LogicalType IVAL = LogicalType::INTERVAL; + + // ---- Accessors ---- + loader.RegisterFunction(ScalarFunction( + "valueAtTimestamp", {TGEOM, TSTZ}, LogicalType::VARCHAR, + Tinstant_value)); + loader.RegisterFunction(ScalarFunction( + "getTime", {TGEOM}, SpansetTypes::tstzspanset(), + TemporalFunctions::Temporal_time)); + loader.RegisterFunction(ScalarFunction( + "duration", {TGEOM}, IVAL, + TemporalFunctions::Temporal_duration)); + loader.RegisterFunction(ScalarFunction( + "duration", {TGEOM, LogicalType::BOOLEAN}, IVAL, + TemporalFunctions::Temporal_duration)); + loader.RegisterFunction(ScalarFunction( + "lowerInc", {TGEOM}, LogicalType::BOOLEAN, + TemporalFunctions::Temporal_lower_inc)); + loader.RegisterFunction(ScalarFunction( + "upperInc", {TGEOM}, LogicalType::BOOLEAN, + TemporalFunctions::Temporal_upper_inc)); + loader.RegisterFunction(ScalarFunction( + "numInstants", {TGEOM}, LogicalType::INTEGER, + TemporalFunctions::Temporal_num_instants)); + loader.RegisterFunction(ScalarFunction( + "instants", {TGEOM}, LogicalType::LIST(TGEOM), + TemporalFunctions::Temporal_instants)); + loader.RegisterFunction(ScalarFunction( + "numSequences", {TGEOM}, LogicalType::INTEGER, + TemporalFunctions::Temporal_num_sequences)); + loader.RegisterFunction(ScalarFunction( + "sequences", {TGEOM}, LogicalType::LIST(TGEOM), + TemporalFunctions::Temporal_sequences)); + loader.RegisterFunction(ScalarFunction( + "startSequence", {TGEOM}, TGEOM, + TemporalFunctions::Temporal_start_sequence)); + loader.RegisterFunction(ScalarFunction( + "endSequence", {TGEOM}, TGEOM, + TemporalFunctions::Temporal_end_sequence)); + loader.RegisterFunction(ScalarFunction( + "sequenceN", {TGEOM, LogicalType::INTEGER}, TGEOM, + TemporalFunctions::Temporal_sequence_n)); + loader.RegisterFunction(ScalarFunction( + "numTimestamps", {TGEOM}, LogicalType::INTEGER, + TemporalFunctions::Temporal_num_timestamps)); + loader.RegisterFunction(ScalarFunction( + "timestamps", {TGEOM}, LogicalType::LIST(TSTZ), + TemporalFunctions::Temporal_timestamps)); + loader.RegisterFunction(ScalarFunction( + "startTimestamp", {TGEOM}, TSTZ, + TemporalFunctions::Temporal_start_timestamptz)); + loader.RegisterFunction(ScalarFunction( + "endTimestamp", {TGEOM}, TSTZ, + TemporalFunctions::Temporal_end_timestamptz)); + loader.RegisterFunction(ScalarFunction( + "timestampN", {TGEOM, LogicalType::INTEGER}, TSTZ, + TemporalFunctions::Temporal_timestamptz_n)); + loader.RegisterFunction(ScalarFunction( + "segments", {TGEOM}, LogicalType::LIST(TGEOM), + TemporalFunctions::Temporal_segments)); + + // ---- Time-domain restrict / minus ---- + for (const auto &t : std::vector>{ + {TSTZ, TemporalFunctions::Temporal_at_timestamptz}, + {SetTypes::tstzset(), TemporalFunctions::Temporal_at_tstzset}, + {SpanTypes::TSTZSPAN(), TemporalFunctions::Temporal_at_tstzspan}, + {SpansetTypes::tstzspanset(), TemporalFunctions::Temporal_at_tstzspanset}}) { + loader.RegisterFunction(ScalarFunction( + "atTime", {TGEOM, t.first}, TGEOM, t.second)); + } + for (const auto &t : std::vector>{ + {TSTZ, TemporalFunctions::Temporal_minus_timestamptz}, + {SetTypes::tstzset(), TemporalFunctions::Temporal_minus_tstzset}, + {SpanTypes::TSTZSPAN(), TemporalFunctions::Temporal_minus_tstzspan}, + {SpansetTypes::tstzspanset(), TemporalFunctions::Temporal_minus_tstzspanset}}) { + loader.RegisterFunction(ScalarFunction( + "minusTime", {TGEOM, t.first}, TGEOM, t.second)); + } + + // beforeTimestamp / afterTimestamp accept timestamptz + loader.RegisterFunction(ScalarFunction( + "beforeTimestamp", {TGEOM, TSTZ}, TGEOM, + TemporalFunctions::Temporal_before_timestamptz)); + loader.RegisterFunction(ScalarFunction( + "afterTimestamp", {TGEOM, TSTZ}, TGEOM, + TemporalFunctions::Temporal_after_timestamptz)); + + // ---- Modifiers (shift / scale / shiftScale / append / insert / update / + // delete) ---- + loader.RegisterFunction(ScalarFunction( + "shiftTime", {TGEOM, IVAL}, TGEOM, + TemporalFunctions::Temporal_shift_time)); + loader.RegisterFunction(ScalarFunction( + "scaleTime", {TGEOM, IVAL}, TGEOM, + TemporalFunctions::Temporal_scale_time)); + loader.RegisterFunction(ScalarFunction( + "shiftScaleTime", {TGEOM, IVAL, IVAL}, TGEOM, + TemporalFunctions::Temporal_shift_scale_time)); + loader.RegisterFunction(ScalarFunction( + "appendInstant", {TGEOM, TGEOM}, TGEOM, + TemporalFunctions::Temporal_append_tinstant)); + loader.RegisterFunction(ScalarFunction( + "appendSequence", {TGEOM, TGEOM}, TGEOM, + TemporalFunctions::Temporal_append_tsequence)); + loader.RegisterFunction(ScalarFunction( + "insert", {TGEOM, TGEOM}, TGEOM, + TemporalFunctions::Temporal_insert)); + loader.RegisterFunction(ScalarFunction( + "insert", {TGEOM, TGEOM, LogicalType::BOOLEAN}, TGEOM, + TemporalFunctions::Temporal_insert)); + loader.RegisterFunction(ScalarFunction( + "update", {TGEOM, TGEOM}, TGEOM, + TemporalFunctions::Temporal_update)); + loader.RegisterFunction(ScalarFunction( + "update", {TGEOM, TGEOM, LogicalType::BOOLEAN}, TGEOM, + TemporalFunctions::Temporal_update)); + loader.RegisterFunction(ScalarFunction( + "deleteTime", {TGEOM, TSTZ}, TGEOM, + TemporalFunctions::Temporal_delete_timestamptz)); + loader.RegisterFunction(ScalarFunction( + "deleteTime", {TGEOM, TSTZ, LogicalType::BOOLEAN}, TGEOM, + TemporalFunctions::Temporal_delete_timestamptz)); + loader.RegisterFunction(ScalarFunction( + "deleteTime", {TGEOM, SetTypes::tstzset()}, TGEOM, + TemporalFunctions::Temporal_delete_tstzset)); + loader.RegisterFunction(ScalarFunction( + "deleteTime", {TGEOM, SetTypes::tstzset(), LogicalType::BOOLEAN}, TGEOM, + TemporalFunctions::Temporal_delete_tstzset)); + loader.RegisterFunction(ScalarFunction( + "deleteTime", {TGEOM, SpanTypes::TSTZSPAN()}, TGEOM, + TemporalFunctions::Temporal_delete_tstzspan)); + loader.RegisterFunction(ScalarFunction( + "deleteTime", {TGEOM, SpanTypes::TSTZSPAN(), LogicalType::BOOLEAN}, TGEOM, + TemporalFunctions::Temporal_delete_tstzspan)); + loader.RegisterFunction(ScalarFunction( + "deleteTime", {TGEOM, SpansetTypes::tstzspanset()}, TGEOM, + TemporalFunctions::Temporal_delete_tstzspanset)); + loader.RegisterFunction(ScalarFunction( + "deleteTime", {TGEOM, SpansetTypes::tstzspanset(), LogicalType::BOOLEAN}, TGEOM, + TemporalFunctions::Temporal_delete_tstzspanset)); + + // ---- Comparison (named functions + operators) ---- + struct CmpEntry { + const char *name; + scalar_function_t fn; + }; + const std::vector named_cmps = { + {"temporal_eq", TemporalFunctions::Temporal_eq}, + {"temporal_ne", TemporalFunctions::Temporal_ne}, + {"temporal_lt", TemporalFunctions::Temporal_lt}, + {"temporal_le", TemporalFunctions::Temporal_le}, + {"temporal_gt", TemporalFunctions::Temporal_gt}, + {"temporal_ge", TemporalFunctions::Temporal_ge}, + }; + for (const auto &c : named_cmps) { + loader.RegisterFunction(ScalarFunction( + c.name, {TGEOM, TGEOM}, LogicalType::BOOLEAN, c.fn)); + } + loader.RegisterFunction(ScalarFunction( + "temporal_cmp", {TGEOM, TGEOM}, LogicalType::INTEGER, + TemporalFunctions::Temporal_cmp)); + + // Operator forms — mirror the registrations tgeometry.cpp does. + const std::vector op_cmps = { + {"=", TemporalFunctions::Temporal_eq}, + {"<>", TemporalFunctions::Temporal_ne}, + {"<", TemporalFunctions::Temporal_lt}, + {"<=", TemporalFunctions::Temporal_le}, + {">", TemporalFunctions::Temporal_gt}, + {">=", TemporalFunctions::Temporal_ge}, + }; + for (const auto &c : op_cmps) { + loader.RegisterFunction(ScalarFunction( + c.name, {TGEOM, TGEOM}, LogicalType::BOOLEAN, c.fn)); + } +} + +void TRGeometryTypes::RegisterTypes(ExtensionLoader &loader) { + loader.RegisterType( "TRGEOMETRY", TRGeometryTypes::TRGEOMETRY()); +} + + +} diff --git a/src/rgeo/trgeometry_in_out.cpp b/src/rgeo/trgeometry_in_out.cpp new file mode 100644 index 0000000..4f36b20 --- /dev/null +++ b/src/rgeo/trgeometry_in_out.cpp @@ -0,0 +1,325 @@ +#include "rgeo/trgeometry.hpp" +#include "duckdb/main/extension/extension_loader.hpp" +#include "duckdb/common/extension_type_info.hpp" +#include +#include +#include +#include "mobilityduck/meos_exec_serial.hpp" + +extern "C" { + #include + #include + #include + #include + #include +} + +// meos_rgeo.h is intentionally not included (its trgeometry_append_tinstant +// prototype takes a MEOS Interval, which collides with duckdb::Interval). +// Declare only the trgeometry_* I/O symbols this translation unit needs; +// every name is the post-uniformization symbol exported from meos_rgeo.h. +extern "C" { + extern Temporal *trgeometry_in(const char *str); + extern Temporal *trgeometry_from_mfjson(const char *mfjson); + extern char *trgeometry_out(const Temporal *temp); +} + +namespace duckdb { + +inline void Tspatial_as_text(DataChunk &args, ExpressionState &state, Vector &result) { + auto count = args.size(); + auto &input_geom_vec = args.data[0]; + + UnaryExecutor::Execute( + input_geom_vec, result, count, + [&](string_t input_geom_str) -> string_t { + const uint8_t *data = reinterpret_cast(input_geom_str.GetData()); + size_t data_size = input_geom_str.GetSize(); + + if (data_size < sizeof(void*)) { + throw InvalidInputException("Invalid TRGEOMETRY data: insufficient size"); + } + + uint8_t *data_copy = (uint8_t*)malloc(data_size); + if (!data_copy) { + throw InvalidInputException("Failed to allocate memory for TRGEOMETRY deserialization"); + } + memcpy(data_copy, data, data_size); + + Temporal *temp = reinterpret_cast(data_copy); + + if (!temp) { + free(data_copy); + throw InvalidInputException("Invalid TRGEOMETRY data: null pointer"); + } + + // The temporal rigid geometry text form is the reference + // geometry, a ';' delimiter and the temporal pose; the + // type-specific trgeometry_out builds exactly that. + char *str = trgeometry_out(temp); + + if (!str) { + free(data_copy); + throw InvalidInputException("Failed to convert TRGEOMETRY to text"); + } + + std::string result_str(str); + string_t stored_result = StringVector::AddString(result, result_str); + + free(str); + free(data_copy); + + return stored_result; + } + ); + + if (count == 1) { + result.SetVectorType(VectorType::CONSTANT_VECTOR); + } +} + +inline void Tspatial_as_ewkt(DataChunk &args, ExpressionState &state, Vector &result) { + auto count = args.size(); + auto &input_geom_vec = args.data[0]; + + UnaryExecutor::Execute( + input_geom_vec, result, count, + [&](string_t input_geom_str) -> string_t { + + const uint8_t *data = reinterpret_cast(input_geom_str.GetData()); + size_t data_size = input_geom_str.GetSize(); + + if (data_size < sizeof(void*)) { + throw InvalidInputException("Invalid TRGEOMETRY data: insufficient size"); + } + + uint8_t *data_copy = (uint8_t*)malloc(data_size); + if (!data_copy) { + throw InvalidInputException("Failed to allocate memory for TRGEOMETRY deserialization"); + } + memcpy(data_copy, data, data_size); + + Temporal *temp = reinterpret_cast(data_copy); + + if (!temp) { + free(data_copy); + throw InvalidInputException("Invalid TRGEOMETRY data: null pointer"); + } + + char *ewkt = tspatial_as_ewkt(temp, 0); + + if (!ewkt) { + free(data_copy); + throw InvalidInputException("Failed to convert TRGEOMETRY to EWKT"); + } + + std::string result_str(ewkt); + string_t stored_result = StringVector::AddString(result, result_str); + + + free(ewkt); + free(data_copy); + + return stored_result; + } + ); + + if (count == 1) { + result.SetVectorType(VectorType::CONSTANT_VECTOR); + } +} + + +bool TrgeometryFunctions::StringToTrgeometry(Vector &source, Vector &result, idx_t count, CastParameters ¶meters) { + UnaryExecutor::Execute( + source, result, count, + [&](string_t input_string) -> string_t { + std::string input_str = input_string.GetString(); + + Temporal *temp = trgeometry_in(input_str.c_str()); + if (!temp) { + throw InvalidInputException("Invalid TRGEOMETRY input: " + input_str); + } + + size_t data_size = temporal_mem_size(temp); + uint8_t *data_buffer = (uint8_t*)malloc(data_size); + if (!data_buffer) { + free(temp); + throw InvalidInputException("Failed to allocate memory for TRGEOMETRY data"); + } + + memcpy(data_buffer, temp, data_size); + + string_t data_string_t(reinterpret_cast(data_buffer), data_size); + string_t stored_data = StringVector::AddStringOrBlob(result, data_string_t); + + free(data_buffer); + free(temp); + + return stored_data; + }); + + if (count == 1) { + result.SetVectorType(VectorType::CONSTANT_VECTOR); + } + return true; +} + +bool TrgeometryFunctions::TrgeometryToString(Vector &source, Vector &result, idx_t count, CastParameters ¶meters) { + UnaryExecutor::Execute( + source, result, count, + [&](string_t input_blob) -> string_t { + const uint8_t *data = reinterpret_cast(input_blob.GetData()); + size_t data_size = input_blob.GetSize(); + + if (data_size < sizeof(void*)) { + throw InvalidInputException("Invalid TRGEOMETRY data: insufficient size"); + } + + uint8_t *data_copy = (uint8_t*)malloc(data_size); + if (!data_copy) { + throw InvalidInputException("Failed to allocate memory for TRGEOMETRY deserialization"); + } + memcpy(data_copy, data, data_size); + + Temporal *temp = reinterpret_cast(data_copy); + if (!temp) { + free(data_copy); + throw InvalidInputException("Invalid TRGEOMETRY data: null pointer"); + } + + char *str = trgeometry_out(temp); + if (!str) { + free(data_copy); + throw InvalidInputException("Failed to convert TRGEOMETRY to string"); + } + + std::string output(str); + string_t stored_result = StringVector::AddString(result, output); + + free(str); + free(data_copy); + + return stored_result; + }); + + if (count == 1) { + result.SetVectorType(VectorType::CONSTANT_VECTOR); + } + return true; +} + +// ---- Spatial-temporal parsers (Binary / HexWKB / MFJSON / Text) ---- +// Used to register the `trgeometryFrom*` overloads. +// `temporal_from_wkb` and `temporal_from_hexwkb` are subtype-agnostic; +// `trgeometry_in` and `trgeometry_from_mfjson` are the renamed, +// header-exported per-type entry points (post API-uniformization they +// are real linkable symbols, so this is a clean clone of the canonical +// template rather than a tspatial_parse work-around). The result is +// stored as a raw blob, the same format every other temporal type uses. + +inline string_t StoreTempAsBlob(Vector &result, Temporal *t) { + size_t sz = temporal_mem_size(t); + string_t stored = StringVector::AddStringOrBlob( + result, string_t(reinterpret_cast(t), sz)); + free(t); + return stored; +} + +inline void TspatialFromWkbExec(DataChunk &args, ExpressionState &, Vector &result) { + UnaryExecutor::Execute( + args.data[0], result, args.size(), + [&](string_t input) -> string_t { + if (input.GetSize() == 0) + throw InvalidInputException("fromBinary: empty WKB input"); + uint8_t *wkb = (uint8_t *)malloc(input.GetSize()); + if (!wkb) throw InternalException("fromBinary: malloc failed"); + memcpy(wkb, input.GetData(), input.GetSize()); + Temporal *t = temporal_from_wkb(wkb, input.GetSize()); + free(wkb); + if (!t) throw InvalidInputException("fromBinary: invalid MEOS-WKB"); + return StoreTempAsBlob(result, t); + }); +} + +inline void TspatialFromHexWkbExec(DataChunk &args, ExpressionState &, Vector &result) { + UnaryExecutor::Execute( + args.data[0], result, args.size(), + [&](string_t input) -> string_t { + std::string hex(input.GetData(), input.GetSize()); + Temporal *t = temporal_from_hexwkb(hex.c_str()); + if (!t) throw InvalidInputException( + "fromHexWKB: invalid hex-encoded MEOS-WKB"); + return StoreTempAsBlob(result, t); + }); +} + +inline void TrgeometryFromTextExec(DataChunk &args, ExpressionState &, Vector &result) { + UnaryExecutor::Execute( + args.data[0], result, args.size(), + [&](string_t input) -> string_t { + std::string s(input.GetData(), input.GetSize()); + Temporal *t = trgeometry_in(s.c_str()); + if (!t) throw InvalidInputException("from*: invalid input"); + return StoreTempAsBlob(result, t); + }); +} + +inline void TrgeometryFromMFJSONExec(DataChunk &args, ExpressionState &, Vector &result) { + UnaryExecutor::Execute( + args.data[0], result, args.size(), + [&](string_t input) -> string_t { + std::string s(input.GetData(), input.GetSize()); + // The renamed trgeometry_from_mfjson symbol is exported from + // meos_rgeo.h; it is the canonical per-type MF-JSON entry + // point the MobilityDB SQL binds trgeometryFromMFJSON to. + Temporal *t = trgeometry_from_mfjson(s.c_str()); + if (!t) throw InvalidInputException("fromMFJSON: invalid input"); + return StoreTempAsBlob(result, t); + }); +} + +void TRGeometryTypes::RegisterScalarInOutFunctions(ExtensionLoader &loader){ + auto TrgeometryAsText = ScalarFunction( + "asText", + {TRGeometryTypes::TRGEOMETRY()}, + LogicalType::VARCHAR, + Tspatial_as_text + ); + duckdb::RegisterSerializedScalarFunction(loader, TrgeometryAsText); + + auto TrgeometryAsEWKT = ScalarFunction( + "asEWKT", + {TRGeometryTypes::TRGEOMETRY()}, + LogicalType::VARCHAR, + Tspatial_as_ewkt + ); + duckdb::RegisterSerializedScalarFunction(loader, TrgeometryAsEWKT); + + // ---- trgeometryFromBinary / FromEWKB (auto-detects format) ---- + const auto B = LogicalType::BLOB; + const auto V = LogicalType::VARCHAR; + const auto T = TRGeometryTypes::TRGEOMETRY(); + duckdb::RegisterSerializedScalarFunction(loader, + ScalarFunction("trgeometryFromBinary", {B}, T, TspatialFromWkbExec)); + duckdb::RegisterSerializedScalarFunction(loader, + ScalarFunction("trgeometryFromEWKB", {B}, T, TspatialFromWkbExec)); + duckdb::RegisterSerializedScalarFunction(loader, + ScalarFunction("trgeometryFromHexWKB", {V}, T, TspatialFromHexWkbExec)); + duckdb::RegisterSerializedScalarFunction(loader, + ScalarFunction("trgeometryFromHexEWKB", {V}, T, TspatialFromHexWkbExec)); + duckdb::RegisterSerializedScalarFunction(loader, + ScalarFunction("trgeometryFromMFJSON", {V}, T, TrgeometryFromMFJSONExec)); + duckdb::RegisterSerializedScalarFunction(loader, + ScalarFunction("trgeometryFromText", {V}, T, TrgeometryFromTextExec)); + duckdb::RegisterSerializedScalarFunction(loader, + ScalarFunction("trgeometryFromEWKT", {V}, T, TrgeometryFromTextExec)); +} + + +void TRGeometryTypes::RegisterCastFunctions(ExtensionLoader &loader) { + loader.RegisterCastFunction( LogicalType::VARCHAR, TRGeometryTypes::TRGEOMETRY(), TrgeometryFunctions::StringToTrgeometry); + loader.RegisterCastFunction( TRGeometryTypes::TRGEOMETRY(), LogicalType::VARCHAR, TrgeometryFunctions::TrgeometryToString); +} + +} diff --git a/test/sql/trgeometry.test b/test/sql/trgeometry.test new file mode 100644 index 0000000..6729b9f --- /dev/null +++ b/test/sql/trgeometry.test @@ -0,0 +1,199 @@ +# name: test/sql/trgeometry.test +# description: Core trgeometry type port — construction, text/EWKT/MFJSON +# I/O and basic accessors. A trgeometry value is a moving +# rigid geometry: a reference geometry rigidly rotated and +# translated over time by an embedded temporal pose, written +# as 'Geometry;Pose@timestamp'. +# group: [sql] + +require mobilityduck + +# Test trgeometry constructor with parentheses (instant) +query I +SELECT asText(trgeometry('Polygon((1 1,2 2,3 1,1 1));Pose(Point(1 1), 0.5)@2000-01-01')); +---- +POLYGON((1 1,2 2,3 1,1 1));Pose(POINT(1 1),0.5)@2000-01-01 00:00:00+01 + +# Test trgeometry constructor without parentheses +query I +SELECT asText(trgeometry 'Polygon((1 1,2 2,3 1,1 1));Pose(Point(1 1), 0.5)@2000-01-01'); +---- +POLYGON((1 1,2 2,3 1,1 1));Pose(POINT(1 1),0.5)@2000-01-01 00:00:00+01 + +# Test asText with continuous sequence +query I +SELECT asText(trgeometry 'Polygon((1 1,2 2,3 1,1 1));[Pose(Point(1 1), 0.2)@2000-01-01, Pose(Point(1 1), 0.4)@2000-01-02, Pose(Point(1 1), 0.5)@2000-01-03]'); +---- +POLYGON((1 1,2 2,3 1,1 1));[Pose(POINT(1 1),0.2)@2000-01-01 00:00:00+01, Pose(POINT(1 1),0.4)@2000-01-02 00:00:00+01, Pose(POINT(1 1),0.5)@2000-01-03 00:00:00+01] + +# Test asText with discrete sequence +query I +SELECT asText(trgeometry 'Polygon((1 1,2 2,3 1,1 1));{Pose(Point(1 1), 0.3)@2000-01-01, Pose(Point(1 1), 0.5)@2000-01-02}'); +---- +POLYGON((1 1,2 2,3 1,1 1));{Pose(POINT(1 1),0.3)@2000-01-01 00:00:00+01, Pose(POINT(1 1),0.5)@2000-01-02 00:00:00+01} + +# Test asEWKT is non-null +query I +SELECT asEWKT(trgeometry 'Polygon((1 1,2 2,3 1,1 1));Pose(Point(1 1), 0.5)@2000-01-01') IS NOT NULL; +---- +true + +# Test geometry/pose/timestamptz instant constructor round-trips +query I +SELECT trgeometry(geometry 'Polygon((1 1,2 2,3 1,1 1))', 'Pose(Point(1 1), 0.5)', timestamptz '2000-01-01') + = trgeometry 'Polygon((1 1,2 2,3 1,1 1));Pose(Point(1 1), 0.5)@2000-01-01'; +---- +true + +# Test geometry/tpose constructor builds a moving rigid geometry that +# round-trips against the canonical text form +query I +SELECT trgeometry(geometry 'Polygon((1 1,2 2,3 1,1 1))', + tpose '[Pose(Point(1 1), 0.2)@2000-01-01, Pose(Point(1 1), 0.4)@2000-01-02]') + = trgeometry 'Polygon((1 1,2 2,3 1,1 1));[Pose(Point(1 1), 0.2)@2000-01-01, Pose(Point(1 1), 0.4)@2000-01-02]'; +---- +true + +# Test the reference geometry accessor +query I +SELECT ST_AsText(geometry(trgeometry 'Polygon((1 1,2 2,3 1,1 1));Pose(Point(1 1), 0.5)@2000-01-01')); +---- +POLYGON ((1 1, 2 2, 3 1, 1 1)) + +# Test MFJSON round-trip is identity (format-agnostic) +query I +SELECT asText(trgeometryFromMFJSON(asMFJSON(trgeometry 'Polygon((1 1,2 2,3 1,1 1));Pose(Point(1 1), 0.5)@2000-01-01'))) + = asText(trgeometry 'Polygon((1 1,2 2,3 1,1 1));Pose(Point(1 1), 0.5)@2000-01-01'); +---- +true + +# Test MFJSON round-trip for a continuous sequence +query I +SELECT asText(trgeometryFromMFJSON(asMFJSON( + trgeometry 'Polygon((1 1,2 2,3 1,1 1));[Pose(Point(1 1), 0.2)@2000-01-01, Pose(Point(1 1), 0.4)@2000-01-02]'))) + = asText(trgeometry 'Polygon((1 1,2 2,3 1,1 1));[Pose(Point(1 1), 0.2)@2000-01-01, Pose(Point(1 1), 0.4)@2000-01-02]'); +---- +true + +# Test trgeometryFromText constructor +query I +SELECT asText(trgeometryFromText('Polygon((1 1,2 2,3 1,1 1));Pose(Point(1 1), 0.5)@2000-01-01')) IS NOT NULL; +---- +true + +# Test binary round-trip is identity +query I +SELECT asText(trgeometryFromBinary(asBinary(trgeometry 'Polygon((1 1,2 2,3 1,1 1));Pose(Point(1 1), 0.5)@2000-01-01'))) + = asText(trgeometry 'Polygon((1 1,2 2,3 1,1 1));Pose(Point(1 1), 0.5)@2000-01-01'); +---- +true + +# Test timeSpan function +query I +SELECT timeSpan(trgeometry 'Polygon((1 1,2 2,3 1,1 1));[Pose(Point(1 1), 0.2)@2000-01-01, Pose(Point(1 1), 0.4)@2000-01-03]'); +---- +[2000-01-01 00:00:00+01, 2000-01-03 00:00:00+01] + +# Test setInterp with discrete interpolation +query I +SELECT tempSubtype(setInterp(trgeometry 'Polygon((1 1,2 2,3 1,1 1));[Pose(Point(1 1), 0.2)@2000-01-01, Pose(Point(1 1), 0.4)@2000-01-02]', 'discrete')); +---- +Sequence + +# Test merge function +query I +SELECT numInstants(merge( + trgeometry 'Polygon((1 1,2 2,3 1,1 1));[Pose(Point(1 1), 0.2)@2000-01-01, Pose(Point(1 1), 0.3)@2000-01-02]', + trgeometry 'Polygon((1 1,2 2,3 1,1 1));[Pose(Point(1 1), 0.3)@2000-01-02, Pose(Point(1 1), 0.5)@2000-01-03]')); +---- +3 + +# Test tempSubtype with instant +query I +SELECT tempSubtype(trgeometry 'Polygon((1 1,2 2,3 1,1 1));Pose(Point(1 1), 0.5)@2000-01-01'); +---- +Instant + +# Test tempSubtype with discrete sequence +query I +SELECT tempSubtype(trgeometry 'Polygon((1 1,2 2,3 1,1 1));{Pose(Point(1 1), 0.3)@2000-01-01, Pose(Point(1 1), 0.5)@2000-01-02}'); +---- +Sequence + +# Test tempSubtype with continuous sequence +query I +SELECT tempSubtype(trgeometry 'Polygon((1 1,2 2,3 1,1 1));[Pose(Point(1 1), 0.2)@2000-01-01, Pose(Point(1 1), 0.4)@2000-01-02]'); +---- +Sequence + +# Test tempSubtype with sequence set +query I +SELECT tempSubtype(trgeometry 'Polygon((1 1,2 2,3 1,1 1));{[Pose(Point(1 1), 0.2)@2000-01-01, Pose(Point(1 1), 0.4)@2000-01-02], [Pose(Point(2 2), 0.6)@2000-01-04, Pose(Point(2 2), 0.6)@2000-01-05]}'); +---- +SequenceSet + +# Test memSize is positive +query I +SELECT memSize(trgeometry 'Polygon((1 1,2 2,3 1,1 1));Pose(Point(1 1), 0.5)@2000-01-01') > 0; +---- +true + +# Test interp accessor +query I +SELECT interp(trgeometry 'Polygon((1 1,2 2,3 1,1 1));[Pose(Point(1 1), 0.2)@2000-01-01, Pose(Point(1 1), 0.4)@2000-01-02]'); +---- +Linear + +# Test getValue returns the pose value text +query I +SELECT getValue(trgeometry 'Polygon((1 1,2 2,3 1,1 1));Pose(Point(1 1), 0.5)@2000-01-01'); +---- +Pose(POINT(1 1),0.5) + +# Test startValue surfaces a non-null moved rigid-geometry snapshot +query I +SELECT startValue(trgeometry 'Polygon((1 1,2 2,3 1,1 1));[Pose(Point(1 1), 0.2)@2000-01-01, Pose(Point(1 1), 0.4)@2000-01-02]') IS NOT NULL; +---- +true + +# Test endValue surfaces a non-null moved rigid-geometry snapshot +query I +SELECT endValue(trgeometry 'Polygon((1 1,2 2,3 1,1 1));[Pose(Point(1 1), 0.2)@2000-01-01, Pose(Point(1 1), 0.4)@2000-01-02]') IS NOT NULL; +---- +true + +# Test startInstant +query I +SELECT asText(startInstant(trgeometry 'Polygon((1 1,2 2,3 1,1 1));[Pose(Point(1 1), 0.2)@2000-01-01, Pose(Point(1 1), 0.4)@2000-01-02]')); +---- +POLYGON((1 1,2 2,3 1,1 1));Pose(POINT(1 1),0.2)@2000-01-01 00:00:00+01 + +# Test endInstant +query I +SELECT asText(endInstant(trgeometry 'Polygon((1 1,2 2,3 1,1 1));[Pose(Point(1 1), 0.2)@2000-01-01, Pose(Point(1 1), 0.4)@2000-01-02]')); +---- +POLYGON((1 1,2 2,3 1,1 1));Pose(POINT(1 1),0.4)@2000-01-02 00:00:00+01 + +# Test instantN +query I +SELECT asText(instantN(trgeometry 'Polygon((1 1,2 2,3 1,1 1));[Pose(Point(1 1), 0.2)@2000-01-01, Pose(Point(1 1), 0.4)@2000-01-02]', 1)); +---- +POLYGON((1 1,2 2,3 1,1 1));Pose(POINT(1 1),0.2)@2000-01-01 00:00:00+01 + +# Test getTimestamp function +query I +SELECT getTimestamp(trgeometry 'Polygon((1 1,2 2,3 1,1 1));Pose(Point(1 1), 0.5)@2023-01-01 10:00:00+00'); +---- +2023-01-01 11:00:00+01 + +# Test numInstants generic accessor +query I +SELECT numInstants(trgeometry 'Polygon((1 1,2 2,3 1,1 1));[Pose(Point(1 1), 0.2)@2000-01-01, Pose(Point(1 1), 0.4)@2000-01-02, Pose(Point(1 1), 0.5)@2000-01-03]'); +---- +3 + +# Test duration generic accessor +query I +SELECT duration(trgeometry 'Polygon((1 1,2 2,3 1,1 1));[Pose(Point(1 1), 0.2)@2000-01-01, Pose(Point(1 1), 0.4)@2000-01-03]'); +---- +2 days diff --git a/vcpkg_ports/meos/portfile.cmake b/vcpkg_ports/meos/portfile.cmake index 7fa4e5e..c804af5 100644 --- a/vcpkg_ports/meos/portfile.cmake +++ b/vcpkg_ports/meos/portfile.cmake @@ -1,24 +1,25 @@ -# TEMPORARY PROVISIONAL PIN. The core tcbuffer and tnpoint ports are -# clean clones that bind the per-type MF-JSON I/O: tcbufferFromMFJSON -# needs the tcbuffer MF-JSON support from MobilityDB PR #1051 -# (feat/tcbuffer-mfjson), and tnpointFromMFJSON needs the network-point -# MF-JSON support from MobilityDB PR #951 (split/tnpoint-mfjson-io, -# wired through the generic temporal_from_mfjson dispatch). Neither is -# on MobilityDB/MobilityDB master yet, so the pin points at an -# integration branch on the contributor fork that composes both PRs on -# top of MobilityDB master: estebanzimanyi/MobilityDB -# meos-mduck-ports-tcbuffer-tnpoint = cherry-pick #1051 -# (e624027f5b59f483476c8e5c26471f9e252a5e61) then #951 -# (800f80bfdc8dc4ce86516aacb85e48131bcf7454); the two only collide in -# disjoint #if CBUFFER vs #if NPOINT blocks of type_in.c / type_out.c -# and were union-merged with no logic change. This is provisional -# pending the #134 -> #145 MobilityDuck chain plus PRs #1051 and #951 -# merging into MobilityDB master. +# TEMPORARY PROVISIONAL PIN. The extended-type ports are clean clones +# that bind the per-type I/O: the tcbuffer / tnpoint MF-JSON support +# (MobilityDB PRs #1051 and #951), the tpose surface, and the renamed +# trgeometry_* C API (the MobilityDB API-uniformization PRs #1066 / +# #1067 / #1069 that promote trgeo_in / trgeo_from_mfjson / trgeo_out +# to exported trgeometry_in / trgeometry_from_mfjson / trgeometry_out) +# are not all on MobilityDB/MobilityDB master yet, so the pin points at +# the verified integration branch on the contributor fork that composes +# them on top of MobilityDB master: estebanzimanyi/MobilityDB +# meos-provisional-6type-base-session @ +# 3af4cb895c92446fa052c90a4ab22cf2c9e96c4a ("Rename the trgeometry +# distance and comparison functions for cross-type API uniformity"). +# This base exposes all six extended types' MEOS surface (tcbuffer and +# tnpoint MF-JSON, tpose, the renamed trgeometry_*, and the tpcpoint / +# tpcpatch base). This is provisional pending the #134 -> #145 +# MobilityDuck chain plus the MF-JSON and trgeometry-rename PRs merging +# into MobilityDB master. # -# Flip-to-merged-master recipe (apply once #1051 AND #951 are merged AND -# #145 has landed): set REPO back to MobilityDB/MobilityDB, set REF to -# the merged master commit that includes both MF-JSON changes, and -# recompute +# Flip-to-merged-master recipe (apply once the MF-JSON and the +# trgeometry-rename PRs are merged AND #145 has landed): set REPO back +# to MobilityDB/MobilityDB, set REF to the merged master commit that +# includes all of those changes, and recompute # curl -sL https://github.com/MobilityDB/MobilityDB/archive/.tar.gz | sha512sum # for SHA512. Then delete this comment block. The OPTIONS below (the #145 # CBUFFER/NPOINT/POSE/RGEO enablers) are unchanged by this pin and compose @@ -26,8 +27,8 @@ vcpkg_from_github( OUT_SOURCE_PATH SOURCE_PATH REPO estebanzimanyi/MobilityDB - REF 1a4f498b1689fdcbe77ae0bbaf8b53df1eb59554 - SHA512 da7c32c8f02b684dd8f68315ad1d128a59aa2027deae971b6fe94cba2de9303ca53b30676c98ba4e48a6721ab181007ce58ca4263ce2037554f171af6eca2f7a + REF 3af4cb895c92446fa052c90a4ab22cf2c9e96c4a + SHA512 381a7a50f587d66bb8b2dc19253cf78ca3b975ab23285a953b4367f118719a3b679a7a53a6f821ee1d1f5d2cb849d8aef822edfc69e8331ebfe5fb1685b6d6fc ) vcpkg_replace_string( From 2b74659c00d4b8597ca93c6c563f9cade6db1653 Mon Sep 17 00:00:00 2001 From: Esteban Zimanyi Date: Wed, 20 May 2026 11:55:01 +0200 Subject: [PATCH 2/3] fix: drop premature `using meosType = MeosType;` alias in tydef.hpp MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `meosType` (lower-case) is the **pre-consolidation** MEOS type name; `MeosType` (upper-case) is the **post-consolidation** target that the upstream rename sweep has not yet reached. The current vcpkg pin (`vcpkg_ports/meos/portfile.cmake` REF f11b7443ee98…) is still pre-consolidation: `meos/include/temporal/meos_catalog.h` line 121 declares the typedef as `} meosType;` and every MEOS API uses the lower-case spelling. MobilityDuck's source code consistently uses `meosType` to match — `grep -rn '\bMeosType\b' src/` finds the name only on the alias line and its comment, nowhere else. c8cad6d added `using meosType = MeosType;` as a forward-looking bridge for the eventual consolidation bump. That bridge points at `MeosType`, which the current pin does NOT yet expose, so it breaks every PR's Linux arm64 build with: /duckdb_build_dir/src/include/tydef.hpp:18:18: error: ‘MeosType’ does not name a type; did you mean ‘meosType’? The fix is to drop the premature alias and replace the misleading comment with one that documents the pre/post-consolidation distinction and the resume path for the next pin bump — at that point a reviewer can either restore the bridge (this time it'll be valid because `MeosType` will exist) or sweep the MobilityDuck source from `meosType` to `MeosType` in a single PR. Unblocks every in-flight PR's Linux arm64 build: #126, #130, #149, #158, #159, #160, plus the entire `feat/*_port_core` extended-type stack (#148/#150/#151/#153/#155/#156). --- src/include/tydef.hpp | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/src/include/tydef.hpp b/src/include/tydef.hpp index b7b2810..b9860ca 100644 --- a/src/include/tydef.hpp +++ b/src/include/tydef.hpp @@ -11,11 +11,27 @@ extern "C" { #include } -// Forward-compat alias for the meosType → MeosType rename (MobilityDB -// pr785-sync-script). Vcpkg's MEOS exposes `MeosType`; existing -// MobilityDuck code still uses `meosType`. This alias bridges the two -// without touching every reference site. -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 { From ede1610e91d1c8a8ca5ba4d69b50534e2ab36a06 Mon Sep 17 00:00:00 2001 From: Esteban Zimanyi Date: Sat, 16 May 2026 11:04:41 +0200 Subject: [PATCH 3/3] Stage icu for the macOS osx_arm64 test path too The stage_icu helper mapped only the Linux uname values, so on the macOS arm64 test runner uname -m returned "arm64" and the icu extension was copied to .duckdb/extensions/v1.4.4/arm64 instead of .../osx_arm64, where DuckDB's autoload looks. The hub fallback is not reliably resolvable on that runner, so the osx_arm64 Test step failed to load the extension. Map the OS and architecture to the DuckDB platform string (linux_amd64, linux_arm64, osx_amd64, osx_arm64) so the locally built icu is staged at the path autoload expects on every tested platform; the Linux mapping is unchanged. Co-Authored-By: Claude Opus 4.7 --- Makefile | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index bc17d05..a948549 100644 --- a/Makefile +++ b/Makefile @@ -11,9 +11,39 @@ include extension-ci-tools/makefiles/duckdb_extension.Makefile # 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: + $(call stage_icu,release) ./build/release/$(TEST_PATH) "$(PROJ_DIR)test/*" test_debug_internal: + $(call stage_icu,debug) ./build/debug/$(TEST_PATH) "$(PROJ_DIR)test/*" test_reldebug_internal: - ./build/reldebug/$(TEST_PATH) "$(PROJ_DIR)test/*" \ No newline at end of file + $(call stage_icu,reldebug) + ./build/reldebug/$(TEST_PATH) "$(PROJ_DIR)test/*"