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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions contrib/postgis/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,33 @@ install(
FILES "${CMAKE_SOURCE_DIR}/postgis_mest.control" "${CMAKE_SOURCE_DIR}/postgis_mest--1.0.sql"
DESTINATION "${POSTGRESQL_SHARE_DIR}/extension")
install(TARGETS postgis_mest DESTINATION "${POSTGRESQL_DYNLIB_DIR}")

#-------------------------------------
# Regression tests
#-------------------------------------

execute_process(
COMMAND ${POSTGRESQL_PG_CONFIG} --pgxs
OUTPUT_STRIP_TRAILING_WHITESPACE
OUTPUT_VARIABLE POSTGRESQL_PGXS)
get_filename_component(POSTGRESQL_PGXS_DIR "${POSTGRESQL_PGXS}" DIRECTORY)
find_program(PG_REGRESS NAMES pg_regress
HINTS "${POSTGRESQL_PGXS_DIR}/../test/regress")

file(GLOB TEST_FILES "${CMAKE_SOURCE_DIR}/sql/*.test.sql")
set(TEST_NAMES "")
foreach(TEST_FILE ${TEST_FILES})
get_filename_component(TEST_NAME "${TEST_FILE}" NAME)
string(REGEX REPLACE "\\.sql$" "" TEST_NAME "${TEST_NAME}")
list(APPEND TEST_NAMES "${TEST_NAME}")
endforeach()
list(SORT TEST_NAMES)

# Runs the tests against a server already holding the installed extension,
# taking PGHOST, PGPORT and PGUSER from the environment
add_custom_target(installcheck
COMMAND ${PG_REGRESS} --inputdir=${CMAKE_SOURCE_DIR}
--outputdir=${CMAKE_CURRENT_BINARY_DIR}
--bindir=${POSTGRESQL_BIN_DIR}
--dbname=contrib_regression ${TEST_NAMES}
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR})
2 changes: 1 addition & 1 deletion contrib/postgis/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ Multi-Entry GiST Indexing for PostGIS

This directory contains an implementation of Multi-Entry GiST indexes for the PostGIS geometry type.

Contrary to the traditional GiST index for PostGIS, MGiST will index collection types with one bounding box per element in the collection. This index will thus mainly benefit datasets containing collections such as multi-points or multi-polygons that are spread out over a large area. Tuples containing single geometries will be indexed using a single bounding box as usual.
Contrary to the traditional GiST index for PostGIS, MGiST will index collection types with one bounding box per element in the collection, together with the bounding box of the whole geometry. The operators the operator class carries are answered on that whole box, so it is itself one of the entries, and every element of a collection is an entry whatever its dimension. This index will thus mainly benefit datasets containing collections such as multi-points or multi-polygons that are spread out over a large area. Tuples containing single geometries will be indexed using a single bounding box as usual.

The MGiST index for geometries currently provides speedups for overlaps `&&` and distance `<->` operators.
However, the PostGIS support functions are not yet implemented for MGiST indexes, so the index will not be used for queries using the `ST_Intersects` or `ST_Contains` functions. To provide speedup for these functions, you will need to add an explicit overlaps test to the query.
Expand Down
55 changes: 55 additions & 0 deletions contrib/postgis/expected/01_create_extension.test.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
-------------------------------------------------------------------------------
--
-- 01_create_extension.test.sql
--
-- Multi-Entry Search Trees for PostGIS
--
-------------------------------------------------------------------------------
CREATE EXTENSION IF NOT EXISTS postgis_mest CASCADE;
NOTICE: installing required extension "postgis"
NOTICE: installing required extension "mest"
-------------------------------------------------------------------------------
-- The geometries every test reads. The multi-part rows leave a wide gap between
-- their members, which is where a key covering only a member stops witnessing
-- an operator the bounding box answers.
-------------------------------------------------------------------------------
DROP TABLE IF EXISTS tbl_geometry;
NOTICE: table "tbl_geometry" does not exist, skipping
CREATE TABLE tbl_geometry(k integer PRIMARY KEY, g geometry);
-- two-part multipolygons, the parts twenty units apart
INSERT INTO tbl_geometry
SELECT i, ST_Collect(ST_MakeEnvelope(i, i, i + 1, i + 1),
ST_MakeEnvelope(i + 20, i + 20, i + 21, i + 21))
FROM generate_series(1, 60) i;
-- collections holding members of different dimensions
INSERT INTO tbl_geometry
SELECT 100 + i, ST_Collect(ST_Point(i + 7.5, i + 7.5),
ST_MakeEnvelope(i, i, i + 1, i + 1))
FROM generate_series(1, 60) i;
-- single-part geometries
INSERT INTO tbl_geometry
SELECT 200 + i, ST_MakeEnvelope(i, i, i + 2, i + 2) FROM generate_series(1, 40) i;
INSERT INTO tbl_geometry
SELECT 300 + i, ST_MakePoint(i, i) FROM generate_series(1, 40) i;
-- multipoints and multilinestrings with separated members
INSERT INTO tbl_geometry
SELECT 400 + i, ST_Collect(ST_Point(i, i), ST_Point(i + 15, i + 15))
FROM generate_series(1, 40) i;
INSERT INTO tbl_geometry
SELECT 500 + i, ST_Collect(
ST_MakeLine(ST_Point(i, i), ST_Point(i + 1, i + 1)),
ST_MakeLine(ST_Point(i + 25, i + 25), ST_Point(i + 26, i + 26)))
FROM generate_series(1, 40) i;
-------------------------------------------------------------------------------
-- The query geometries.
-------------------------------------------------------------------------------
DROP TABLE IF EXISTS tbl_geomquery;
NOTICE: table "tbl_geomquery" does not exist, skipping
CREATE TABLE tbl_geomquery(k integer PRIMARY KEY, g geometry);
INSERT INTO tbl_geomquery
SELECT i, ST_MakeEnvelope(x, y, x + 3, y + 3)
FROM (SELECT i, (i * 7 % 80)::float8 AS x, (i * 13 % 80)::float8 AS y
FROM generate_series(1, 200) i) s;
ANALYZE tbl_geometry;
ANALYZE tbl_geomquery;
-------------------------------------------------------------------------------
65 changes: 65 additions & 0 deletions contrib/postgis/expected/02_geometry_topops.test.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
-------------------------------------------------------------------------------
--
-- 02_geometry_topops.test.sql
--
-- Multi-Entry Search Trees for PostGIS
--
-- The multi-entry index answers the same rows as a sequential scan. The
-- operator the class carries is answered on the bounding box of a geometry, so
-- every row whose box meets the query must be reached through a key of that
-- row.
--
-------------------------------------------------------------------------------
DROP INDEX IF EXISTS tbl_geometry_mrtree_idx;
NOTICE: index "tbl_geometry_mrtree_idx" does not exist, skipping
DROP TABLE IF EXISTS test_topops;
NOTICE: table "test_topops" does not exist, skipping
CREATE TABLE test_topops(
op CHAR(3),
leftarg TEXT,
rightarg TEXT,
no_idx BIGINT,
mrtree_idx BIGINT
);
-------------------------------------------------------------------------------
-- The counts a sequential scan gives
-------------------------------------------------------------------------------
INSERT INTO test_topops(op, leftarg, rightarg, no_idx)
SELECT '&&', 'geometry', 'geometry', COUNT(*)
FROM tbl_geomquery q, tbl_geometry g WHERE g.g && q.g;
INSERT INTO test_topops(op, leftarg, rightarg, no_idx)
SELECT '&&', 'geometry', 'self', COUNT(*)
FROM tbl_geometry g1, tbl_geometry g2 WHERE g1.g && g2.g;
-------------------------------------------------------------------------------
-- The counts the multi-entry R-tree gives
-------------------------------------------------------------------------------
CREATE INDEX tbl_geometry_mrtree_idx ON tbl_geometry USING mgist(g);
SET enable_seqscan = off;
UPDATE test_topops SET mrtree_idx = ( SELECT COUNT(*)
FROM tbl_geomquery q, tbl_geometry g WHERE g.g && q.g )
WHERE op = '&&' AND leftarg = 'geometry' AND rightarg = 'geometry';
UPDATE test_topops SET mrtree_idx = ( SELECT COUNT(*)
FROM tbl_geometry g1, tbl_geometry g2 WHERE g1.g && g2.g )
WHERE op = '&&' AND leftarg = 'geometry' AND rightarg = 'self';
RESET enable_seqscan;
DROP INDEX tbl_geometry_mrtree_idx;
-------------------------------------------------------------------------------
-- A count of zero would let every row above agree vacuously
-------------------------------------------------------------------------------
SELECT op, leftarg, rightarg, no_idx > 0 AS no_idx_is_positive
FROM test_topops ORDER BY op, leftarg, rightarg;
op | leftarg | rightarg | no_idx_is_positive
-----+----------+----------+--------------------
&& | geometry | geometry | t
&& | geometry | self | t
(2 rows)

SELECT * FROM test_topops
WHERE no_idx <> mrtree_idx OR no_idx IS NULL OR mrtree_idx IS NULL
ORDER BY op, leftarg, rightarg;
op | leftarg | rightarg | no_idx | mrtree_idx
----+---------+----------+--------+------------
(0 rows)

DROP TABLE test_topops;
-------------------------------------------------------------------------------
46 changes: 37 additions & 9 deletions contrib/postgis/postgis_mest.c
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,32 @@ PG_MODULE_MAGIC;
* M(SP-)GiST extract methods
*****************************************************************************/

/**
* @brief Return true if the members of a geometry of the type are the parts it
* is composed of
*
* A curve polygon and a compound curve answer true to lwtype_is_collection
* while their members are the rings and the arcs of a single geometry, so the
* type is what decides this, never that predicate.
*/
static bool
geometry_type_is_multipart(uint32_t type)
{
return (type == MULTIPOINTTYPE || type == MULTILINETYPE ||
type == MULTIPOLYGONTYPE || type == COLLECTIONTYPE ||
type == MULTICURVETYPE || type == MULTISURFACETYPE ||
type == POLYHEDRALSURFACETYPE || type == TINTYPE);
}

PG_FUNCTION_INFO_V1(geometry_mest_extract);
/**
* @brief Multi-Entry GiST extract method for geometries
*
* The geometry itself is the first key, so that the operators the operator
* class answers on the bounding box are witnessed by a key of every value, and
* each member of a multi-part geometry is a key of its own. Every member is
* kept, whatever its dimension, so that no part of the value goes unindexed.
*/
Datum
geometry_mest_extract(PG_FUNCTION_ARGS)
{
Expand All @@ -37,29 +62,32 @@ geometry_mest_extract(PG_FUNCTION_ARGS)
// bool **nullFlags = (bool **) PG_GETARG_POINTER(2);

uint32_t gstype = gserialized_get_type(gs);
if (! lwtype_is_collection(gstype))
Datum *keys;

if (! geometry_type_is_multipart(gstype))
{
keys = palloc(sizeof(Datum));
keys[0] = PointerGetDatum(gs);
*nkeys = 1;
PG_RETURN_POINTER(&gs);
PG_RETURN_POINTER(keys);
}

LWGEOM *lwgeom = lwgeom_from_gserialized(gs);
LWCOLLECTION *lwcoll = lwcollection_extract((LWCOLLECTION *) lwgeom, 0);
LWCOLLECTION *lwcoll = lwgeom_as_lwcollection(lwgeom);

*nkeys = lwcoll->ngeoms;
Datum *keys = palloc(sizeof(Datum) * lwcoll->ngeoms);
for (int i = 0; i < lwcoll->ngeoms; ++i)
*nkeys = (int32) lwcoll->ngeoms + 1;
keys = palloc(sizeof(Datum) * (*nkeys));
keys[0] = PointerGetDatum(gs);
for (uint32_t i = 0; i < lwcoll->ngeoms; ++i)
{
size_t size;
GSERIALIZED *g = gserialized_from_lwgeom(lwcoll->geoms[i], &size);
SET_VARSIZE(g, size);
keys[i] = PointerGetDatum(g);
keys[i + 1] = PointerGetDatum(g);
}

lwgeom_free(lwgeom);
lwcollection_free(lwcoll);

PG_FREE_IF_COPY(gs, 0);
PG_RETURN_POINTER(keys);
}

Expand Down
62 changes: 62 additions & 0 deletions contrib/postgis/sql/01_create_extension.test.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
-------------------------------------------------------------------------------
--
-- 01_create_extension.test.sql
--
-- Multi-Entry Search Trees for PostGIS
--
-------------------------------------------------------------------------------

CREATE EXTENSION IF NOT EXISTS postgis_mest CASCADE;

-------------------------------------------------------------------------------
-- The geometries every test reads. The multi-part rows leave a wide gap between
-- their members, which is where a key covering only a member stops witnessing
-- an operator the bounding box answers.
-------------------------------------------------------------------------------

DROP TABLE IF EXISTS tbl_geometry;
CREATE TABLE tbl_geometry(k integer PRIMARY KEY, g geometry);

-- two-part multipolygons, the parts twenty units apart
INSERT INTO tbl_geometry
SELECT i, ST_Collect(ST_MakeEnvelope(i, i, i + 1, i + 1),
ST_MakeEnvelope(i + 20, i + 20, i + 21, i + 21))
FROM generate_series(1, 60) i;

-- collections holding members of different dimensions
INSERT INTO tbl_geometry
SELECT 100 + i, ST_Collect(ST_Point(i + 7.5, i + 7.5),
ST_MakeEnvelope(i, i, i + 1, i + 1))
FROM generate_series(1, 60) i;

-- single-part geometries
INSERT INTO tbl_geometry
SELECT 200 + i, ST_MakeEnvelope(i, i, i + 2, i + 2) FROM generate_series(1, 40) i;
INSERT INTO tbl_geometry
SELECT 300 + i, ST_MakePoint(i, i) FROM generate_series(1, 40) i;

-- multipoints and multilinestrings with separated members
INSERT INTO tbl_geometry
SELECT 400 + i, ST_Collect(ST_Point(i, i), ST_Point(i + 15, i + 15))
FROM generate_series(1, 40) i;
INSERT INTO tbl_geometry
SELECT 500 + i, ST_Collect(
ST_MakeLine(ST_Point(i, i), ST_Point(i + 1, i + 1)),
ST_MakeLine(ST_Point(i + 25, i + 25), ST_Point(i + 26, i + 26)))
FROM generate_series(1, 40) i;

-------------------------------------------------------------------------------
-- The query geometries.
-------------------------------------------------------------------------------

DROP TABLE IF EXISTS tbl_geomquery;
CREATE TABLE tbl_geomquery(k integer PRIMARY KEY, g geometry);
INSERT INTO tbl_geomquery
SELECT i, ST_MakeEnvelope(x, y, x + 3, y + 3)
FROM (SELECT i, (i * 7 % 80)::float8 AS x, (i * 13 % 80)::float8 AS y
FROM generate_series(1, 200) i) s;

ANALYZE tbl_geometry;
ANALYZE tbl_geomquery;

-------------------------------------------------------------------------------
70 changes: 70 additions & 0 deletions contrib/postgis/sql/02_geometry_topops.test.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
-------------------------------------------------------------------------------
--
-- 02_geometry_topops.test.sql
--
-- Multi-Entry Search Trees for PostGIS
--
-- The multi-entry index answers the same rows as a sequential scan. The
-- operator the class carries is answered on the bounding box of a geometry, so
-- every row whose box meets the query must be reached through a key of that
-- row.
--
-------------------------------------------------------------------------------

DROP INDEX IF EXISTS tbl_geometry_mrtree_idx;

DROP TABLE IF EXISTS test_topops;
CREATE TABLE test_topops(
op CHAR(3),
leftarg TEXT,
rightarg TEXT,
no_idx BIGINT,
mrtree_idx BIGINT
);

-------------------------------------------------------------------------------
-- The counts a sequential scan gives
-------------------------------------------------------------------------------

INSERT INTO test_topops(op, leftarg, rightarg, no_idx)
SELECT '&&', 'geometry', 'geometry', COUNT(*)
FROM tbl_geomquery q, tbl_geometry g WHERE g.g && q.g;

INSERT INTO test_topops(op, leftarg, rightarg, no_idx)
SELECT '&&', 'geometry', 'self', COUNT(*)
FROM tbl_geometry g1, tbl_geometry g2 WHERE g1.g && g2.g;

-------------------------------------------------------------------------------
-- The counts the multi-entry R-tree gives
-------------------------------------------------------------------------------

CREATE INDEX tbl_geometry_mrtree_idx ON tbl_geometry USING mgist(g);

SET enable_seqscan = off;

UPDATE test_topops SET mrtree_idx = ( SELECT COUNT(*)
FROM tbl_geomquery q, tbl_geometry g WHERE g.g && q.g )
WHERE op = '&&' AND leftarg = 'geometry' AND rightarg = 'geometry';

UPDATE test_topops SET mrtree_idx = ( SELECT COUNT(*)
FROM tbl_geometry g1, tbl_geometry g2 WHERE g1.g && g2.g )
WHERE op = '&&' AND leftarg = 'geometry' AND rightarg = 'self';

RESET enable_seqscan;

DROP INDEX tbl_geometry_mrtree_idx;

-------------------------------------------------------------------------------
-- A count of zero would let every row above agree vacuously
-------------------------------------------------------------------------------

SELECT op, leftarg, rightarg, no_idx > 0 AS no_idx_is_positive
FROM test_topops ORDER BY op, leftarg, rightarg;

SELECT * FROM test_topops
WHERE no_idx <> mrtree_idx OR no_idx IS NULL OR mrtree_idx IS NULL
ORDER BY op, leftarg, rightarg;

DROP TABLE test_topops;

-------------------------------------------------------------------------------
Loading