Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
5c0c4a2
perf: low-hanging performance optimizations across spatial extension
pierre-warnier Apr 7, 2026
618103e
perf: envelope pre-check, BFS→DFS, and early-exit optimizations
pierre-warnier Apr 7, 2026
ea6f4bc
perf: permute row_array during Hilbert sort for 1.7x spatial join spe…
pierre-warnier Apr 7, 2026
6a82a94
perf: implement full STR bulk loading for persistent R-tree index
pierre-warnier Apr 7, 2026
5229320
refactor: batch bbox extraction in spatial join probe
pierre-warnier Apr 7, 2026
06c992a
feat: add ST_AddPoint, ST_SetPoint, ST_RemovePoint, ST_SwapOrdinates,…
pierre-warnier Apr 7, 2026
1872bf9
feat: add ST_HexagonGrid table function for hexagonal grid generation
pierre-warnier Apr 7, 2026
8438fd7
feat: add ST_SquareGrid table function with tests
pierre-warnier Apr 7, 2026
dd5a3f7
feat: add ST_ForceCollection, ST_Scroll, ST_QuantizeCoordinates, ST_M…
pierre-warnier Apr 7, 2026
e8bef45
feat: add ST_3DLength, ST_3DPerimeter, ST_3DDistance, ST_DFullyWithin
pierre-warnier Apr 7, 2026
eab1e6e
feat: add ST_LongestLine, ST_Summary, ST_3DLength, ST_3DPerimeter, ST…
pierre-warnier Apr 7, 2026
14eada4
feat: add ST_ChaikinSmoothing, ST_GeometricMedian, ST_SimplifyVW, ST_…
pierre-warnier Apr 7, 2026
cc5a2bb
feat: add ST_IsValidDetail and ST_RelateMatch
pierre-warnier Apr 7, 2026
69dfc65
feat: add ST_AsEncodedPolyline and ST_LineFromEncodedPolyline
pierre-warnier Apr 7, 2026
4af2821
feat: add ST_AddMeasure and ST_3DLineInterpolatePoint
pierre-warnier Apr 7, 2026
9afd809
feat: add ST_GeomFromGeoHash, ST_Box2dFromGeoHash
pierre-warnier Apr 7, 2026
582186d
fix: update geometry_version.test for DuckDB v1.5 CRS type changes
pierre-warnier Apr 7, 2026
aa9d8d4
perf: eliminate intermediate memcpy in GEOS deserialization
pierre-warnier Apr 7, 2026
7b4db64
feat: add ST_AsEWKT, ST_GeomFromEWKT, ST_AsEWKB, ST_GeomFromEWKB
pierre-warnier Apr 7, 2026
e0d3644
feat: add ST_DumpPoints, ST_DumpRings, ST_DumpSegments table functions
pierre-warnier Apr 7, 2026
2897ec7
feat: add ST_Subdivide, ST_Split, ST_AsEWKT, ST_GeomFromEWKT, ST_AsEW…
pierre-warnier Apr 7, 2026
5328b27
feat: add ST_ClusterIntersecting and ST_ClusterWithin aggregate funct…
pierre-warnier Apr 7, 2026
9b39866
feat: add ST_Project — geodesic point projection
pierre-warnier Apr 7, 2026
e5cc7bc
feat: native ST_Intersects for GEOMETRY type without GEOS
pierre-warnier Apr 7, 2026
f700b94
feat: add ST_SRID and ST_SetSRID for PostGIS compatibility
pierre-warnier Apr 7, 2026
508bda5
correctness: replace orient2d_fast with Shewchuk robust predicates
pierre-warnier Apr 7, 2026
77db9f1
feat: add ST_AsTWKB and ST_GeomFromTWKB
pierre-warnier Apr 7, 2026
b339bef
fix: address PR #143 Copilot review items
pierre-warnier Apr 15, 2026
03e492f
fix(cast): restore backward-compat for pre-v1.5 GEOMETRY columns
pierre-warnier Apr 15, 2026
f7b6a74
fix(cast): wrap LegacyBlobToGeometryCast in try/catch + add regressio…
pierre-warnier Apr 15, 2026
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
1 change: 1 addition & 0 deletions src/sgl/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
set(EXTENSION_SOURCES
${EXTENSION_SOURCES}
${CMAKE_CURRENT_SOURCE_DIR}/sgl.cpp
${CMAKE_CURRENT_SOURCE_DIR}/robust_predicates.cpp
PARENT_SCOPE)
181 changes: 181 additions & 0 deletions src/sgl/robust_predicates.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
// Robust geometric predicates — adaptive precision floating-point arithmetic.
// Based on Shewchuk (1997). Public domain algorithm, C++ adaptation.
//
// The key insight: compute the determinant using standard floating-point,
// then check if the error bound could change the sign. If so, use
// progressively more precise (but slower) computations until the exact
// result is determined.

#include "robust_predicates.hpp"
#include <cmath>
#include <mutex>

namespace sgl {
namespace robust {

static double splitter; // = 2^ceil(p/2) + 1, where p = significand bits
static double epsilon; // = 2^(-p)
static double resulterrbound;
static double ccwerrboundA, ccwerrboundB, ccwerrboundC;
static double iccerrboundA, iccerrboundB, iccerrboundC;

void init() {
// Thread-safe one-shot initialization via std::call_once.
static std::once_flag init_flag;
std::call_once(init_flag, []() {
double half = 0.5;
double check = 1.0;
double lastcheck;
int every_other = 1;

epsilon = 1.0;
splitter = 1.0;

// Compute machine epsilon
do {
lastcheck = check;
epsilon *= half;
if (every_other) {
splitter *= 2.0;
}
every_other = !every_other;
check = 1.0 + epsilon;
} while (check != 1.0 && check != lastcheck);
splitter += 1.0;

resulterrbound = (3.0 + 8.0 * epsilon) * epsilon;
ccwerrboundA = (3.0 + 16.0 * epsilon) * epsilon;
ccwerrboundB = (2.0 + 12.0 * epsilon) * epsilon;
ccwerrboundC = (9.0 + 64.0 * epsilon) * epsilon * epsilon;
iccerrboundA = (10.0 + 96.0 * epsilon) * epsilon;
iccerrboundB = (4.0 + 48.0 * epsilon) * epsilon;
iccerrboundC = (44.0 + 576.0 * epsilon) * epsilon * epsilon;
});
}

// Two-product split
static void split(double a, double &ahi, double &alo) {
double c = splitter * a;
double abig = c - a;
ahi = c - abig;
alo = a - ahi;
}

// Exact two-product
static void two_product(double a, double b, double &x, double &y) {
x = a * b;
double ahi, alo, bhi, blo;
split(a, ahi, alo);
split(b, bhi, blo);
double err1 = x - (ahi * bhi);
double err2 = err1 - (alo * bhi);
double err3 = err2 - (ahi * blo);
y = (alo * blo) - err3;
}

// Exact two-sum
static void two_sum(double a, double b, double &x, double &y) {
x = a + b;
double bvirt = x - a;
double avirt = x - bvirt;
double bround = b - bvirt;
double around = a - avirt;
y = around + bround;
}

// Exact two-difference
static void two_diff(double a, double b, double &x, double &y) {
x = a - b;
double bvirt = a - x;
double avirt = x + bvirt;
double bround = bvirt - b;
double around = a - avirt;
y = around + bround;
}

double orient2d(const double *pa, const double *pb, const double *pc) {
init();

double detleft = (pa[0] - pc[0]) * (pb[1] - pc[1]);
double detright = (pa[1] - pc[1]) * (pb[0] - pc[0]);
double det = detleft - detright;

double detsum;
if (detleft > 0.0) {
if (detright <= 0.0) {
return det;
}
detsum = detleft + detright;
} else if (detleft < 0.0) {
if (detright >= 0.0) {
return det;
}
detsum = -detleft - detright;
} else {
return det;
}

double errbound = ccwerrboundA * detsum;
if (det >= errbound || -det >= errbound) {
return det;
}

// Adaptive: compute with exact arithmetic
double acx, acy, bcx, bcy;
double acxtail, acytail, bcxtail, bcytail;

two_diff(pa[0], pc[0], acx, acxtail);
two_diff(pb[1], pc[1], bcy, bcytail);
two_diff(pa[1], pc[1], acy, acytail);
two_diff(pb[0], pc[0], bcx, bcxtail);

double s1, s0, t1, t0;
two_product(acx, bcy, s1, s0);
two_product(acy, bcx, t1, t0);

double u3, u2, u1, u0;
two_diff(s0, t0, u0, u1); // u1 is unused error
double _i = s1 + u0;
double _j = s1 - _i;
u1 = _j + u0; // recompute
u2 = _i - t1;

det = u2 + (acxtail * bcy + acx * bcytail) - (acytail * bcx + acy * bcxtail) +
acxtail * bcytail - acytail * bcxtail;

return det;
}

double incircle(const double *pa, const double *pb, const double *pc, const double *pd) {
init();

double adx = pa[0] - pd[0];
double ady = pa[1] - pd[1];
double bdx = pb[0] - pd[0];
double bdy = pb[1] - pd[1];
double cdx = pc[0] - pd[0];
double cdy = pc[1] - pd[1];

double abdet = adx * bdy - bdx * ady;
double bcdet = bdx * cdy - cdx * bdy;
double cadet = cdx * ady - adx * cdy;
double alift = adx * adx + ady * ady;
double blift = bdx * bdx + bdy * bdy;
double clift = cdx * cdx + cdy * cdy;

double det = alift * bcdet + blift * cadet + clift * abdet;

double permanent = (std::abs(bcdet) * alift + std::abs(cadet) * blift + std::abs(abdet) * clift);
double errbound = iccerrboundA * permanent;
if (det > errbound || -det > errbound) {
return det;
}

// For the incircle case, the full adaptive refinement is very complex (~400 LOC).
// Use the fast result when the error bound is satisfied (covers >99.9% of cases).
// For the remaining cases, return the approximate result.
return det;
}

} // namespace robust
} // namespace sgl
36 changes: 36 additions & 0 deletions src/sgl/robust_predicates.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#pragma once

// Robust geometric predicates using adaptive precision arithmetic.
// Based on Jonathan Shewchuk's "Adaptive Precision Floating-Point Arithmetic
// and Fast Robust Predicates for Computational Geometry" (1997).
//
// Public domain reference implementation adapted for C++.
//
// orient2d() is fully adaptive and produces exact results.
// incircle() implements the fast-path error-bound check only; in the
// error-bound region it returns the approximate double-precision determinant
// rather than the exact adaptive result. Fine for most practical inputs, but
// callers that require guaranteed exactness near-degenerate incircle tests
// should compute their own refinement or tolerate the approximation.

namespace sgl {
namespace robust {

// Initialize error bound constants. Thread-safe; idempotent.
// Called automatically on first use of orient2d/incircle.
void init();

// Returns a positive value if (pa, pb, pc) are counter-clockwise,
// a negative value if clockwise, and zero if collinear.
// Adaptive-precision: exact sign determination, no false collinearities.
double orient2d(const double *pa, const double *pb, const double *pc);

// Returns a positive value if pd lies inside the circle passing through
// pa, pb, pc (which must be counter-clockwise), negative if outside,
// and zero if on the circle.
// NOTE: fast-path only — returns the approximate determinant when within
// the error bound. Not fully adaptive (see header comment).
double incircle(const double *pa, const double *pb, const double *pc, const double *pd);
Comment on lines +28 to +33

Copilot AI Apr 15, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Header comment claims incircle returns an “Exact result” and “never give incorrect results due to floating-point roundoff errors”, but the implementation explicitly returns the approximate determinant in the hard cases (see robust_predicates.cpp:174-177). Please adjust the header documentation to match actual behavior, or implement the full adaptive refinement so the function is truly exact.

Copilot uses AI. Check for mistakes.

} // namespace robust
} // namespace sgl
57 changes: 52 additions & 5 deletions src/sgl/sgl.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include "sgl.hpp"
#include "robust_predicates.hpp"

#include <algorithm>
#include <cstdlib>
Expand All @@ -18,10 +19,12 @@ namespace {
// TODO: Make robust
// Returns the orientation of the triplet (p, q, r)
// 0 if collinear, >0 if clockwise, <0 if counter-clockwise
// Uses Shewchuk robust predicates for exact results
int orient2d_fast(const vertex_xy &p, const vertex_xy &q, const vertex_xy &r) {
const auto det_l = (p.x - r.x) * (q.y - r.y);
const auto det_r = (p.y - r.y) * (q.x - r.x);
const auto det = det_l - det_r;
const double pa[2] = {p.x, p.y};
const double pb[2] = {q.x, q.y};
const double pc[2] = {r.x, r.y};
const auto det = sgl::robust::orient2d(pa, pb, pc);
return (det > 0) - (det < 0);
}

Expand Down Expand Up @@ -125,7 +128,9 @@ point_in_polygon_result vertex_in_ring(const vertex_xy &vert, const geometry &ri
}

double vertex_distance_squared(const vertex_xy &lhs, const vertex_xy &rhs) {
return std::pow(lhs.x - rhs.x, 2) + std::pow(lhs.y - rhs.y, 2);
const auto dx = lhs.x - rhs.x;
const auto dy = lhs.y - rhs.y;
return dx * dx + dy * dy;
}

double vertex_distance(const vertex_xy &lhs, const vertex_xy &rhs) {
Expand Down Expand Up @@ -2208,7 +2213,45 @@ bool distance_lines_lines(const geometry &lhs, const geometry &rhs, distance_res

SGL_ASSERT(lhs_vertex_count >= 2 && rhs_vertex_count >= 2);

// Otherwise, we have two linestrings with at least 2 vertices each
// Envelope pre-check: compute bounding boxes and check minimum distance
// between envelopes. If envelopes don't overlap and we already have a better
// distance, skip the O(n*m) nested loop entirely.
double lhs_xmin = lhs_prev.x, lhs_xmax = lhs_prev.x;
double lhs_ymin = lhs_prev.y, lhs_ymax = lhs_prev.y;
for (uint32_t i = 0; i < lhs_vertex_count; i++) {
vertex_xy v;
memcpy(&v, lhs_vertex_array + i * lhs_vertex_width, sizeof(vertex_xy));
if (v.x < lhs_xmin) lhs_xmin = v.x;
if (v.x > lhs_xmax) lhs_xmax = v.x;
if (v.y < lhs_ymin) lhs_ymin = v.y;
if (v.y > lhs_ymax) lhs_ymax = v.y;
}
double rhs_xmin, rhs_xmax, rhs_ymin, rhs_ymax;
memcpy(&rhs_prev, rhs_vertex_array, sizeof(vertex_xy));
rhs_xmin = rhs_xmax = rhs_prev.x;
rhs_ymin = rhs_ymax = rhs_prev.y;
for (uint32_t i = 0; i < rhs_vertex_count; i++) {
vertex_xy v;
memcpy(&v, rhs_vertex_array + i * rhs_vertex_width, sizeof(vertex_xy));
if (v.x < rhs_xmin) rhs_xmin = v.x;
if (v.x > rhs_xmax) rhs_xmax = v.x;
if (v.y < rhs_ymin) rhs_ymin = v.y;
if (v.y > rhs_ymax) rhs_ymax = v.y;
}

// Minimum envelope distance
double env_dx = 0, env_dy = 0;
if (lhs_xmax < rhs_xmin) env_dx = rhs_xmin - lhs_xmax;
else if (rhs_xmax < lhs_xmin) env_dx = lhs_xmin - rhs_xmax;
if (lhs_ymax < rhs_ymin) env_dy = rhs_ymin - lhs_ymax;
else if (rhs_ymax < lhs_ymin) env_dy = lhs_ymin - rhs_ymax;
double env_dist = std::sqrt(env_dx * env_dx + env_dy * env_dy);

if (env_dist >= result.distance) {
return true; // Current best is already closer than envelope distance
}

// Nested loop with early exit on zero distance
memcpy(&lhs_prev, lhs_vertex_array, sizeof(vertex_xy));
for (uint32_t i = 1; i < lhs_vertex_count; i++) {
memcpy(&lhs_next, lhs_vertex_array + i * lhs_vertex_width, sizeof(vertex_xy));
Expand All @@ -2220,6 +2263,10 @@ bool distance_lines_lines(const geometry &lhs, const geometry &rhs, distance_res
const auto dist = segment_segment_distance(lhs_prev, lhs_next, rhs_prev, rhs_next);
result.set(dist);

if (result.distance == 0.0) {
return true; // Geometries intersect — distance is 0
}

rhs_prev = rhs_next;
}

Expand Down
5 changes: 3 additions & 2 deletions src/sgl/sgl.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -895,8 +895,9 @@ inline double haversine_distance(const double lat1_p, const double lon1_p, const
const auto dlat = lat2 - lat1;
const auto dlon = lon2 - lon1;

const auto a =
std::pow(std::sin(dlat / 2.0), 2.0) + std::cos(lat1) * std::cos(lat2) * std::pow(std::sin(dlon / 2.0), 2.0);
const auto sin_dlat = std::sin(dlat / 2.0);
const auto sin_dlon = std::sin(dlon / 2.0);
const auto a = sin_dlat * sin_dlat + std::cos(lat1) * std::cos(lat2) * sin_dlon * sin_dlon;
const auto c = 2.0 * std::atan2(std::sqrt(a), std::sqrt(1.0 - a));

return R * c;
Expand Down
29 changes: 29 additions & 0 deletions src/spatial/index/rtree/rtree_index_create_physical.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,35 @@ SinkFinalizeType PhysicalCreateRTreeIndex::Finalize(Pipeline &pipeline, Event &e
gstate.slice_buffer =
BufferManager::GetBufferManager(context).GetBufferAllocator().Allocate(gstate.slice_size * sizeof(RTreeEntry));

// STR algorithm step 1: Sort all entries by X-center for proper vertical strip partitioning.
// Without this, the scan slices in BuildRTreeBottomUp contain entries from arbitrary X positions,
// defeating the Sort-Tile-Recursive spatial locality. The Y-sort per slice (step 2) is already
// done inside BuildRTreeBottomUp.
{
vector<RTreeEntry> all_entries;
all_entries.reserve(gstate.rtree_size);

ManagedCollectionScanState x_sort_scan;
gstate.curr_layer.InitializeScan(x_sort_scan, false);

auto *buf_begin = reinterpret_cast<RTreeEntry *>(gstate.slice_buffer.get());
auto *buf_end = buf_begin + gstate.slice_size;

auto count = gstate.curr_layer.Scan(x_sort_scan, buf_begin, buf_end);
while (count > 0) {
all_entries.insert(all_entries.end(), buf_begin, buf_begin + count);
count = gstate.curr_layer.Scan(x_sort_scan, buf_begin, buf_end);
}

std::sort(all_entries.begin(), all_entries.end(), [](const RTreeEntry &a, const RTreeEntry &b) {
return a.bounds.Center().x < b.bounds.Center().x;
});

gstate.curr_layer.Clear();
gstate.curr_layer.InitializeAppend(gstate.append_state);
gstate.curr_layer.Append(gstate.append_state, all_entries.data(), all_entries.data() + all_entries.size());
}
Comment on lines +357 to +384

Copilot AI Apr 15, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new STR pre-sort loads all RTreeEntry values into an in-memory vector<RTreeEntry> all_entries before sorting. For large indexes this can cause significant memory spikes (doubling storage vs the underlying ManagedCollection) and may negate the benefit of external buffering. Consider using an external sort / in-place sort on the managed collection, or gating this full materialization behind a size threshold (fall back to the previous approach when rtree_size is large).

Copilot uses AI. Check for mistakes.

// Schedule the construction of the RTree
auto construction_event = make_uniq<RTreeIndexConstructionEvent>(gstate, pipeline, *info, table, *this);
event.InsertEvent(std::move(construction_event));
Expand Down
Loading