From 2a1ddcb39587fd98b39a5b206a26d7bfbe82c4d4 Mon Sep 17 00:00:00 2001 From: recon Date: Thu, 11 Jun 2026 23:46:39 +0200 Subject: [PATCH] feat(parity): bind TJSONB temporal JSONB type Adds the TJSONB type backed by MEOS's tjsonb family. The type is stored as a BLOB alias. Constructors accept a JSON string, a (JSON, timestamptz) pair, or a (JSON, tstzspan) pair. The sequence constructor takes a LIST(TJSONB). Value accessors startValue/endValue/valueAtTimestamp return VARCHAR. Generic temporal operations (timeSpan, tempSubtype, interp, memSize, merge, setInterp, tjsonbInst) are wired via the shared TemporalFunctions helpers. Round-trip I/O covers tjsonbFromHexWKB / tjsonbFromHexEWKB / tjsonbFromBinary / tjsonbFromMFJSON and asHexWKB / asHexEWKB output. A shared TemporalToBlob / BlobToTemporal utility in src/temporal/temporal_blob consolidates the blob round-trip idiom used across all type bindings. Smoke tests cover instant round-trip, two-argument construction, start/end value, step sequence, tempSubtype, and HexWKB round-trip. --- CMakeLists.txt | 3 + src/include/json/tjsonb.hpp | 26 +++ src/include/temporal/temporal_blob.hpp | 18 ++ src/json/tjsonb.cpp | 270 +++++++++++++++++++++++++ src/json/tjsonb_in_out.cpp | 148 ++++++++++++++ src/mobilityduck_extension.cpp | 6 + src/temporal/temporal_blob.cpp | 26 +++ test/sql/parity/027_tjsonb.test | 64 ++++++ vcpkg_ports/meos/portfile.cmake | 101 ++++++++- 9 files changed, 656 insertions(+), 6 deletions(-) create mode 100644 src/include/json/tjsonb.hpp create mode 100644 src/include/temporal/temporal_blob.hpp create mode 100644 src/json/tjsonb.cpp create mode 100644 src/json/tjsonb_in_out.cpp create mode 100644 src/temporal/temporal_blob.cpp create mode 100644 test/sql/parity/027_tjsonb.test diff --git a/CMakeLists.txt b/CMakeLists.txt index 1cc6f8ff..fb30859e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -108,6 +108,9 @@ set(EXTENSION_SOURCES src/geo/tgeogpoint.cpp src/geo/tgeogpoint_in_out.cpp src/geo/tgeogpoint_ops.cpp + src/temporal/temporal_blob.cpp + src/json/tjsonb.cpp + src/json/tjsonb_in_out.cpp src/h3/th3index.cpp src/index/rtree_module.cpp src/single_tile_getters.cpp diff --git a/src/include/json/tjsonb.hpp b/src/include/json/tjsonb.hpp new file mode 100644 index 00000000..3736dc9b --- /dev/null +++ b/src/include/json/tjsonb.hpp @@ -0,0 +1,26 @@ +#pragma once + +#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 TJsonbTypes { + static LogicalType TJSONB(); + static void RegisterTypes(ExtensionLoader &loader); + static void RegisterScalarFunctions(ExtensionLoader &loader); + static void RegisterCastFunctions(ExtensionLoader &loader); + static void RegisterScalarInOutFunctions(ExtensionLoader &loader); +}; + +struct TjsonbFunctions { + static bool StringToTjsonb(Vector &source, Vector &result, idx_t count, + CastParameters ¶meters); + static bool TjsonbToString(Vector &source, Vector &result, idx_t count, + CastParameters ¶meters); +}; + +} // namespace duckdb diff --git a/src/include/temporal/temporal_blob.hpp b/src/include/temporal/temporal_blob.hpp new file mode 100644 index 00000000..f4986c67 --- /dev/null +++ b/src/include/temporal/temporal_blob.hpp @@ -0,0 +1,18 @@ +#pragma once + +// Canonical round-trip between a DuckDB blob (string_t) and a MEOS Temporal +// value. Every type binding shares this single definition, so the +// function-pointer registrations across the bindings all resolve to one body. + +#include "meos_wrapper_simple.hpp" +#include "common.hpp" + +namespace duckdb { + +// Copy t into result's string heap and free t; returns the stored blob. +string_t TemporalToBlob(Vector &result, Temporal *t); + +// Copy the blob bytes into a freshly malloc'd Temporal* owned by the caller. +Temporal *BlobToTemporal(string_t blob); + +} // namespace duckdb diff --git a/src/json/tjsonb.cpp b/src/json/tjsonb.cpp new file mode 100644 index 00000000..c05153b4 --- /dev/null +++ b/src/json/tjsonb.cpp @@ -0,0 +1,270 @@ +#include "json/tjsonb.hpp" +#include "temporal/temporal_blob.hpp" +#include "temporal/span.hpp" +#include "temporal/temporal_functions.hpp" +#include "duckdb/main/extension/extension_loader.hpp" +#include "duckdb/common/extension_type_info.hpp" +#include "mobilityduck/meos_exec_serial.hpp" +#include "time_util.hpp" + +extern "C" { + #include + #include +} + +/* meos_json.h requires pgtypes headers that are not installed in the public + * MEOS package. Jsonb is used only as an opaque pointer here; forward-declare + * the struct and the eight MEOS entry points actually called by this file. */ +struct JsonbData; +typedef struct JsonbData Jsonb; + +extern "C" { + extern Temporal *tjsonb_in(const char *str); + extern Jsonb *jsonb_in(const char *str); + extern TInstant *tjsonbinst_make(const Jsonb *jsonb, TimestampTz t); + extern TSequence *tjsonbseq_from_base_tstzspan(const Jsonb *jsonb, const Span *sp); + extern Jsonb *tjsonb_start_value(const Temporal *temp); + extern Jsonb *tjsonb_end_value(const Temporal *temp); + extern bool tjsonb_value_at_timestamptz(const Temporal *temp, TimestampTz t, + bool strict, Jsonb **value); + extern char *jsonb_out(const Jsonb *jb); +} + +namespace duckdb { + +LogicalType TJsonbTypes::TJSONB() { + auto type = LogicalType(LogicalTypeId::BLOB); + type.SetAlias("TJSONB"); + return type; +} + +void TJsonbTypes::RegisterTypes(ExtensionLoader &loader) { + loader.RegisterType("TJSONB", TJsonbTypes::TJSONB()); +} + +/* ------------------------------------------------------------------ + * Constructors + * ------------------------------------------------------------------ */ + +static void Tjsonb_constructor(DataChunk &args, ExpressionState &state, Vector &result) { + UnaryExecutor::Execute( + args.data[0], result, args.size(), + [&](string_t input_str) -> string_t { + std::string s = input_str.GetString(); + Temporal *temp = tjsonb_in(s.c_str()); + if (!temp) + throw InvalidInputException("Invalid TJSONB input: " + s); + return TemporalToBlob(result, temp); + }); +} + +/* Two-argument instant constructor: TJSONB(json_text, timestamptz) */ +static void Tjsonbinst_constructor(DataChunk &args, ExpressionState &state, Vector &result) { + BinaryExecutor::Execute( + args.data[0], args.data[1], result, args.size(), + [&](string_t json_str, timestamp_tz_t t) -> string_t { + std::string s = json_str.GetString(); + Jsonb *jb = jsonb_in(s.c_str()); + if (!jb) + throw InvalidInputException("Invalid JSON input: " + s); + timestamp_tz_t meos_ts = DuckDBToMeosTimestamp(t); + TInstant *inst = tjsonbinst_make(jb, static_cast(meos_ts.value)); + free(jb); + if (!inst) + throw InvalidInputException("Failed to create TJSONB instant"); + return TemporalToBlob(result, reinterpret_cast(inst)); + }); +} + +/* Step sequence from a constant JSON value over a tstzspan */ +static void Tjsonb_sequence_from_tstzspan(DataChunk &args, ExpressionState &state, Vector &result) { + BinaryExecutor::Execute( + args.data[0], args.data[1], result, args.size(), + [&](string_t json_str, string_t span_blob) -> string_t { + std::string sj = json_str.GetString(); + Jsonb *jb = jsonb_in(sj.c_str()); + if (!jb) + throw InvalidInputException("Invalid JSON input: " + sj); + size_t sp_sz = span_blob.GetSize(); + uint8_t *sp_copy = static_cast(malloc(sp_sz)); + if (!sp_copy) { free(jb); throw InternalException("malloc failed for span copy"); } + memcpy(sp_copy, span_blob.GetData(), sp_sz); + Span *sp = reinterpret_cast(sp_copy); + TSequence *seq = tjsonbseq_from_base_tstzspan(jb, sp); + free(jb); + free(sp_copy); + if (!seq) + throw InvalidInputException("Failed to create TJSONB sequence"); + return TemporalToBlob(result, reinterpret_cast(seq)); + }); +} + +/* Extract TInstant* array from a LIST(TJSONB) vector entry */ +static TInstant **temparr_extract_tjsonb(Vector &arr_vec, list_entry_t entry, int *count) { + auto &child = ListVector::GetEntry(arr_vec); + auto len = entry.length; + auto off = entry.offset; + if (len == 0) { *count = 0; return nullptr; } + *count = static_cast(len); + TInstant **insts = static_cast(malloc(sizeof(TInstant *) * len)); + if (!insts) { *count = 0; return nullptr; } + for (idx_t i = 0; i < len; i++) { + string_t blob = FlatVector::GetData(child)[off + i]; + size_t sz = blob.GetSize(); + uint8_t *copy = static_cast(malloc(sz)); + if (!copy) { + for (idx_t j = 0; j < i; j++) free(insts[j]); + free(insts); *count = 0; return nullptr; + } + memcpy(copy, blob.GetData(), sz); + insts[i] = reinterpret_cast(copy); + } + return insts; +} + +/* Sequence constructor from LIST(TJSONB) instants */ +static void Tjsonb_sequence_constructor(DataChunk &args, ExpressionState &state, Vector &result) { + const char *default_interp = "step"; + auto count = args.size(); + + args.data[0].Flatten(count); + result.Flatten(count); + + auto arr_data = FlatVector::GetData(args.data[0]); + auto result_data = FlatVector::GetData(result); + auto &arr_val = FlatVector::Validity(args.data[0]); + auto &res_val = FlatVector::Validity(result); + + interpType interp = interptype_from_string(default_interp); + + for (idx_t i = 0; i < count; i++) { + if (!arr_val.RowIsValid(i)) { res_val.SetInvalid(i); continue; } + int ninsts = 0; + TInstant **insts = temparr_extract_tjsonb(args.data[0], arr_data[i], &ninsts); + if (!insts || ninsts == 0) { res_val.SetInvalid(i); continue; } + TSequence *seq = tsequence_make( + (TInstant **) insts, ninsts, + true, true, interp, true); + for (int j = 0; j < ninsts; j++) free(insts[j]); + free(insts); + if (!seq) { res_val.SetInvalid(i); continue; } + size_t sz = temporal_mem_size(reinterpret_cast(seq)); + result_data[i] = StringVector::AddStringOrBlob( + result, reinterpret_cast(seq), sz); + free(seq); + } +} + +/* ------------------------------------------------------------------ + * Value accessors + * ------------------------------------------------------------------ */ + +static void Tjsonb_start_value(DataChunk &args, ExpressionState &state, Vector &result) { + UnaryExecutor::Execute( + args.data[0], result, args.size(), + [&](string_t input_blob) -> string_t { + Temporal *temp = BlobToTemporal(input_blob); + Jsonb *jb = tjsonb_start_value(temp); + free(temp); + if (!jb) + throw InvalidInputException("tjsonb startValue: null result"); + char *str = jsonb_out(jb); + free(jb); + if (!str) + throw InvalidInputException("tjsonb startValue: jsonb_out failed"); + string_t out = StringVector::AddString(result, str); + free(str); + return out; + }); +} + +static void Tjsonb_end_value(DataChunk &args, ExpressionState &state, Vector &result) { + UnaryExecutor::Execute( + args.data[0], result, args.size(), + [&](string_t input_blob) -> string_t { + Temporal *temp = BlobToTemporal(input_blob); + Jsonb *jb = tjsonb_end_value(temp); + free(temp); + if (!jb) + throw InvalidInputException("tjsonb endValue: null result"); + char *str = jsonb_out(jb); + free(jb); + if (!str) + throw InvalidInputException("tjsonb endValue: jsonb_out failed"); + string_t out = StringVector::AddString(result, str); + free(str); + return out; + }); +} + +static void Tjsonb_value_at_timestamp(DataChunk &args, ExpressionState &state, Vector &result) { + BinaryExecutor::Execute( + args.data[0], args.data[1], result, args.size(), + [&](string_t input_blob, timestamp_tz_t t) -> string_t { + Temporal *temp = BlobToTemporal(input_blob); + timestamp_tz_t meos_ts = DuckDBToMeosTimestamp(t); + Jsonb *jb = nullptr; + bool found = tjsonb_value_at_timestamptz( + temp, static_cast(meos_ts.value), true, &jb); + free(temp); + if (!found || !jb) + throw InvalidInputException("tjsonb valueAtTimestamp: no value at given timestamp"); + char *str = jsonb_out(jb); + free(jb); + if (!str) + throw InvalidInputException("tjsonb valueAtTimestamp: jsonb_out failed"); + string_t out = StringVector::AddString(result, str); + free(str); + return out; + }); +} + +/* ------------------------------------------------------------------ + * Registration + * ------------------------------------------------------------------ */ + +void TJsonbTypes::RegisterScalarFunctions(ExtensionLoader &loader) { + const auto T = TJsonbTypes::TJSONB(); + const auto V = LogicalType::VARCHAR; + const auto TS = LogicalType::TIMESTAMP_TZ; + + /* Constructors */ + RegisterSerializedScalarFunction(loader, + ScalarFunction("TJSONB", {V}, T, Tjsonb_constructor)); + RegisterSerializedScalarFunction(loader, + ScalarFunction("TJSONB", {V, TS}, T, Tjsonbinst_constructor)); + RegisterSerializedScalarFunction(loader, + ScalarFunction("TJSONB", {V, SpanTypes::TSTZSPAN()}, T, + Tjsonb_sequence_from_tstzspan)); + + RegisterSerializedScalarFunction(loader, + ScalarFunction("tjsonbSeq", {LogicalType::LIST(T)}, T, + Tjsonb_sequence_constructor)); + + /* Generic temporal functions applied to TJSONB */ + RegisterSerializedScalarFunction(loader, + ScalarFunction("timeSpan", {T}, SpanTypes::TSTZSPAN(), + TemporalFunctions::Temporal_to_tstzspan)); + RegisterSerializedScalarFunction(loader, + ScalarFunction("tempSubtype", {T}, V, TemporalFunctions::Temporal_subtype)); + RegisterSerializedScalarFunction(loader, + ScalarFunction("interp", {T}, V, TemporalFunctions::Temporal_interp)); + RegisterSerializedScalarFunction(loader, + ScalarFunction("memSize", {T}, LogicalType::INTEGER, TemporalFunctions::Temporal_mem_size)); + RegisterSerializedScalarFunction(loader, + ScalarFunction("merge", {T, T}, T, TemporalFunctions::Temporal_merge)); + RegisterSerializedScalarFunction(loader, + ScalarFunction("setInterp", {T, V}, T, TemporalFunctions::Temporal_set_interp)); + RegisterSerializedScalarFunction(loader, + ScalarFunction("tjsonbInst", {T}, T, TemporalFunctions::Temporal_to_tinstant)); + + /* Value accessors */ + RegisterSerializedScalarFunction(loader, + ScalarFunction("startValue", {T}, V, Tjsonb_start_value)); + RegisterSerializedScalarFunction(loader, + ScalarFunction("endValue", {T}, V, Tjsonb_end_value)); + RegisterSerializedScalarFunction(loader, + ScalarFunction("valueAtTimestamp", {T, TS}, V, Tjsonb_value_at_timestamp)); +} + +} // namespace duckdb diff --git a/src/json/tjsonb_in_out.cpp b/src/json/tjsonb_in_out.cpp new file mode 100644 index 00000000..0130c58f --- /dev/null +++ b/src/json/tjsonb_in_out.cpp @@ -0,0 +1,148 @@ +#include "json/tjsonb.hpp" +#include "temporal/temporal_blob.hpp" +#include "duckdb/main/extension/extension_loader.hpp" +#include "duckdb/common/extension_type_info.hpp" +#include "mobilityduck/meos_exec_serial.hpp" + +extern "C" { + #include + #include +} + +/* meos_json.h requires pgtypes headers not installed in the public MEOS package. + * Forward-declare the three entry points used by this translation unit. */ +extern "C" { + extern Temporal *tjsonb_in(const char *str); + extern char *tjsonb_out(const Temporal *temp); + extern Temporal *tjsonb_from_mfjson(const char *str); +} + +namespace duckdb { + +bool TjsonbFunctions::StringToTjsonb(Vector &source, Vector &result, idx_t count, + CastParameters ¶meters) { + UnaryExecutor::Execute( + source, result, count, + [&](string_t input_string) -> string_t { + std::string s = input_string.GetString(); + Temporal *temp = tjsonb_in(s.c_str()); + if (!temp) + throw InvalidInputException("Invalid TJSONB input: " + s); + return TemporalToBlob(result, temp); + }); + return true; +} + +bool TjsonbFunctions::TjsonbToString(Vector &source, Vector &result, idx_t count, + CastParameters ¶meters) { + UnaryExecutor::Execute( + source, result, count, + [&](string_t input_blob) -> string_t { + Temporal *temp = BlobToTemporal(input_blob); + char *str = tjsonb_out(temp); + free(temp); + if (!str) + throw InvalidInputException("Failed to serialize TJSONB to string"); + string_t stored = StringVector::AddString(result, str); + free(str); + return stored; + }); + return true; +} + +static void TjsonbFromWkbExec(DataChunk &args, ExpressionState &, Vector &result) { + UnaryExecutor::Execute( + args.data[0], result, args.size(), + [&](string_t input) -> string_t { + if (input.GetSize() == 0) + throw InvalidInputException("tjsonbFromBinary: empty input"); + uint8_t *wkb = static_cast(malloc(input.GetSize())); + if (!wkb) throw InternalException("tjsonbFromBinary: malloc failed"); + memcpy(wkb, input.GetData(), input.GetSize()); + Temporal *t = temporal_from_wkb(wkb, input.GetSize()); + free(wkb); + if (!t) throw InvalidInputException("tjsonbFromBinary: invalid WKB"); + return TemporalToBlob(result, t); + }); +} + +static void TjsonbFromHexWkbExec(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("tjsonbFromHexWKB: invalid hex-encoded WKB"); + return TemporalToBlob(result, t); + }); +} + +template +static void TjsonbFromStringExec(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("tjsonbFrom*: invalid input"); + return TemporalToBlob(result, t); + }); +} + +static void TjsonbAsHexWkbExec(DataChunk &args, ExpressionState &, Vector &result, + uint8_t variant) { + UnaryExecutor::Execute( + args.data[0], result, args.size(), + [&](string_t input) -> string_t { + Temporal *t = BlobToTemporal(input); + size_t sz = 0; + char *hex = temporal_as_hexwkb(t, variant, &sz); + free(t); + (void) sz; + if (!hex) throw InternalException("asHexWKB: temporal_as_hexwkb failed"); + string_t stored = StringVector::AddString(result, hex); + free(hex); + return stored; + }); +} + +void TJsonbTypes::RegisterCastFunctions(ExtensionLoader &loader) { + RegisterMeosCastFunction(loader, LogicalType::VARCHAR, TJsonbTypes::TJSONB(), + TjsonbFunctions::StringToTjsonb); + RegisterMeosCastFunction(loader, TJsonbTypes::TJSONB(), LogicalType::VARCHAR, + TjsonbFunctions::TjsonbToString); +} + +void TJsonbTypes::RegisterScalarInOutFunctions(ExtensionLoader &loader) { + const auto B = LogicalType::BLOB; + const auto V = LogicalType::VARCHAR; + const auto T = TJsonbTypes::TJSONB(); + + RegisterSerializedScalarFunction(loader, + ScalarFunction("tjsonbFromBinary", {B}, T, TjsonbFromWkbExec)); + RegisterSerializedScalarFunction(loader, + ScalarFunction("tjsonbFromEWKB", {B}, T, TjsonbFromWkbExec)); + RegisterSerializedScalarFunction(loader, + ScalarFunction("tjsonbFromHexWKB", {V}, T, TjsonbFromHexWkbExec)); + RegisterSerializedScalarFunction(loader, + ScalarFunction("tjsonbFromHexEWKB", {V}, T, TjsonbFromHexWkbExec)); + RegisterSerializedScalarFunction(loader, + ScalarFunction("tjsonbFromMFJSON", {V}, T, + TjsonbFromStringExec<&tjsonb_from_mfjson>)); + RegisterSerializedScalarFunction(loader, + ScalarFunction("tjsonbFromText", {V}, T, + TjsonbFromStringExec<&tjsonb_in>)); + + RegisterSerializedScalarFunction(loader, + ScalarFunction("asHexWKB", {T}, V, + [](DataChunk &a, ExpressionState &s, Vector &r) { + TjsonbAsHexWkbExec(a, s, r, 0x00); + })); + RegisterSerializedScalarFunction(loader, + ScalarFunction("asHexEWKB", {T}, V, + [](DataChunk &a, ExpressionState &s, Vector &r) { + TjsonbAsHexWkbExec(a, s, r, 0x04); + })); +} + +} // namespace duckdb diff --git a/src/mobilityduck_extension.cpp b/src/mobilityduck_extension.cpp index ee4a4508..6c875a95 100644 --- a/src/mobilityduck_extension.cpp +++ b/src/mobilityduck_extension.cpp @@ -18,6 +18,7 @@ #include "geo/tgeogpoint.hpp" #include "geo/tgeogpoint_ops.hpp" #include "h3/th3index.hpp" +#include "json/tjsonb.hpp" #include "temporal/span.hpp" #include "temporal/span_aggregates.hpp" #include "temporal/temporal_aggregates.hpp" @@ -350,6 +351,11 @@ static void LoadInternal(ExtensionLoader &loader) { SpansetTypes::RegisterCastFunctions(loader); SpansetTypes::RegisterScalarFunctions(loader); + TJsonbTypes::RegisterTypes(loader); + TJsonbTypes::RegisterCastFunctions(loader); + TJsonbTypes::RegisterScalarFunctions(loader); + TJsonbTypes::RegisterScalarInOutFunctions(loader); + TRTreeModule::RegisterRTreeIndex(loader); TRTreeModule::RegisterIndexScan(loader); TRTreeModule::RegisterScanOptimizer(loader); diff --git a/src/temporal/temporal_blob.cpp b/src/temporal/temporal_blob.cpp new file mode 100644 index 00000000..773cae21 --- /dev/null +++ b/src/temporal/temporal_blob.cpp @@ -0,0 +1,26 @@ +// Single definition of the canonical DuckDB-blob <-> MEOS Temporal round-trip +// shared by every type binding. + +#include "temporal/temporal_blob.hpp" + +#include +#include + +namespace duckdb { + +string_t TemporalToBlob(Vector &result, Temporal *t) { + size_t sz = temporal_mem_size(t); + string_t out = StringVector::AddStringOrBlob( + result, reinterpret_cast(t), sz); + free(t); + return out; +} + +Temporal *BlobToTemporal(string_t blob) { + size_t sz = blob.GetSize(); + uint8_t *copy = static_cast(malloc(sz)); + memcpy(copy, blob.GetData(), sz); + return reinterpret_cast(copy); +} + +} // namespace duckdb diff --git a/test/sql/parity/027_tjsonb.test b/test/sql/parity/027_tjsonb.test new file mode 100644 index 00000000..90805198 --- /dev/null +++ b/test/sql/parity/027_tjsonb.test @@ -0,0 +1,64 @@ +# name: test/sql/parity/027_tjsonb.test +# description: Smoke tests for the TJSONB temporal type (tjsonb family). +# group: [sql] + +require mobilityduck + +# Round-trip a TJSONB instant through the cast. +query I +SELECT TJSONB('{"k":1}@2000-01-01')::VARCHAR; +---- +"{\"k\": 1}"@2000-01-01 00:00:00+01 + +# Two-argument instant constructor from JSON text + timestamptz. +query I +SELECT TJSONB('{"x":42}', TIMESTAMPTZ '2020-06-01 00:00:00+00')::VARCHAR; +---- +"{\"x\": 42}"@2020-06-01 02:00:00+02 + +# startValue returns the JSON of the first instant. +query I +SELECT startValue(TJSONB('{"a":1}@2000-01-01')); +---- +{"a": 1} + +# endValue returns the JSON of the last instant. +query I +SELECT endValue(TJSONB('{"a":1}@2000-01-01')); +---- +{"a": 1} + +# TJSONB over a step sequence: two instants. +query I +SELECT TJSONB('[{"v":1}@2000-01-01, {"v":2}@2000-01-02]')::VARCHAR; +---- +["{\"v\": 1}"@2000-01-01 00:00:00+01, "{\"v\": 2}"@2000-01-02 00:00:00+01] + +# startValue of a sequence. +query I +SELECT startValue(TJSONB('[{"v":1}@2000-01-01, {"v":2}@2000-01-02]')); +---- +{"v": 1} + +# endValue of a sequence. +query I +SELECT endValue(TJSONB('[{"v":1}@2000-01-01, {"v":2}@2000-01-02]')); +---- +{"v": 2} + +# tempSubtype distinguishes instant from sequence. +query I +SELECT tempSubtype(TJSONB('{"k":1}@2000-01-01')); +---- +Instant + +query I +SELECT tempSubtype(TJSONB('[{"k":1}@2000-01-01, {"k":2}@2000-01-02]')); +---- +Sequence + +# HexWKB round-trip. +query I +SELECT tjsonbFromHexWKB(asHexWKB(TJSONB('{"k":1}@2000-01-01')))::VARCHAR; +---- +"{\"k\": 1}"@2000-01-01 00:00:00+01 diff --git a/vcpkg_ports/meos/portfile.cmake b/vcpkg_ports/meos/portfile.cmake index d2ed8cc0..7378bda5 100644 --- a/vcpkg_ports/meos/portfile.cmake +++ b/vcpkg_ports/meos/portfile.cmake @@ -61,6 +61,31 @@ if(NOT _MEOS_H3_INC) message(FATAL_ERROR "MEOS port: cannot locate vcpkg-installed h3api.h under ${CURRENT_INSTALLED_DIR}/include or ${CURRENT_INSTALLED_DIR}/include/h3") endif() +# json-c's FindJSON-C.cmake uses hardcoded system hints (/usr/lib, /usr/include) +# that miss vcpkg's installed layout. Resolve the library and include paths +# explicitly so they are pre-set as cache variables before FindJSON-C runs. +set(_meos_jsonc_lib_candidates + "${CURRENT_INSTALLED_DIR}/lib/libjson-c.so" + "${CURRENT_INSTALLED_DIR}/lib/libjson-c.a" + "${CURRENT_INSTALLED_DIR}/lib/libjson-c${CMAKE_SHARED_LIBRARY_SUFFIX}" + "${CURRENT_INSTALLED_DIR}/lib/libjson-c${CMAKE_STATIC_LIBRARY_SUFFIX}") +set(_MEOS_JSONC_LIB "") +foreach(_cand IN LISTS _meos_jsonc_lib_candidates) + if(EXISTS "${_cand}") + set(_MEOS_JSONC_LIB "${_cand}") + break() + endif() +endforeach() +if(NOT _MEOS_JSONC_LIB) + message(FATAL_ERROR "MEOS port: cannot locate vcpkg-installed libjson-c under ${CURRENT_INSTALLED_DIR}/lib") +endif() +# json-c headers install under include/json-c/; FindJSON-C.cmake searches for +# json.h with PATH_SUFFIXES json-c, so pass the parent include directory. +set(_MEOS_JSONC_INC "${CURRENT_INSTALLED_DIR}/include/json-c") +if(NOT EXISTS "${_MEOS_JSONC_INC}/json.h") + message(FATAL_ERROR "MEOS port: cannot locate vcpkg-installed json.h under ${CURRENT_INSTALLED_DIR}/include/json-c") +endif() + # Upstream gap: `meos/src/CMakeLists.txt` is missing the # `if(H3) add_subdirectory(h3) endif()` block. The top-level # `meos/CMakeLists.txt` references `$` when H3=ON, @@ -80,6 +105,69 @@ if(JSON) endif()]=] ) +# Upstream gap: `temporal_parse` in `meos/src/temporal/type_parser.c` routes any +# input that starts with '{' to the discrete-sequence parser, consuming the '{' as +# the outer sequence delimiter. For T_TJSONB, a bare instant like +# `{"k":1}@2000-01-01` also starts with '{' (the JSON object delimiter), so it is +# incorrectly dispatched to tdiscseq_parse which then tries to parse `"k":1}@...` +# as a temporal instant and fails with "Missing delimeter character '@'". +# +# Fix: after peeking inside the outer '{', distinguish the three cases: +# - next char is '[' or '(' → sequence set (existing behaviour) +# - next char is '{' → discrete sequence (first instant's value starts with '{') +# - anything else AND basetype == T_JSONB → JSON-object instant; restore and +# parse via tinstant_parse +# For non-T_JSONB types no observable behaviour change: their instant values never +# start with '{', so the second condition (!=T_JSONB) keeps them in tdiscseq_parse. +vcpkg_replace_string( + "${SOURCE_PATH}/meos/src/temporal/type_parser.c" + [=[ else if (**str == '{') + { + const char *bak = *str; + p_obrace(str); + p_whitespace(str); + if (**str == '[' || **str == '(') + { + *str = bak; + result = (Temporal *) tsequenceset_parse(str, temptype, interp); + } + else + { + *str = bak; + result = (Temporal *) tdiscseq_parse(str, temptype); + } + }]=] + [=[ else if (**str == '{') + { + const char *bak = *str; + p_obrace(str); + p_whitespace(str); + if (**str == '[' || **str == '(') + { + *str = bak; + result = (Temporal *) tsequenceset_parse(str, temptype, interp); + } + else if (**str == '{' || temptype_basetype(temptype) != T_JSONB) + { + /* Discrete sequence: either next token is another '{' (e.g. first + * instant's JSON-object value) or the base type never starts with '{' + * so the outer '{' is definitely the sequence delimiter. */ + *str = bak; + result = (Temporal *) tdiscseq_parse(str, temptype); + } + else + { + /* The outer '{' belongs to the base value itself (e.g. a JSON object). + * Restore and parse as a temporal instant. */ + *str = bak; + TInstant *inst = tinstant_parse(str, temptype, true); + if (! inst) + return NULL; + result = (Temporal *) inst; + } + }]=] +) + # Upstream gap: `pgtypes/libpq/pqformat.h` contains a deprecated # static-inline helper `pq_sendint` that calls `elog()`. In the # standalone MEOS build the pgtypes shim does not declare `elog`, and @@ -192,6 +280,9 @@ vcpkg_cmake_configure( -DH3=ON "-DH3_LIBRARY=${_MEOS_H3_LIB}" "-DH3_INCLUDE_DIR=${_MEOS_H3_INC}" + -DJSON=ON + "-DJSON-C_LIBRARIES=${_MEOS_JSONC_LIB}" + "-DJSON-C_INCLUDE_DIRS=${_MEOS_JSONC_INC}" -DBUILD_SHARED_LIBS=ON # Build only the MEOS library, not the MEOS C test binaries: those link # the GEOS C++ API, which the arm64-linux vcpkg triplet does not carry. @@ -202,14 +293,12 @@ vcpkg_cmake_configure( vcpkg_cmake_install() -# meos_tls.h and meos_json.h are not listed in the upstream install() rules at -# this pin. meos_tls.h is included verbatim by the cmake-generated meos.h; -# meos_json.h exposes the public JSONB / temporal-JSONB API used by TJSONB -# bindings. Copy both alongside the other installed headers. +# meos_tls.h is not listed in the upstream install() rules at this pin. +# It is included verbatim by the cmake-generated meos.h; copy it alongside +# the other installed headers. meos_json.h is installed automatically by +# cmake when JSON=ON (as the stripped export variant). file(COPY "${SOURCE_PATH}/meos/include/meos_tls.h" DESTINATION "${CURRENT_PACKAGES_DIR}/include") -file(COPY "${SOURCE_PATH}/meos/include/meos_json.h" - DESTINATION "${CURRENT_PACKAGES_DIR}/include") file(MAKE_DIRECTORY "${CURRENT_PACKAGES_DIR}/share/meos") file(WRITE "${CURRENT_PACKAGES_DIR}/share/meos/MEOSConfig.cmake" [=[