Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,39 @@ include extension-ci-tools/makefiles/duckdb_extension.Makefile
# both MEOS (meos_initialize_timezone) and DuckDB (DBConfig::SetOptionByName
# "TimeZone") to Europe/Brussels. Tests pass on any OS timezone — the
# extension is the single source of truth, no TZ env var needed.
#
# LoadInternal also calls ExtensionHelper::AutoLoadExtension(db, "icu") so
# the timezone option is honoured. Autoload looks for the extension on disk
# at $HOME/.duckdb/extensions/<duckdb_version>/<platform>/icu.duckdb_extension
# and falls back to a hub download. That fails both inside the linux_amd64
# test docker container (empty path, no network egress) and on the macOS
# osx_arm64 test runner (hub icu not reliably resolvable). We copy the
# icu.duckdb_extension that was built locally as part of this extension's
# build (declared in extension_config.cmake) into the expected path,
# matched to the DuckDB platform string, before running the unittester.
DUCKDB_VERSION_TAG := v1.4.4

define stage_icu
@if [ -f ./build/$(1)/extension/icu/icu.duckdb_extension ]; then \
case "$$(uname -s)-$$(uname -m)" in \
Linux-x86_64) platform=linux_amd64 ;; \
Linux-aarch64) platform=linux_arm64 ;; \
Darwin-arm64) platform=osx_arm64 ;; \
Darwin-x86_64) platform=osx_amd64 ;; \
*) platform=$$(uname -m) ;; \
esac; \
target=$$HOME/.duckdb/extensions/$(DUCKDB_VERSION_TAG)/$$platform; \
mkdir -p "$$target" && cp -f ./build/$(1)/extension/icu/icu.duckdb_extension "$$target/" && \
echo "Staged icu.duckdb_extension at $$target/"; \
fi
endef

test_release_internal:
$(call stage_icu,release)
./build/release/$(TEST_PATH) "$(PROJ_DIR)test/*"
test_debug_internal:
$(call stage_icu,debug)
./build/debug/$(TEST_PATH) "$(PROJ_DIR)test/*"
test_reldebug_internal:
./build/reldebug/$(TEST_PATH) "$(PROJ_DIR)test/*"
$(call stage_icu,reldebug)
./build/reldebug/$(TEST_PATH) "$(PROJ_DIR)test/*"
5 changes: 3 additions & 2 deletions src/geo/geoset.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include "geo/geoset.hpp"
#include "mobilityduck/meos_guarded_cast.hpp"
#include "tydef.hpp"
#include "geo_util.hpp"
#include "duckdb/common/types/data_chunk.hpp"
Expand Down Expand Up @@ -36,12 +37,12 @@ void SpatialSetType::RegisterTypes(ExtensionLoader &loader){
}

void SpatialSetType::RegisterCastFunctions(ExtensionLoader &loader) {
loader.RegisterCastFunction(
duckdb::RegisterGuardedCastFunction(loader,
LogicalType::VARCHAR,
SpatialSetType::geomset(),
SpatialSetFunctions::Text_to_geoset
);
loader.RegisterCastFunction(
duckdb::RegisterGuardedCastFunction(loader,
LogicalType::VARCHAR,
SpatialSetType::geogset(),
SpatialSetFunctions::Text_to_geoset
Expand Down
15 changes: 8 additions & 7 deletions src/geo/stbox.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include "meos_wrapper_simple.hpp"
#include "mobilityduck/meos_guarded_cast.hpp"

#include "common.hpp"
#include "geo/stbox.hpp"
Expand Down Expand Up @@ -27,43 +28,43 @@ void StboxType::RegisterType(ExtensionLoader &loader) {
}

void StboxType::RegisterCastFunctions(ExtensionLoader &loader) {
loader.RegisterCastFunction(
duckdb::RegisterGuardedCastFunction(loader,
LogicalType::VARCHAR,
STBOX(),
StboxFunctions::Stbox_in_cast
);

loader.RegisterCastFunction(
duckdb::RegisterGuardedCastFunction(loader,
STBOX(),
LogicalType::VARCHAR,
StboxFunctions::Stbox_out
);

loader.RegisterCastFunction(
duckdb::RegisterGuardedCastFunction(loader,
GeoTypes::GEOMETRY(),
STBOX(),
StboxFunctions::Geo_to_stbox_cast
);

loader.RegisterCastFunction(
duckdb::RegisterGuardedCastFunction(loader,
LogicalType::TIMESTAMP_TZ,
STBOX(),
StboxFunctions::Timestamptz_to_stbox_cast
);

loader.RegisterCastFunction(
duckdb::RegisterGuardedCastFunction(loader,
SetTypes::tstzset(),
STBOX(),
StboxFunctions::Tstzset_to_stbox_cast
);

loader.RegisterCastFunction(
duckdb::RegisterGuardedCastFunction(loader,
SpanTypes::TSTZSPAN(),
STBOX(),
StboxFunctions::Tstzspan_to_stbox_cast
);

loader.RegisterCastFunction(
duckdb::RegisterGuardedCastFunction(loader,
SpansetTypes::tstzspanset(),
STBOX(),
StboxFunctions::Tstzspanset_to_stbox_cast
Expand Down
9 changes: 5 additions & 4 deletions src/geo/tgeogpoint.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include "meos_wrapper_simple.hpp"
#include "mobilityduck/meos_guarded_cast.hpp"

#include "common.hpp"
#include "geo/tgeogpoint.hpp"
Expand Down Expand Up @@ -43,25 +44,25 @@ void TgeogpointType::RegisterType(ExtensionLoader &loader) {
}

void TgeogpointType::RegisterCastFunctions(ExtensionLoader &loader) {
loader.RegisterCastFunction(
duckdb::RegisterGuardedCastFunction(loader,
LogicalType::VARCHAR,
TGEOGPOINT(),
TgeogpointFunctions::Tpoint_in
);

loader.RegisterCastFunction(
duckdb::RegisterGuardedCastFunction(loader,
TGEOGPOINT(),
LogicalType::VARCHAR,
TemporalFunctions::Temporal_out
);

loader.RegisterCastFunction(
duckdb::RegisterGuardedCastFunction(loader,
TGEOGPOINT(),
StboxType::STBOX(),
TgeompointFunctions::Tspatial_to_stbox_cast
);

loader.RegisterCastFunction(
duckdb::RegisterGuardedCastFunction(loader,
TGEOGPOINT(),
SpanTypes::TSTZSPAN(),
TgeompointFunctions::Temporal_to_tstzspan_cast
Expand Down
5 changes: 3 additions & 2 deletions src/geo/tgeogpoint_in_out.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include "geo/tgeogpoint.hpp"
#include "mobilityduck/meos_guarded_cast.hpp"
#include "geo/tgeogpoint_functions.hpp"
#include "duckdb/main/extension/extension_loader.hpp"
#include "duckdb/common/extension_type_info.hpp"
Expand Down Expand Up @@ -215,8 +216,8 @@ void TGeogpointType::RegisterScalarInOutFunctions(ExtensionLoader &loader){


void TGeogpointType::RegisterCastFunctions(ExtensionLoader &loader) {
loader.RegisterCastFunction( LogicalType::VARCHAR, TGeogpointType::TGEOGPOINT(), TgeogpointFunctions::StringToTgeogpoint);
loader.RegisterCastFunction( TGeogpointType::TGEOGPOINT(), LogicalType::VARCHAR, TgeogpointFunctions::TgeogpointToString);
duckdb::RegisterGuardedCastFunction(loader, LogicalType::VARCHAR, TGeogpointType::TGEOGPOINT(), TgeogpointFunctions::StringToTgeogpoint);
duckdb::RegisterGuardedCastFunction(loader, TGeogpointType::TGEOGPOINT(), LogicalType::VARCHAR, TgeogpointFunctions::TgeogpointToString);
}

}
5 changes: 3 additions & 2 deletions src/geo/tgeography_in_out.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include "geo/tgeography.hpp"
#include "mobilityduck/meos_guarded_cast.hpp"
#include "duckdb/main/extension/extension_loader.hpp"
#include "duckdb/common/extension_type_info.hpp"
#include <regex>
Expand Down Expand Up @@ -288,8 +289,8 @@ void TGeographyTypes::RegisterScalarInOutFunctions(ExtensionLoader &loader){


void TGeographyTypes::RegisterCastFunctions(ExtensionLoader &loader) {
loader.RegisterCastFunction( LogicalType::VARCHAR, TGeographyTypes::TGEOGRAPHY(), TgeographyFunctions::StringToTgeography);
loader.RegisterCastFunction( TGeographyTypes::TGEOGRAPHY(), LogicalType::VARCHAR, TgeographyFunctions::TgeographyToString);
duckdb::RegisterGuardedCastFunction(loader, LogicalType::VARCHAR, TGeographyTypes::TGEOGRAPHY(), TgeographyFunctions::StringToTgeography);
duckdb::RegisterGuardedCastFunction(loader, TGeographyTypes::TGEOGRAPHY(), LogicalType::VARCHAR, TgeographyFunctions::TgeographyToString);
}

}
5 changes: 3 additions & 2 deletions src/geo/tgeometry_in_out.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include "geo/tgeometry.hpp"
#include "mobilityduck/meos_guarded_cast.hpp"
#include "duckdb/main/extension/extension_loader.hpp"
#include "duckdb/common/extension_type_info.hpp"
#include <regex>
Expand Down Expand Up @@ -292,8 +293,8 @@ void TGeometryTypes::RegisterScalarInOutFunctions(ExtensionLoader &loader){


void TGeometryTypes::RegisterCastFunctions(ExtensionLoader &loader) {
loader.RegisterCastFunction( LogicalType::VARCHAR, TGeometryTypes::TGEOMETRY(), TgeometryFunctions::StringToTgeometry);
loader.RegisterCastFunction( TGeometryTypes::TGEOMETRY(), LogicalType::VARCHAR, TgeometryFunctions::TgeometryToString);
duckdb::RegisterGuardedCastFunction(loader, LogicalType::VARCHAR, TGeometryTypes::TGEOMETRY(), TgeometryFunctions::StringToTgeometry);
duckdb::RegisterGuardedCastFunction(loader, TGeometryTypes::TGEOMETRY(), LogicalType::VARCHAR, TgeometryFunctions::TgeometryToString);
}

}
9 changes: 5 additions & 4 deletions src/geo/tgeompoint.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include "meos_wrapper_simple.hpp"
#include "mobilityduck/meos_guarded_cast.hpp"

#include "common.hpp"
#include "geo/tgeompoint.hpp"
Expand Down Expand Up @@ -34,25 +35,25 @@ void TgeompointType::RegisterType(ExtensionLoader &loader) {
}

void TgeompointType::RegisterCastFunctions(ExtensionLoader &loader) {
loader.RegisterCastFunction(
duckdb::RegisterGuardedCastFunction(loader,
LogicalType::VARCHAR,
TGEOMPOINT(),
TgeompointFunctions::Tpoint_in
);

loader.RegisterCastFunction(
duckdb::RegisterGuardedCastFunction(loader,
TGEOMPOINT(),
LogicalType::VARCHAR,
TemporalFunctions::Temporal_out
);

loader.RegisterCastFunction(
duckdb::RegisterGuardedCastFunction(loader,
TGEOMPOINT(),
StboxType::STBOX(),
TgeompointFunctions::Tspatial_to_stbox_cast
);

loader.RegisterCastFunction(
duckdb::RegisterGuardedCastFunction(loader,
TGEOMPOINT(),
SpanTypes::TSTZSPAN(),
TgeompointFunctions::Temporal_to_tstzspan_cast
Expand Down
46 changes: 46 additions & 0 deletions src/include/mobilityduck/meos_error_guard.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#pragma once

#include <setjmp.h>
#include <string>
#include <utility>

#include "duckdb/common/exception.hpp"

namespace duckdb {

// MEOS reports errors through a C callback installed with
// meos_initialize_error_handler(). Throwing a C++ exception directly out of
// that callback unwinds *through* MEOS's C stack frames, which is undefined
// behaviour: MEOS's thread-local error buffer, the lwgeom WKT parser and the
// GEOS context are left half-updated, so the next MEOS call SIGSEGVs
// non-deterministically (see project_mobilityduck_cast_segv). Instead the
// handler siglongjmp()s back to the nearest guarded boundary and the DuckDB
// exception is thrown there, from pure C++ frames only.
//
// Defined in mobilityduck_extension.cpp (the translation unit that owns the
// MEOS error handler).
extern thread_local sigjmp_buf MeosJmpBuf;
extern thread_local bool MeosGuardActive;
extern thread_local std::string MeosErrMsg;

// Run body() with a MEOS longjmp landing pad installed. If MEOS raises an
// error (level >= ERROR) anywhere inside body(), control returns here and a
// DuckDB InvalidInputException is thrown from C++ — no exception ever unwinds
// through MEOS C frames. Non-reentrant by construction: the guarded boundary
// is the outermost point of a single scalar/cast execution; DuckDB does not
// nest one registered function's executor inside another's. MeosGuardActive
// and MeosErrMsg have thread/static storage, so they are exempt from the
// setjmp local-clobber rule; this is the standard safe setjmp-wrapper shape
// (sigsetjmp in a function that then invokes a callback).
template <class Body>
inline void MeosGuardedRun(Body &&body) {
MeosGuardActive = true;
if (sigsetjmp(MeosJmpBuf, 0) != 0) {
MeosGuardActive = false;
throw InvalidInputException(MeosErrMsg);
}
body();
MeosGuardActive = false;
}

} // namespace duckdb
7 changes: 6 additions & 1 deletion src/include/mobilityduck/meos_exec_serial.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

#include "duckdb/function/scalar_function.hpp"
#include "duckdb/main/extension/extension_loader.hpp"
#include "mobilityduck/meos_error_guard.hpp"

namespace duckdb {

Expand All @@ -26,7 +27,11 @@ inline ScalarFunction WrapScalarFunctionWithMeosExecMutex(ScalarFunction sf) {
scalar_function_t orig = std::move(sf.function);
sf.function = [orig = std::move(orig)](DataChunk &args, ExpressionState &state, Vector &result) {
std::lock_guard<std::mutex> guard(MeosSerializedExecMutex());
orig(args, state, result);
// Run inside the MEOS longjmp landing pad: a MEOS error becomes a
// clean DuckDB exception thrown from C++ here, never unwound through
// MEOS C frames. The lock_guard stays outside so the mutex is
// released by normal C++ unwinding when that exception propagates.
MeosGuardedRun([&]() { orig(args, state, result); });
};
return sf;
}
Expand Down
58 changes: 58 additions & 0 deletions src/include/mobilityduck/meos_guarded_cast.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#pragma once

#include <mutex>

#include "duckdb/common/helper.hpp"
#include "duckdb/function/cast/default_casts.hpp"
#include "duckdb/main/extension/extension_loader.hpp"
#include "mobilityduck/meos_error_guard.hpp"
#include "mobilityduck/meos_exec_serial.hpp"

namespace duckdb {

// DuckDB cast functions are raw `cast_function_t` pointers, not std::function,
// so they cannot be wrapped by a capturing lambda the way serialized scalar
// functions are (see meos_exec_serial.hpp). To run a MEOS-backed cast inside
// the longjmp landing pad we carry the original function pointer in the
// per-cast BoundCastData payload and dispatch through one generic trampoline.
// Without this, a MEOS error during a VARCHAR -> mobility-type cast unwinds a
// C++ exception through MEOS C frames (undefined behaviour -> the documented
// non-deterministic cast SIGSEGV).
struct MeosGuardedCastData : public BoundCastData {
explicit MeosGuardedCastData(cast_function_t orig_p) : orig(orig_p) {
}
cast_function_t orig;
unique_ptr<BoundCastData> Copy() const override {
return make_uniq<MeosGuardedCastData>(orig);
}
};

inline bool MeosGuardedCastTrampoline(Vector &source, Vector &result, idx_t count,
CastParameters &parameters) {
cast_function_t orig = parameters.cast_data->Cast<MeosGuardedCastData>().orig;
bool ok = true;
// MEOS global state (the pg-derived session timezone and its transition
// cache reached via timestamp2tm/localsub, plus the legacy GEOS context)
// is not thread-safe. Casts must take the SAME serialization mutex as
// scalar functions (meos_exec_serial.hpp), otherwise a cast parsing a
// timestamp on one DuckDB worker thread races a scalar's
// tinstant_to_string on another -> SIGSEGV in localsub. Mutex outside the
// guard so it is released by normal unwinding if the guard throws.
std::lock_guard<std::mutex> serialize(MeosSerializedExecMutex());
MeosGuardedRun([&]() { ok = orig(source, result, count, parameters); });
return ok;
}

// Drop-in replacement for `loader.RegisterCastFunction(source, target, fn[, cost])`
// that runs `fn` inside the MEOS longjmp guard. Cost default mirrors DuckDB's
// own RegisterCastFunction default (-1).
inline void RegisterGuardedCastFunction(ExtensionLoader &loader, const LogicalType &source,
const LogicalType &target, cast_function_t orig,
int64_t implicit_cast_cost = -1) {
loader.RegisterCastFunction(
source, target,
BoundCastInfo(MeosGuardedCastTrampoline, make_uniq<MeosGuardedCastData>(orig)),
implicit_cast_cost);
}

} // namespace duckdb
Loading
Loading