diff --git a/CMakeLists.txt b/CMakeLists.txt index 95928ed3..7b5b3313 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -89,6 +89,8 @@ set(EXTENSION_SOURCES src/geo/tgeography_ops.cpp src/pointcloud/tpcpoint.cpp src/pointcloud/tpcpoint_in_out.cpp + src/pointcloud/tpcpatch.cpp + src/pointcloud/tpcpatch_in_out.cpp src/geo/tgeogpoint.cpp src/geo/tgeogpoint_in_out.cpp src/geo/tgeogpoint_ops.cpp diff --git a/src/include/pointcloud/tpcpatch.hpp b/src/include/pointcloud/tpcpatch.hpp new file mode 100644 index 00000000..0f14a0ff --- /dev/null +++ b/src/include/pointcloud/tpcpatch.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 TPcpatchTypes { + static LogicalType TPCPATCH(); + static LogicalType GEOMETRY(); + static void RegisterTypes(ExtensionLoader &loader); + static void RegisterScalarFunctions(ExtensionLoader &loader); + static void RegisterCastFunctions(ExtensionLoader &loader); + static void RegisterScalarInOutFunctions(ExtensionLoader &loader); +}; + +struct TpcpatchFunctions { + static bool StringToTpcpatch(Vector &source, Vector &result, idx_t count, CastParameters ¶meters); + static bool TpcpatchToString(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 232e2676..3306baf0 100644 --- a/src/mobilityduck_extension.cpp +++ b/src/mobilityduck_extension.cpp @@ -31,6 +31,7 @@ #include "geo/tgeography.hpp" #include "geo/tgeography_ops.hpp" #include "pointcloud/tpcpoint.hpp" +#include "pointcloud/tpcpatch.hpp" #include "geo/tgeogpoint.hpp" #include "geo/tgeogpoint_ops.hpp" #include "temporal/span.hpp" @@ -413,6 +414,12 @@ static void LoadInternal(ExtensionLoader &loader) { TPcpointTypes::RegisterCastFunctions(loader); TPcpointTypes::RegisterScalarInOutFunctions(loader); + // Extended temporal type tpcpatch (requires the MEOS POINTCLOUD module). + TPcpatchTypes::RegisterScalarFunctions(loader); + TPcpatchTypes::RegisterTypes(loader); + TPcpatchTypes::RegisterCastFunctions(loader); + TPcpatchTypes::RegisterScalarInOutFunctions(loader); + SetTypes::RegisterTypes(loader); SetTypes::RegisterCastFunctions(loader); SetTypes::RegisterScalarFunctions(loader); diff --git a/src/pointcloud/tpcpatch.cpp b/src/pointcloud/tpcpatch.cpp new file mode 100644 index 00000000..0018f7ef --- /dev/null +++ b/src/pointcloud/tpcpatch.cpp @@ -0,0 +1,1531 @@ +#include "pointcloud/tpcpatch.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 +} + +// The pgpointcloud module header meos_pointcloud.h is intentionally not +// included here. MEOS exposes NO type-specific tpcpatch_* temporal +// entry points (no tpcpatch_in / tpcpatch_out / tpcpatch_from_mfjson); +// the canonical MobilityDB SQL binds tpcpatch_in / tpcpatch_out to the +// subtype-agnostic generic Temporal_* dispatch (temporal_in(str, +// T_TPCPATCH) / temporal_out). Only the base pgpointcloud value type +// pcpatch has type-specific symbols, and just the schema-free ones are +// declared locally below (the same local-extern technique the sibling +// ports use). Pcpatch is an opaque varlena (pgpointcloud +// SERIALIZED_PATCH); the schema-aware coordinate accessors +// (atGeometry, eIntersects, the per-dimension getters) require a +// registered PCSCHEMA from the pgpointcloud catalog, which is not +// available in a standalone DuckDB context, so they are deliberately +// not bound. pcpatch_npoints / pcpatch_get_pcid read the serialized +// header fields and are schema-free. +extern "C" { + typedef struct Pcpatch Pcpatch; + extern Pcpatch *pcpatch_hex_in(const char *str); + extern char *pcpatch_hex_out(const Pcpatch *pt, int maxdd); + extern uint32_t pcpatch_get_pcid(const Pcpatch *pt); + extern uint32_t pcpatch_npoints(const Pcpatch *pt); +} + + +namespace duckdb { + +LogicalType TPcpatchTypes::TPCPATCH() { + auto type = LogicalType(LogicalTypeId::BLOB); + type.SetAlias("TPCPATCH"); + return type; +} + +/* + * Constructors +*/ + +static void Tpcpatch_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(); + + // No tpcpatch_in exists; the canonical SQL binds tpcpatch_in + // to the generic Temporal_in, i.e. temporal_in(str, + // T_TPCPATCH). + Temporal *tinst = temporal_in(input.c_str(), T_TPCPATCH); + if (!tinst) { + throw InvalidInputException("Invalid TPCPATCH 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 TPCPATCH 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); + } +} + +static void Tpcpatchinst_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 tpcpatch value type is a pgpointcloud pcpatch, an + // opaque varlena parsed from its canonical hex-WKB text + // form via the schema-free pcpatch_hex_in. + Pcpatch *pt = pcpatch_hex_in(value.c_str()); + + if (pt == NULL) { + throw InvalidInputException("Invalid pcpatch format: " + value); + } + + timestamp_tz_t meos_timestamp = DuckDBToMeosTimestamp(t); + // No tpcpatchinst_make exists; the generic tinstant_make + // builds a T_TPCPATCH instant from the pcpatch Datum. + TInstant *inst = tinstant_make(Datum(pt), T_TPCPATCH, + static_cast(meos_timestamp.value)); + + if (inst == NULL) { + free(pt); + 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(pt); + throw InvalidInputException("Failed to allocate memory to pcpatch 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(pt); + + return stored_data; + + }); + + if (count == 1) { + result.SetVectorType(VectorType::CONSTANT_VECTOR); + } +} + + +static void Tpcpatch_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(); + + Pcpatch *pt = pcpatch_hex_in(geom_value.c_str()); + + if(pt == NULL){ + throw InvalidInputException("Invalid pcpatch 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(pt), T_TPCPATCH, span_cmp, interp); + + if (seq == NULL) { + free(pt); + 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(pt); + 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(pt); + + return stored_data; + + }); + + if (count == 1){ + result.SetVectorType(VectorType::CONSTANT_VECTOR); + } +} + +TInstant **temparr_extract_pcp(Vector &tpcpatch_arr_vec, list_entry_t list_entry, int *count) { + auto &child_vector = ListVector::GetEntry(tpcpatch_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 Tpcpatch_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 &tpcpatch_arr_vec = args.data[0]; + tpcpatch_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 tpcpatch_data = FlatVector::GetData(tpcpatch_arr_vec); + auto result_data = FlatVector::GetData(result); + + // Get validity masks + auto &tpcpatch_validity = FlatVector::Validity(tpcpatch_arr_vec); + auto &result_validity = FlatVector::Validity(result); + + for (idx_t i = 0; i < count; i++) { + if (!tpcpatch_validity.RowIsValid(i)) { + result_validity.SetInvalid(i); + continue; + } + + try { + list_entry_t list_entry = tpcpatch_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_pcp(tpcpatch_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 +*/ + +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 TPCPATCH data: null pointer"); + } + + Span *timespan = temporal_to_tstzspan(temp); + + if (!timespan) { + throw InvalidInputException("Failed to extract timespan from TPCPATCH"); + } + + 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 +*/ + +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 TPCPATCH data: null pointer"); + } + + TInstant *inst = temporal_to_tinstant(temp); + if (!inst) { + throw InvalidInputException("Failed to convert TPCPATCH 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); + } +} + + +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 TPCPATCH 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); + } +} + + +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 TPCPATCH data: null pointer"); + } + + std::string tgeom2 = tgeom2_str_t.GetString(); + + Temporal *temp2 = reinterpret_cast(const_cast(tgeom2.c_str())); + if (!temp2) { + throw InvalidInputException("Invalid TPCPATCH data: null pointer"); + } + + Temporal *result_temp = temporal_merge(temp1, temp2); + if (!result_temp) { + throw InvalidInputException("Failed to merge temporal pgpointcloud points"); + } + + // 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 +*/ + +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 TPCPATCH 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); + } +} + + + + +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 TPCPATCH 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); + } +} + +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 TPCPATCH data: null pointer"); + } + + size_t mem_size = temporal_mem_size(temp); + + + return static_cast(mem_size); + }); + + if (count == 1) { + result.SetVectorType(VectorType::CONSTANT_VECTOR); + } +} + +// ---- pcpatch value accessors ---- +// The tpcpatch value type is a pgpointcloud pcpatch: an opaque varlena +// (pgpointcloud SERIALIZED_PATCH) that is not a registered DuckDB type. +// getValue / startValue / endValue surface it in its canonical hex-WKB +// text form via the schema-free pcpatch_hex_out, mirroring tpcpatch_out. +// pcid(tpcpatch) returns the pgpointcloud schema id and numPoints the +// patch point count, both via the schema-free pcpatch_get_pcid / +// pcpatch_npoints header reads. The schema-aware accessors +// (atGeometry, eIntersects, per-dimension getters) are not bound: they +// need a registered PCSCHEMA, which a standalone DuckDB process does +// not have. + +static Pcpatch *pcpatch_from_instant_value(const TInstant *inst) { + Datum d = tinstant_value(inst); + return reinterpret_cast(d); +} + +static 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 + // pcpatch value (datum_copy), which the caller owns. + Pcpatch *pt = pcpatch_from_instant_value(tinst); + + char *str = pcpatch_hex_out(pt, 15); + if (!str) { + free(pt); + throw InvalidInputException("Failed to convert pcpatch value to text"); + } + std::string output(str); + string_t stored_result = StringVector::AddString(result, output); + + free(str); + free(pt); + + return stored_result; + }); + + if (count == 1) { + result.SetVectorType(VectorType::CONSTANT_VECTOR); + } +} + + + +static 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())); + + // temporal_start_value returns a freshly allocated copy of + // the pcpatch value (datum_copy), which the caller owns. + Datum start_datum = temporal_start_value(temp); + + Pcpatch *pt = reinterpret_cast(start_datum); + char *str = pcpatch_hex_out(pt, 15); + if (!str) { + free(pt); + throw InvalidInputException("Failed to convert pcpatch value to text"); + } + std::string output(str); + string_t stored_result = StringVector::AddString(result, output); + + free(str); + free(pt); + + return stored_result; + }); + + if (count == 1) { + result.SetVectorType(VectorType::CONSTANT_VECTOR); + } +} + + +static 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())); + + // temporal_end_value returns a freshly allocated copy of + // the pcpatch value (datum_copy), which the caller owns. + Datum end_datum = temporal_end_value(temp); + + Pcpatch *pt = reinterpret_cast(end_datum); + char *str = pcpatch_hex_out(pt, 15); + if (!str) { + free(pt); + throw InvalidInputException("Failed to convert pcpatch value to text"); + } + std::string output(str); + string_t stored_result = StringVector::AddString(result, output); + + free(str); + free(pt); + + return stored_result; + }); + + if (count == 1) { + result.SetVectorType(VectorType::CONSTANT_VECTOR); + } +} + +static void Tpcpatch_pcid(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) -> int32_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 pcpatch value (datum_copy), which the caller owns. + Datum start_datum = temporal_start_value(temp); + Pcpatch *pt = reinterpret_cast(start_datum); + + // pcpatch_get_pcid is schema-free (reads the serialized + // pcid field of the opaque pgpointcloud patch). + uint32_t pcid = pcpatch_get_pcid(pt); + free(pt); + return static_cast(pcid); + }); + + if (count == 1) { + result.SetVectorType(VectorType::CONSTANT_VECTOR); + } +} + +// numPoints(tpcpatch) — the patch point count of the start-instant +// pcpatch value, via the schema-free pcpatch_npoints header read. This +// is the tpcpatch-specific residual with no tpcpoint analog (the +// canonical MobilityDB SQL exposes numPoints / startNumPoints / +// endNumPoints for tpcpatch only). +static void Tpcpatch_numpoints(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) -> int32_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 pcpatch value (datum_copy), which the caller owns. + Datum start_datum = temporal_start_value(temp); + Pcpatch *pt = reinterpret_cast(start_datum); + + // pcpatch_npoints is schema-free (reads the serialized + // npoints field of the opaque pgpointcloud patch). + uint32_t np = pcpatch_npoints(pt); + free(pt); + return static_cast(np); + }); + + if (count == 1) { + result.SetVectorType(VectorType::CONSTANT_VECTOR); + } +} + + +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; + }); + + if (count == 1) { + result.SetVectorType(VectorType::CONSTANT_VECTOR); + } +} + +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; + }); + + if (count == 1) { + result.SetVectorType(VectorType::CONSTANT_VECTOR); + } +} + +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; + }); + + if (count == 1) { + result.SetVectorType(VectorType::CONSTANT_VECTOR); + } +} + +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; + }); + + if (count == 1) { + result.SetVectorType(VectorType::CONSTANT_VECTOR); + } +} + + + + +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; + }); + + if (count == 1) { + result.SetVectorType(VectorType::CONSTANT_VECTOR); + } +} + + +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 TPCPATCH data: insufficient size"); + } + + uint8_t *data_copy = (uint8_t*)malloc(data_size); + if (!data_copy) { + throw InvalidInputException("Failed to allocate memory for TPCPATCH deserialization"); + } + memcpy(data_copy, data, data_size); + + TInstant *temp = reinterpret_cast(data_copy); + + if (!temp) { + free(data_copy); + throw InvalidInputException("Invalid TPCPATCH 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 TPcpatchTypes::RegisterScalarFunctions(ExtensionLoader &loader) { + + auto tpcpatch_function = ScalarFunction( + "TPCPATCH", + {LogicalType::VARCHAR}, + TPcpatchTypes::TPCPATCH(), + Tpcpatch_constructor + ); + duckdb::RegisterSerializedScalarFunction(loader, tpcpatch_function); + + auto tpcpatch_from_timestamp_function = ScalarFunction( + "TPCPATCH", + {LogicalType::VARCHAR, LogicalType::TIMESTAMP_TZ}, + TPcpatchTypes::TPCPATCH(), + Tpcpatchinst_constructor); + duckdb::RegisterSerializedScalarFunction(loader, tpcpatch_from_timestamp_function); + + auto tpcpatch_from_tstzspan_function = ScalarFunction( + "TPCPATCH", + {LogicalType::VARCHAR, SpanTypes::TSTZSPAN(), LogicalType::VARCHAR}, + TPcpatchTypes::TPCPATCH(), + Tpcpatch_sequence_from_tstzspan + ); + duckdb::RegisterSerializedScalarFunction(loader, tpcpatch_from_tstzspan_function); + + auto tpcpatch_from_tstzspan_default = ScalarFunction( + "TPCPATCH", + {LogicalType::VARCHAR, SpanTypes::TSTZSPAN()}, + TPcpatchTypes::TPCPATCH(), + Tpcpatch_sequence_from_tstzspan + ); + duckdb::RegisterSerializedScalarFunction(loader, tpcpatch_from_tstzspan_default); + + auto tpcpatchseqarr_1param= ScalarFunction( + "tpcpatchSeq", + {LogicalType::LIST(TPcpatchTypes::TPCPATCH())}, + TPcpatchTypes::TPCPATCH(), + Tpcpatch_sequence_constructor + ); + duckdb::RegisterSerializedScalarFunction(loader, tpcpatchseqarr_1param); + + auto tpcpatchseqarr_2params = ScalarFunction( + "tpcpatchSeq", + {LogicalType::LIST(TPcpatchTypes::TPCPATCH()), LogicalType::VARCHAR}, + TPcpatchTypes::TPCPATCH(), + Tpcpatch_sequence_constructor + ); + duckdb::RegisterSerializedScalarFunction(loader, tpcpatchseqarr_2params); + + auto tpcpatchseqarr_3params = ScalarFunction( + "tpcpatchSeq", + {LogicalType::LIST(TPcpatchTypes::TPCPATCH()), LogicalType::VARCHAR, LogicalType::BOOLEAN}, + TPcpatchTypes::TPCPATCH(), + Tpcpatch_sequence_constructor + ); + duckdb::RegisterSerializedScalarFunction(loader, tpcpatchseqarr_3params); + + auto tpcpatchseqarr_4params = ScalarFunction( + "tpcpatchSeq", + {LogicalType::LIST(TPcpatchTypes::TPCPATCH()), LogicalType::VARCHAR, LogicalType::BOOLEAN, LogicalType::BOOLEAN}, + TPcpatchTypes::TPCPATCH(), + Tpcpatch_sequence_constructor + ); + duckdb::RegisterSerializedScalarFunction(loader, tpcpatchseqarr_4params); + + auto tpcpatch_to_timespan_function = ScalarFunction( + "timeSpan", + {TPcpatchTypes::TPCPATCH()}, + SpanTypes::TSTZSPAN(), + Temporal_to_tstzspan); + duckdb::RegisterSerializedScalarFunction(loader, tpcpatch_to_timespan_function); + + auto tpcpatch_to_tinstant_function = ScalarFunction( + "tpcpatchInst", + {TPcpatchTypes::TPCPATCH()}, + TPcpatchTypes::TPCPATCH(), + Temporal_to_tinstant); + duckdb::RegisterSerializedScalarFunction(loader, tpcpatch_to_tinstant_function); + + + auto setInterp_function = ScalarFunction( + "setInterp", + {TPcpatchTypes::TPCPATCH(), LogicalType::VARCHAR}, + TPcpatchTypes::TPCPATCH(), + Temporal_set_interp + ); + duckdb::RegisterSerializedScalarFunction(loader, setInterp_function); + + + auto merge_function = ScalarFunction( + "merge", + {TPcpatchTypes::TPCPATCH(), TPcpatchTypes::TPCPATCH()}, + TPcpatchTypes::TPCPATCH(), + Temporal_merge + ); + duckdb::RegisterSerializedScalarFunction(loader, merge_function); + + auto tempSubtype_function = ScalarFunction( + "tempSubtype", + {TPcpatchTypes::TPCPATCH()}, + LogicalType::VARCHAR, + Temporal_subtype + ); + duckdb::RegisterSerializedScalarFunction(loader, tempSubtype_function); + + auto interp_function = ScalarFunction( + "interp", + {TPcpatchTypes::TPCPATCH()}, + LogicalType::VARCHAR, + Temporal_interp + ); + duckdb::RegisterSerializedScalarFunction(loader, interp_function); + + auto memSize_function = ScalarFunction( + "memSize", + {TPcpatchTypes::TPCPATCH()}, + LogicalType::INTEGER, + Temporal_mem_size + ); + duckdb::RegisterSerializedScalarFunction(loader, memSize_function); + + auto getValue_function = ScalarFunction( + "getValue", + {TPcpatchTypes::TPCPATCH()}, + LogicalType::VARCHAR, + Tinstant_value + ); + duckdb::RegisterSerializedScalarFunction(loader, getValue_function); + + + auto tpcpatch_start_value_function = ScalarFunction( + "startValue", + {TPcpatchTypes::TPCPATCH()}, + LogicalType::VARCHAR, + Temporal_start_value + ); + duckdb::RegisterSerializedScalarFunction(loader, tpcpatch_start_value_function); + + auto tpcpatch_end_value_function = ScalarFunction( + "endValue", + {TPcpatchTypes::TPCPATCH()}, + LogicalType::VARCHAR, + Temporal_end_value + ); + duckdb::RegisterSerializedScalarFunction(loader, tpcpatch_end_value_function); + + auto tpcpatch_pcid_function = ScalarFunction( + "pcid", + {TPcpatchTypes::TPCPATCH()}, + LogicalType::INTEGER, + Tpcpatch_pcid + ); + duckdb::RegisterSerializedScalarFunction(loader, tpcpatch_pcid_function); + + auto tpcpatch_numpoints_function = ScalarFunction( + "numPoints", + {TPcpatchTypes::TPCPATCH()}, + LogicalType::INTEGER, + Tpcpatch_numpoints + ); + duckdb::RegisterSerializedScalarFunction(loader, tpcpatch_numpoints_function); + + auto startInstant_function = ScalarFunction( + "startInstant", + {TPcpatchTypes::TPCPATCH()}, + TPcpatchTypes::TPCPATCH(), + Temporal_start_instant + ); + duckdb::RegisterSerializedScalarFunction(loader, startInstant_function); + + auto endInstant_function = ScalarFunction( + "endInstant", + {TPcpatchTypes::TPCPATCH()}, + TPcpatchTypes::TPCPATCH(), + Temporal_end_instant + ); + duckdb::RegisterSerializedScalarFunction(loader, endInstant_function); + + auto instantN_function = ScalarFunction( + "instantN", + {TPcpatchTypes::TPCPATCH(), LogicalType::INTEGER}, + TPcpatchTypes::TPCPATCH(), + Temporal_instant_n + ); + duckdb::RegisterSerializedScalarFunction(loader, instantN_function); + + + auto tpcpatch_gettimestamptz_function = ScalarFunction( + "getTimestamp", + {TPcpatchTypes::TPCPATCH()}, + LogicalType::TIMESTAMP_TZ, + Tinstant_timestamptz); + duckdb::RegisterSerializedScalarFunction(loader, tpcpatch_gettimestamptz_function); + + + // =================================================================== + // Foundational tpcpatch 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 = TPcpatchTypes::TPCPATCH(); + 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 TPcpatchTypes::RegisterTypes(ExtensionLoader &loader) { + loader.RegisterType( "TPCPATCH", TPcpatchTypes::TPCPATCH()); +} + + +} diff --git a/src/pointcloud/tpcpatch_in_out.cpp b/src/pointcloud/tpcpatch_in_out.cpp new file mode 100644 index 00000000..fdef7326 --- /dev/null +++ b/src/pointcloud/tpcpatch_in_out.cpp @@ -0,0 +1,263 @@ +#include "pointcloud/tpcpatch.hpp" +#include "duckdb/main/extension/extension_loader.hpp" +#include "duckdb/common/extension_type_info.hpp" +#include +#include +#include +#include "temporal/temporal_functions.hpp" +#include "mobilityduck/meos_exec_serial.hpp" + +extern "C" { + #include + #include + #include + #include +} + +namespace duckdb { + +// tpcpatch exposes NO type-specific tpcpatch_in / tpcpatch_out symbol. +// The canonical MobilityDB SQL binds tpcpatch_in / tpcpatch_out to the +// subtype-agnostic generic Temporal_in / Temporal_out, i.e. +// temporal_in(str, T_TPCPATCH) / temporal_out. Likewise the canonical +// SQL exposes NO asText / asEWKT for tpcpatch (only asBinary / asHexWKB +// / asMFJSON, which are the generic Temporal_* wrappers), and the +// schema-dependent EWKT path is not meaningful for a pgpointcloud value +// without a registered PCSCHEMA — so this port does not register an +// asText / asEWKT overload (no unverified-parity over-emission). Text +// I/O is provided by the VARCHAR <-> TPCPATCH cast below. + +bool TpcpatchFunctions::StringToTpcpatch(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(); + + // No tpcpatch_in; route through the generic dispatch with + // the T_TPCPATCH temporal type, the same path the canonical + // MobilityDB SQL binds tpcpatch_in to. + Temporal *temp = temporal_in(input_str.c_str(), T_TPCPATCH); + if (!temp) { + throw InvalidInputException("Invalid TPCPATCH 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 TPCPATCH 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 TpcpatchFunctions::TpcpatchToString(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 TPCPATCH data: insufficient size"); + } + + uint8_t *data_copy = (uint8_t*)malloc(data_size); + if (!data_copy) { + throw InvalidInputException("Failed to allocate memory for TPCPATCH deserialization"); + } + memcpy(data_copy, data, data_size); + + Temporal *temp = reinterpret_cast(data_copy); + if (!temp) { + free(data_copy); + throw InvalidInputException("Invalid TPCPATCH data: null pointer"); + } + + char *str = temporal_out(temp, 15); + if (!str) { + free(data_copy); + throw InvalidInputException("Failed to convert TPCPATCH 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 `tpcpatchFrom*` overloads. +// `temporal_from_wkb`, `temporal_from_hexwkb`, `temporal_from_mfjson` +// and `temporal_in` are all subtype-agnostic; tpcpatch has no +// type-specific *_in / *_from_mfjson symbol, so every constructor +// routes through the generic dispatch with the T_TPCPATCH temporal +// type, exactly as the canonical MobilityDB SQL binds tpcpatchFromBinary +// / tpcpatchFromHexWKB to the generic Temporal_from_wkb / +// Temporal_from_hexwkb handlers. 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 TpcpatchFromTextExec(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()); + // No tpcpatch_in; route through the generic dispatch. + Temporal *t = temporal_in(s.c_str(), T_TPCPATCH); + if (!t) throw InvalidInputException("from*: invalid input"); + return StoreTempAsBlob(result, t); + }); +} + +inline void TpcpatchFromMFJSONExec(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()); + // tpcpatch exposes no header-declared *_from_mfjson symbol; + // route through the generic dispatch with the T_TPCPATCH + // temporal type, the same path the canonical MobilityDB SQL + // binds tpcpatchFromMFJSON to. + Temporal *t = temporal_from_mfjson(s.c_str(), T_TPCPATCH); + if (!t) throw InvalidInputException("fromMFJSON: invalid input"); + return StoreTempAsBlob(result, t); + }); +} + +// asMFJSON(tpcpatch, options int4 DEFAULT 0, flags int4 DEFAULT 0, +// maxdecimaldigits int4 DEFAULT 15) — canonical MobilityDB signature. +// MEOS's temporal_as_mfjson renders the tpcpatch via its dedicated +// tpcpatch_as_mfjson path (pcid / npoints / 2D bounds, all from the +// serialized value), so it is schema-free-safe. +static void TpcpatchAsMfjsonExec(DataChunk &args, ExpressionState &, Vector &result) { + const idx_t count = args.size(); + const idx_t cc = args.ColumnCount(); + for (idx_t i = 0; i < cc; i++) args.data[i].Flatten(count); + auto in = FlatVector::GetData(args.data[0]); + auto out = FlatVector::GetData(result); + auto &valid = FlatVector::Validity(result); + for (idx_t row = 0; row < count; row++) { + if (!FlatVector::Validity(args.data[0]).RowIsValid(row)) { valid.SetInvalid(row); continue; } + std::string buf(in[row].GetData(), in[row].GetSize()); + Temporal *t = reinterpret_cast(const_cast(buf.c_str())); + int options = (cc > 1) ? FlatVector::GetData(args.data[1])[row] : 0; + int flags = (cc > 2) ? FlatVector::GetData(args.data[2])[row] : 0; + int maxdd = (cc > 3) ? FlatVector::GetData(args.data[3])[row] : 15; + // Match the canonical Temporal_as_mfjson clamp (precision in [0, 15]). + if (maxdd > 15) maxdd = 15; else if (maxdd < 0) maxdd = 0; + char *json = temporal_as_mfjson(t, (options & 1) != 0, flags, maxdd, nullptr); + if (!json) { valid.SetInvalid(row); continue; } + out[row] = StringVector::AddString(result, json); + free(json); + } + if (count == 1) result.SetVectorType(VectorType::CONSTANT_VECTOR); +} + +void TPcpatchTypes::RegisterScalarInOutFunctions(ExtensionLoader &loader){ + // ---- tpcpatchFromBinary / FromHexWKB / FromMFJSON / FromText ---- + const auto B = LogicalType::BLOB; + const auto V = LogicalType::VARCHAR; + const auto T = TPcpatchTypes::TPCPATCH(); + duckdb::RegisterSerializedScalarFunction(loader, + ScalarFunction("tpcpatchFromBinary", {B}, T, TspatialFromWkbExec)); + duckdb::RegisterSerializedScalarFunction(loader, + ScalarFunction("tpcpatchFromHexWKB", {V}, T, TspatialFromHexWkbExec)); + duckdb::RegisterSerializedScalarFunction(loader, + ScalarFunction("tpcpatchFromMFJSON", {V}, T, TpcpatchFromMFJSONExec)); + duckdb::RegisterSerializedScalarFunction(loader, + ScalarFunction("tpcpatchFromText", {V}, T, TpcpatchFromTextExec)); + + // ---- asBinary / asHexWKB (subtype-agnostic MEOS-WKB output) ---- + // Mirror the canonical MobilityDB tpcpatch signatures + // asBinary(tpcpatch, endianencoding text DEFAULT '') and + // asHexWKB(tpcpatch, endianencoding text DEFAULT ''): the optional + // second argument selects the WKB endianness. Both bind to the + // generic Temporal_as_wkb / Temporal_as_hexwkb wrappers (a tpcpatch + // serializes as a plain subtype-agnostic MEOS-WKB temporal). + duckdb::RegisterSerializedScalarFunction(loader, + ScalarFunction("asBinary", {T}, B, TemporalFunctions::Temporal_as_wkb)); + duckdb::RegisterSerializedScalarFunction(loader, + ScalarFunction("asBinary", {T, V}, B, TemporalFunctions::Temporal_as_wkb)); + duckdb::RegisterSerializedScalarFunction(loader, + ScalarFunction("asHexWKB", {T}, V, TemporalFunctions::Temporal_as_hexwkb)); + duckdb::RegisterSerializedScalarFunction(loader, + ScalarFunction("asHexWKB", {T, V}, V, TemporalFunctions::Temporal_as_hexwkb)); + + // ---- asMFJSON(tpcpatch[, options int[, flags int[, maxdecimaldigits int]]]) ---- + const auto I = LogicalType::INTEGER; + duckdb::RegisterSerializedScalarFunction(loader, + ScalarFunction("asMFJSON", {T}, V, TpcpatchAsMfjsonExec)); + duckdb::RegisterSerializedScalarFunction(loader, + ScalarFunction("asMFJSON", {T, I}, V, TpcpatchAsMfjsonExec)); + duckdb::RegisterSerializedScalarFunction(loader, + ScalarFunction("asMFJSON", {T, I, I}, V, TpcpatchAsMfjsonExec)); + duckdb::RegisterSerializedScalarFunction(loader, + ScalarFunction("asMFJSON", {T, I, I, I}, V, TpcpatchAsMfjsonExec)); +} + + +void TPcpatchTypes::RegisterCastFunctions(ExtensionLoader &loader) { + loader.RegisterCastFunction( LogicalType::VARCHAR, TPcpatchTypes::TPCPATCH(), TpcpatchFunctions::StringToTpcpatch); + loader.RegisterCastFunction( TPcpatchTypes::TPCPATCH(), LogicalType::VARCHAR, TpcpatchFunctions::TpcpatchToString); +} + +} diff --git a/test/sql/tpcpatch.test b/test/sql/tpcpatch.test new file mode 100644 index 00000000..ac30a946 --- /dev/null +++ b/test/sql/tpcpatch.test @@ -0,0 +1,142 @@ +# name: test/sql/tpcpatch.test +# description: Core tpcpatch type port — construction, text/WKB I/O and +# basic accessors. A tpcpatch value is a temporal +# pgpointcloud patch: an opaque pgpointcloud pcpatch +# (SERIALIZED_PATCH varlena, hex-WKB at the MEOS layer) that +# moves over time. MEOS exposes no type-specific +# tpcpatch_in/_out — the type binds to the generic +# temporal_in(str, T_TPCPATCH) / temporal_out dispatch, the +# same path the canonical MobilityDB SQL uses. Multi-instant +# sequences need a registered pgpointcloud PCSCHEMA (a +# catalog the standalone engine does not have), so these +# self-contained checks exercise the schema-free instant +# path and the deterministic rejection of invalid input. +# The pcpatch hex literal below is pcid 1, uncompressed, +# two points (1 1 1) and (2 2 2); the canonical serialized +# output normalizes the header (4000... in -> 0001... out). +# group: [sql] + +require mobilityduck + +# Test tpcpatch constructor with parentheses (instant, hex-WKB pcpatch) +query I +SELECT (tpcpatch('40000000010000000000000002000000000000000000F03F000000000000F03F000000000000F03F000000000000004000000000000000400000000000000040@2024-01-01'))::text; +---- +00010000010000000000000002000000000000000000F03F000000000000F03F000000000000F03F000000000000004000000000000000400000000000000040@2024-01-01 00:00:00+01 + +# Test tpcpatch constructor without parentheses +query I +SELECT (tpcpatch '40000000010000000000000002000000000000000000F03F000000000000F03F000000000000F03F000000000000004000000000000000400000000000000040@2024-01-01')::text; +---- +00010000010000000000000002000000000000000000F03F000000000000F03F000000000000F03F000000000000004000000000000000400000000000000040@2024-01-01 00:00:00+01 + +# Test the VARCHAR <-> TPCPATCH cast round-trips through temporal_out +query I +SELECT (tpcpatch '40000000010000000000000002000000000000000000F03F000000000000F03F000000000000F03F000000000000004000000000000000400000000000000040@2024-01-01')::text; +---- +00010000010000000000000002000000000000000000F03F000000000000F03F000000000000F03F000000000000004000000000000000400000000000000040@2024-01-01 00:00:00+01 + +# Test tempSubtype with instant +query I +SELECT tempSubtype(tpcpatch '40000000010000000000000002000000000000000000F03F000000000000F03F000000000000F03F000000000000004000000000000000400000000000000040@2024-01-01'); +---- +Instant + +# Test interp accessor on an instant +query I +SELECT interp(tpcpatch '40000000010000000000000002000000000000000000F03F000000000000F03F000000000000F03F000000000000004000000000000000400000000000000040@2024-01-01'); +---- +None + +# Test memSize is positive +query I +SELECT memSize(tpcpatch '40000000010000000000000002000000000000000000F03F000000000000F03F000000000000F03F000000000000004000000000000000400000000000000040@2024-01-01') > 0; +---- +true + +# Test getValue surfaces the pcpatch value in canonical hex-WKB text +query I +SELECT getValue(tpcpatch '40000000010000000000000002000000000000000000F03F000000000000F03F000000000000F03F000000000000004000000000000000400000000000000040@2024-01-01'); +---- +00010000010000000000000002000000000000000000F03F000000000000F03F000000000000F03F000000000000004000000000000000400000000000000040 + +# Test startValue surfaces the pcpatch value in canonical hex-WKB text +query I +SELECT startValue(tpcpatch '40000000010000000000000002000000000000000000F03F000000000000F03F000000000000F03F000000000000004000000000000000400000000000000040@2024-01-01'); +---- +00010000010000000000000002000000000000000000F03F000000000000F03F000000000000F03F000000000000004000000000000000400000000000000040 + +# Test pcid returns the pgpointcloud schema id (schema-free header read) +query I +SELECT pcid(tpcpatch '40000000010000000000000002000000000000000000F03F000000000000F03F000000000000F03F000000000000004000000000000000400000000000000040@2024-01-01'); +---- +1 + +# Test numPoints returns the patch point count (schema-free header read) +query I +SELECT numPoints(tpcpatch '40000000010000000000000002000000000000000000F03F000000000000F03F000000000000F03F000000000000004000000000000000400000000000000040@2024-01-01'); +---- +2 + +# Test getTimestamp on an instant +query I +SELECT getTimestamp(tpcpatch '40000000010000000000000002000000000000000000F03F000000000000F03F000000000000F03F000000000000004000000000000000400000000000000040@2023-01-01 10:00:00+00'); +---- +2023-01-01 11:00:00+01 + +# Test tpcpatchInst transformation round-trips the instant +query I +SELECT (tpcpatchInst(tpcpatch '40000000010000000000000002000000000000000000F03F000000000000F03F000000000000F03F000000000000004000000000000000400000000000000040@2024-01-01'))::text; +---- +00010000010000000000000002000000000000000000F03F000000000000F03F000000000000F03F000000000000004000000000000000400000000000000040@2024-01-01 00:00:00+01 + +# Test startInstant +query I +SELECT (startInstant(tpcpatch '40000000010000000000000002000000000000000000F03F000000000000F03F000000000000F03F000000000000004000000000000000400000000000000040@2024-01-01'))::text; +---- +00010000010000000000000002000000000000000000F03F000000000000F03F000000000000F03F000000000000004000000000000000400000000000000040@2024-01-01 00:00:00+01 + +# Test binary round-trip is identity (subtype-agnostic MEOS-WKB) +query I +SELECT (tpcpatchFromBinary(asBinary(tpcpatch '40000000010000000000000002000000000000000000F03F000000000000F03F000000000000F03F000000000000004000000000000000400000000000000040@2024-01-01')))::text + = (tpcpatch '40000000010000000000000002000000000000000000F03F000000000000F03F000000000000F03F000000000000004000000000000000400000000000000040@2024-01-01')::text; +---- +true + +# Test hex-WKB round-trip is identity +query I +SELECT (tpcpatchFromHexWKB(asHexWKB(tpcpatch '40000000010000000000000002000000000000000000F03F000000000000F03F000000000000F03F000000000000004000000000000000400000000000000040@2024-01-01')))::text + = (tpcpatch '40000000010000000000000002000000000000000000F03F000000000000F03F000000000000F03F000000000000004000000000000000400000000000000040@2024-01-01')::text; +---- +true + +# Test tpcpatchFromText constructor +query I +SELECT (tpcpatchFromText('40000000010000000000000002000000000000000000F03F000000000000F03F000000000000F03F000000000000004000000000000000400000000000000040@2024-01-01'))::text IS NOT NULL; +---- +true + +# Test timeSpan function +query I +SELECT timeSpan(tpcpatch '40000000010000000000000002000000000000000000F03F000000000000F03F000000000000F03F000000000000004000000000000000400000000000000040@2024-01-01'); +---- +[2024-01-01 00:00:00+01, 2024-01-01 00:00:00+01] + +# Test numInstants generic accessor +query I +SELECT numInstants(tpcpatch '40000000010000000000000002000000000000000000F03F000000000000F03F000000000000F03F000000000000004000000000000000400000000000000040@2024-01-01'); +---- +1 + +# Test asMFJSON produces valid MF-JSON (pcid / npoints / 2D bounds from the +# serialized value; schema-free) +query I +SELECT asMFJSON(tpcpatch '40000000010000000000000002000000000000000000F03F000000000000F03F000000000000F03F000000000000004000000000000000400000000000000040@2024-01-01') IS NOT NULL; +---- +true + +# Test invalid input is rejected deterministically +statement error +SELECT tpcpatch('not-a-valid-tpcpatch'); +---- +Missing delimeter character