diff --git a/.github/workflows/c.yml b/.github/workflows/c.yml
deleted file mode 100644
index b00b272..0000000
--- a/.github/workflows/c.yml
+++ /dev/null
@@ -1,23 +0,0 @@
-name: C/C++ CI
-
-on:
- push:
- branches: [ "main" ]
- pull_request:
- branches: [ "main" ]
-
-jobs:
- build:
-
- runs-on: ubuntu-latest
-
- steps:
- - uses: actions/checkout@v3
- - name: configure
- run: ./configure
- - name: make
- run: make
- - name: make check
- run: make check
- - name: make distcheck
- run: make distcheck
diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml
deleted file mode 100644
index cf8e1ce..0000000
--- a/.github/workflows/cmake.yml
+++ /dev/null
@@ -1,37 +0,0 @@
-name: CMake
-
-on:
- push:
- branches: [ "main" ]
- pull_request:
- branches: [ "main" ]
-
-env:
- # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.)
- BUILD_TYPE: Release
-
-jobs:
- build:
- # The CMake configure and build commands are platform agnostic and should work equally well on Windows or Mac.
- # You can convert this to a matrix build if you need cross-platform coverage.
- # See: https://docs.github.com/en/free-pro-team@latest/actions/learn-github-actions/managing-complex-workflows#using-a-build-matrix
- runs-on: ubuntu-latest
-
- steps:
- - uses: actions/checkout@v3
-
- - name: Configure CMake
- # Configure CMake in a 'build' subdirectory. `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make.
- # See https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html?highlight=cmake_build_type
- run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}}
-
- - name: Build
- # Build your program with the given configuration
- run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}}
-
- - name: Test
- working-directory: ${{github.workspace}}/build
- # Execute tests defined by the CMake configuration.
- # See https://cmake.org/cmake/help/latest/manual/ctest.1.html for more detail
- run: ctest -C ${{env.BUILD_TYPE}}
-
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
new file mode 100644
index 0000000..f76f70d
--- /dev/null
+++ b/.github/workflows/lint.yml
@@ -0,0 +1,51 @@
+name: Lint
+
+# Static analysis only -- no compiling. This project's real build needs
+# PostgreSQL + Citus + PostGIS headers, some of which turned out to be
+# undocumented/version-specific gaps not shipped by any package (see
+# third_party/postgis-lwgeom/README.md), making a genuine `cmake --build`
+# too fragile to run reliably as a PR gate for now. These jobs instead
+# catch real bugs/style issues in the C sources, shell scripts, and the
+# workflow files themselves, without needing that environment at all.
+
+on:
+ push:
+ branches: [ "main" ]
+ pull_request:
+ branches: [ "main" ]
+
+jobs:
+ actionlint:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: raven-actions/actionlint@v2.2.0
+
+ shellcheck:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: ludeeus/action-shellcheck@2.0.0
+ with:
+ scandir: './scripts'
+ severity: warning
+
+ cppcheck:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Install cppcheck
+ run: sudo apt-get update && sudo apt-get install -y cppcheck
+ - name: Run cppcheck
+ run: |
+ # 'style' is deliberately excluded: it's almost entirely const-
+ # correctness/redundant-condition suggestions with no real bugs
+ # found in this codebase, and including it would make this job
+ # fail on pre-existing code with nothing actionable for a PR to
+ # fix. 'warning'/'performance'/'portability' plus cppcheck's
+ # always-on checks (missingReturn, uninitvar, null dereference)
+ # are what actually caught real bugs while writing this job.
+ cppcheck --enable=warning,performance,portability \
+ --suppress=missingInclude --suppress=missingIncludeSystem \
+ --inline-suppr \
+ --error-exitcode=1 -I include src
diff --git a/CMakeLists.txt b/CMakeLists.txt
index a99b03e..f48a607 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -131,11 +131,10 @@ if(PROJ_INCLUDE_DIRS)
endif()
# liblwgeom.h (used for GBOX) is PostGIS' own internal header and is not
-# shipped by any postgresql-*-postgis-3 package; it must be sourced from a
-# PostGIS/MobilityDB checkout. See README for how to place a copy under
-# /usr/local/include/postgis-lwgeom/liblwgeom (alongside its required
-# ../postgis_config.h).
-include_directories(SYSTEM /usr/local/include/postgis-lwgeom/liblwgeom)
+# shipped by any postgresql-*-postgis-3 package -- see
+# third_party/postgis-lwgeom/README.md for why a copy is vendored directly
+# in this repo instead of requiring a manual host-wide install step.
+include_directories(SYSTEM "${CMAKE_CURRENT_SOURCE_DIR}/third_party/postgis-lwgeom/liblwgeom")
#-------------------
# add the MobilityDB link
diff --git a/README.md b/README.md
index eb5067c..f997a3d 100644
--- a/README.md
+++ b/README.md
@@ -19,6 +19,7 @@ Distributed MobilityDB is an open-source extension for PostgreSQL tailored to ha
- [Use Cases](#use-cases)
- [OpenStreetMap (OSM) Data](#openstreetmap-osm-data)
- [Automatic Identification System (AIS) Data](#automatic-identification-system-ais-data)
+ - [BerlinMOD Benchmark Data](#berlinmod-benchmark-data)
- [Global Surface Summary of the Day (GSOD) Data](#global-surface-summary-of-the-day-gsod-data)
- [Contributing](#contributing)
- [Contact Us](#contact-us)
@@ -70,22 +71,24 @@ CREATE EXTENSION Distributed_MobilityDB CASCADE;
### Creating Distributed Tables
-The `create_spatiotemporal_distributed_table()` function is utilized to define a distributed table that is partitioned using a Multidimensional Tiling method. It splits the input table into several tiles stored in separate PostgreSQL tables.
+The `create_spatiotemporal_distributed_table()` function is utilized to define a distributed table that is partitioned using a Multidimensional Tiling method. It splits the input table into several tiles stored in separate PostgreSQL tables. It can also create a Citus **reference table** instead (a table replicated as-is to every node, with no tiling at all) via the `is_reference_table` flag -- useful for smaller lookup/dimension tables that need to be joined against a distributed table without any repartitioning.
**Function:** `create_spatiotemporal_distributed_table`
| Argument | Required | Description |
|---|---|---|
| `table_name_in` | Yes | Name of the input table. |
-| `num_tiles` | Yes | Number of generated tiles. |
-| `table_name_out` | Yes | Name of the distributed table. |
-| `tiling_method` | Yes | Name of the tiling method: crange, hierarchical, grid. |
-| `tiling_granularity` | No | The tiling granularity. Defaults to the value chosen by the tiling method's granularity selection process, which picks between shape- and point-based strategies to create load-balanced tiles. Set this to customize the tiling granularity. |
-| `tiling_type` | No | The tiling type of the tiling method: `temporal`, `spatial`, or `spatiotemporal`. Defaults based on the given column type. |
-| `colocation_table` | No | Colocate the input table with another table, e.g. to create tiles based on given boundaries such as province borders. Used together with `colocation_column`. |
-| `colocation_column` | No | The colocation column to use with `colocation_table`. |
-| `physical_partitioning` | No | Whether or not to physically partition data. |
-| `object_segmentation` | No | Whether or not to segment the input spatiotemporal column. |
+| `table_name_out` | Yes | Name of the distributed (or reference) table to create. Must not already exist. |
+| `num_tiles` | No | Number of generated tiles. Defaults to `1`. Ignored when `is_reference_table` is `true` -- reference tables aren't tiled -- except that any value other than `1` is rejected outright rather than silently ignored, to catch accidental misuse. |
+| `tiling_method` | No | Name of the tiling method: crange, hierarchical, grid. Defaults to `crange`. Ignored when `is_reference_table` is `true`. |
+| `tiling_granularity` | No | The tiling granularity. Defaults to the value chosen by the tiling method's granularity selection process, which picks between shape- and point-based strategies to create load-balanced tiles. Set this to customize the tiling granularity. Ignored when `is_reference_table` is `true`. |
+| `tiling_type` | No | The tiling type of the tiling method: `temporal`, `spatial`, or `spatiotemporal`. Defaults based on the given column type. Ignored when `is_reference_table` is `true`. |
+| `colocation_table` | No | Colocate the input table with another table, e.g. to create tiles based on given boundaries such as province borders. Used together with `colocation_column`. Ignored when `is_reference_table` is `true`. |
+| `colocation_column` | No | The colocation column to use with `colocation_table`. Ignored when `is_reference_table` is `true`. |
+| `spatiotemporal_col_name` | No | Name of the spatiotemporal/geometry column to distribute on. Defaults to the column detected automatically from the input table's type. Ignored when `is_reference_table` is `true`. |
+| `physical_partitioning` | No | Whether or not to physically partition data. Defaults to `true`. Ignored when `is_reference_table` is `true`. |
+| `shape_segmentation` | No | Whether or not to segment the input spatiotemporal column across tiles. Defaults to `true`. Ignored when `is_reference_table` is `true`. |
+| `is_reference_table` | No | If `true`, skip tiling entirely and create `table_name_out` as a Citus reference table (a full replica of `table_name_in` on every node) via `create_reference_table()`. Defaults to `false`. |
By utilizing the `create_spatiotemporal_distributed_table()` function with these arguments, you can easily create a distributed table that suits your data management needs.
@@ -189,6 +192,48 @@ WHERE Destination = 'Kalundborg'
AND timespan(Trip) > '5 days';
```
+### BerlinMOD Benchmark Data
+
+**Description:** BerlinMOD is a standard benchmark for moving object databases: a synthetic data generator producing vehicle trip trajectories across a road network, together with the 17 standard BerlinMOD/R benchmark queries. The full set of queries, adapted to run against a distributed `Trips` table, is available in [`demo_queries/berlinmod`](demo_queries/berlinmod), along with the distribution/setup script.
+
+**Download:** https://github.com/MobilityDB/MobilityDB-BerlinMOD
+
+**Reference:** https://github.com/MobilityDB/MobilityDB-BerlinMOD/blob/master/BerlinMOD/berlinmod_r_queries.sql
+
+```sql
+-- Input table
+CREATE TABLE Trips (
+ TripId int,
+ VehicleId int,
+ Trip tgeompoint
+);
+
+-- Distribute the trips table into 4 tiles using the spatiotemporal column: tgeompoint(sequence)
+SELECT create_spatiotemporal_distributed_table(table_name_in => 'trips', num_tiles => 4,
+ table_name_out => 'trips_4t', tiling_method => 'crange', tiling_type => 'spatiotemporal');
+
+-- Query 4: Which vehicles have passed the points from Points?
+SELECT DISTINCT p.PointId, p.Geom, v.Licence
+FROM trips_4t t, Vehicles v, Points p
+WHERE t.VehicleId = v.VehicleId
+ AND ST_Intersects(trajectory(t.Trip), p.Geom)
+ORDER BY p.PointId, v.Licence;
+
+-- Query 6 (Distance-Join): What are the pairs of licence plate numbers of "trucks"
+-- that have ever been as close as 10m or less to each other?
+WITH Temp(Licence, VehicleId, Trip) AS (
+ SELECT v.Licence, t.VehicleId, t.Trip
+ FROM trips_4t t, Vehicles v
+ WHERE t.VehicleId = v.VehicleId AND v.VehicleType = 'truck'
+)
+SELECT t1.Licence, t2.Licence
+FROM Temp t1, Temp t2
+WHERE t1.VehicleId < t2.VehicleId
+ AND t1.Trip && expandSpace(t2.Trip, 10)
+ AND eDwithin(t1.Trip, t2.Trip, 10.0)
+ORDER BY t1.Licence, t2.Licence;
+```
+
### Global Surface Summary of the Day (GSOD) Data
**Description:** GSOD data is a collection of daily weather observations from weather stations around the world. It includes information such as temperature, time, location, humidity, and atmospheric pressure.
diff --git a/demo_queries/berlinmod/partitioning.sql b/demo_queries/berlinmod/partitioning.sql
new file mode 100644
index 0000000..a61e801
--- /dev/null
+++ b/demo_queries/berlinmod/partitioning.sql
@@ -0,0 +1,68 @@
+-----------------------------------------------------------------------------------------------------------------------
+-- BerlinMOD setup: distribute Trips as a spatiotemporal-tiled table, and the
+-- reference tables the BerlinMOD/R queries join against (Vehicles, Licences,
+-- Points, Regions, Instants, Periods) as Citus reference tables -- both via
+-- create_spatiotemporal_distributed_table(), using its is_reference_table
+-- flag for the latter.
+--
+-- Assumes the BerlinMOD data has already been generated/loaded, e.g. via
+-- https://github.com/MobilityDB/MobilityDB-BerlinMOD (berlinmod_datagenerator.sql
+-- or berlinmod_load.sql), so tables Trips, Vehicles, Licences, Points,
+-- Regions, Instants and Periods already exist and are populated.
+--
+-- Each source table is renamed to lowercase first (e.g. Trips -> trips) so
+-- create_spatiotemporal_distributed_table() can create its distributed
+-- output under a new, descriptive name: trips_16t for the tiled table
+-- (matching the "_Nt" convention used elsewhere in this repo's demos), and
+--
_ref for each reference table.
+-----------------------------------------------------------------------------------------------------------------------
+
+-----------------------------------------------------------------------------------------------------------------------
+-- Trips
+-- The one distributed spatiotemporal table.
+-----------------------------------------------------------------------------------------------------------------------
+SELECT create_spatiotemporal_distributed_table(table_name_in => 'trips', table_name_out => 'trips_16t',
+ num_tiles => 16, tiling_method => 'crange', tiling_type => 'spatiotemporal');
+
+-----------------------------------------------------------------------------------------------------------------------
+-- Reference tables
+-- Replicated to every node (num_tiles is omitted -- it's not meaningful for
+-- a reference table, and defaults to the only value is_reference_table
+-- accepts), so they can be joined against trips_16t without any
+-- repartitioning.
+-----------------------------------------------------------------------------------------------------------------------
+SELECT create_spatiotemporal_distributed_table(table_name_in => 'vehicles', table_name_out => 'vehicles_ref',
+ is_reference_table => true);
+
+SELECT create_spatiotemporal_distributed_table(table_name_in => 'licences', table_name_out => 'licences_ref',
+ is_reference_table => true);
+
+SELECT create_spatiotemporal_distributed_table(table_name_in => 'points', table_name_out => 'points_ref',
+ is_reference_table => true);
+
+SELECT create_spatiotemporal_distributed_table(table_name_in => 'regions', table_name_out => 'regions_ref',
+ is_reference_table => true);
+
+SELECT create_spatiotemporal_distributed_table(table_name_in => 'instants', table_name_out => 'instants_ref',
+ is_reference_table => true);
+
+SELECT create_spatiotemporal_distributed_table(table_name_in => 'periods', table_name_out => 'periods_ref',
+ is_reference_table => true);
+
+-----------------------------------------------------------------------------------------------------------------------
+-- Sample views
+-- The standard queries restrict several reference tables to a small sample
+-- (suffix 1/2) to keep query result sizes reasonable.
+-----------------------------------------------------------------------------------------------------------------------
+CREATE OR REPLACE VIEW Licences1 (LicenceId, Licence, VehicleId) AS
+ SELECT LicenceId, Licence, VehicleId FROM licences_ref LIMIT 10;
+CREATE OR REPLACE VIEW Licences2 (LicenceId, Licence, VehicleId) AS
+ SELECT LicenceId, Licence, VehicleId FROM licences_ref LIMIT 10 OFFSET 10;
+CREATE OR REPLACE VIEW Points1 (PointId, Geom) AS
+ SELECT PointId, Geom FROM points_ref LIMIT 10;
+CREATE OR REPLACE VIEW Regions1 (RegionId, Geom) AS
+ SELECT RegionId, Geom FROM regions_ref LIMIT 10;
+CREATE OR REPLACE VIEW Instants1 (InstantId, Instant) AS
+ SELECT InstantId, Instant FROM instants_ref LIMIT 10;
+CREATE OR REPLACE VIEW Periods1 (PeriodId, Period) AS
+ SELECT PeriodId, Period FROM periods_ref LIMIT 10;
diff --git a/demo_queries/berlinmod/queries.sql b/demo_queries/berlinmod/queries.sql
new file mode 100644
index 0000000..97441a8
--- /dev/null
+++ b/demo_queries/berlinmod/queries.sql
@@ -0,0 +1,229 @@
+-----------------------------------------------------------------------------------------------------------------------
+-- The 17 standard BerlinMOD/R benchmark queries, adapted to run against the
+-- tables distributed in partitioning.sql: the spatiotemporal-tiled
+-- trips_16t, and the reference tables vehicles_ref, licences_ref,
+-- points_ref, regions_ref, instants_ref, periods_ref. Original queries:
+-- https://github.com/MobilityDB/MobilityDB-BerlinMOD/blob/master/BerlinMOD/berlinmod_r_queries.sql
+-----------------------------------------------------------------------------------------------------------------------
+
+-----------------------------------------------------------------------------------------------------------------------
+-- Q1) What are the models of the vehicles with licence plate numbers from Licences?
+-----------------------------------------------------------------------------------------------------------------------
+SELECT DISTINCT l.Licence, v.Model AS Model
+FROM vehicles_ref v, licences_ref l
+WHERE v.Licence = l.Licence;
+
+-----------------------------------------------------------------------------------------------------------------------
+-- Q2) How many vehicles exist that are passenger cars?
+-----------------------------------------------------------------------------------------------------------------------
+SELECT COUNT(Licence)
+FROM vehicles_ref v
+WHERE VehicleType = 'passenger';
+
+-----------------------------------------------------------------------------------------------------------------------
+-- Q3) Where have the vehicles with licences from Licences1 been at each of the instants from Instants1?
+-----------------------------------------------------------------------------------------------------------------------
+SELECT DISTINCT l.Licence, i.InstantId, i.Instant AS Instant,
+ valueAtTimestamp(t.Trip, i.Instant) AS Location
+FROM trips_16t t, Licences1 l, Instants1 i
+WHERE t.VehicleId = l.VehicleId AND t.Trip::tstzspan @> i.Instant
+ORDER BY l.Licence, i.InstantId;
+
+-----------------------------------------------------------------------------------------------------------------------
+-- Q4) Which vehicles have passed the points from Points?
+-----------------------------------------------------------------------------------------------------------------------
+SELECT DISTINCT p.PointId, p.Geom, v.Licence
+FROM trips_16t t, vehicles_ref v, points_ref p
+WHERE t.VehicleId = v.VehicleId
+ AND ST_Intersects(trajectory(t.Trip), p.Geom)
+ORDER BY p.PointId, v.Licence;
+
+-----------------------------------------------------------------------------------------------------------------------
+-- Q5) What is the minimum distance between places, where a vehicle with a licence from
+-- Licences1 and a vehicle with a licence from Licences2 have been?
+-----------------------------------------------------------------------------------------------------------------------
+SELECT l1.Licence AS Licence1, l2.Licence AS Licence2,
+ MIN(nearestapproachdistance(t1.Trip, t2.Trip)) AS MinDist
+FROM trips_16t t1, Licences1 l1, trips_16t t2, Licences2 l2
+WHERE t1.VehicleId = l1.VehicleId AND t2.VehicleId = l2.VehicleId
+GROUP BY l1.Licence, l2.Licence
+ORDER BY l1.Licence, l2.Licence;
+
+-----------------------------------------------------------------------------------------------------------------------
+-- Q6) What are the pairs of licence plate numbers of "trucks" that have ever been as close
+-- as 10m or less to each other?
+-----------------------------------------------------------------------------------------------------------------------
+WITH Temp(Licence, VehicleId, Trip) AS (
+ SELECT v.Licence, t.VehicleId, t.Trip
+ FROM trips_16t t, vehicles_ref v
+ WHERE t.VehicleId = v.VehicleId AND v.VehicleType = 'truck'
+)
+SELECT t1.Licence, t2.Licence
+FROM Temp t1, Temp t2
+WHERE t1.VehicleId < t2.VehicleId
+ AND t1.Trip && expandSpace(t2.Trip, 10)
+ AND eDwithin(t1.Trip, t2.Trip, 10.0)
+ORDER BY t1.Licence, t2.Licence;
+
+-----------------------------------------------------------------------------------------------------------------------
+-- Q7) What are the licence plate numbers of the passenger cars that have reached the points
+-- from Points first of all passenger cars during the complete observation period?
+-----------------------------------------------------------------------------------------------------------------------
+WITH Temp AS (
+ SELECT DISTINCT v.Licence, p.PointId, p.Geom,
+ MIN(startTimestamp(atValues(t.Trip, p.Geom))) AS Instant
+ FROM trips_16t t, vehicles_ref v, points_ref p
+ WHERE t.VehicleId = v.VehicleId AND v.VehicleType = 'passenger'
+ AND ST_Intersects(trajectory(t.Trip), p.Geom)
+ GROUP BY v.Licence, p.PointId, p.Geom
+)
+SELECT t1.Licence, t1.PointId, t1.Geom, t1.Instant
+FROM Temp t1
+WHERE t1.Instant <= ALL (
+ SELECT t2.Instant
+ FROM Temp t2
+ WHERE t1.PointId = t2.PointId
+)
+ORDER BY t1.PointId, t1.Licence;
+
+-----------------------------------------------------------------------------------------------------------------------
+-- Q8) What are the overall travelled distances of the vehicles with licence plate numbers
+-- from Licences1 during the periods from Periods1?
+-----------------------------------------------------------------------------------------------------------------------
+SELECT l.Licence, p.PeriodId, p.Period,
+ SUM(length(atTime(t.Trip, p.Period))) AS Dist
+FROM trips_16t t, Licences1 l, Periods1 p
+WHERE t.VehicleId = l.VehicleId AND t.Trip && p.Period
+GROUP BY l.Licence, p.PeriodId, p.Period
+ORDER BY l.Licence, p.PeriodId;
+
+-----------------------------------------------------------------------------------------------------------------------
+-- Q9) What is the longest distance that was travelled by a vehicle during each of the periods
+-- from Periods?
+-----------------------------------------------------------------------------------------------------------------------
+WITH Distances AS (
+ SELECT p.PeriodId, p.Period, t.VehicleId,
+ SUM(length(atTime(t.Trip, p.Period))) AS Dist
+ FROM trips_16t t, periods_ref p
+ WHERE t.Trip && p.Period
+ GROUP BY p.PeriodId, p.Period, t.VehicleId
+)
+SELECT PeriodId, Period, MAX(Dist) AS MaxDist
+FROM Distances
+GROUP BY PeriodId, Period
+ORDER BY PeriodId;
+
+-----------------------------------------------------------------------------------------------------------------------
+-- Q10) When and where did the vehicles with licence plate numbers from Licences1 meet other
+-- vehicles (distance < 3m) and what are the latter licences?
+-----------------------------------------------------------------------------------------------------------------------
+WITH Temp AS (
+ SELECT l1.Licence AS Licence1, t2.VehicleId AS Car2Id,
+ whenTrue(tDwithin(t1.Trip, t2.Trip, 3.0)) AS Periods
+ FROM trips_16t t1, Licences1 l1, trips_16t t2, vehicles_ref v
+ WHERE t1.VehicleId = l1.VehicleId AND t2.VehicleId = v.VehicleId
+ AND t1.VehicleId <> t2.VehicleId AND t2.Trip && expandSpace(t1.Trip, 3)
+)
+SELECT Licence1, Car2Id, Periods
+FROM Temp
+WHERE Periods IS NOT NULL;
+
+-----------------------------------------------------------------------------------------------------------------------
+-- Q11) Which vehicles passed a point from Points1 at one of the instants from Instants1?
+-----------------------------------------------------------------------------------------------------------------------
+WITH Temp AS (
+ SELECT p.PointId, p.Geom, i.InstantId, i.Instant, t.VehicleId
+ FROM trips_16t t, Points1 p, Instants1 i
+ WHERE t.Trip @> stbox(p.Geom, i.Instant)
+ AND valueAtTimestamp(t.Trip, i.Instant) = p.Geom
+)
+SELECT t.PointId, t.Geom, t.InstantId, t.Instant, v.Licence
+FROM Temp t JOIN vehicles_ref v ON t.VehicleId = v.VehicleId
+ORDER BY t.PointId, t.InstantId, v.Licence;
+
+-----------------------------------------------------------------------------------------------------------------------
+-- Q12) Which vehicles met at a point from Points1 at an instant from Instants1?
+-----------------------------------------------------------------------------------------------------------------------
+WITH Temp AS (
+ SELECT DISTINCT p.PointId, p.Geom, i.InstantId, i.Instant, t.VehicleId
+ FROM trips_16t t, Points1 p, Instants1 i
+ WHERE t.Trip @> stbox(p.Geom, i.Instant)
+ AND valueAtTimestamp(t.Trip, i.Instant) = p.Geom
+)
+SELECT DISTINCT t1.PointId, t1.Geom, t1.InstantId, t1.Instant,
+ v1.Licence AS Licence1, v2.Licence AS Licence2
+FROM Temp t1 JOIN vehicles_ref v1 ON t1.VehicleId = v1.VehicleId JOIN
+ Temp t2 ON t1.VehicleId < t2.VehicleId AND t1.PointID = t2.PointID AND
+ t1.InstantId = t2.InstantId JOIN vehicles_ref v2 ON t2.VehicleId = v2.VehicleId
+ORDER BY t1.PointId, t1.InstantId, v1.Licence, v2.Licence;
+
+-----------------------------------------------------------------------------------------------------------------------
+-- Q13) Which vehicles travelled within one of the regions from Regions1 during the periods
+-- from Periods1?
+-----------------------------------------------------------------------------------------------------------------------
+WITH Temp AS (
+ SELECT DISTINCT r.RegionId, p.PeriodId, p.Period, t.VehicleId
+ FROM trips_16t t, Regions1 r, Periods1 p
+ WHERE t.Trip && stbox(r.Geom, p.Period)
+ AND ST_Intersects(trajectory(atTime(t.Trip, p.Period)), r.Geom)
+)
+SELECT DISTINCT t.RegionId, t.PeriodId, t.Period, v.Licence
+FROM Temp t, vehicles_ref v
+WHERE t.VehicleId = v.VehicleId
+ORDER BY t.RegionId, t.PeriodId, v.Licence;
+
+-----------------------------------------------------------------------------------------------------------------------
+-- Q14) Which vehicles travelled within one of the regions from Regions1 at one of the
+-- instants from Instants1?
+-----------------------------------------------------------------------------------------------------------------------
+WITH Temp AS (
+ SELECT DISTINCT r.RegionId, i.InstantId, i.Instant, t.VehicleId
+ FROM trips_16t t, Regions1 r, Instants1 i
+ WHERE t.Trip && stbox(r.Geom, i.Instant)
+ AND ST_Contains(r.Geom, valueAtTimestamp(t.Trip, i.Instant))
+)
+SELECT DISTINCT t.RegionId, t.InstantId, t.Instant, v.Licence
+FROM Temp t JOIN vehicles_ref v ON t.VehicleId = v.VehicleId
+ORDER BY t.RegionId, t.InstantId, v.Licence;
+
+-----------------------------------------------------------------------------------------------------------------------
+-- Q15) Which vehicles passed a point from Points1 during a period from Periods1?
+-----------------------------------------------------------------------------------------------------------------------
+WITH Temp AS (
+ SELECT DISTINCT pt.PointId, pt.Geom, pr.PeriodId, pr.Period, t.VehicleId
+ FROM trips_16t t, Points1 pt, Periods1 pr
+ WHERE t.Trip && stbox(pt.Geom, pr.Period)
+ AND ST_Intersects(trajectory(atTime(t.Trip, pr.Period)), pt.Geom)
+)
+SELECT DISTINCT t.PointId, t.Geom, t.PeriodId, t.Period, v.Licence
+FROM Temp t, vehicles_ref v
+WHERE t.VehicleId = v.VehicleId
+ORDER BY t.PointId, t.PeriodId, v.Licence;
+
+-----------------------------------------------------------------------------------------------------------------------
+-- Q16) List the pairs of licences for vehicles, the first from Licences1, the second from
+-- Licences2, where the corresponding vehicles are both present within a region from Regions1
+-- during a period from Periods1, but do not meet each other there and then.
+-----------------------------------------------------------------------------------------------------------------------
+SELECT p.PeriodId, p.Period, r.RegionId,
+ l1.Licence AS Licence1, l2.Licence AS Licence2
+FROM trips_16t t1, Licences1 l1, trips_16t t2, Licences2 l2, Periods1 p, Regions1 r
+WHERE t1.VehicleId = l1.VehicleId AND t2.VehicleId = l2.VehicleId
+ AND l1.Licence < l2.Licence
+ AND ST_Intersects(trajectory(atTime(t1.Trip, p.Period)), r.Geom)
+ AND ST_Intersects(trajectory(atTime(t2.Trip, p.Period)), r.Geom)
+ AND aDisjoint(atTime(t1.Trip, p.Period), atTime(t2.Trip, p.Period))
+ORDER BY p.PeriodId, r.RegionId, l1.Licence, l2.Licence;
+
+-----------------------------------------------------------------------------------------------------------------------
+-- Q17) Which point(s) from Points have been visited by a maximum number of different vehicles?
+-----------------------------------------------------------------------------------------------------------------------
+WITH PointCount AS (
+ SELECT p.PointId, COUNT(DISTINCT t.VehicleId) AS Hits
+ FROM trips_16t t, points_ref p
+ WHERE ST_Intersects(trajectory(t.Trip), p.Geom)
+ GROUP BY p.PointId
+)
+SELECT PointId, Hits
+FROM PointCount AS p
+WHERE p.Hits = (SELECT MAX(Hits) FROM PointCount);
diff --git a/sql/partitioning/tiling.sql b/sql/partitioning/tiling.sql
index 08038f1..fca7b0e 100644
--- a/sql/partitioning/tiling.sql
+++ b/sql/partitioning/tiling.sql
@@ -3,10 +3,16 @@ SET search_path = SCHEMA,public;
--------------------------------------------------------------------------------------------------------------------------------------------------------
-- MD Tiling - Generic (Spatiotemporal)
--------------------------------------------------------------------------------------------------------------------------------------------------------
+-- num_tiles moved after table_name_out (and gained a default) so a
+-- reference table can be created without mentioning num_tiles at all; since
+-- that changes the parameter type order, the old signature is a distinct
+-- overload as far as Postgres is concerned and must be dropped explicitly
+-- or it would keep existing alongside this one.
+DROP FUNCTION IF EXISTS create_spatiotemporal_distributed_table(text, integer, text, text, text, text, text, text, varchar(50), boolean, boolean);
CREATE OR REPLACE FUNCTION create_spatiotemporal_distributed_table(
table_name_in text,
- num_tiles integer,
table_name_out text,
+ num_tiles integer DEFAULT 1,
tiling_method text DEFAULT 'crange',
tiling_granularity text default NULL,
tiling_type text default NULL,
@@ -14,7 +20,8 @@ CREATE OR REPLACE FUNCTION create_spatiotemporal_distributed_table(
colocation_column text default NULL,
spatiotemporal_col_name varchar(50) default NULL,
physical_partitioning boolean default TRUE,
- shape_segmentation boolean default TRUE
+ shape_segmentation boolean default TRUE,
+ is_reference_table boolean default FALSE
)
RETURNS boolean AS $$
DECLARE
@@ -32,6 +39,33 @@ BEGIN
IF temp IS NOT NULL THEN
RAISE EXCEPTION 'Please use different table name or drop it before calling this function!';
END IF;
+
+ -- Reference (broadcast) table: no spatiotemporal tiling at all -- just
+ -- copy the data into table_name_out and hand it to Citus' own
+ -- create_reference_table(), which replicates the whole table to every
+ -- node so it can be joined against any distributed table without
+ -- repartitioning. tiling_method/etc are all ignored in this path since
+ -- there's no tiling to do. num_tiles defaults to 1 so callers don't
+ -- need to think about it for a reference table, but a reference table
+ -- is never tiled, so any other value is rejected outright rather than
+ -- silently ignored.
+ IF is_reference_table THEN
+ IF num_tiles != 1 THEN
+ RAISE EXCEPTION 'num_tiles must be 1 (or omitted) when is_reference_table is true -- reference tables are replicated whole, not tiled; got %', num_tiles;
+ END IF;
+ RAISE INFO 'Creating reference (broadcast) table %', table_name_out;
+ EXECUTE format('CREATE TABLE %I (LIKE %I INCLUDING ALL)', table_name_out, table_name_in);
+ EXECUTE format('INSERT INTO %I SELECT * FROM %I', table_name_out, table_name_in);
+ -- create_reference_table() takes a regclass; casting table_name_out
+ -- (plain text) to regclass directly applies standard unquoted-
+ -- identifier folding (lowercasing it), which doesn't match the
+ -- case-preserved table just created above via %I whenever
+ -- table_name_out has any uppercase letters. Quoting it through %I
+ -- first makes the regclass cast resolve the exact same identifier.
+ EXECUTE format('SELECT create_reference_table(%L::regclass)', format('%I', table_name_out));
+ RETURN true;
+ END IF;
+
temp_start_time := clock_timestamp();
-- Preprocessing
RAISE INFO 'Collecting information:';
diff --git a/src/catalog/table_ops.c b/src/catalog/table_ops.c
index 4eaef80..48a708c 100644
--- a/src/catalog/table_ops.c
+++ b/src/catalog/table_ops.c
@@ -101,6 +101,7 @@ DistributedColumnType(Oid relationId)
return SPATIAL;
else if(strcmp(columnType, "tgeompoint") == 0)
return SPATIOTEMPORAL;
+ return DIFFTYPE;
}
else
return DIFFTYPE;
diff --git a/src/executor/multi_phase_executor.c b/src/executor/multi_phase_executor.c
index 8966df7..98f4ca4 100644
--- a/src/executor/multi_phase_executor.c
+++ b/src/executor/multi_phase_executor.c
@@ -541,7 +541,18 @@ ConstructPredicatePushDownQuery(PlanTask *plan, char * query_string, MultiPhaseE
multiPhaseExecutor->tasks = lappend(multiPhaseExecutor->tasks, task);
}
-/* GetTaskType renders task's ExecTaskType as a human-readable label for EXPLAIN output. */
+/*
+ * GetTaskType renders task's ExecTaskType as a human-readable label for
+ * EXPLAIN output. Every ExecTaskType that ExplainPlanStrategies can
+ * actually iterate over (multiPhaseExecutor->tasks -- never the separate
+ * coordTasks list, so INTERMEDIATEScan/FINALScan don't reach here today)
+ * needs a case; falling off the end without returning left this Datum
+ * uninitialized, and the caller's DatumGetCString/appendStringInfo("%s")
+ * would then dereference whatever garbage pointer was left in the return
+ * register -- e.g. for a PushDownScan task (a single distributed table
+ * joined against reference tables only, no self-join), crashing EXPLAIN
+ * outright.
+ */
extern Datum
GetTaskType(ExecutorTask *task)
{
@@ -549,6 +560,14 @@ GetTaskType(ExecutorTask *task)
return CStringGetDatum("Neighbor Scan");
else if (task->taskType == SelfTilingScan)
return CStringGetDatum("Self Tiling Scan");
+ else if (task->taskType == PushDownScan)
+ return CStringGetDatum("Push Down Scan");
+ else if (task->taskType == INTERMEDIATEScan)
+ return CStringGetDatum("Intermediate Scan");
+ else if (task->taskType == FINALScan)
+ return CStringGetDatum("Final Scan");
+ else
+ return CStringGetDatum("Unknown Scan");
}
/*
diff --git a/src/planner/distributed_mobilitydb_explain.c b/src/planner/distributed_mobilitydb_explain.c
index 7d72288..f59dbed 100644
--- a/src/planner/distributed_mobilitydb_explain.c
+++ b/src/planner/distributed_mobilitydb_explain.c
@@ -23,6 +23,7 @@
#include "utils/planner_utils.h"
#include "catalog/nodes.h"
#include "planner/planner_strategies.h"
+#include "catalog/table_ops.h"
#include
#include
#include
@@ -320,10 +321,11 @@ ExplainOneTask(ExecutorTask *task, STMultirelation *base,ExplainState *es, int i
}
/*
- * GetLocalQuery pins query_string to one representative tile by appending a
+ * GetLocalQuery pins query_string to one representative tile by adding a
* literal `.tile_key = rand_tile` predicate for every range table
- * entry it references, so Citus' shard pruning narrows each table down to
- * the single matching shard instead of planning across all of them.
+ * entry that actually has a tile_key column, so Citus' shard pruning
+ * narrows each such table down to the single matching shard instead of
+ * planning across all of them.
*/
static char *
GetLocalQuery(char *query_string, Oid base, ExecTaskType taskType, int rand_tile)
@@ -333,18 +335,43 @@ GetLocalQuery(char *query_string, Oid base, ExecTaskType taskType, int rand_tile
Query *query = ParseQueryString(query_string, NULL, 0);
List *rangeTableList = ExtractRangeTableEntryList(query);
- StringInfo pinnedQuery = makeStringInfo();
- appendStringInfo(pinnedQuery, "%s", query_string);
+ StringInfo tileKeyConditions = makeStringInfo();
ListCell *rangeTableCell = NULL;
foreach(rangeTableCell, rangeTableList)
{
RangeTblEntry *rangeTableEntry = (RangeTblEntry *) lfirst(rangeTableCell);
- appendStringInfo(pinnedQuery, " AND %s.%s = %d", rangeTableEntry->eref->aliasname,
+ /* Only tables tiled by this extension's own machinery (a
+ * distributed spatiotemporal table, or a plain table reshuffled
+ * to be colocated with one) actually have a tile_key column --
+ * unconditionally pinning every range table entry (as this used
+ * to) added "alias.tile_key = N" for reference tables too (e.g.
+ * vehicles_ref/points_ref), which have no such column at all,
+ * producing "column v.tile_key does not exist" instead of a plan. */
+ if (!IsDistributedSpatiotemporalTable(rangeTableEntry->relid) &&
+ !IsReshuffledTable(rangeTableEntry->relid))
+ continue;
+ appendStringInfo(tileKeyConditions, "%s.%s = %d AND ", rangeTableEntry->eref->aliasname,
Var_Catalog_Tile_Key, rand_tile);
}
- return pinnedQuery->data;
+ if (tileKeyConditions->len == 0)
+ return query_string;
+
+ /*
+ * Inserted right after the query's own WHERE keyword rather than
+ * appended at the very end -- appending unconditionally landed these
+ * AND-joined conditions after a trailing ORDER BY whenever the task
+ * query had one (e.g. Q16's per-tile query), silently folding them
+ * into the ORDER BY expression list instead of the WHERE clause:
+ * "ORDER BY ..., l2.licence AND t1.tile_key = 5 AND ..." parses as one
+ * AND-expression whose left operand is l2.licence (text), producing
+ * "argument of AND must be type boolean, not type text" instead of
+ * pinning the query to one tile.
+ */
+ StringInfo key = makeStringInfo();
+ appendStringInfo(key, "WHERE %s", tileKeyConditions->data);
+ return replaceWord(query_string, "where", key->data);
}
/*
diff --git a/src/planner/distributed_mobilitydb_planner.c b/src/planner/distributed_mobilitydb_planner.c
index 3fd58cb..a2e4404 100644
--- a/src/planner/distributed_mobilitydb_planner.c
+++ b/src/planner/distributed_mobilitydb_planner.c
@@ -29,6 +29,7 @@
#include "distributed_functions/coordinator_operations.h"
#include "distributed_functions/worker_operations.h"
#include "nodes/makefuncs.h"
+#include "nodes/nodeFuncs.h"
#include "general/spatiotemporal_processing.h"
#include "general/rte.h"
@@ -37,6 +38,10 @@ static void analyzeDistributedSpatiotemporalTables(List *rangeTableList,
DistributedSpatiotemporalQueryPlan *distPlan);
static void PlanInitialization(DistributedSpatiotemporalQueryPlan *distPlan);
static void checkQueryType(Query *parse, DistributedSpatiotemporalQueryPlan *distPlan);
+static void ProcessQueryPredicates(Query *parse, DistributedSpatiotemporalQueryPlan *distPlan);
+static void ProcessPredicateClause(DistributedSpatiotemporalQueryPlan *distPlan, Node *clause);
+static bool SelectListPredicateWalker(Node *node, DistributedSpatiotemporalQueryPlan *distPlan);
+static void AnalyseSelectListPredicates(Query *parse, DistributedSpatiotemporalQueryPlan *distPlan);
static bool needsDistributedSpatiotemporalPlanning(DistributedSpatiotemporalQueryPlan *distPlan);
static bool StrategiesInclude(List *strategies, StrategyType type);
static PlannedStmt * EarlyQueryCheck(Query *parse, const char *query_string, int cursorOptions,
@@ -351,8 +356,22 @@ EarlyQueryCheck(Query *parse, const char *query_string, int cursorOptions, Param
bool res = false;
if (query_string == NULL)
return result;
- foreach(rangeTableCell, parse->rtable) {
+ /* parse->rtable only holds the OUTER query's own range table -- a query
+ * that references its distributed spatiotemporal table exclusively
+ * inside a CTE (e.g. "WITH Temp AS (SELECT ... FROM trips_16t t1,
+ * trips_16t t2 ...) SELECT ... FROM Temp") has just an RTE_CTE entry
+ * here, so this loop never saw the real table and always deferred such
+ * queries straight to Citus' own planner -- which then rejects a
+ * same-table self-join baked inside the CTE outright, since it has no
+ * way to push it down or materialize-and-rejoin it the way it can for a
+ * CTE referenced (self-joined) from outside. ExtractRangeTableEntryList
+ * recurses into CTEs/subqueries so the table is actually found here,
+ * letting the query into our own planning pipeline instead. */
+ List *rangeTableList = ExtractRangeTableEntryList(parse);
+ foreach(rangeTableCell, rangeTableList) {
RangeTblEntry *rangeTableEntry = (RangeTblEntry *) lfirst(rangeTableCell);
+ if (rangeTableEntry->rtekind != RTE_RELATION)
+ continue;
if (IsDistributedSpatiotemporalTable(rangeTableEntry->relid) && parse->commandType == CMD_SELECT)
{
/* at least one distriubted table */
@@ -398,8 +417,37 @@ PlanInitialization(DistributedSpatiotemporalQueryPlan *distPlan)
static void
checkQueryType(Query *parse, DistributedSpatiotemporalQueryPlan *distPlan)
{
- // extract where clause qualifiers and verify we can plan for them
+ ProcessQueryPredicates(parse, distPlan);
+ /* A self-join whose spatiotemporal predicate lives entirely inside a
+ * CTE's own definition (e.g. Q10: "WITH Temp AS (SELECT ...
+ * whenTrue(tDwithin(t1.Trip, t2.Trip, 3.0)) ... FROM trips_16t t1, ...,
+ * trips_16t t2, ... ) SELECT ... FROM Temp") is invisible to the scan
+ * above, since parse->jointree/parse->targetList only cover the OUTER
+ * query -- the outer query here just references "Temp" once, with no
+ * spatiotemporal predicate of its own. Scan each CTE's own query the
+ * same way so its self-join still gets a strategy chosen. */
+ ListCell *cteCell;
+ foreach(cteCell, parse->cteList)
+ {
+ CommonTableExpr *cte = (CommonTableExpr *) lfirst(cteCell);
+ if (!IsA(cte->ctequery, Query))
+ continue;
+ ProcessQueryPredicates((Query *) cte->ctequery, distPlan);
+ }
+ /* TODO: The rest is excluded for now and will be added after testing the main features */
+}
+
+/*
+ * ProcessQueryPredicates scans a single query's WHERE clause and, if that
+ * doesn't already pick a strategy, its SELECT list, for a registered
+ * intersection/distance predicate. Called once for the outer query and once
+ * per CTE by checkQueryType, since a CTE's own self-join is otherwise never
+ * visible from the outer query's jointree/targetList.
+ */
+static void
+ProcessQueryPredicates(Query *parse, DistributedSpatiotemporalQueryPlan *distPlan)
+{
List *whereClauseList = WhereClauseList(parse->jointree);
ListCell *clauseCell = NULL;
if (whereClauseList == NIL && parse->hasSubLinks)
@@ -407,6 +455,38 @@ checkQueryType(Query *parse, DistributedSpatiotemporalQueryPlan *distPlan)
/* TODO: subquery is excluded for now */
ereport(ERROR, (errmsg("A sub query is not supported yet in Distributed MobilityDB!")));
}
+ /* Iterate over the where clause conditions */
+ foreach(clauseCell, whereClauseList)
+ {
+ Node *clause = (Node *) lfirst(clauseCell);
+ ProcessPredicateClause(distPlan, clause);
+ }
+ /* A self-join whose only spatiotemporal computation lives in the SELECT
+ * list (e.g. MIN(nearestapproachdistance(t1.Trip, t2.Trip)), with no
+ * spatiotemporal predicate in the WHERE clause at all) never reaches the
+ * loop above, since WhereClauseList only sees WHERE-clause conjuncts --
+ * leaving no strategy chosen and Citus rejecting the resulting
+ * unconditioned self cross-join. Only run this fallback scan when the
+ * WHERE clause didn't already pick a strategy, so existing queries are
+ * unaffected. */
+ if (list_length(distPlan->strategies) == 0)
+ {
+ AnalyseSelectListPredicates(parse, distPlan);
+ }
+}
+
+/*
+ * ProcessPredicateClause inspects a single predicate node (either a
+ * WHERE-clause conjunct or a spatiotemporal function call found inside the
+ * SELECT list) and, if it's a registered intersection/distance operation,
+ * chooses the strategy needed to plan it.
+ */
+static void
+ProcessPredicateClause(DistributedSpatiotemporalQueryPlan *distPlan, Node *clause)
+{
+ if (NodeIsEqualsOpExpr(clause))
+ return;
+
/* Reference tables are already replicated to every node, so a join
* against one never needs the NonColocation strategy's reshuffle --
* Citus can push the predicate down to each shard directly. Subtracting
@@ -415,107 +495,144 @@ checkQueryType(Query *parse, DistributedSpatiotemporalQueryPlan *distPlan)
* genuine single-table query below. */
int effectiveDiffCount = distPlan->tablesList->diffCount - distPlan->tablesList->refCount;
int effectiveLength = distPlan->tablesList->length - distPlan->tablesList->refCount;
- /* Iterate over the where clause conditions */
- foreach(clauseCell, whereClauseList)
- {
- Node *clause = (Node *) lfirst(clauseCell);
- if (!NodeIsEqualsOpExpr(clause))
+ Oid predicateOid;
+ List *predicateArgs;
+
+ /*
+ * MobilityDB/PostGIS join predicates such as eDwithin(...) or
+ * ST_Intersects(...) parse as FuncExpr, not OpExpr -- casting
+ * blindly to OpExpr (as this used to) silently failed to
+ * recognize them (or worse, read OpExpr-shaped fields out of a
+ * FuncExpr node), so joins using them fell through to Citus'
+ * own planner, which rejects any join not on distribution
+ * columns.
+ */
+ if (!(GetPredicateOidAndArgs(clause, &predicateOid, &predicateArgs) &&
+ predicateOid > 0 && list_length(predicateArgs) >= 2))
+ return;
+
+ if (IsIntersectionOperation(predicateOid))
+ {
+ if (effectiveDiffCount > 1)
{
- Oid predicateOid;
- List *predicateArgs;
-
- /*
- * MobilityDB/PostGIS join predicates such as eDwithin(...) or
- * ST_Intersects(...) parse as FuncExpr, not OpExpr -- casting
- * blindly to OpExpr (as this used to) silently failed to
- * recognize them (or worse, read OpExpr-shaped fields out of a
- * FuncExpr node), so joins using them fell through to Citus'
- * own planner, which rejects any join not on distribution
- * columns.
- */
- if (GetPredicateOidAndArgs(clause, &predicateOid, &predicateArgs) &&
- predicateOid > 0 && list_length(predicateArgs) >= 2)
+ /* Intersection join between two distinct tables: must colocate them first. */
+ AddStrategy(distPlan, NonColocation);
+ }
+ else if (effectiveLength == 1)
+ {
+ /* Single-table intersection: decide between rebalancing tiles to fit the
+ * query's search box or simply pushing the predicate to each worker. */
+ Datum rangeBox = get_query_range(distPlan->tablesList, clause);
+ if (!IsDatumEmpty(rangeBox) &&
+ CheckTileRebalancerActivation(distPlan->tablesList, clause, rangeBox))
{
- if (IsIntersectionOperation(predicateOid))
- {
- if (effectiveDiffCount > 1)
- {
- /* Intersection join between two distinct tables: must colocate them first. */
- AddStrategy(distPlan, NonColocation);
- }
- else if (effectiveLength == 1)
- {
- /* Single-table intersection: decide between rebalancing tiles to fit the
- * query's search box or simply pushing the predicate to each worker. */
- Datum rangeBox = get_query_range(distPlan->tablesList, clause);
- if (!IsDatumEmpty(rangeBox) &&
- CheckTileRebalancerActivation(distPlan->tablesList, clause, rangeBox))
- {
- AddStrategy(distPlan, TileScanRebalancer);
- distPlan->range_bbox = rangeBox;
- }
- else
- AddStrategy(distPlan, PredicatePushDown);
- }
- else
- {
- /* By default: multiple references to the same colocated table (self-join). */
- AddStrategy(distPlan, Colocation);
- }
- }
- else if (IsDistanceOperation(predicateOid))
- {
- if (distPlan->tablesList->simCount >= 1)
- AddStrategy(distPlan, Colocation);
- /* The NonColocation strategy is triggered by default until the analysis
- * changes it -- except when the only "different" tables besides one
- * distributed spatiotemporal table are reference tables (refCount > 0
- * guards this so behavior is untouched whenever no reference table is
- * involved), which Citus can push the predicate down to directly with
- * no reshuffle needed. */
- if (effectiveDiffCount > 1 || distPlan->tablesList->refCount == 0)
- {
- AddStrategy(distPlan, NonColocation);
- }
- else if (effectiveLength == 1)
- {
- AddStrategy(distPlan, PredicatePushDown);
- }
- if(distPlan->predicatesList->predicateType == DISTANCE)
- {
- ereport(ERROR, (errmsg("Currently, we do not support using more than "
- "one distance operation in the same query !")));
- }
- distPlan->predicatesList->predicateInfo->distancePredicate = (DistancePredicate *)palloc0(
- sizeof(DistancePredicate));
- distPlan->predicatesList->predicateInfo->distancePredicate = analyseDistancePredicate(clause);
- distPlan->predicatesList->predicateType = DISTANCE;
- }
- else
- {
- ListCell *arg;
- foreach(arg, predicateArgs)
- {
- Node *node = (Node *) lfirst(arg);
- if (!IsA(node, Const))
- continue;
- Oid arg_oid = ((Const *)node)->consttype;
- if (IsDistanceOperation(arg_oid))
- {
- if (distPlan->tablesList->simCount >= 1)
- AddStrategy(distPlan, Colocation);
- distPlan->predicatesList->predicateInfo->distancePredicate =
- analyseDistancePredicate(node);
- AddStrategy(distPlan, NonColocation);
- distPlan->predicatesList->predicateType = DISTANCE;
- }
- }
- }
+ AddStrategy(distPlan, TileScanRebalancer);
+ distPlan->range_bbox = rangeBox;
}
+ else
+ AddStrategy(distPlan, PredicatePushDown);
+ }
+ else
+ {
+ /* By default: multiple references to the same colocated table (self-join). */
+ AddStrategy(distPlan, Colocation);
}
}
- /* TODO: The rest is excluded for now and will be added after testing the main features */
+ else if (IsDistanceOperation(predicateOid))
+ {
+ if (distPlan->tablesList->simCount >= 1)
+ AddStrategy(distPlan, Colocation);
+ /* The NonColocation strategy is triggered by default until the analysis
+ * changes it -- except when the only "different" tables besides one
+ * distributed spatiotemporal table are reference tables (refCount > 0
+ * guards this so behavior is untouched whenever no reference table is
+ * involved), which Citus can push the predicate down to directly with
+ * no reshuffle needed. */
+ if (effectiveDiffCount > 1 || distPlan->tablesList->refCount == 0)
+ {
+ AddStrategy(distPlan, NonColocation);
+ }
+ else if (effectiveLength == 1)
+ {
+ AddStrategy(distPlan, PredicatePushDown);
+ }
+ if(distPlan->predicatesList->predicateType == DISTANCE)
+ {
+ ereport(ERROR, (errmsg("Currently, we do not support using more than "
+ "one distance operation in the same query !")));
+ }
+ distPlan->predicatesList->predicateInfo->distancePredicate = (DistancePredicate *)palloc0(
+ sizeof(DistancePredicate));
+ distPlan->predicatesList->predicateInfo->distancePredicate = analyseDistancePredicate(clause);
+ distPlan->predicatesList->predicateType = DISTANCE;
+ }
+ else
+ {
+ ListCell *arg;
+ foreach(arg, predicateArgs)
+ {
+ Node *node = (Node *) lfirst(arg);
+ if (!IsA(node, Const))
+ continue;
+ Oid arg_oid = ((Const *)node)->consttype;
+ if (IsDistanceOperation(arg_oid))
+ {
+ if (distPlan->tablesList->simCount >= 1)
+ AddStrategy(distPlan, Colocation);
+ distPlan->predicatesList->predicateInfo->distancePredicate =
+ analyseDistancePredicate(node);
+ AddStrategy(distPlan, NonColocation);
+ distPlan->predicatesList->predicateType = DISTANCE;
+ }
+ }
+ }
+}
+
+/*
+ * SelectListPredicateWalker recurses through a SELECT-list expression (e.g.
+ * into an Aggref's arguments) looking for a registered spatiotemporal
+ * predicate function/operator. A matched node is handed to
+ * ProcessPredicateClause and not recursed into further, since a registered
+ * predicate's own arguments (plain columns) never nest another one.
+ */
+static bool
+SelectListPredicateWalker(Node *node, DistributedSpatiotemporalQueryPlan *distPlan)
+{
+ if (node == NULL)
+ return false;
+
+ if (IsA(node, FuncExpr) || IsA(node, OpExpr))
+ {
+ Oid predicateOid;
+ List *predicateArgs;
+ if (GetPredicateOidAndArgs(node, &predicateOid, &predicateArgs) &&
+ predicateOid > 0 && list_length(predicateArgs) >= 2 &&
+ (IsIntersectionOperation(predicateOid) || IsDistanceOperation(predicateOid)))
+ {
+ ProcessPredicateClause(distPlan, node);
+ return false;
+ }
+ }
+ return expression_tree_walker(node, SelectListPredicateWalker, (void *) distPlan);
+}
+
+/*
+ * AnalyseSelectListPredicates scans the SELECT list's target entries for a
+ * registered spatiotemporal predicate function used inside an aggregate
+ * (e.g. MIN(nearestapproachdistance(t1.Trip, t2.Trip))), so a self-join
+ * whose only spatiotemporal computation lives in the SELECT list still gets
+ * a strategy chosen.
+ */
+static void
+AnalyseSelectListPredicates(Query *parse, DistributedSpatiotemporalQueryPlan *distPlan)
+{
+ ListCell *cell;
+ foreach(cell, parse->targetList)
+ {
+ TargetEntry *targetEntry = (TargetEntry *) lfirst(cell);
+ SelectListPredicateWalker((Node *) targetEntry->expr, distPlan);
+ }
}
/*
diff --git a/src/planner/planner_strategies.c b/src/planner/planner_strategies.c
index 178d594..2e4b765 100644
--- a/src/planner/planner_strategies.c
+++ b/src/planner/planner_strategies.c
@@ -100,6 +100,15 @@ PlanReshufflingNonStRteWithStRte(DistributedSpatiotemporalQueryPlan *distPlan, R
foreach(rangeTableCell, distPlan->tablesList->tables)
{
Rte * rteNode = (Rte *) lfirst(rangeTableCell);
+ /* TODO(known bug, tracked separately -- not fixed here): Rte.RteType
+ * is declared `bool` in include/general/rte.h but the RteType enum
+ * it holds has three values (STRte=0, CitusRte=1, LocalRte=2);
+ * storing LocalRte truncates to the same bool value CitusRte
+ * produces, so `== LocalRte` (comparing against the int literal 2)
+ * can never be true here. Needs Rte.RteType changed to the real
+ * enum type plus an audit of every ->RteType comparison in the
+ * codebase before it's safe to fix. */
+ // cppcheck-suppress compareBoolExpressionWithInt
if (rteNode->RteType == CitusRte || rteNode->RteType == LocalRte)
distPlan->reshuffledTable = rteNode;
else if (rteNode->RteType == STRte){
@@ -115,6 +124,7 @@ PlanReshufflingNonStRteWithStRte(DistributedSpatiotemporalQueryPlan *distPlan, R
((RangeTblEntry *)lfirst(citusNode->rangeTableCell))->relid));
createReshufflingPlanForNonstRte(distPlan);
}
+ // cppcheck-suppress compareBoolExpressionWithInt -- see TODO above on the same known Rte.RteType bug
else if (distPlan->reshuffledTable->RteType == LocalRte)
{
/* The rte can be either broadcasted or partitioned using the same tiling scheme of the given
@@ -448,6 +458,7 @@ getReshuffledColumns(DistributedSpatiotemporalQueryPlan *distPlan, Oid oid)
}
return reshuffledTableColumns;
}
+ elog(ERROR, "Could not read column list for relation %u", oid);
}
/*
@@ -537,15 +548,53 @@ PredicatePushDownStrategyPlan(DistributedSpatiotemporalQueryPlan *distPlan)
{
PlanTask * strategy = (PlanTask *) palloc0(sizeof(PlanTask));
strategy->type = PredicatePushDown;
- strategy->tbl1 = (STMultirelation *) list_nth(distPlan->tablesList->tables, 0);
- strategy->tbl2 = (STMultirelation *) list_nth(distPlan->tablesList->tables, 1);
+ /* tablesList->tables holds Rte wrappers (STRte/CitusRte/LocalRte), not
+ * bare STMultirelation pointers -- list_nth(...)[0]/[1] cast directly
+ * to STMultirelation* (as this used to) read a Citus/local reference
+ * table's Rte wrapper as if it were the spatiotemporal table's own
+ * struct whenever one of the query's other tables sorted before it, a
+ * type confusion that left task->catalog_filtered pointing at garbage
+ * and crashed EXPLAIN's "Task Count: %d" (task->catalog_filtered->
+ * candidates). Find the actual STRte entry instead; PredicatePushDown
+ * only ever needs the one spatiotemporal table its predicate pushes
+ * down onto (see ConstructPredicatePushDownQuery, which only reads
+ * tbl1), so tbl2 is left unset.
+ */
+ ListCell *rangeTableCell = NULL;
+ STMultirelation *stTable = NULL;
+ foreach(rangeTableCell, distPlan->tablesList->tables)
+ {
+ Rte *rteNode = (Rte *) lfirst(rangeTableCell);
+ if (rteNode->RteType == STRte)
+ {
+ stTable = (STMultirelation *) rteNode->rte;
+ break;
+ }
+ }
+ if (stTable == NULL)
+ ereport(ERROR, (errmsg("PredicatePushDown strategy requires a spatiotemporal table")));
+ strategy->tbl1 = stTable;
strategy->tileKey = (Datum) Var_Catalog_Tile_Key;
distPlan->strategyPlans = lappend(distPlan->strategyPlans, strategy);
}
-/* AddStrategy appends `type` to distPlan's list of chosen StrategyTypes. */
+/* AddStrategy appends `type` to distPlan's list of chosen StrategyTypes, if
+ * not already present. checkQueryType calls this once per registered
+ * predicate clause, and a query can have more than one predicate that maps
+ * to the same strategy between the same table pair (e.g. Q16's two
+ * ST_Intersects clauses plus an aDisjoint clause all resolve to Colocation)
+ * -- appending unconditionally queued the same strategy's plan/task/query
+ * multiple times, and ConstructGeneralQuery's UNION of one query per
+ * strategies-list entry then UNIONed several copies of a query that already
+ * has its own trailing ORDER BY, which Postgres rejects outright. */
extern
void AddStrategy(DistributedSpatiotemporalQueryPlan *distPlan, StrategyType type)
{
+ ListCell *cell;
+ foreach(cell, distPlan->strategies)
+ {
+ if ((StrategyType) lfirst_int(cell) == type)
+ return;
+ }
distPlan->strategies = lappend(distPlan->strategies, (Datum *)type);
}
\ No newline at end of file
diff --git a/src/planner/query_parameters.c b/src/planner/query_parameters.c
index ab1797f..7f94a22 100644
--- a/src/planner/query_parameters.c
+++ b/src/planner/query_parameters.c
@@ -14,6 +14,7 @@
#include "postgres.h"
#include
+#include
#include "planner/predicate_management.h"
#include "multirelation/multirelation_utils.h"
#include "planner/distributed_mobilitydb_planner.h"
@@ -68,20 +69,75 @@ static void ExplainMainPredicate(PredicateType predicateType, PredicateInfo * pr
* touched by the query along with its tiling method, local index, and tile
* count; repeated references to the same table (self-joins) are skipped
* after the first.
+ *
+ * The header counts distinguish genuinely-distributed (tiled) spatiotemporal
+ * tables from replicated reference tables -- tablesList->length/diffCount
+ * count every range-table entry or distinct relid regardless of kind, so a
+ * query joining one tiled table against several reference tables (e.g. a
+ * self-join on trips_16t plus 4 reference-table references) used to print
+ * as "Distributed Tables:6" / "different tables: 4", reading as if several
+ * genuinely-sharded tables were involved instead of one.
*/
static void ExplainDistributedTables(STMultirelations *tablesList, ExplainState *es, int indent_group)
{
+ List *seenDistributedRelids = NIL;
+ List *seenReferenceRelids = NIL;
+ int distributedOccurrences = 0;
+ int referenceOccurrences = 0;
+ ListCell *countCell = NULL;
+ foreach(countCell, tablesList->tables)
+ {
+ Rte *rteNode = (Rte *) lfirst(countCell);
+ if (rteNode->RteType == STRte)
+ {
+ STMultirelation *st = (STMultirelation *) rteNode->rte;
+ distributedOccurrences++;
+ if (!list_member_oid(seenDistributedRelids, st->catalogTableInfo.table_oid))
+ seenDistributedRelids = lappend_oid(seenDistributedRelids, st->catalogTableInfo.table_oid);
+ }
+ else if (rteNode->RteType == CitusRte)
+ {
+ CitusRteNode *citusRte = (CitusRteNode *) rteNode->rte;
+ /* Reference tables report Citus' "none" partition method
+ * ('n', DISTRIBUTE_BY_NONE) -- the same value plain Citus
+ * local tables report, but a CitusRteNode only ever exists for
+ * a hash/range-distributed table or a reference table (see
+ * analyzeDistributedSpatiotemporalTables), so "not hash, not
+ * range" reliably means "reference table" here. */
+ if (citusRte->partitionMethod != DISTRIBUTE_BY_HASH &&
+ citusRte->partitionMethod != DISTRIBUTE_BY_RANGE)
+ {
+ Oid relid = ((RangeTblEntry *) lfirst(citusRte->rangeTableCell))->relid;
+ referenceOccurrences++;
+ if (!list_member_oid(seenReferenceRelids, relid))
+ seenReferenceRelids = lappend_oid(seenReferenceRelids, relid);
+ }
+ }
+ }
+
+ int distinctDistributedCount = list_length(seenDistributedRelids);
+
appendStringInfoSpaces(es->str, es->indent * indent_group);
- appendStringInfo(es->str, "-> Distributed Tables:%d\n", tablesList->length);
+ appendStringInfo(es->str, "-> Distributed Tables:%d\n", distinctDistributedCount);
es->indent += indent_group;
ExplainOpenGroup("TablesInfo", "Distributed Tables Info", true, es);
appendStringInfoSpaces(es->str, es->indent * indent_group);
- appendStringInfo(es->str, "Number of similar tables: %d\n", tablesList->simCount);
+ /* "Similar"/"different" here are scoped to genuinely-distributed (tiled)
+ * tables only -- self-joins (e.g. trips_16t as both t1 and t2) count as
+ * "similar", and distinct distributed tables (e.g. two different tiled
+ * tables joined together) count as "different". Reference tables are
+ * reported separately below, never folded into either count. */
+ appendStringInfo(es->str, "Number of similar tables: %d\n",
+ distributedOccurrences - distinctDistributedCount);
+ appendStringInfoSpaces(es->str, es->indent * indent_group);
+ appendStringInfo(es->str, "Number of different tables: %d\n", distinctDistributedCount);
appendStringInfoSpaces(es->str, es->indent * indent_group);
- if (tablesList->diffCount > 1)
- appendStringInfo(es->str, "Number of different tables: %d\n", tablesList->diffCount);
+ if (referenceOccurrences > 0)
+ appendStringInfo(es->str, "Replicated (reference) tables: %d (%d reference%s)\n",
+ list_length(seenReferenceRelids), referenceOccurrences,
+ referenceOccurrences == 1 ? "" : "s");
else
- appendStringInfo(es->str, "Number of different tables: %d\n", 0);
+ appendStringInfo(es->str, "Replicated (reference) tables: 0\n");
appendStringInfoSpaces(es->str, es->indent * indent_group);
ListCell *rangeTableCell = NULL;
char * check = NULL;
diff --git a/src/utils/helper_functions.c b/src/utils/helper_functions.c
index 87a6d16..6aec911 100644
--- a/src/utils/helper_functions.c
+++ b/src/utils/helper_functions.c
@@ -90,14 +90,18 @@ char* replaceWord( char* s, char* oldW, char* newW)
return bstr;
}
-/* extract_between returns a newly allocated copy of the substring of str found strictly between markers p1 and p2. */
+/*
+ * extract_between returns a newly allocated copy of the substring of str
+ * found strictly between markers p1 and p2, or NULL if either marker isn't
+ * found (or allocation fails).
+ */
extern
char * extract_between(const char *str, const char *p1, const char *p2) {
const char *i1 = strstr(str, p1);
if (i1 != NULL) {
const size_t pl1 = strlen(p1);
const char *i2 = strstr(i1 + pl1, p2);
- if (p2 != NULL) {
+ if (i2 != NULL) {
/* Found both markers, extract text. */
const size_t mlen = i2 - (i1 + pl1);
char *ret = malloc(mlen + 1);
@@ -108,6 +112,7 @@ char * extract_between(const char *str, const char *p1, const char *p2) {
}
}
}
+ return NULL;
}
/* change_sentence returns a newly allocated copy of sentence with the first occurrence of find replaced by replace. */
diff --git a/src/utils/planner_utils.c b/src/utils/planner_utils.c
index 440ad53..fb0c66b 100644
--- a/src/utils/planner_utils.c
+++ b/src/utils/planner_utils.c
@@ -26,7 +26,14 @@
extern STMultirelationCatalog
GetTilingSchemeInfo(Oid relationId)
{
- STMultirelationCatalog catalog;
+ /* groupCol/internalType/reshuffledTable aren't read from
+ * pg_dist_spatiotemporal_tables here (they're filled in by other code
+ * paths later) -- zero-initializing means they default to a safe NULL
+ * instead of whatever garbage was already on the stack, which
+ * previously clobbered the zeroed STMultirelation this gets copied
+ * into (see GetMultirelationInfo, which palloc0's its multirelation
+ * before this overwrites catalogTableInfo wholesale). */
+ STMultirelationCatalog catalog = {0};
Datum datumArray[Natts_MTS];
bool isNullArray[Natts_MTS];
ScanKeyData scanKey[1];