From dc2c099d7bd0d3bc917929147190d1293cd32fc6 Mon Sep 17 00:00:00 2001 From: Esteban Zimanyi Date: Tue, 26 May 2026 01:27:13 +0200 Subject: [PATCH] Add the core tcbuffer type port Bring the temporal circular buffer (tcbuffer) type into MobilityDuck: construction, text/EWKT/WKB I/O, casts and accessors, gated by the CBUFFER config flag that mirrors the MEOS CBUFFER build module. Disabling CBUFFER drops the tcbuffer sources and its registration in LoadInternal. MFJSON input binds to MEOS tcbuffer_from_mfjson, which the pinned MEOS does not yet expose, so it is omitted here. (cherry picked from commit 61049d571a9ec240386e8a8d878544ccf91fc735) (cherry picked from commit 5c1257ab525b48fecc6d428d55fc0109a242faaf) --- CMakeLists.txt | 11 + src/geo/tcbuffer.cpp | 1434 ++++++++++++++++++++++++++++++++ src/geo/tcbuffer_in_out.cpp | 285 +++++++ src/include/geo/tcbuffer.hpp | 29 + src/mobilityduck_extension.cpp | 11 + test/sql/tcbuffer.test | 238 ++++++ 6 files changed, 2008 insertions(+) create mode 100644 src/geo/tcbuffer.cpp create mode 100644 src/geo/tcbuffer_in_out.cpp create mode 100644 src/include/geo/tcbuffer.hpp create mode 100644 test/sql/tcbuffer.test diff --git a/CMakeLists.txt b/CMakeLists.txt index 155fa1c6..c36dabd4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -97,6 +97,17 @@ set(EXTENSION_SOURCES src/index/rtree_optimize_scan.cpp ) +# Extended temporal type families, each gated by a config flag that mirrors the +# corresponding MEOS build module (vcpkg_ports/meos/portfile.cmake). Disabling a +# flag drops the family's sources and its registration (see LoadInternal). +option(CBUFFER "Build the tcbuffer temporal type" ON) +if(CBUFFER) + add_definitions(-DCBUFFER=1) + list(APPEND EXTENSION_SOURCES + src/geo/tcbuffer.cpp + src/geo/tcbuffer_in_out.cpp) +endif() + build_static_extension(${TARGET_NAME} ${EXTENSION_SOURCES}) build_loadable_extension(${TARGET_NAME} "" ${EXTENSION_SOURCES}) diff --git a/src/geo/tcbuffer.cpp b/src/geo/tcbuffer.cpp new file mode 100644 index 00000000..eafb2d20 --- /dev/null +++ b/src/geo/tcbuffer.cpp @@ -0,0 +1,1434 @@ +#include "geo/tcbuffer.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 +} + + +namespace duckdb { + +LogicalType TCBufferTypes::TCBUFFER() { + auto type = LogicalType(LogicalTypeId::BLOB); + type.SetAlias("TCBUFFER"); + return type; +} + +/* + * Constructors +*/ + +static void Tcbuffer_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(); + + Temporal *tinst = tcbuffer_in(input.c_str()); + if (!tinst) { + throw InvalidInputException("Invalid TCBUFFER 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 TCBUFFER 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; + }); + +} + +static void Tcbufferinst_constructor(DataChunk &args, ExpressionState &state, Vector &result) { + auto count = args.size(); + auto &value_vec = args.data[0]; + auto &t_vec = args.data[1]; + + BinaryExecutor::Execute( + value_vec, t_vec, result, count, + [&](string_t value_str, timestamp_tz_t t) -> string_t { + std::string value = value_str.GetString(); + + // The tcbuffer value type is a Cbuffer (point + radius) + // varlena, parsed from its canonical text form, e.g. + // 'Cbuffer(Point(1 1), 0.5)'. + Cbuffer *cb = cbuffer_in(value.c_str()); + + if (cb == NULL) { + throw InvalidInputException("Invalid circular buffer format: " + value); + } + + timestamp_tz_t meos_timestamp = DuckDBToMeosTimestamp(t); + // No tcbufferinst_make exists; the generic tinstant_make + // builds a T_TCBUFFER instant from the Cbuffer Datum. + TInstant *inst = tinstant_make(Datum(cb), T_TCBUFFER, + static_cast(meos_timestamp.value)); + + if (inst == NULL) { + free(cb); + 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(cb); + throw InvalidInputException("Failed to allocate memory to circular buffer 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(cb); + + return stored_data; + + }); + +} + + +static void Tcbuffer_sequence_from_tstzspan(DataChunk &args, ExpressionState &state, Vector &result) { + const char* default_interp = "step"; + auto count = args.size(); + auto arg_count = args.ColumnCount(); + + auto &input_geom_vec = args.data[0]; + auto &span_vec = args.data[1]; + + // Check if interpolation parameter is provided + Vector *interp_vec = nullptr; + if (arg_count > 2) { + interp_vec = &args.data[2]; + } + + BinaryExecutor::Execute( + input_geom_vec, span_vec, result, count, + [&](string_t input_geom_str, string_t span_str)-> string_t{ + std::string geom_value = input_geom_str.GetString(); + + Cbuffer *cb = cbuffer_in(geom_value.c_str()); + + if(cb == NULL){ + throw InvalidInputException("Invalid circular buffer format: "+ geom_value); + } + + std::string input = span_str.GetString(); + + Span *span_cmp = reinterpret_cast(const_cast(input.c_str())); + + // Use default interpolation or provided value + interpType interp = interptype_from_string(default_interp); + if (interp_vec) { + std::string interp_string = default_interp; + interp = interptype_from_string(interp_string.c_str()); + } + + TSequence *seq = tsequence_from_base_tstzspan(Datum(cb), T_TCBUFFER, span_cmp, interp); + + if (seq == NULL) { + free(cb); + throw InvalidInputException("Failed to create TSequence"); + } + + size_t seq_size = temporal_mem_size((Temporal*)seq); + + uint8_t *seq_buffer = (uint8_t *)malloc(seq_size); + if (!seq_buffer) { + free(seq); + free(cb); + throw InvalidInputException("Failed to allocate memory for sequence data"); + } + + memcpy(seq_buffer, seq, seq_size); + + string_t seq_string_t((char*) seq_buffer, seq_size); + string_t stored_data = StringVector::AddStringOrBlob(result, seq_string_t); + + free(seq_buffer); + free(seq); + free(cb); + + return stored_data; + + }); + +} + +TInstant **temparr_extract_cb(Vector &tcbuffer_arr_vec, list_entry_t list_entry, int *count) { + auto &child_vector = ListVector::GetEntry(tcbuffer_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; +} + +static void Tcbuffer_sequence_constructor(DataChunk &args, ExpressionState &state, Vector &result) { + // Default values + const char* default_interp = "step"; + bool default_lower_inc = true; + bool default_upper_inc = true; + + auto count = args.size(); + auto arg_count = args.ColumnCount(); + + + auto &tcbuffer_arr_vec = args.data[0]; + tcbuffer_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 tcbuffer_data = FlatVector::GetData(tcbuffer_arr_vec); + auto result_data = FlatVector::GetData(result); + + // Get validity masks + auto &tcbuffer_validity = FlatVector::Validity(tcbuffer_arr_vec); + auto &result_validity = FlatVector::Validity(result); + + for (idx_t i = 0; i < count; i++) { + if (!tcbuffer_validity.RowIsValid(i)) { + result_validity.SetInvalid(i); + continue; + } + + try { + list_entry_t list_entry = tcbuffer_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_cb(tcbuffer_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); + } + } + +} + + + + +/* + * Conversions +*/ + +static 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 TCBUFFER data: null pointer"); + } + + Span *timespan = temporal_to_tstzspan(temp); + + if (!timespan) { + throw InvalidInputException("Failed to extract timespan from TCBUFFER"); + } + + 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; + } + ); + +} + +/* + * Transformations +*/ + +static 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 TCBUFFER data: null pointer"); + } + + TInstant *inst = temporal_to_tinstant(temp); + if (!inst) { + throw InvalidInputException("Failed to convert TCBUFFER 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; + } + ); + +} + + +static 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 TCBUFFER 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; + }); + +} + + +static 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 TCBUFFER data: null pointer"); + } + + std::string tgeom2 = tgeom2_str_t.GetString(); + + Temporal *temp2 = reinterpret_cast(const_cast(tgeom2.c_str())); + if (!temp2) { + throw InvalidInputException("Invalid TCBUFFER data: null pointer"); + } + + Temporal *result_temp = temporal_merge(temp1, temp2); + if (!result_temp) { + throw InvalidInputException("Failed to merge temporal circular buffers"); + } + + // 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; + }); + +} + + +/* + * Accessor Functions +*/ + +static 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 TCBUFFER 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; + }); + +} + + + + +static 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 TCBUFFER 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; + }); + +} + +static 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 TCBUFFER data: null pointer"); + } + + size_t mem_size = temporal_mem_size(temp); + + + return static_cast(mem_size); + }); + +} + +// ---- Circular-buffer value accessors ---- +// The tcbuffer value type is a Cbuffer (point + radius), which is not +// a registered DuckDB type. getValue / startValue / endValue surface it +// in its canonical text form (`Cbuffer(Point(x y), r)`), mirroring the +// asText output. point(tcbuffer) and radius(tcbuffer) expose the two +// components per the manual model: point() returns the geometry, radius() +// returns the float. + +// Return a pointer to the Cbuffer value stored inline in the instant +// (tinstant_value_p, the inline pointer, as MEOS's own tinstant_to_string +// does). The pointer aliases the instant, so the caller must NOT free it. +inline const Cbuffer *cbuffer_from_instant_value(const TInstant *inst) { + return reinterpret_cast(tinstant_value_p(inst)); +} + +// The tcbuffer value accessors render the Cbuffer in its WKT form +// `Cbuffer(POINT(x y),r)`, matching MobilityDB's asText(getValue(cbuffer)). +// +// IMPORTANT — these helpers carry tcbuffer-specific names on purpose. Generic +// names (Tinstant_value / Temporal_start_value / Temporal_end_value) are also +// defined at namespace scope in tgeometry/tgeography/tgeogpoint .cpp with the +// same signature; that is an ODR violation, so the linker keeps a single +// (geometry) definition for all of them and a generically-named tcbuffer +// value accessor silently renders the cbuffer as a geometry ("Unknown +// geometry type: 0"). Keep these names unique. +static void Tcbuffer_get_value(DataChunk &args, ExpressionState &state, Vector &result) { + auto count = args.size(); + UnaryExecutor::Execute( + args.data[0], result, count, + [&](string_t input_str) -> string_t { + std::string input = input_str.GetString(); + Temporal *temp = reinterpret_cast(const_cast(input.c_str())); + const Cbuffer *cb = cbuffer_from_instant_value(temporal_start_inst(temp)); + char *str = cbuffer_as_text(cb, 15); + if (!str) + throw InvalidInputException("Failed to convert circular buffer value to text"); + string_t out = StringVector::AddString(result, std::string(str)); + free(str); + return out; + }); + +} + + + +static void Tcbuffer_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())); + + // Pointer to the first instant's inline Cbuffer (no copy). + const Cbuffer *cb = cbuffer_from_instant_value(temporal_start_inst(temp)); + + // cbuffer_as_text renders the WKT form `Cbuffer(POINT(x y),r)`, + // matching MobilityDB's asText(startValue(cbuffer)). + char *str = cbuffer_as_text(cb, 15); + if (!str) + throw InvalidInputException("Failed to convert circular buffer value to text"); + std::string output(str); + string_t stored_result = StringVector::AddString(result, output); + + free(str); + + return stored_result; + }); + +} + + +static void Tcbuffer_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())); + + // Pointer to the last instant's inline Cbuffer (no copy). + const Cbuffer *cb = cbuffer_from_instant_value(temporal_end_inst(temp)); + + // cbuffer_as_text renders the WKT form `Cbuffer(POINT(x y),r)`, + // matching MobilityDB's asText(endValue(cbuffer)). + char *str = cbuffer_as_text(cb, 15); + if (!str) + throw InvalidInputException("Failed to convert circular buffer value to text"); + std::string output(str); + string_t stored_result = StringVector::AddString(result, output); + + free(str); + + return stored_result; + }); + +} + +static void Tcbuffer_point(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())); + + // temporal_start_value returns a freshly allocated copy of + // the Cbuffer value (datum_copy), which the caller owns. + Datum start_datum = temporal_start_value(temp); + Cbuffer *cb = reinterpret_cast(start_datum); + + // cbuffer_point returns a freshly allocated GSERIALIZED copy. + GSERIALIZED *gs = cbuffer_point(cb); + if (!gs) { + free(cb); + throw InvalidInputException("Failed to extract point from circular buffer"); + } + + string_t geometry_blob = GSerializedToGeometry(gs, state, result); + string_t stored_result = StringVector::AddStringOrBlob(result, geometry_blob); + + free(gs); + free(cb); + + return stored_result; + }); + +} + +static void Tcbuffer_radius(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) -> double { + std::string input = input_str.GetString(); + + Temporal *temp = reinterpret_cast(const_cast(input.c_str())); + + // temporal_start_value returns a freshly allocated copy of + // the Cbuffer value (datum_copy), which the caller owns. + Datum start_datum = temporal_start_value(temp); + Cbuffer *cb = reinterpret_cast(start_datum); + + double r = cbuffer_radius(cb); + free(cb); + return r; + }); + +} + + +static 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; + }); + +} + +static 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; + }); + +} + +static 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; + }); + +} + +static 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; + }); + +} + + + + +static 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; + }); + +} + + +static 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 TCBUFFER data: insufficient size"); + } + + uint8_t *data_copy = (uint8_t*)malloc(data_size); + if (!data_copy) { + throw InvalidInputException("Failed to allocate memory for TCBUFFER deserialization"); + } + memcpy(data_copy, data, data_size); + + TInstant *temp = reinterpret_cast(data_copy); + + if (!temp) { + free(data_copy); + throw InvalidInputException("Invalid TCBUFFER 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; + } + ); + +} + + +void TCBufferTypes::RegisterScalarFunctions(ExtensionLoader &loader) { + + auto tcbuffer_function = ScalarFunction( + "TCBUFFER", + {LogicalType::VARCHAR}, + TCBufferTypes::TCBUFFER(), + Tcbuffer_constructor + ); + duckdb::RegisterSerializedScalarFunction(loader, tcbuffer_function); + + auto tcbuffer_from_timestamp_function = ScalarFunction( + "TCBUFFER", + {LogicalType::VARCHAR, LogicalType::TIMESTAMP_TZ}, + TCBufferTypes::TCBUFFER(), + Tcbufferinst_constructor); + duckdb::RegisterSerializedScalarFunction(loader, tcbuffer_from_timestamp_function); + + auto tcbuffer_from_tstzspan_function = ScalarFunction( + "TCBUFFER", + {LogicalType::VARCHAR, SpanTypes::TSTZSPAN(), LogicalType::VARCHAR}, + TCBufferTypes::TCBUFFER(), + Tcbuffer_sequence_from_tstzspan + ); + duckdb::RegisterSerializedScalarFunction(loader, tcbuffer_from_tstzspan_function); + + auto tcbuffer_from_tstzspan_default = ScalarFunction( + "TCBUFFER", + {LogicalType::VARCHAR, SpanTypes::TSTZSPAN()}, + TCBufferTypes::TCBUFFER(), + Tcbuffer_sequence_from_tstzspan + ); + duckdb::RegisterSerializedScalarFunction(loader, tcbuffer_from_tstzspan_default); + + auto tcbufferseqarr_1param= ScalarFunction( + "tcbufferSeq", + {LogicalType::LIST(TCBufferTypes::TCBUFFER())}, + TCBufferTypes::TCBUFFER(), + Tcbuffer_sequence_constructor + ); + duckdb::RegisterSerializedScalarFunction(loader, tcbufferseqarr_1param); + + auto tcbufferseqarr_2params = ScalarFunction( + "tcbufferSeq", + {LogicalType::LIST(TCBufferTypes::TCBUFFER()), LogicalType::VARCHAR}, + TCBufferTypes::TCBUFFER(), + Tcbuffer_sequence_constructor + ); + duckdb::RegisterSerializedScalarFunction(loader, tcbufferseqarr_2params); + + auto tcbufferseqarr_3params = ScalarFunction( + "tcbufferSeq", + {LogicalType::LIST(TCBufferTypes::TCBUFFER()), LogicalType::VARCHAR, LogicalType::BOOLEAN}, + TCBufferTypes::TCBUFFER(), + Tcbuffer_sequence_constructor + ); + duckdb::RegisterSerializedScalarFunction(loader, tcbufferseqarr_3params); + + auto tcbufferseqarr_4params = ScalarFunction( + "tcbufferSeq", + {LogicalType::LIST(TCBufferTypes::TCBUFFER()), LogicalType::VARCHAR, LogicalType::BOOLEAN, LogicalType::BOOLEAN}, + TCBufferTypes::TCBUFFER(), + Tcbuffer_sequence_constructor + ); + duckdb::RegisterSerializedScalarFunction(loader, tcbufferseqarr_4params); + + auto tcbuffer_to_timespan_function = ScalarFunction( + "timeSpan", + {TCBufferTypes::TCBUFFER()}, + SpanTypes::TSTZSPAN(), + Temporal_to_tstzspan); + duckdb::RegisterSerializedScalarFunction(loader, tcbuffer_to_timespan_function); + + auto tcbuffer_to_tinstant_function = ScalarFunction( + "tcbufferInst", + {TCBufferTypes::TCBUFFER()}, + TCBufferTypes::TCBUFFER(), + Temporal_to_tinstant); + duckdb::RegisterSerializedScalarFunction(loader, tcbuffer_to_tinstant_function); + + + auto setInterp_function = ScalarFunction( + "setInterp", + {TCBufferTypes::TCBUFFER(), LogicalType::VARCHAR}, + TCBufferTypes::TCBUFFER(), + Temporal_set_interp + ); + duckdb::RegisterSerializedScalarFunction(loader, setInterp_function); + + + auto merge_function = ScalarFunction( + "merge", + {TCBufferTypes::TCBUFFER(), TCBufferTypes::TCBUFFER()}, + TCBufferTypes::TCBUFFER(), + Temporal_merge + ); + duckdb::RegisterSerializedScalarFunction(loader, merge_function); + + auto tempSubtype_function = ScalarFunction( + "tempSubtype", + {TCBufferTypes::TCBUFFER()}, + LogicalType::VARCHAR, + Temporal_subtype + ); + duckdb::RegisterSerializedScalarFunction(loader, tempSubtype_function); + + auto interp_function = ScalarFunction( + "interp", + {TCBufferTypes::TCBUFFER()}, + LogicalType::VARCHAR, + Temporal_interp + ); + duckdb::RegisterSerializedScalarFunction(loader, interp_function); + + auto memSize_function = ScalarFunction( + "memSize", + {TCBufferTypes::TCBUFFER()}, + LogicalType::INTEGER, + Temporal_mem_size + ); + duckdb::RegisterSerializedScalarFunction(loader, memSize_function); + + auto getValue_function = ScalarFunction( + "getValue", + {TCBufferTypes::TCBUFFER()}, + LogicalType::VARCHAR, + Tcbuffer_get_value + ); + duckdb::RegisterSerializedScalarFunction(loader, getValue_function); + + + auto tcbuffer_start_value_function = ScalarFunction( + "startValue", + {TCBufferTypes::TCBUFFER()}, + LogicalType::VARCHAR, + Tcbuffer_start_value + ); + duckdb::RegisterSerializedScalarFunction(loader, tcbuffer_start_value_function); + + auto tcbuffer_end_value_function = ScalarFunction( + "endValue", + {TCBufferTypes::TCBUFFER()}, + LogicalType::VARCHAR, + Tcbuffer_end_value + ); + duckdb::RegisterSerializedScalarFunction(loader, tcbuffer_end_value_function); + + auto tcbuffer_point_function = ScalarFunction( + "point", + {TCBufferTypes::TCBUFFER()}, + GeoTypes::GEOMETRY(), + Tcbuffer_point + ); + duckdb::RegisterSerializedScalarFunction(loader, tcbuffer_point_function); + + auto tcbuffer_radius_function = ScalarFunction( + "radius", + {TCBufferTypes::TCBUFFER()}, + LogicalType::DOUBLE, + Tcbuffer_radius + ); + duckdb::RegisterSerializedScalarFunction(loader, tcbuffer_radius_function); + + auto startInstant_function = ScalarFunction( + "startInstant", + {TCBufferTypes::TCBUFFER()}, + TCBufferTypes::TCBUFFER(), + Temporal_start_instant + ); + duckdb::RegisterSerializedScalarFunction(loader, startInstant_function); + + auto endInstant_function = ScalarFunction( + "endInstant", + {TCBufferTypes::TCBUFFER()}, + TCBufferTypes::TCBUFFER(), + Temporal_end_instant + ); + duckdb::RegisterSerializedScalarFunction(loader, endInstant_function); + + auto instantN_function = ScalarFunction( + "instantN", + {TCBufferTypes::TCBUFFER(), LogicalType::INTEGER}, + TCBufferTypes::TCBUFFER(), + Temporal_instant_n + ); + duckdb::RegisterSerializedScalarFunction(loader, instantN_function); + + + auto tcbuffer_gettimestamptz_function = ScalarFunction( + "getTimestamp", + {TCBufferTypes::TCBUFFER()}, + LogicalType::TIMESTAMP_TZ, + Tinstant_timestamptz); + duckdb::RegisterSerializedScalarFunction(loader, tcbuffer_gettimestamptz_function); + + + // =================================================================== + // Foundational tcbuffer 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 = TCBufferTypes::TCBUFFER(); + const LogicalType TSTZ = LogicalType::TIMESTAMP_TZ; + const LogicalType IVAL = LogicalType::INTERVAL; + + // ---- Accessors ---- + // NOTE: valueAtTimestamp is registered with a unary executor and ignores + // its timestamp argument (pre-existing); it needs a real + // value-at-timestamp implementation. Kept as-is here to preserve behavior. + loader.RegisterFunction(ScalarFunction( + "valueAtTimestamp", {TGEOM, TSTZ}, LogicalType::VARCHAR, + Tcbuffer_get_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 TCBufferTypes::RegisterTypes(ExtensionLoader &loader) { + loader.RegisterType( "TCBUFFER", TCBufferTypes::TCBUFFER()); +} + + +} diff --git a/src/geo/tcbuffer_in_out.cpp b/src/geo/tcbuffer_in_out.cpp new file mode 100644 index 00000000..2af9502b --- /dev/null +++ b/src/geo/tcbuffer_in_out.cpp @@ -0,0 +1,285 @@ +#include "geo/tcbuffer.hpp" +#include "temporal/temporal_blob.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 +} + +namespace duckdb { + +static 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 TCBUFFER data: insufficient size"); + } + + uint8_t *data_copy = (uint8_t*)malloc(data_size); + if (!data_copy) { + throw InvalidInputException("Failed to allocate memory for TCBUFFER deserialization"); + } + memcpy(data_copy, data, data_size); + + Temporal *temp = reinterpret_cast(data_copy); + + if (!temp) { + free(data_copy); + throw InvalidInputException("Invalid TCBUFFER data: null pointer"); + } + + // maxdecimaldigits = 15, matching MobilityDB's asText(tcbuffer) + // default; 0 truncated the cbuffer radius to an integer (0.5 -> 0). + char *str = tspatial_as_text(temp, 15); + + if (!str) { + free(data_copy); + throw InvalidInputException("Failed to convert TCBUFFER to text"); + } + + std::string result_str(str); + string_t stored_result = StringVector::AddString(result, result_str); + + free(str); + free(data_copy); + + return stored_result; + } + ); + +} + +static 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 TCBUFFER data: insufficient size"); + } + + uint8_t *data_copy = (uint8_t*)malloc(data_size); + if (!data_copy) { + throw InvalidInputException("Failed to allocate memory for TCBUFFER deserialization"); + } + memcpy(data_copy, data, data_size); + + Temporal *temp = reinterpret_cast(data_copy); + + if (!temp) { + free(data_copy); + throw InvalidInputException("Invalid TCBUFFER data: null pointer"); + } + + // maxdecimaldigits = 15 (see Tspatial_as_text): 0 truncated the + // cbuffer radius to an integer. + char *ewkt = tspatial_as_ewkt(temp, 15); + + if (!ewkt) { + free(data_copy); + throw InvalidInputException("Failed to convert TCBUFFER to EWKT"); + } + + std::string result_str(ewkt); + string_t stored_result = StringVector::AddString(result, result_str); + + + free(ewkt); + free(data_copy); + + return stored_result; + } + ); + +} + + +bool TcbufferFunctions::StringToTcbuffer(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 = tcbuffer_in(input_str.c_str()); + if (!temp) { + throw InvalidInputException("Invalid TCBUFFER 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 TCBUFFER 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; + }); + + return true; +} + +bool TcbufferFunctions::TcbufferToString(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 TCBUFFER data: insufficient size"); + } + + uint8_t *data_copy = (uint8_t*)malloc(data_size); + if (!data_copy) { + throw InvalidInputException("Failed to allocate memory for TCBUFFER deserialization"); + } + memcpy(data_copy, data, data_size); + + Temporal *temp = reinterpret_cast(data_copy); + if (!temp) { + free(data_copy); + throw InvalidInputException("Invalid TCBUFFER data: null pointer"); + } + + char *str = temporal_out(temp, 15); + if (!str) { + free(data_copy); + throw InvalidInputException("Failed to convert TCBUFFER to string"); + } + + std::string output(str); + string_t stored_result = StringVector::AddString(result, output); + + free(str); + free(data_copy); + + return stored_result; + }); + + return true; +} + +// ---- Spatial-temporal parsers (Binary / HexWKB / MFJSON / Text) ---- +// Used to register the `tcbufferFrom*` overloads. +// `temporal_from_wkb` and `temporal_from_hexwkb` are subtype-agnostic; +// `tcbuffer_from_mfjson` and `tcbuffer_in` are per-type. The result is +// stored as a raw blob, the same format every other temporal type uses. + + +static 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 TemporalToBlob(result, t); + }); +} + +static 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 TemporalToBlob(result, t); + }); +} + +template +static void TspatialFromStringExec(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 = FN(s.c_str()); + if (!t) throw InvalidInputException("from*: invalid input"); + return TemporalToBlob(result, t); + }); +} + +void TCBufferTypes::RegisterScalarInOutFunctions(ExtensionLoader &loader){ + auto TcbufferAsText = ScalarFunction( + "asText", + {TCBufferTypes::TCBUFFER()}, + LogicalType::VARCHAR, + Tspatial_as_text + ); + duckdb::RegisterSerializedScalarFunction(loader, TcbufferAsText); + + auto TcbufferAsEWKT = ScalarFunction( + "asEWKT", + {TCBufferTypes::TCBUFFER()}, + LogicalType::VARCHAR, + Tspatial_as_ewkt + ); + duckdb::RegisterSerializedScalarFunction(loader, TcbufferAsEWKT); + + // ---- tcbufferFromBinary / FromEWKB (auto-detects format) ---- + const auto B = LogicalType::BLOB; + const auto V = LogicalType::VARCHAR; + const auto T = TCBufferTypes::TCBUFFER(); + duckdb::RegisterSerializedScalarFunction(loader, + ScalarFunction("tcbufferFromBinary", {B}, T, TspatialFromWkbExec)); + duckdb::RegisterSerializedScalarFunction(loader, + ScalarFunction("tcbufferFromEWKB", {B}, T, TspatialFromWkbExec)); + duckdb::RegisterSerializedScalarFunction(loader, + ScalarFunction("tcbufferFromHexWKB", {V}, T, TspatialFromHexWkbExec)); + duckdb::RegisterSerializedScalarFunction(loader, + ScalarFunction("tcbufferFromHexEWKB", {V}, T, TspatialFromHexWkbExec)); + duckdb::RegisterSerializedScalarFunction(loader, + ScalarFunction("tcbufferFromMFJSON", {V}, T, + TspatialFromStringExec<&tcbuffer_from_mfjson>)); + duckdb::RegisterSerializedScalarFunction(loader, + ScalarFunction("tcbufferFromText", {V}, T, + TspatialFromStringExec<&tcbuffer_in>)); + duckdb::RegisterSerializedScalarFunction(loader, + ScalarFunction("tcbufferFromEWKT", {V}, T, + TspatialFromStringExec<&tcbuffer_in>)); +} + + +void TCBufferTypes::RegisterCastFunctions(ExtensionLoader &loader) { + RegisterMeosCastFunction(loader, LogicalType::VARCHAR, TCBufferTypes::TCBUFFER(), TcbufferFunctions::StringToTcbuffer); + RegisterMeosCastFunction(loader, TCBufferTypes::TCBUFFER(), LogicalType::VARCHAR, TcbufferFunctions::TcbufferToString); +} + +} diff --git a/src/include/geo/tcbuffer.hpp b/src/include/geo/tcbuffer.hpp new file mode 100644 index 00000000..0222bafc --- /dev/null +++ b/src/include/geo/tcbuffer.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 TCBufferTypes { + static LogicalType TCBUFFER(); + static LogicalType GEOMETRY(); + static void RegisterTypes(ExtensionLoader &loader); + static void RegisterScalarFunctions(ExtensionLoader &loader); + static void RegisterCastFunctions(ExtensionLoader &loader); + static void RegisterScalarInOutFunctions(ExtensionLoader &loader); +}; + +struct TcbufferFunctions { + static bool StringToTcbuffer(Vector &source, Vector &result, idx_t count, CastParameters ¶meters); + static bool TcbufferToString(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 c48c1569..b557dc8b 100644 --- a/src/mobilityduck_extension.cpp +++ b/src/mobilityduck_extension.cpp @@ -11,6 +11,9 @@ #include "geo/tgeompoint.hpp" #include "geo/tgeogpoint.hpp" #include "duckdb.hpp" +#if CBUFFER +#include "geo/tcbuffer.hpp" +#endif #include "geo/tgeometry.hpp" #include "geo/tgeometry_ops.hpp" #include "geo/tgeography.hpp" @@ -332,6 +335,14 @@ static void LoadInternal(ExtensionLoader &loader) { TGeogpointType::RegisterScalarInOutFunctions(loader); TGeogpointOps::RegisterScalarFunctions(loader); + // Extended temporal type tcbuffer (requires the MEOS CBUFFER module). +#if CBUFFER + TCBufferTypes::RegisterTypes(loader); + TCBufferTypes::RegisterCastFunctions(loader); + TCBufferTypes::RegisterScalarFunctions(loader); + TCBufferTypes::RegisterScalarInOutFunctions(loader); +#endif + SetTypes::RegisterTypes(loader); SetTypes::RegisterCastFunctions(loader); SetTypes::RegisterScalarFunctions(loader); diff --git a/test/sql/tcbuffer.test b/test/sql/tcbuffer.test new file mode 100644 index 00000000..15e1c576 --- /dev/null +++ b/test/sql/tcbuffer.test @@ -0,0 +1,238 @@ +# name: test/sql/tcbuffer.test +# description: Core tcbuffer type port — construction, text/EWKT/MFJSON I/O +# and basic accessors. +# group: [sql] + +require mobilityduck + +# Test tcbuffer constructor with parentheses +query I +SELECT asText(tcbuffer('Cbuffer(Point(1 1), 0.5)@2000-01-01')); +---- +Cbuffer(POINT(1 1),0.5)@2000-01-01 00:00:00+01 + +# Test tcbuffer constructor without parentheses +query I +SELECT asText(tcbuffer 'Cbuffer(Point(1 1), 0.5)@2000-01-01'); +---- +Cbuffer(POINT(1 1),0.5)@2000-01-01 00:00:00+01 + +# Test asText with continuous sequence +query I +SELECT asText(tcbuffer '[Cbuffer(Point(1 1), 0.2)@2000-01-01, Cbuffer(Point(2 2), 0.4)@2000-01-02, Cbuffer(Point(3 3), 0.5)@2000-01-03]'); +---- +[Cbuffer(POINT(1 1),0.2)@2000-01-01 00:00:00+01, Cbuffer(POINT(2 2),0.4)@2000-01-02 00:00:00+01, Cbuffer(POINT(3 3),0.5)@2000-01-03 00:00:00+01] + +# Test asText with discrete sequence +query I +SELECT asText(tcbuffer '{Cbuffer(Point(1 1), 0.3)@2000-01-01, Cbuffer(Point(2 2), 0.5)@2000-01-02}'); +---- +{Cbuffer(POINT(1 1),0.3)@2000-01-01 00:00:00+01, Cbuffer(POINT(2 2),0.5)@2000-01-02 00:00:00+01} + +# Test asEWKT +query I +SELECT asEWKT(tcbuffer 'Cbuffer(Point(1 1), 0.5)@2000-01-01'); +---- +Cbuffer(POINT(1 1),0.5)@2000-01-01 00:00:00+01 + +# Test value-and-timestamp constructor round-trips +query I +SELECT tcbuffer('Cbuffer(Point(1 1), 0.5)', timestamptz '2012-01-01 08:00:00') + = tcbuffer 'Cbuffer(Point(1 1), 0.5)@2012-01-01 08:00:00'; +---- +true + +# Test value-and-tstzspan constructor produces a 2-instant sequence +query I +SELECT numInstants(tcbuffer('Cbuffer(Point(1 1), 0.5)', tstzspan '[2001-01-01, 2001-01-02]')); +---- +2 + +# Test value-and-tstzspan constructor with step interpolation +query I +SELECT interp(tcbuffer('Cbuffer(Point(1 1), 0.5)', tstzspan '[2001-01-01, 2001-01-02]', 'step')); +---- +Step + +# Test tcbufferSeq with step interpolation and bounds. +# Step-interpolated sequences carry the "Interp=Step;" prefix in their text +# output (MobilityDB 152_tcbuffer reference); tcbuffer's default interp is +# also step (next case), so the default form gets the same prefix. +query I +SELECT asText(tcbufferSeq(ARRAY[tcbuffer 'Cbuffer(Point(1 1), 0.2)@2001-01-01', 'Cbuffer(Point(2 2), 0.4)@2001-01-02'], 'step', 'true','true')); +---- +Interp=Step;[Cbuffer(POINT(1 1),0.2)@2001-01-01 00:00:00+01, Cbuffer(POINT(2 2),0.4)@2001-01-02 00:00:00+01] + +# Test tcbufferSeq with default parameters (default interpolation is step) +query I +SELECT asText(tcbufferSeq(ARRAY[tcbuffer 'Cbuffer(Point(1 1), 0.2)@2001-01-01', 'Cbuffer(Point(2 2), 0.4)@2001-01-02'])); +---- +Interp=Step;[Cbuffer(POINT(1 1),0.2)@2001-01-01 00:00:00+01, Cbuffer(POINT(2 2),0.4)@2001-01-02 00:00:00+01] + +# Test tcbufferSeq with discrete interpolation +query I +SELECT asText(tcbufferSeq(ARRAY[tcbuffer 'Cbuffer(Point(1 1), 0.2)@2001-01-01', 'Cbuffer(Point(2 2), 0.4)@2001-01-02'], 'discrete')); +---- +{Cbuffer(POINT(1 1),0.2)@2001-01-01 00:00:00+01, Cbuffer(POINT(2 2),0.4)@2001-01-02 00:00:00+01} + +# Test MFJSON input via the FromMFJSON constructor reconstructs the value +# (the #1051 MFJSON schema: typestring MovingCircularBuffer, value object +# {"point":[x,y],"radius":r}). +query I +SELECT tcbufferFromMFJSON('{"type":"MovingCircularBuffer","values":[{"point":[1,1],"radius":0.5}],"datetimes":["2000-01-01T00:00:00+01"],"interpolation":"None"}') + = tcbuffer 'Cbuffer(Point(1 1), 0.5)@2000-01-01'; +---- +true + +# Test MFJSON input round-trip for a continuous sequence +query I +SELECT tcbufferFromMFJSON('{"type":"MovingCircularBuffer","values":[{"point":[1,1],"radius":0.2},{"point":[2,2],"radius":0.4}],"datetimes":["2000-01-01T00:00:00+01","2000-01-02T00:00:00+01"],"lower_inc":true,"upper_inc":true,"interpolation":"Linear"}') + = tcbuffer '[Cbuffer(Point(1 1), 0.2)@2000-01-01, Cbuffer(Point(2 2), 0.4)@2000-01-02]'; +---- +true + +# Test tcbufferFromText constructor +query I +SELECT tcbufferFromText('Cbuffer(Point(1 1), 0.5)@2000-01-01') + = tcbuffer 'Cbuffer(Point(1 1), 0.5)@2000-01-01'; +---- +true + +# Test tcbufferFromEWKT constructor +query I +SELECT tcbufferFromEWKT('Cbuffer(Point(1 1), 0.5)@2000-01-01') + = tcbuffer 'Cbuffer(Point(1 1), 0.5)@2000-01-01'; +---- +true + +# Test timeSpan function +query I +SELECT timeSpan(tcbuffer 'Cbuffer(Point(1 1), 0.5)@2023-01-01 10:00:00+00'); +---- +[2023-01-01 11:00:00+01, 2023-01-01 11:00:00+01] + +# Test tcbufferInst function +query I +SELECT asText(tcbufferInst(tcbuffer 'Cbuffer(Point(1 1), 0.5)@2023-01-01 10:00:00+00')); +---- +Cbuffer(POINT(1 1),0.5)@2023-01-01 11:00:00+01 + +# Test setInterp with discrete interpolation +query I +SELECT asEWKT(setInterp(tcbuffer 'Cbuffer(Point(1 1), 0.5)@2000-01-01', 'discrete')); +---- +{Cbuffer(POINT(1 1),0.5)@2000-01-01 00:00:00+01} + +# Test setInterp with step interpolation +query I +SELECT asEWKT(setInterp(tcbuffer 'Cbuffer(Point(1 1), 0.5)@2000-01-01', 'step')); +---- +Interp=Step;[Cbuffer(POINT(1 1),0.5)@2000-01-01 00:00:00+01] + +# Test merge function +query I +SELECT asText(merge(tcbuffer 'Cbuffer(Point(1 1), 0.5)@2000-01-01', tcbuffer 'Cbuffer(Point(1 1), 0.5)@2000-01-02')); +---- +{Cbuffer(POINT(1 1),0.5)@2000-01-01 00:00:00+01, Cbuffer(POINT(1 1),0.5)@2000-01-02 00:00:00+01} + +# Test tempSubtype with instant +query I +SELECT tempSubtype(tcbuffer 'Cbuffer(Point(1 1), 0.5)@2000-01-01'); +---- +Instant + +# Test tempSubtype with discrete sequence +query I +SELECT tempSubtype(tcbuffer '{Cbuffer(Point(1 1), 0.3)@2000-01-01, Cbuffer(Point(2 2), 0.5)@2000-01-02}'); +---- +Sequence + +# Test tempSubtype with continuous sequence +query I +SELECT tempSubtype(tcbuffer '[Cbuffer(Point(1 1), 0.2)@2000-01-01, Cbuffer(Point(2 2), 0.4)@2000-01-02]'); +---- +Sequence + +# Test tempSubtype with sequence set +query I +SELECT tempSubtype(tcbuffer '{[Cbuffer(Point(1 1), 0.2)@2000-01-01, Cbuffer(Point(2 2), 0.4)@2000-01-02],[Cbuffer(Point(3 3), 0.6)@2000-01-03, Cbuffer(Point(3 3), 0.6)@2000-01-04]}'); +---- +SequenceSet + +# Test memSize is positive +query I +SELECT memSize(tcbuffer 'Cbuffer(Point(1 1), 0.5)@2000-01-01') > 0; +---- +true + +# Test interp accessor +query I +SELECT interp(tcbuffer '[Cbuffer(Point(1 1), 0.2)@2000-01-01, Cbuffer(Point(2 2), 0.4)@2000-01-02]'); +---- +Linear + +# Test getValue function returns the cbuffer value text +query I +SELECT getValue(tcbuffer 'Cbuffer(Point(1 1), 0.5)@2000-01-01'); +---- +Cbuffer(POINT(1 1),0.5) + +# Test startValue function +query I +SELECT startValue(tcbuffer '[Cbuffer(Point(1 1), 0.2)@2000-01-01, Cbuffer(Point(2 2), 0.4)@2000-01-02, Cbuffer(Point(3 3), 0.5)@2000-01-03]'); +---- +Cbuffer(POINT(1 1),0.2) + +# Test endValue function +query I +SELECT endValue(tcbuffer '[Cbuffer(Point(1 1), 0.2)@2000-01-01, Cbuffer(Point(2 2), 0.4)@2000-01-02, Cbuffer(Point(3 3), 0.5)@2000-01-03]'); +---- +Cbuffer(POINT(3 3),0.5) + +# Test radius accessor +query I +SELECT radius(tcbuffer 'Cbuffer(Point(1 1), 0.5)@2000-01-01'); +---- +0.5 + +# Test point accessor is the buffer centre geometry +query I +SELECT ST_AsText(point(tcbuffer 'Cbuffer(Point(3 4), 0.5)@2000-01-01')); +---- +POINT (3 4) + +# Test startInstant +query I +SELECT asText(startInstant(tcbuffer '[Cbuffer(Point(1 1), 0.2)@2000-01-01, Cbuffer(Point(2 2), 0.4)@2000-01-02]')); +---- +Cbuffer(POINT(1 1),0.2)@2000-01-01 00:00:00+01 + +# Test endInstant +query I +SELECT asText(endInstant(tcbuffer '[Cbuffer(Point(1 1), 0.2)@2000-01-01, Cbuffer(Point(2 2), 0.4)@2000-01-02]')); +---- +Cbuffer(POINT(2 2),0.4)@2000-01-02 00:00:00+01 + +# Test instantN +query I +SELECT asText(instantN(tcbuffer '[Cbuffer(Point(1 1), 0.2)@2000-01-01, Cbuffer(Point(2 2), 0.4)@2000-01-02]', 1)); +---- +Cbuffer(POINT(1 1),0.2)@2000-01-01 00:00:00+01 + +# Test getTimestamp function +query I +SELECT getTimestamp(tcbuffer 'Cbuffer(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(tcbuffer '[Cbuffer(Point(1 1), 0.2)@2000-01-01, Cbuffer(Point(2 2), 0.4)@2000-01-02, Cbuffer(Point(3 3), 0.5)@2000-01-03]'); +---- +3 + +# Test duration generic accessor +query I +SELECT duration(tcbuffer '[Cbuffer(Point(1 1), 0.2)@2000-01-01, Cbuffer(Point(2 2), 0.4)@2000-01-03]'); +---- +2 days