blob: 8dee5561fd54547632f6ec28b962e08791daae9c [file] [log] [blame]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
import pytest
import shapely
from sedonadb.testing import PostGIS, SedonaDB, geom_or_null, val_or_null
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
@pytest.mark.parametrize(
("geom", "expected"),
[
(None, None),
("POINT EMPTY", 0.0),
("LINESTRING EMPTY", 0.0),
("POLYGON EMPTY", 0.0),
("MULTIPOINT EMPTY", 0.0),
("MULTILINESTRING EMPTY", 0.0),
("MULTIPOLYGON EMPTY", 0.0),
("GEOMETRYCOLLECTION EMPTY", 0.0),
("POINT (5 2)", 0.0),
("MULTIPOINT ((0 0), (1 1))", 0.0),
("LINESTRING (0 0, 1 1)", 0.0),
("MULTILINESTRING ((0 0, 1 1), (1 1, 2 2))", 0.0),
("POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", 1.0),
(
"MULTIPOLYGON (((0 0, 1 0, 1 1, 0 1, 0 0)), ((0 0, 1 0, 1 1, 0 1, 0 0)))",
2.0,
),
(
"GEOMETRYCOLLECTION (POINT (0 0), POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0)), LINESTRING (0 0, 1 1), GEOMETRYCOLLECTION (POLYGON ((0 0, -1 0, -1 -1, 0 -1, 0 0))))",
2.0,
),
],
)
def test_st_area(eng, geom, expected):
eng = eng.create_or_skip()
eng.assert_query_result(f"SELECT ST_Area({geom_or_null(geom)})", expected)
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
@pytest.mark.parametrize(
("geom", "expected"),
[
(
"POINT (1 1)",
b"\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0\x3f\x00\x00\x00\x00\x00\x00\xf0\x3f",
),
(
"POINT EMPTY",
b"\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf8\x7f\x00\x00\x00\x00\x00\x00\xf8\x7f",
),
(
"LINESTRING (0 0, 1 2, 3 4)",
b"\x01\x02\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0\x3f\x00\x00\x00\x00\x00\x00\x00\x40\x00\x00\x00\x00\x00\x00\x08\x40\x00\x00\x00\x00\x00\x00\x10\x40",
),
("LINESTRING EMPTY", b"\x01\x02\x00\x00\x00\x00\x00\x00\x00"),
(
"POINT ZM (0 0 0 0)",
b"\x01\xb9\x0b\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
),
(
"GEOMETRYCOLLECTION (POINT (0 0), POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0)))",
b"\x01\x07\x00\x00\x00\x02\x00\x00\x00\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x03\x00\x00\x00\x01\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
),
],
)
def test_st_asbinary(eng, geom, expected):
eng = eng.create_or_skip()
eng.assert_query_result(f"SELECT ST_AsBinary({geom_or_null(geom)})", expected)
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
@pytest.mark.parametrize(
("geom"),
[
None,
# geoarrow-c returns POINT (nan nan) instead of POINT EMPTY
"POINT EMPTY",
"LINESTRING EMPTY",
"POLYGON EMPTY",
"MULTIPOINT EMPTY",
"MULTILINESTRING EMPTY",
"MULTIPOLYGON EMPTY",
"GEOMETRYCOLLECTION EMPTY",
"POINT(1 1)",
"LINESTRING(0 0,1 1)",
"POLYGON((0 0,1 0,1 1,0 1,0 0))",
"MULTIPOINT((0 0),(1 1))",
"MULTILINESTRING((0 0,1 1),(1 1,2 2))",
"MULTIPOLYGON(((0 0,1 0,1 1,0 1,0 0)),((0 0,1 0,1 1,0 1,0 0)))",
"GEOMETRYCOLLECTION(POINT(0 0),POLYGON((0 0,1 0,1 1,0 1,0 0)),LINESTRING(0 0,1 1),GEOMETRYCOLLECTION(POLYGON((0 0,-1 0,-1 -1,0 -1,0 0))))",
"POINT Z(0 0 0)",
"POINT ZM(0 0 0 0)",
"LINESTRING M(0 0 0,1 1 1)",
],
)
def test_st_astext(eng, geom):
eng = eng.create_or_skip()
expected = geom
if isinstance(eng, PostGIS) and expected is not None:
expected = expected.replace(r"M(", r"M (")
expected = expected.replace(r"Z(", r"Z (")
eng.assert_query_result(f"SELECT ST_AsText({geom_or_null(geom)})", expected)
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
@pytest.mark.parametrize(
("geom1", "geom2", "expected"),
[
# TODO: PostGIS fails without explicit ::GEOMETRY type cast, but casting
# doesn't work on SedonaDB yet.
# (None, None, None),
("POINT (0 0)", None, None),
(None, "POINT (0 0)", None),
("POINT (0 0)", "POINT (0 0)", None),
("POINT (0 0)", "POINT (1 1)", 0.7853981633974483), # 45 / 180 * PI
("POINT (0 0)", "POINT (-1 -1)", 3.9269908169872414), # 225 / 180 * PI
],
)
def test_st_azimuth(eng, geom1, geom2, expected):
eng = eng.create_or_skip()
eng.assert_query_result(
f"SELECT ST_Azimuth({geom_or_null(geom1)}, {geom_or_null(geom2)})",
expected,
numeric_epsilon=1e-8,
)
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
@pytest.mark.parametrize(
("geom", "expected_boundary"),
[
(None, None),
("LINESTRING(1 1, 0 0, -1 1)", "MULTIPOINT (1 1, -1 1)"),
("POLYGON((1 1,0 0, -1 1, 1 1))", "LINESTRING (1 1, 0 0, -1 1, 1 1)"),
(
"LINESTRING(100 150,50 60, 70 80, 160 170)",
"MULTIPOINT (100 150, 160 170)",
),
(
"POLYGON (( 10 130, 50 190, 110 190, 140 150, 150 80, 100 10, 20 40, 10 130 ), ( 70 40, 100 50, 120 80, 80 110, 50 90, 70 40 ))",
"MULTILINESTRING ((10 130, 50 190, 110 190, 140 150, 150 80, 100 10, 20 40, 10 130), (70 40, 100 50, 120 80, 80 110, 50 90, 70 40))",
),
(
"MULTILINESTRING ((1 1, 2 2), (3 3, 4 4))",
"MULTIPOINT (1 1, 2 2, 3 3, 4 4)",
),
(
"MULTILINESTRING ((10 10, 20 20), (30 30, 40 40, 30 30))",
"MULTIPOINT (10 10, 20 20)",
),
("POLYGON((0 0, 0 1, 1 1, 1 0, 0 0))", "LINESTRING (0 0, 0 1, 1 1, 1 0, 0 0)"),
(
"MULTIPOLYGON (((0 0, 0 1, 1 1, 1 0, 0 0)), ((10 10, 10 11, 11 11, 11 10, 10 10)))",
"MULTILINESTRING ((0 0, 0 1, 1 1, 1 0, 0 0), (10 10, 10 11, 11 11, 11 10, 10 10))",
),
(
"MULTIPOLYGON (((0 0, 0 10, 10 10, 10 0, 0 0), (2 2, 2 8, 8 8, 8 2, 2 2)))",
"MULTILINESTRING ((0 0, 0 10, 10 10, 10 0, 0 0), (2 2, 2 8, 8 8, 8 2, 2 2))", # Note: Order of points in inner ring may vary by implementation, but boundary is correct.
),
(
"GEOMETRYCOLLECTION(POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0)), GEOMETRYCOLLECTION(LINESTRING(10 10, 10 20)))",
"GEOMETRYCOLLECTION (MULTIPOINT (10 10, 10 20), LINESTRING (0 0, 0 1, 1 1, 1 0, 0 0))",
),
(
"GEOMETRYCOLLECTION(LINESTRING(1 1,2 2),GEOMETRYCOLLECTION(POLYGON((3 3,4 4,5 5,3 3)),GEOMETRYCOLLECTION(LINESTRING(6 6,7 7),POLYGON((8 8,9 9,10 10,8 8)))))",
"GEOMETRYCOLLECTION (MULTIPOINT (1 1, 2 2, 6 6, 7 7), MULTILINESTRING ((3 3, 4 4, 5 5, 3 3), (8 8, 9 9, 10 10, 8 8)))",
),
(
"GEOMETRYCOLLECTION(LINESTRING(10 10,20 20),GEOMETRYCOLLECTION(POLYGON((30 30,40 40,50 50,30 30)),GEOMETRYCOLLECTION(LINESTRING(60 60,70 70),LINESTRING(80 80,90 90))))",
"GEOMETRYCOLLECTION (MULTIPOINT (10 10, 20 20, 60 60, 70 70, 80 80, 90 90), LINESTRING (30 30, 40 40, 50 50, 30 30))",
),
(
"GEOMETRYCOLLECTION(POLYGON((1 1,2 2,3 3,1 1)),GEOMETRYCOLLECTION(LINESTRING(4 4,5 5),GEOMETRYCOLLECTION(POLYGON((6 6,7 7,8 8,6 6)),LINESTRING(9 9,10 10))))",
"GEOMETRYCOLLECTION (MULTIPOINT (4 4, 5 5, 9 9, 10 10), MULTILINESTRING ((1 1, 2 2, 3 3, 1 1), (6 6, 7 7, 8 8, 6 6)))",
),
(
"GEOMETRYCOLLECTION(LINESTRING(1 1,1 10,10 10,10 1,1 1),GEOMETRYCOLLECTION(LINESTRING(2 2,2 9,9 9,9 2,2 2),GEOMETRYCOLLECTION(POLYGON((3 3,3 8,8 8,8 3,3 3)),POLYGON((4 4,4 7,7 7,7 4,4 4)))))",
"MULTILINESTRING ((3 3, 3 8, 8 8, 8 3, 3 3), (4 4, 4 7, 7 7, 7 4, 4 4))",
),
(
"GEOMETRYCOLLECTION(POLYGON((0 0,10 0,10 10,0 10,0 0)),GEOMETRYCOLLECTION(LINESTRING(1 1,9 9),GEOMETRYCOLLECTION(LINESTRING(1 9,9 1),POLYGON((2 2,8 2,8 8,2 8,2 2)))))",
"GEOMETRYCOLLECTION (MULTIPOINT (1 1, 9 9, 1 9, 9 1), MULTILINESTRING ((0 0, 10 0, 10 10, 0 10, 0 0), (2 2, 8 2, 8 8, 2 8, 2 2)))",
),
(
"GEOMETRYCOLLECTION(GEOMETRYCOLLECTION(GEOMETRYCOLLECTION(LINESTRING(1 2,3 4),POLYGON((5 6,7 8,9 10,5 6))),POLYGON((11 12,13 14,15 16,11 12))),LINESTRING(17 18,19 20))",
"GEOMETRYCOLLECTION (MULTIPOINT (1 2, 3 4, 17 18, 19 20), MULTILINESTRING ((5 6, 7 8, 9 10, 5 6), (11 12, 13 14, 15 16, 11 12)))",
),
],
)
def test_st_boundary(eng, geom, expected_boundary):
eng = eng.create_or_skip()
eng.assert_query_result(
f"SELECT ST_Boundary({geom_or_null(geom)})", expected_boundary
)
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
@pytest.mark.parametrize(
("geom", "is_empty"),
[
("POINT (5 10)", True),
("POINT (0 0)", True),
("POINT (-1 -5)", True),
("MULTIPOINT (100 200)", True),
("MULTIPOINT (5 10, 15 20)", True),
("MULTIPOINT (1 1, 2 2, 3 3, 1 1)", True),
("LINESTRING(10 10, 20 20, 30 10, 10 10)", True),
("MULTILINESTRING ((0 0, 0 1, 1 0, 0 0), (10 10, 10 20, 20 10, 10 10))", True),
],
)
def test_st_boundary_empty(eng, geom, is_empty):
eng = eng.create_or_skip()
eng.assert_query_result(
f"SELECT ST_IsEmpty(ST_Boundary({geom_or_null(geom)}))", is_empty
)
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
@pytest.mark.parametrize(
("geom", "dist", "expected_area"),
[
(None, None, None),
(None, 1.0, None),
("POINT (1 1)", None, None),
("POINT (1 1)", 0, 0),
("POINT EMPTY", 1, 0),
("LINESTRING EMPTY", 1.0, 0),
("POLYGON EMPTY", 1.0, 0),
("POINT (0 0)", 1.0, 3.121445152258052),
("POINT (0 0)", 2.0, 12.485780609032208),
("LINESTRING (0 0, 1 1)", 1.0, 5.949872277004242),
("LINESTRING (0 0, 1 1)", 2.0, 18.14263485852459),
("POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", 2.0, 21.48578060903221),
("MULTIPOINT ((0 0), (1 1))", 1.0, 5.682167728387077),
(
"GEOMETRYCOLLECTION (POINT (0 0), LINESTRING (0 0, 1 1), POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0)))",
1.0,
8.121445152256216,
),
],
)
def test_st_buffer(eng, geom, dist, expected_area):
eng = eng.create_or_skip()
eng.assert_query_result(
f"SELECT ST_Area(ST_Buffer({geom_or_null(geom)}, {val_or_null(dist)}))",
expected_area,
numeric_epsilon=1e-9,
)
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
@pytest.mark.parametrize(
("geom", "dist", "buffer_style_parameters", "expected_area"),
[
(None, None, None, None),
("POINT(100 90)", 50, "'quad_segs=8'", 7803.612880645131),
(
"LINESTRING(50 50,150 150,150 50)",
10,
"'endcap=round join=round'",
5016.204476944362,
),
(
"POLYGON((0 0, 0 10, 10 10, 10 0, 0 0))",
2,
"'join=miter'",
196.0,
),
(
"LINESTRING(0 0, 10 0)",
5,
"'endcap=square'",
200.0,
),
(
"POINT(0 0)",
10,
"'quad_segs=4'",
306.1467458920718,
),
(
"POINT(0 0)",
10,
"'quad_segs=16'",
313.654849054594,
),
(
"LINESTRING(0 0, 100 0, 100 100)",
5,
"'join=bevel'",
2065.536128806451,
),
(
"LINESTRING(0 0, 50 0)",
10,
"'endcap=flat'",
1000.0,
),
(
"POLYGON((0 0, 0 20, 20 20, 20 0, 0 0))",
-2,
"'join=round'",
256.0,
),
(
"POLYGON((0 0, 0 100, 100 100, 100 0, 0 0), (20 20, 20 80, 80 80, 80 20, 20 20))",
5,
"'join=round quad_segs=4'",
9576.536686473019,
),
(
"MULTIPOINT((10 10), (30 30))",
5,
"'quad_segs=8'",
156.0722576129026,
),
(
"GEOMETRYCOLLECTION(POINT(10 10), LINESTRING(50 50, 60 60))",
3,
"'endcap=round join=round'",
141.0388264830308,
),
(
"POLYGON((0 0, 0 10, 10 10, 10 0, 0 0))",
0,
"'join=miter'",
100.0,
),
(
"POINT(0 0)",
0.1,
"'quad_segs=8'",
0.031214451522580514,
),
(
"LINESTRING(0 0, 50 0, 50 50)",
10,
"'join=miter miter_limit=2'",
2312.1445152258043,
),
(
"LINESTRING(0 0, 0 100)",
10,
"'side=left'",
1000.0,
),
# GEOS version difference: GEOS 3.9 (PostGIS) returns 16285.08 with artifacts
# GEOS 3.12+ (SedonaDB) returns 12713.61 without artifacts (more accurate)
# See: https://github.com/libgeos/geos/commit/091f6d99
(
"LINESTRING (50 50, 150 150, 150 50)",
100,
"'side=right'",
12713.605978550266,
),
(
"POLYGON ((50 50, 50 150, 150 150, 150 50, 50 50))",
20,
"'side=left'",
10000.0, # GEOS 3.9 (PostGIS): 19248.58
),
(
"POLYGON ((50 50, 50 150, 150 150, 150 50, 50 50))",
20,
"'side=right endcap=flat'",
6400.0, # GEOS 3.9 (PostGIS): 3600.0
),
(
"LINESTRING (50 50, 150 150, 150 50)",
100,
"'side=both'",
69888.089291866,
),
],
)
def test_st_buffer_style_parameters(
eng, geom, dist, buffer_style_parameters, expected_area
):
eng = eng.create_or_skip()
eng.assert_query_result(
f"SELECT ST_Area(ST_Buffer({geom_or_null(geom)}, {val_or_null(dist)}, {val_or_null(buffer_style_parameters)}))",
expected_area,
numeric_epsilon=1e-9,
)
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
@pytest.mark.parametrize(
("geom", "expected"),
[
(None, None),
("POINT (0 0)", "POINT (0 0)"),
("LINESTRING (0 0, 1 1)", "POINT (0.5 0.5)"),
("POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", "POINT (0.5 0.5)"),
("MULTIPOINT ((0 0), (1 1))", "POINT (0.5 0.5)"),
("MULTILINESTRING ((0 0, 1 1), (1 1, 2 2))", "POINT (1 1)"),
(
"MULTIPOLYGON (((0 0, 1 0, 1 1, 0 1, 0 0)), ((0 0, 1 0, 1 1, 0 1, 0 0)))",
"POINT (0.5 0.5)",
),
(
"GEOMETRYCOLLECTION (POINT (0 0), LINESTRING (0 0, 1 1), POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0)))",
"POINT (0.5 0.5)",
),
# Failing: issue with testing code: geoarrow-c rendering POINT (nan, nan)
# instead of POINT EMPTY
# https://github.com/geoarrow/geoarrow-c/issues/143
("POINT EMPTY", "POINT (nan nan)"),
("LINESTRING EMPTY", "POINT (nan nan)"),
("POLYGON EMPTY", "POINT (nan nan)"),
("MULTIPOINT EMPTY", "POINT (nan nan)"),
("MULTILINESTRING EMPTY", "POINT (nan nan)"),
("MULTIPOLYGON EMPTY", "POINT (nan nan)"),
("GEOMETRYCOLLECTION EMPTY", "POINT (nan nan)"),
],
)
def test_st_centroid(eng, geom, expected):
eng = eng.create_or_skip()
eng.assert_query_result(f"SELECT ST_Centroid({geom_or_null(geom)})", expected)
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
@pytest.mark.parametrize(
("geom", "expected"),
[
(None, None),
# POINTS - Always simple (single point has no self-intersections)
("POINT (1 1)", True),
("POINT EMPTY", True), # Empty geometry is simple
# MULTIPOINTS
("MULTIPOINT (1 1, 2 2, 3 3)", True), # Distinct points
("MULTIPOINT (1 1, 2 2, 1 1)", False), # Duplicate points make it non-simple
("MULTIPOINT EMPTY", True), # Empty multipoint
("MULTIPOINT (1 1, 2 2, 3 3)", True),
# LINESTRINGS
("LINESTRING (0 0, 1 1)", True), # Simple straight line
("LINESTRING (0 0, 1 1, 2 2)", True), # Simple line, collinear points
("LINESTRING (0 0, 1 1, 0 1, 1 0)", False), # Self-intersecting (bowtie shape)
("LINESTRING(1 1,2 2,2 3.5,1 3,1 2,2 1)", False), # Complex self-intersection
(
"LINESTRING (0 0, 1 1, 0 0)",
False,
), # Closed loop with repeated start/end but intersects at interior
("LINESTRING (0 0, 1 1, 1 0, 0 0)", True), # Simple closed ring (triangle)
("LINESTRING EMPTY", True), # Empty linestring
# POLYGONS
("POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0))", True), # Simple rectangle
(
"POLYGON ((0 0, 1 1, 0 1, 1 0, 0 0))",
False,
), # Bowtie polygon - self-intersecting
(
"POLYGON((1 2, 3 4, 5 6, 1 2))",
False,
), # Degenerate polygon - zero-area Triangle
(
"Polygon((0 0, 2 0, 1 1, 2 2, 0 2, 1 1, 0 0))",
False,
), # Star shape with self-intersection
(
"POLYGON ((0 0, 3 0, 3 3, 0 3, 0 0), (1 1, 2 1, 2 2, 1 2, 1 1))",
True,
), # Polygon with hole, valid
(
"POLYGON ((0 0, 3 0, 3 3, 0 3, 0 0), (1 1, 0 2, 2 2, 1 1))",
True,
), # Valid OGC Polygon (is also considered 'Simple' by OGC standard)
# MULTILINESTRINGS
(
"MULTILINESTRING ((0 0, 1 1), (1 1, 2 2))",
True,
), # Touching at endpoints only
("MULTILINESTRING ((0 0, 2 2), (0 2, 2 0))", False), # Lines cross in middle
("MULTILINESTRING ((0 0, 1 1), (2 2, 3 3))", True), # Disjoint lines
(
"MULTILINESTRING ((0 0, 1 1, 2 2), (2 2, 3 3))",
True,
), # Connected at endpoint
(
"MULTILINESTRING ((0 0, 2 0, 2 2, 0 2, 0 0), (1 1, 3 1, 3 3, 1 3, 1 1))",
False,
), # Not simple: The two rings overlap and intersect (2 1), violating the MULTILINESTRING simplicity rule.
("MULTILINESTRING ((0 0, 2 2), (1 0, 1 2))", False), # Lines intersect at (1,1)
("MULTILINESTRING EMPTY", True), # Empty multilinestring
# MULTIPOLYGONS
("MULTIPOLYGON (((0 0, 0 1, 1 1, 1 0, 0 0)))", True), # Single simple polygon
(
"MULTIPOLYGON (((0 0, 0 2, 2 2, 2 0, 0 0)), ((3 0, 3 2, 5 2, 5 0, 3 0)))",
True,
), # Two disjoint polygons
(
"MULTIPOLYGON (((0 0, 0 2, 2 2, 2 0, 0 0)), ((1 1, 1 3, 3 3, 3 1, 1 1)))",
True,
), # Touching at point
(
"MULTIPOLYGON (((0 0, 0 3, 3 3, 3 0, 0 0)), ((1 1, 1 2, 2 2, 2 1, 1 1)))",
True,
), # One inside another (donut)
(
"MULTIPOLYGON (((0 0, 0 2, 2 2, 2 0, 0 0)), ((0 0, 0 1, 1 1, 1 0, 0 0)))",
True,
), # Simple: The boundaries do not cross
("MULTIPOLYGON EMPTY", True), # Empty multipolygon
# GEOMETRYCOLLECTIONS
(
"GEOMETRYCOLLECTION (POINT (1 1), LINESTRING (0 0, 1 1))",
True,
), # Simple components
(
"GEOMETRYCOLLECTION (LINESTRING (0 0, 2 2), LINESTRING (0 2, 2 0))",
True,
),
("GEOMETRYCOLLECTION EMPTY", True), # Empty collection
# EDGE CASES
("POINT (1 1)", True), # Repeated for completeness
(
"LINESTRING (1 1, 1 1)",
True,
), # Simple: Start and end points are the only intersecting points.
(
"POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0), (0.2 0.2, 0.2 0.8, 0.8 0.8, 0.8 0.2, 0.2 0.2))",
True,
), # Proper hole
(
"POLYGON ((0 0, 2 0, 2 2, 0 2, 0 0), (0.5 0.5, 1.5 0.5, 1.5 1.5, 0.5 1.5, 0.5 0.5))",
True,
), # Another valid hole
(
"LINESTRING (0 0, 1 0, 1 1, 0 1, 0.5 1, 0.5 0)",
False,
), # Self-touching at non-endpoint
],
)
def test_st_issimple(eng, geom, expected):
eng = eng.create_or_skip()
eng.assert_query_result(f"SELECT ST_IsSimple({geom_or_null(geom)})", expected)
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
@pytest.mark.parametrize(
("geom", "expected"),
[
(None, None),
("POINT (0 0)", True),
("POINT EMPTY", True),
("LINESTRING (0 0, 1 1)", True),
("LINESTRING (0 0, 1 1, 1 0, 0 1)", True),
(
"MULTILINESTRING ((0 0, 1 1), (0 0, 1 1, 1 0, 0 1))",
True,
),
("LINESTRING EMPTY", True),
# Invalid LineStrings
("LINESTRING (0 0, 0 0)", False), # Degenerate - both points identical
("LINESTRING (0 0, 0 0, 0 0)", False), # All points identical
# Invalid MultiLineStrings
("MULTILINESTRING ((0 0, 0 0), (1 1, 2 2))", False), # Degenerate component
(
"MULTILINESTRING ((0 0, 0 0), (1 1, 1 1))",
False,
), # Multiple degenerate components
("POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0))", True),
("POLYGON EMPTY", True),
("POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0), (1 1, 1 2, 2 2, 2 1, 1 1))", True),
# Invalid Polygons
# Self-intersecting polygon (bowtie)
("POLYGON ((0 0, 1 1, 0 1, 1 0, 0 0))", False),
# Inner ring shares an edge with the outer ring
("POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0), (0 0, 0 1, 1 1, 1 0, 0 0))", False),
# Self-intersecting polygon (figure-8)
("Polygon((0 0, 2 0, 1 1, 2 2, 0 2, 1 1, 0 0))", False),
# Inner ring touches the outer ring at a point
(
"POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0), (1 10, 1 9, 2 9, 2 10, 1 10))",
False,
),
# Overlapping polygons in a multipolygon
(
"MULTIPOLYGON (((0 0, 2 0, 2 2, 0 2, 0 0)), ((1 1, 3 1, 3 3, 1 3, 1 1)))",
False,
),
(
"MULTIPOLYGON (((0 0, 1 0, 1 1, 0 1, 0 0)), ((2 2, 3 2, 3 3, 2 3, 2 2)))",
True,
),
# Geometry collection with an invalid polygon
("GEOMETRYCOLLECTION (POLYGON ((0 0, 1 1, 0 1, 1 0, 0 0)))", False),
("GEOMETRYCOLLECTION (POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0)))", True),
],
)
def test_st_isvalid(eng, geom, expected):
eng = eng.create_or_skip()
eng.assert_query_result(f"SELECT ST_IsValid({geom_or_null(geom)})", expected)
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
@pytest.mark.parametrize(
("geom", "expected"),
[
(None, None),
("POINT (0 0)", "POINT (0 0)"),
("MULTIPOINT (0 0, 1 1)", "LINESTRING (0 0, 1 1)"),
("MULTIPOINT (0 0, 1 1, 1 0)", "POLYGON ((0 0, 1 1, 1 0, 0 0))"),
("MULTIPOINT (0 0, 1 1, 1 0, 0.5 0.25)", "POLYGON ((0 0, 1 1, 1 0, 0 0))"),
],
)
def test_st_convexhull(eng, geom, expected):
eng = eng.create_or_skip()
eng.assert_query_result(f"SELECT ST_ConvexHull({geom_or_null(geom)})", expected)
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
@pytest.mark.parametrize(
("geom", "expected"),
[
(None, None),
("POINT (0 0)", "POINT (0 0)"),
("POINT EMPTY", "POINT EMPTY"),
("LINESTRING (0 0, 1 1, 2 2)", "LINESTRING (0 0, 1 1, 2 2)"),
("LINESTRING EMPTY", "LINESTRING EMPTY"),
("POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))"),
("MULTIPOINT ((0 0), (1 1), (2 2))", "MULTIPOINT (0 0, 1 1, 2 2)"),
(
"MULTIPOLYGON (((0 0, 1 0, 1 1, 0 1, 0 0)), ((1 0, 2 0, 2 1, 1 1, 1 0)))",
"POLYGON ((0 0, 0 1, 1 1, 2 1, 2 0, 1 0, 0 0))",
),
(
"MULTIPOLYGON (((0 0, 1 0, 1 1, 0 1, 0 0)), ((2 2, 3 2, 3 3, 2 3, 2 2)))",
"MULTIPOLYGON (((0 1, 1 1, 1 0, 0 0, 0 1)), ((2 3, 3 3, 3 2, 2 2, 2 3)))",
),
(
"GEOMETRYCOLLECTION (POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0)), POLYGON ((1 0, 2 0, 2 1, 1 1, 1 0)))",
"POLYGON ((0 0, 0 1, 1 1, 2 1, 2 0, 1 0, 0 0))",
),
],
)
def test_st_unaryunion(eng, geom, expected):
eng = eng.create_or_skip()
if expected is None:
eng.assert_query_result(f"SELECT ST_UnaryUnion({geom_or_null(geom)})", expected)
elif "EMPTY" in expected.upper():
eng.assert_query_result(
f"SELECT ST_IsEmpty(ST_UnaryUnion({geom_or_null(geom)}))", True
)
else:
eng.assert_query_result(
f"SELECT ST_Equals(ST_UnaryUnion({geom_or_null(geom)}), {geom_or_null(expected)})",
True,
)
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
def test_st_makeline(eng):
eng = eng.create_or_skip()
eng.assert_query_result(
"SELECT ST_MakeLine(ST_Point(0, 1), ST_Point(2, 3))", "LINESTRING (0 1, 2 3)"
)
eng.assert_query_result(
"SELECT ST_MakeLine(ST_Point(0, 1), ST_GeomFromText('LINESTRING (0 1, 2 3)'))",
"LINESTRING (0 1, 2 3)",
)
eng.assert_query_result(
"SELECT ST_MakeLine(ST_Point(0, 1), ST_GeomFromText('LINESTRING (2 3, 4 5)'))",
"LINESTRING (0 1, 2 3, 4 5)",
)
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
@pytest.mark.parametrize(
("geom", "expected"),
[
(None, None),
("POINT EMPTY", 0),
("LINESTRING EMPTY", 1),
("POLYGON EMPTY", 2),
("MULTIPOINT EMPTY", 0),
("MULTILINESTRING EMPTY", 1),
("MULTIPOLYGON EMPTY", 2),
("GEOMETRYCOLLECTION EMPTY", 0),
("POINT (0 0)", 0),
("LINESTRING (0 0, 1 1)", 1),
("POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", 2),
("MULTIPOINT ((0 0), (1 1))", 0),
("MULTILINESTRING ((0 0, 1 1), (1 1, 2 2))", 1),
("MULTIPOLYGON (((0 0, 1 0, 1 1, 0 1, 0 0)), ((0 0, 1 0, 1 1, 0 1, 0 0)))", 2),
# Ensure GeometryCollections with different nested geometries are handled correctly
("GEOMETRYCOLLECTION (POINT (0 0))", 0),
("GEOMETRYCOLLECTION (POINT (0 0), LINESTRING (0 0, 1 1))", 1),
(
"GEOMETRYCOLLECTION (POINT (0 0), LINESTRING (0 0, 1 1), POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0)))",
2,
),
(
"GEOMETRYCOLLECTION (POINT (0 0), GEOMETRYCOLLECTION (LINESTRING (0 0, 1 1)))",
1,
),
("POINT Z (0 0 0)", 0),
("POINT ZM (0 0 0 0)", 0),
],
)
def test_st_dimension(eng, geom, expected):
eng = eng.create_or_skip()
eng.assert_query_result(f"SELECT ST_Dimension({geom_or_null(geom)})", expected)
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
def test_st_dump(eng):
is_postgis = eng == PostGIS
eng = eng.create_or_skip()
cases = [
{"input": "POINT (1 2)", "expected": [{"path": [], "geom": "POINT (1 2)"}]},
{
"input": "LINESTRING (1 1, 2 2)",
"expected": [{"path": [], "geom": "LINESTRING (1 1, 2 2)"}],
},
{
"input": "POLYGON ((1 1, 2 2, 2 1, 1 1))",
"expected": [{"path": [], "geom": "POLYGON ((1 1, 2 2, 2 1, 1 1))"}],
},
{
"input": "MULTIPOINT (0 1, 1 2)",
"expected": [
{
"path": [1],
"geom": "POINT (0 1)",
},
{
"path": [2],
"geom": "POINT (1 2)",
},
],
},
{
"input": "MULTILINESTRING ((1 1, 2 2), EMPTY, (3 3, 4 4))",
"expected": [
{
"path": [1],
"geom": "LINESTRING (1 1, 2 2)",
},
{
"path": [2],
"geom": "LINESTRING EMPTY",
},
{
"path": [3],
"geom": "LINESTRING (3 3, 4 4)",
},
],
},
{
"input": "MULTIPOLYGON (((1 1, 2 2, 2 1, 1 1)), EMPTY, ((3 3, 4 4, 4 3, 3 3)))",
"expected": [
{
"path": [1],
"geom": "POLYGON ((1 1, 2 2, 2 1, 1 1))",
},
{
"path": [2],
"geom": "POLYGON EMPTY",
},
{
"path": [3],
"geom": "POLYGON ((3 3, 4 4, 4 3, 3 3))",
},
],
},
{
"input": "GEOMETRYCOLLECTION (POINT (1 2), MULTILINESTRING ((1 1, 2 2), EMPTY, (3 3, 4 4)), LINESTRING (1 1, 2 2))",
"expected": [
{
"path": [1],
"geom": "POINT (1 2)",
},
{
"path": [2, 1],
"geom": "LINESTRING (1 1, 2 2)",
},
{
"path": [2, 2],
"geom": "LINESTRING EMPTY",
},
{
"path": [2, 3],
"geom": "LINESTRING (3 3, 4 4)",
},
{
"path": [3],
"geom": "LINESTRING (1 1, 2 2)",
},
],
},
{
"input": "GEOMETRYCOLLECTION (POINT (1 2), GEOMETRYCOLLECTION (MULTILINESTRING ((1 1, 2 2), EMPTY, (3 3, 4 4)), LINESTRING (1 1, 2 2)))",
"expected": [
{
"path": [1],
"geom": "POINT (1 2)",
},
{
"path": [2, 1, 1],
"geom": "LINESTRING (1 1, 2 2)",
},
{
"path": [2, 1, 2],
"geom": "LINESTRING EMPTY",
},
{
"path": [2, 1, 3],
"geom": "LINESTRING (3 3, 4 4)",
},
{
"path": [2, 2],
"geom": "LINESTRING (1 1, 2 2)",
},
],
},
]
for case in cases:
if is_postgis:
result = eng.execute_and_collect(
f"SELECT ST_Dump({geom_or_null(case['input'])})"
)
else:
result = eng.execute_and_collect(
f"SELECT unnest(ST_Dump({geom_or_null(case['input'])}))"
)
df = eng.result_to_pandas(result)
for i in df.index:
actual = df.iat[i, 0]
expected = case["expected"][i]
assert list(actual.keys()) == ["path", "geom"]
if actual["path"].size == 0:
assert len(expected["path"]) == 0
else:
actual["path"] == expected["path"]
assert actual["geom"] == shapely.from_wkt(expected["geom"]).wkb
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
@pytest.mark.parametrize(
("geom", "expected"),
[
(None, None),
# Failing: issue with testing code: geoarrow-c rendering POINT (nan, nan)
# instead of POINT EMPTY
# https://github.com/geoarrow/geoarrow-c/issues/143
("POINT EMPTY", "POINT (nan nan)"),
("POLYGON EMPTY", "POLYGON EMPTY"),
("LINESTRING EMPTY", "LINESTRING EMPTY"),
("MULTIPOINT EMPTY", "MULTIPOINT EMPTY"),
("MULTILINESTRING EMPTY", "MULTILINESTRING EMPTY"),
("MULTIPOLYGON EMPTY", "MULTIPOLYGON EMPTY"),
("GEOMETRYCOLLECTION EMPTY", "GEOMETRYCOLLECTION EMPTY"),
("POINT (0 0)", "POINT (0 0)"),
("LINESTRING (0 0, 1 1)", "POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0))"),
("LINESTRING (0 0, 0 1)", "LINESTRING (0 0, 0 1)"),
("MULTIPOINT ((0 0), (1 1))", "POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0))"),
(
"GEOMETRYCOLLECTION (POINT (0 0), LINESTRING (0 0, 1 1), POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0)))",
"POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0))",
),
],
)
def test_st_envelope(eng, geom, expected):
eng = eng.create_or_skip()
eng.assert_query_result(f"SELECT ST_Envelope({geom_or_null(geom)})", expected)
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
@pytest.mark.parametrize(
("geom", "expected"),
[
(None, None),
("POINT EMPTY", "POINT (nan nan)"),
("POLYGON EMPTY", "POLYGON EMPTY"),
("LINESTRING EMPTY", "LINESTRING EMPTY"),
("MULTIPOINT EMPTY", "MULTIPOINT EMPTY"),
("MULTILINESTRING EMPTY", "MULTILINESTRING EMPTY"),
("MULTIPOLYGON EMPTY", "MULTIPOLYGON EMPTY"),
("GEOMETRYCOLLECTION EMPTY", "GEOMETRYCOLLECTION EMPTY"),
("POINT (0 1)", "POINT (1 0)"),
("LINESTRING (0 1, 2 3)", "LINESTRING (1 0, 3 2)"),
("MULTIPOINT (0 1, 2 3)", "MULTIPOINT (1 0, 3 2)"),
(
"GEOMETRYCOLLECTION (POINT (1 2), LINESTRING (3 4, 5 6), POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0)))",
"GEOMETRYCOLLECTION (POINT (2 1), LINESTRING (4 3, 6 5), POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0)))",
),
],
)
def test_st_flipcoordinates(eng, geom, expected):
eng = eng.create_or_skip()
eng.assert_query_result(
f"SELECT ST_FlipCoordinates({geom_or_null(geom)})", expected
)
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
@pytest.mark.parametrize(
("geom", "expected"),
[
(None, None),
("POINT EMPTY", "ST_Point"),
("POLYGON EMPTY", "ST_Polygon"),
("LINESTRING EMPTY", "ST_LineString"),
("MULTIPOINT EMPTY", "ST_MultiPoint"),
("MULTILINESTRING EMPTY", "ST_MultiLineString"),
("MULTIPOLYGON EMPTY", "ST_MultiPolygon"),
("GEOMETRYCOLLECTION EMPTY", "ST_GeometryCollection"),
("POINT (0 0)", "ST_Point"),
("LINESTRING (0 0, 1 1)", "ST_LineString"),
("POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", "ST_Polygon"),
("MULTIPOINT ((0 0), (1 1))", "ST_MultiPoint"),
("MULTILINESTRING ((0 0, 1 1), (1 1, 2 2))", "ST_MultiLineString"),
(
"MULTIPOLYGON (((0 0, 1 0, 1 1, 0 1, 0 0)), ((0 0, 1 0, 1 1, 0 1, 0 0)))",
"ST_MultiPolygon",
),
(
"GEOMETRYCOLLECTION (POINT (0 0), LINESTRING (0 0, 1 1), POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0)))",
"ST_GeometryCollection",
),
],
)
def test_st_geometrytype(eng, geom, expected):
if eng == PostGIS and geom is None:
# PostGIS ST_GeometryType doesn't work unless we explicitly cast to null
arg = "NULL::geometry"
else:
arg = geom_or_null(geom)
eng = eng.create_or_skip()
eng.assert_query_result(f"SELECT ST_GeometryType({arg})", expected)
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
@pytest.mark.parametrize(
("wkt", "expected"),
[
(None, None),
("POINT (0 0)", "POINT (0 0)"),
("LINESTRING EMPTY", "LINESTRING EMPTY"),
("MULTIPOINT ((0 0), (1 1))", "MULTIPOINT (0 0, 1 1)"),
("POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0))", "POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0))"),
],
)
def test_st_geogfromtext(eng, wkt, expected):
if wkt is not None:
wkt = f"'{wkt}'"
eng = eng.create_or_skip()
eng.assert_query_result(f"SELECT ST_GeogFromText({val_or_null(wkt)})", expected)
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
@pytest.mark.parametrize(
("wkt", "expected"),
[
(None, None),
("POINT (0 0)", "POINT (0 0)"),
("LINESTRING EMPTY", "LINESTRING EMPTY"),
("MULTIPOINT ((0 0), (1 1))", "MULTIPOINT (0 0, 1 1)"),
("POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0))", "POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0))"),
],
)
def test_st_geomfromtext(eng, wkt, expected):
if wkt is not None:
wkt = f"'{wkt}'"
eng = eng.create_or_skip()
eng.assert_query_result(f"SELECT ST_GeomFromText({val_or_null(wkt)})", expected)
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
@pytest.mark.parametrize(
("geom"),
[
"POINT (1 1)",
"POINT EMPTY",
"LINESTRING EMPTY",
"POLYGON EMPTY",
"GEOMETRYCOLLECTION EMPTY",
"POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))",
"MULTILINESTRING ((0 0, 1 1), (1 1, 2 2))",
"GEOMETRYCOLLECTION (POINT (0 0), LINESTRING (0 0, 1 1), POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0)))",
],
)
def test_st_geogfromwkb(eng, geom):
eng = eng.create_or_skip()
expected = geom
if geom == "POINT EMPTY":
# arrow-c returns POINT (nan nan) instead of POINT EMPTY
expected = "POINT (nan nan)"
if geom is None:
wkb = val_or_null(None)
else:
wkb = shapely.from_wkt(geom).wkb
if isinstance(eng, SedonaDB):
wkb = "0x" + wkb.hex()
elif isinstance(eng, PostGIS):
wkb = r"\x" + wkb.hex()
wkb = f"'{wkb}'::bytea"
else:
raise
eng.assert_query_result(f"SELECT ST_GeogFromWKB({wkb})", expected)
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
@pytest.mark.parametrize(
("geom"),
[
"POINT (1 1)",
"POINT EMPTY",
"LINESTRING EMPTY",
"POLYGON EMPTY",
"GEOMETRYCOLLECTION EMPTY",
"POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))",
"MULTILINESTRING ((0 0, 1 1), (1 1, 2 2))",
"GEOMETRYCOLLECTION (POINT (0 0), LINESTRING (0 0, 1 1), POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0)))",
],
)
def test_st_geomfromwkb(eng, geom):
eng = eng.create_or_skip()
expected = geom
if geom == "POINT EMPTY":
# arrow-c returns POINT (nan nan) instead of POINT EMPTY
expected = "POINT (nan nan)"
if geom is None:
wkb = val_or_null(None)
else:
wkb = shapely.from_wkt(geom).wkb
if isinstance(eng, SedonaDB):
wkb = "0x" + wkb.hex()
elif isinstance(eng, PostGIS):
wkb = r"\x" + wkb.hex()
wkb = f"'{wkb}'::bytea"
else:
raise
eng.assert_query_result(f"SELECT ST_GeomFromWKB({wkb})", expected)
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
@pytest.mark.parametrize(
("geom", "index", "expected"),
[
# 1. POINT
("POINT(1 1)", 1, "POINT (1 1)"), # n=1 (Valid)
("POINT(1 1)", 2, None), # n=2 (OOB)
("POINT(1 1)", 99, None), # n=99 (Large OOB)
# 2. LINESTRING
("LINESTRING(2 2, 3 3, 4 4)", 1, "LINESTRING (2 2, 3 3, 4 4)"), # n=1 (Valid)
(None, 2, None), # Null input (n=2)
("LINESTRING(2 2, 3 3, 4 4)", 0, None), # n=0 (OOB)
# 3. POLYGON
(
"POLYGON((0 0, 1 0, 1 1, 0 0))",
1,
"POLYGON ((0 0, 1 0, 1 1, 0 0))",
), # n=1 (Valid)
("POLYGON((0 0, 1 0, 1 1, 0 0))", 3, None), # n=3 (OOB)
# 4. MULTIPOINT
("MULTIPOINT((1 1), (2 2), (3 3))", 2, "POINT (2 2)"), # n=2 (Valid)
("MULTIPOINT((1 1), (2 2), (3 3))", 3, "POINT (3 3)"), # n=3 (Valid)
(None, 0, None), # Null Input (n=0)
("MULTIPOINT((1 1), (2 2), (3 3))", 1, "POINT (1 1)"), # n=1 (Valid)
("MULTIPOINT((1 1), (2 2), (3 3))", 0, None), # n=0 (OOB)
# 5. MULTILINESTRING
(
"MULTILINESTRING((1 1, 2 2), (3 3, 4 4))",
1,
"LINESTRING (1 1, 2 2)",
), # n=1 (Valid)
("MULTILINESTRING((1 1, 2 2), (3 3, 4 4))", 3, None), # n=3 (OOB)
(
"MULTILINESTRING((1 1, 2 2), (3 3, 4 4))",
2,
"LINESTRING (3 3, 4 4)",
), # n=2 (Valid)
# 6. MULTIPOLYGON
(
"MULTIPOLYGON(((0 0, 1 1, 0 1, 0 0)), ((5 5, 6 6, 5 6, 5 5)))",
2,
"POLYGON ((5 5, 6 6, 5 6, 5 5))",
), # n=2 (Valid)
("MULTIPOLYGON(((0 0, 1 1, 0 1, 0 0)))", 2, None), # n=2 (OOB)
(
"MULTIPOLYGON(((0 0, 1 1, 0 1, 0 0)), ((5 5, 6 6, 5 6, 5 5)))",
1,
"POLYGON ((0 0, 1 1, 0 1, 0 0))",
), # n=1 (Valid)
("MULTIPOLYGON EMPTY", 1, None), # Empty Multi (n=1)
# 7. GEOMETRYCOLLECTION
(
"GEOMETRYCOLLECTION(POINT(10 10), LINESTRING(20 20, 30 30), POLYGON((1 1, 2 2, 1 2, 1 1)))",
1,
"POINT (10 10)",
), # n=1 (Point)
(
"GEOMETRYCOLLECTION(POINT(10 10), LINESTRING(20 20, 30 30), POLYGON((1 1, 2 2, 1 2, 1 1)))",
2,
"LINESTRING (20 20, 30 30)",
), # n=2 (LineString)
("GEOMETRYCOLLECTION(POINT(10 10))", 2, None), # n=2 (OOB)
(
"GEOMETRYCOLLECTION(POINT(1 1), GEOMETRYCOLLECTION(LINESTRING(2 2, 3 3)))",
1,
"POINT (1 1)",
), # n=1 (Nested: Point)
(
"GEOMETRYCOLLECTION(POINT(1 1), GEOMETRYCOLLECTION(LINESTRING(2 2, 3 3)))",
2,
"GEOMETRYCOLLECTION (LINESTRING (2 2, 3 3))",
), # n=2 (Nested: GC)
("GEOMETRYCOLLECTION(POINT(1 1))", 0, None), # n=0 (OOB)
],
)
def test_st_geometryn(eng, geom, index, expected):
eng = eng.create_or_skip()
eng.assert_query_result(
f"SELECT ST_GeometryN({geom_or_null(geom)}, {val_or_null(index)})", expected
)
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
@pytest.mark.parametrize(
("geom", "expected"),
[
(None, None),
("POINT EMPTY", False),
("POINT Z EMPTY", True),
("POINT M EMPTY", False),
("POINT ZM EMPTY", True),
("POINT Z (0 0 0)", True),
("POINT M (0 0 0)", False),
("POINT ZM (0 0 0 0)", True),
("LINESTRING EMPTY", False),
("LINESTRING Z EMPTY", True),
("LINESTRING Z (0 0 0, 1 1 1)", True),
("POLYGON EMPTY", False),
("MULTIPOINT ((0 0), (1 1))", False),
("MULTIPOINT Z ((0 0 0))", True),
("MULTIPOINT ZM ((0 0 0 0))", True),
("GEOMETRYCOLLECTION EMPTY", False),
# Z-dim specified only in the nested geometry
("GEOMETRYCOLLECTION (POINT Z (0 0 0))", True),
# Z-dim specified on both levels
("GEOMETRYCOLLECTION Z (POINT Z (0 0 0))", True),
("GEOMETRYCOLLECTION (GEOMETRYCOLLECTION (POINT Z (0 0 0)))", True),
],
)
def test_st_hasz(eng, geom, expected):
eng = eng.create_or_skip()
eng.assert_query_result(f"SELECT ST_HasZ({geom_or_null(geom)})", expected)
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
@pytest.mark.parametrize(
("geom", "expected"),
[
(None, None),
("POINT EMPTY", False),
("POINT Z EMPTY", False),
("POINT M EMPTY", True),
("POINT ZM EMPTY", True),
("POINT Z (0 0 0)", False),
("POINT M (0 0 0)", True),
("POINT ZM (0 0 0 0)", True),
("LINESTRING EMPTY", False),
("LINESTRING M EMPTY", True),
("LINESTRING M (0 0 0, 1 1 1)", True),
("POLYGON EMPTY", False),
("MULTIPOINT ((0 0), (1 1))", False),
("GEOMETRYCOLLECTION EMPTY", False),
("GEOMETRYCOLLECTION (POINT M (0 0 0))", True),
("GEOMETRYCOLLECTION (GEOMETRYCOLLECTION (POINT M (0 0 0)))", True),
],
)
def test_st_hasm(eng, geom, expected):
eng = eng.create_or_skip()
eng.assert_query_result(f"SELECT ST_HasM({geom_or_null(geom)})", expected)
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
@pytest.mark.parametrize(
("geom", "expected"),
[
(None, None),
("POINT EMPTY", True),
("LINESTRING EMPTY", True),
("POLYGON EMPTY", True),
("MULTIPOINT EMPTY", True),
("MULTILINESTRING EMPTY", True),
("MULTIPOLYGON EMPTY", True),
("GEOMETRYCOLLECTION EMPTY", True),
("POINT (0 0)", False),
("LINESTRING (0 0, 1 1)", False),
("POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", False),
("MULTIPOINT ((0 0), (1 1))", False),
("MULTILINESTRING ((0 0, 1 1), (1 1, 2 2))", False),
(
"MULTIPOLYGON (((0 0, 1 0, 1 1, 0 1, 0 0)), ((0 0, 1 0, 1 1, 0 1, 0 0)))",
False,
),
(
"GEOMETRYCOLLECTION (POINT (0 0), LINESTRING (0 0, 1 1), POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0)))",
False,
),
],
)
def test_st_isempty(eng, geom, expected):
eng = eng.create_or_skip()
eng.assert_query_result(f"SELECT ST_IsEmpty({geom_or_null(geom)})", expected)
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
@pytest.mark.parametrize(
("geom", "expected"),
[
(None, None),
("LINESTRING(0 0, 1 1)", False),
("LINESTRING(0 0, 0 1, 1 1, 0 0)", True),
("MULTILINESTRING((0 0, 0 1, 1 1, 0 0),(0 0, 1 1))", False),
("POINT(0 0)", True),
("MULTIPOINT((0 0), (1 1))", True),
("POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", True),
("GEOMETRYCOLLECTION (LINESTRING(0 0, 0 1, 1 1, 0 0))", True),
(
"GEOMETRYCOLLECTION (LINESTRING(0 0, 0 1, 1 1, 0 0), LINESTRING(0 0, 1 1))",
False,
),
("POINT EMPTY", False),
("LINESTRING EMPTY", False),
("POLYGON EMPTY", False),
("MULTIPOINT EMPTY", False),
("MULTILINESTRING EMPTY", False),
("MULTIPOLYGON EMPTY", False),
("GEOMETRYCOLLECTION EMPTY", False),
("GEOMETRYCOLLECTION (LINESTRING EMPTY)", False),
],
)
def test_st_isclosed(eng, geom, expected):
eng = eng.create_or_skip()
eng.assert_query_result(f"SELECT ST_IsClosed({geom_or_null(geom)})", expected)
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
@pytest.mark.parametrize(
("geom", "expected"),
[
(None, None),
("POINT EMPTY", False),
("LINESTRING EMPTY", False),
("POLYGON EMPTY", False),
("MULTIPOINT EMPTY", True),
("MULTILINESTRING EMPTY", True),
("MULTIPOLYGON EMPTY", True),
("GEOMETRYCOLLECTION EMPTY", True),
("GEOMETRYCOLLECTION (LINESTRING EMPTY)", True),
("POINT(0 0)", False),
("LINESTRING(0 0, 1 1)", False),
("POLYGON((0 0, 1 0, 0 1, 0 0))", False),
("MULTIPOINT((0 0), (1 1))", True),
("MULTILINESTRING((0 0, 0 1, 1 1, 0 0),(0 0, 1 1))", True),
("MULTIPOLYGON(((0 0, 1 0, 1 1, 0 1, 0 0)))", True),
("GEOMETRYCOLLECTION (LINESTRING(0 0, 0 1, 1 1, 0 0))", True),
(
"GEOMETRYCOLLECTION (LINESTRING(0 0, 0 1, 1 1, 0 0), MULTIPOINT((2 2), (3 3)))",
True,
),
],
)
def test_st_iscollection(eng, geom, expected):
eng = eng.create_or_skip()
eng.assert_query_result(f"SELECT ST_IsCollection({geom_or_null(geom)})", expected)
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
@pytest.mark.parametrize(
("geom", "expected"),
[
(None, None),
# Valid rings (closed + simple)
("LINESTRING(0 0, 0 1, 1 1, 1 0, 0 0)", True),
("LINESTRING(0 0, 1 0, 1 1, 0 0)", True),
("LINESTRING(0 0, 2 2, 1 2, 0 0)", True),
# Closed but self-intersecting - bowtie shape (not simple)
("LINESTRING(0 0, 0 1, 1 0, 1 1, 0 0)", False),
# Not closed
("LINESTRING(0 0, 1 1)", False),
("LINESTRING(2 0, 2 2, 3 3)", False),
("LINESTRING(0 0, 2 2)", False),
# Empty geometries
("LINESTRING EMPTY", False),
("POINT EMPTY", False),
("POLYGON EMPTY", False),
("MULTIPOLYGON EMPTY", False),
("GEOMETRYCOLLECTION EMPTY", False),
],
)
def test_st_isring(eng, geom, expected):
"""Test ST_IsRing with LineString geometries.
ST_IsRing returns true if the geometry is a closed and simple LineString.
"""
eng = eng.create_or_skip()
eng.assert_query_result(f"SELECT ST_IsRing({geom_or_null(geom)})", expected)
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
@pytest.mark.parametrize(
("geom"),
[
"POINT(0 0)",
"MULTIPOINT((0 0), (1 1))",
"POLYGON((0 0, 1 0, 1 1, 0 1, 0 0))",
"MULTILINESTRING((0 0, 0 1, 1 1, 1 0, 0 0))",
"GEOMETRYCOLLECTION(LINESTRING(0 0, 0 1, 1 1, 1 0, 0 0))",
],
)
def test_st_isring_non_linestring_error(eng, geom):
"""Test that ST_IsRing throws errors for non-LineString non-empty geometries.
Both SedonaDB and PostGIS throw errors when ST_IsRing is called on
non-LineString geometry types (PostGIS compatibility).
"""
eng = eng.create_or_skip()
with pytest.raises(Exception, match="linear|linestring"):
eng.assert_query_result(f"SELECT ST_IsRing(ST_GeomFromText('{geom}'))", None)
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
@pytest.mark.parametrize(
("geom", "expected"),
[
(None, None),
("POINT EMPTY", 0),
("LINESTRING EMPTY", 0),
("POINT (0 0)", 0),
("LINESTRING (0 0, 0 1)", 1),
("MULTIPOINT ((0 0), (1 1))", 0),
("MULTILINESTRING ((0 0, 1 1), (1 1, 2 2))", 2.8284271247461903),
# Polygons contribute 0 because perimeters aren't included in the length calculation
("POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", 0),
("MULTIPOLYGON (((0 0, 1 0, 1 1, 0 1, 0 0)), ((0 0, 1 0, 1 1, 0 1, 0 0)))", 0),
(
"GEOMETRYCOLLECTION (LINESTRING (0 0, 1 1), POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0)), LINESTRING (0 0, 1 1))",
2.8284271247461903,
),
],
)
def test_st_length(eng, geom, expected):
eng = eng.create_or_skip()
eng.assert_query_result(f"SELECT ST_Length({geom_or_null(geom)})", expected)
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
@pytest.mark.parametrize(
("geom", "expected"),
[
(None, None),
("POINT EMPTY", 0),
("LINESTRING EMPTY", 0),
("POLYGON EMPTY", 0),
("MULTIPOINT EMPTY", 0),
("MULTILINESTRING EMPTY", 0),
("MULTIPOLYGON EMPTY", 0),
("GEOMETRYCOLLECTION EMPTY", 0),
("GEOMETRYCOLLECTION (LINESTRING EMPTY, MULTIPOINT ((0 0), (1 1), (2 2)))", 2),
("POINT(0 0)", 1),
("LINESTRING(0 0, 1 1)", 1),
("POLYGON((0 0, 1 0, 0 1, 0 0))", 1),
("MULTIPOINT ((0 0), (1 1), (2 2))", 3),
("MULTILINESTRING((0 0, 0 1, 1 1, 0 0), (0 0, 1 1))", 2),
("MULTIPOLYGON (((0 0, 1 0, 1 1, 0 1, 0 0)), ((0 0, 1 0, 1 1, 0 1, 0 0)))", 2),
("GEOMETRYCOLLECTION (MULTIPOINT ((0 0), (1 1), (2 2)))", 1),
(
"GEOMETRYCOLLECTION (GEOMETRYCOLLECTION (POINT (0 0), POINT (1 1)), MULTIPOINT((2 2), (3 3)))",
2,
),
],
)
def test_st_numgeometries(eng, geom, expected):
eng = eng.create_or_skip()
eng.assert_query_result(f"SELECT ST_NumGeometries({geom_or_null(geom)})", expected)
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
@pytest.mark.parametrize(
("geom", "expected"),
[
(None, None),
("POINT EMPTY", 0),
("LINESTRING EMPTY", 0),
("POINT (0 0)", 0),
("LINESTRING (0 0, 0 1)", 0),
("MULTIPOINT ((0 0), (1 1))", 0),
("MULTILINESTRING ((0 0, 1 1), (1 1, 2 2))", 0),
# Polygons contribute 0 because perimeters aren't included in the length calculation
("POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", 4),
("MULTIPOLYGON (((0 0, 1 0, 1 1, 0 1, 0 0)), ((0 0, 1 0, 1 1, 0 1, 0 0)))", 8),
(
"GEOMETRYCOLLECTION (POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0)), LINESTRING (0 0, 1 1), POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0)))",
8,
),
],
)
def test_st_perimeter(eng, geom, expected):
if eng == PostGIS and geom is None:
# PostGIS ST_Perimeter doesn't work unless we explicitly cast to null
arg = "NULL::geometry"
else:
arg = geom_or_null(geom)
eng = eng.create_or_skip()
eng.assert_query_result(f"SELECT ST_Perimeter({arg})", expected)
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
@pytest.mark.parametrize(
("geom", "expected"),
[
(None, None),
("LINESTRING EMPTY", "LINESTRING EMPTY"),
("LINESTRING(0 0, 1 1, 2 2)", "LINESTRING (2 2, 1 1, 0 0)"),
("POINT (1 2)", "POINT (1 2)"),
("POLYGON ((0 0, 1 0, 2 2, 1 2, 0 0))", "POLYGON ((0 0, 1 2, 2 2, 1 0, 0 0))"),
# Note MultiPoints don't change since each point is separate (e.g not a line string)
("MULTIPOINT (1 2, 3 4)", "MULTIPOINT (1 2, 3 4)"),
(
"MULTIPOLYGON (((0 0, 1 0, 1 1, 0 2, 0 0)), ((5 5, 6 0, 7 1, 0 1, 5 5)))",
"MULTIPOLYGON (((0 0, 0 2, 1 1, 1 0, 0 0)), ((5 5, 0 1, 7 1, 6 0, 5 5)))",
),
(
"GEOMETRYCOLLECTION (POINT (1 2), LINESTRING (3 4, 5 6), POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0)))",
"GEOMETRYCOLLECTION (POINT (1 2), LINESTRING (5 6, 3 4), POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0)))",
),
],
)
def test_st_reverse(eng, geom, expected):
eng = eng.create_or_skip()
eng.assert_query_result(f"SELECT ST_Reverse({geom_or_null(geom)})", expected)
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
@pytest.mark.parametrize(
("x", "y", "expected"),
[
(None, None, None),
(1, None, None),
(None, 1, None),
(1, 1, "POINT (1 1)"),
(1.0, 1.0, "POINT (1 1)"),
(10, -1.5, "POINT (10 -1.5)"),
],
)
def test_st_geogpoint(eng, x, y, expected):
eng = eng.create_or_skip()
if eng == SedonaDB:
eng.assert_query_result(
f"SELECT ST_GeogPoint({val_or_null(x)}, {val_or_null(y)})", expected
)
else:
eng.assert_query_result(
f"SELECT ST_Point({val_or_null(x)}, {val_or_null(y)}) as geography",
expected,
)
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
@pytest.mark.parametrize(
("x", "y", "expected"),
[
(None, None, None),
(1, None, None),
(None, 1, None),
(1, 1, "POINT (1 1)"),
(1.0, 1.0, "POINT (1 1)"),
(10, -1.5, "POINT (10 -1.5)"),
],
)
def test_st_point(eng, x, y, expected):
eng = eng.create_or_skip()
eng.assert_query_result(
f"SELECT ST_Point({val_or_null(x)}, {val_or_null(y)})", expected
)
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
@pytest.mark.parametrize(
("x", "y", "srid", "expected"),
[
(None, None, None, None),
(1, 1, None, None),
(1, 1, 0, 0),
(1, 1, 4326, 4326),
(1, 1, "4326", 4326),
],
)
def test_st_point_with_srid(eng, x, y, srid, expected):
eng = eng.create_or_skip()
eng.assert_query_result(
f"SELECT ST_SRID(ST_Point({val_or_null(x)}, {val_or_null(y)}, {val_or_null(srid)}))",
expected,
)
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
@pytest.mark.parametrize(
("x", "y", "z", "expected"),
[
(None, None, None, None),
(1, None, None, None),
(None, 1, None, None),
(None, None, 1, None),
(1, 1, 1, "POINT Z (1 1 1)"),
(1.0, 1.0, 1.0, "POINT Z (1 1 1)"),
(10, -1.5, 1.0, "POINT Z (10 -1.5 1)"),
],
)
def test_st_pointz(eng, x, y, z, expected):
eng = eng.create_or_skip()
eng.assert_query_result(
f"SELECT ST_PointZ({val_or_null(x)}, {val_or_null(y)}, {val_or_null(z)})",
expected,
)
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
@pytest.mark.parametrize(
("x", "y", "m", "expected"),
[
(None, None, None, None),
(1, None, None, None),
(None, 1, None, None),
(None, None, 1, None),
(1, 1, 1, "POINT M (1 1 1)"),
(1.0, 1.0, 1.0, "POINT M (1 1 1)"),
(10, -1.5, 1.0, "POINT M (10 -1.5 1)"),
],
)
def test_st_pointm(eng, x, y, m, expected):
eng = eng.create_or_skip()
eng.assert_query_result(
f"SELECT ST_PointM({val_or_null(x)}, {val_or_null(y)}, {val_or_null(m)})",
expected,
)
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
@pytest.mark.parametrize(
("geometry", "expected", "expected_n"),
[
("POINT (1 2)", "MULTIPOINT (1 2)", 1),
("LINESTRING (1 2, 3 4, 5 6)", "MULTIPOINT (1 2, 3 4, 5 6)", 3),
(
"POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0))",
"MULTIPOINT (0 0, 10 0, 10 10, 0 10, 0 0)",
5,
),
(
"POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0), (1 1, 3 1, 1 3, 1 1))",
"MULTIPOINT (0 0, 10 0, 10 10, 0 10, 0 0, 1 1, 3 1, 1 3, 1 1)",
9,
),
("MULTIPOINT (1 2, 3 4, 5 6, 7 8)", "MULTIPOINT (1 2, 3 4, 5 6, 7 8)", 4),
(
"MULTILINESTRING ((1 2, 3 4), EMPTY, (5 6, 7 8))",
"MULTIPOINT (1 2, 3 4, 5 6, 7 8)",
4,
),
(
"MULTIPOLYGON (((0 0, 10 0, 10 10, 0 10, 0 0)), EMPTY, ((0 0, 5 0, 0 5, 0 0), (1 1, 3 1, 1 3, 1 1)))",
"MULTIPOINT (0 0, 10 0, 10 10, 0 10, 0 0, 0 0, 5 0, 0 5, 0 0, 1 1, 3 1, 1 3, 1 1)",
13,
),
(
"GEOMETRYCOLLECTION (POINT (1 2), LINESTRING EMPTY, LINESTRING (3 4, 5 6))",
"MULTIPOINT (1 2, 3 4, 5 6)",
3,
),
("LINESTRING Z (1 2 3, 4 5 6, 7 8 9)", "MULTIPOINT Z (1 2 3, 4 5 6, 7 8 9)", 3),
("LINESTRING M (1 2 3, 4 5 6, 7 8 9)", "MULTIPOINT M (1 2 3, 4 5 6, 7 8 9)", 3),
(
"LINESTRING ZM (1 2 3 4, 5 6 7 8, 9 0 1 2)",
"MULTIPOINT ZM (1 2 3 4, 5 6 7 8, 9 0 1 2)",
3,
),
("POINT EMPTY", "MULTIPOINT EMPTY", 0),
("LINESTRING EMPTY", "MULTIPOINT EMPTY", 0),
("POLYGON EMPTY", "MULTIPOINT EMPTY", 0),
("MULTIPOINT EMPTY", "MULTIPOINT EMPTY", 0),
("MULTILINESTRING EMPTY", "MULTIPOINT EMPTY", 0),
("MULTIPOLYGON EMPTY", "MULTIPOINT EMPTY", 0),
("GEOMETRYCOLLECTION EMPTY", "MULTIPOINT EMPTY", 0),
(None, None, None),
],
)
def test_st_points(eng, geometry, expected, expected_n):
eng = eng.create_or_skip()
eng.assert_query_result(
f"SELECT ST_Points({geom_or_null(geometry)})",
expected,
)
eng.assert_query_result(
f"SELECT ST_NPoints({geom_or_null(geometry)})",
expected_n,
)
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
@pytest.mark.parametrize(
("geometry", "n", "expected"),
[
("LINESTRING (1 2, 3 4, 5 6)", 1, "POINT (1 2)"),
("LINESTRING (1 2, 3 4, 5 6)", 2, "POINT (3 4)"),
("LINESTRING (1 2, 3 4, 5 6)", -1, "POINT (5 6)"),
("LINESTRING Z (1 2 3, 3 4 5, 5 6 7)", 1, "POINT Z (1 2 3)"),
("LINESTRING Z (1 2 3, 3 4 5, 5 6 7)", 2, "POINT Z (3 4 5)"),
("LINESTRING Z (1 2 3, 3 4 5, 5 6 7)", -1, "POINT Z (5 6 7)"),
("LINESTRING ZM (1 2 3 4, 3 4 5 6, 5 6 7 8)", 1, "POINT ZM (1 2 3 4)"),
("LINESTRING ZM (1 2 3 4, 3 4 5 6, 5 6 7 8)", 2, "POINT ZM (3 4 5 6)"),
("LINESTRING ZM (1 2 3 4, 3 4 5 6, 5 6 7 8)", -1, "POINT ZM (5 6 7 8)"),
# invalid n
("LINESTRING (1 2, 3 4, 5 6)", 0, None),
("LINESTRING (1 2, 3 4, 5 6)", 4, None),
("LINESTRING (1 2, 3 4, 5 6)", -4, None),
# other geometries
("POINT (1 2)", 1, None),
("POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0))", 1, None),
("MULTILINESTRING ((1 2, 3 4), (5 6, 7 8))", 1, None),
("GEOMETRYCOLLECTION (POINT (1 2), LINESTRING (3 4, 5 6))", 1, None),
# empty geometries
("POINT EMPTY", 1, None),
("LINESTRING EMPTY", 1, None),
("POLYGON EMPTY", 1, None),
("MULTIPOINT EMPTY", 1, None),
("MULTILINESTRING EMPTY", 1, None),
("MULTIPOLYGON EMPTY", 1, None),
("GEOMETRYCOLLECTION EMPTY", 1, None),
# null
(None, None, None),
(None, 1, None),
],
)
def test_st_pointn(eng, geometry, n, expected):
eng = eng.create_or_skip()
eng.assert_query_result(
f"SELECT ST_PointN({geom_or_null(geometry)}, {val_or_null(n)})",
expected,
)
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
@pytest.mark.parametrize(
("geometry", "expected"),
[
(None, None),
("POINT EMPTY", None),
("LINESTRING EMPTY", None),
("POLYGON EMPTY", None),
("MULTIPOINT EMPTY", None),
("MULTILINESTRING EMPTY", None),
("MULTIPOLYGON EMPTY", None),
("GEOMETRYCOLLECTION EMPTY", None),
("LINESTRING (1 2, 3 4, 5 6)", "POINT (1 2)"),
("LINESTRING Z (1 2 3, 3 4 5, 5 6 7)", "POINT Z (1 2 3)"),
("LINESTRING M (1 2 3, 3 4 5, 5 6 7)", "POINT M (1 2 3)"),
("LINESTRING ZM (1 2 3 4, 3 4 5 6, 5 6 7 8)", "POINT ZM (1 2 3 4)"),
("POINT (1 2)", "POINT (1 2)"),
("POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0))", "POINT (0 0)"),
("MULTIPOINT (0 0, 10 0, 10 10, 0 10, 0 0)", "POINT (0 0)"),
("MULTILINESTRING ((1 2, 3 4), (5 6, 7 8))", "POINT (1 2)"),
("MULTIPOLYGON (((0 0, 10 0, 10 10, 0 10, 0 0)))", "POINT (0 0)"),
("GEOMETRYCOLLECTION (POINT (1 2), LINESTRING (3 4, 5 6))", "POINT (1 2)"),
(
"GEOMETRYCOLLECTION (GEOMETRYCOLLECTION (GEOMETRYCOLLECTION (POINT (1 2), LINESTRING (3 4, 5 6))))",
"POINT (1 2)",
),
],
)
def test_st_start_point(eng, geometry, expected):
eng = eng.create_or_skip()
eng.assert_query_result(
f"SELECT ST_StartPoint({geom_or_null(geometry)})",
expected,
)
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
@pytest.mark.parametrize(
("geometry", "expected"),
[
(None, None),
("POINT EMPTY", None),
("LINESTRING EMPTY", None),
("POLYGON EMPTY", None),
("MULTIPOINT EMPTY", None),
("MULTILINESTRING EMPTY", None),
("MULTIPOLYGON EMPTY", None),
("GEOMETRYCOLLECTION EMPTY", None),
("LINESTRING (1 2, 3 4, 5 6)", "POINT (5 6)"),
("LINESTRING Z (1 2 3, 3 4 5, 5 6 7)", "POINT Z (5 6 7)"),
("LINESTRING M (1 2 3, 3 4 5, 5 6 7)", "POINT M (5 6 7)"),
("LINESTRING ZM (1 2 3 4, 3 4 5 6, 5 6 7 8)", "POINT ZM (5 6 7 8)"),
("POINT (1 2)", None),
("POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0))", None),
("MULTILINESTRING ((1 2, 3 4), (5 6, 7 8))", None),
],
)
def test_st_end_point(eng, geometry, expected):
eng = eng.create_or_skip()
eng.assert_query_result(
f"SELECT ST_EndPoint({geom_or_null(geometry)})",
expected,
)
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
@pytest.mark.parametrize(
("x", "y", "z", "m", "expected"),
[
(None, None, None, None, None),
(1, None, None, None, None),
(None, 1, None, None, None),
(None, None, 1, None, None),
(None, None, None, 1, None),
(1, 1, 1, 1, "POINT ZM (1 1 1 1)"),
(1.0, 1.0, 1.0, 1.0, "POINT ZM (1 1 1 1)"),
(10, -1.5, 1.0, 1.0, "POINT ZM (10 -1.5 1 1)"),
],
)
def test_st_pointzm(eng, x, y, z, m, expected):
eng = eng.create_or_skip()
eng.assert_query_result(
f"SELECT ST_PointZM({val_or_null(x)}, {val_or_null(y)}, {val_or_null(z)}, {val_or_null(m)})",
expected,
)
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
@pytest.mark.parametrize(
("geom", "expected"),
[
(
"LINESTRING(0 0, 0 1, 1 1, 1 0, 0 0)",
"GEOMETRYCOLLECTION (POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0)))",
),
(
"POLYGON((0 0, 10 0, 10 10, 0 10, 0 0))",
"GEOMETRYCOLLECTION (POLYGON ((0 0, 0 10, 10 10, 10 0, 0 0)))",
),
(
"POLYGON((0 0, 10 0, 10 10, 0 10, 0 0), (2 2, 2 8, 8 8, 8 2, 2 2))",
"GEOMETRYCOLLECTION (POLYGON ((0 0, 0 10, 10 10, 10 0, 0 0), (2 2, 8 2, 8 8, 2 8, 2 2)), POLYGON ((2 2, 2 8, 8 8, 8 2, 2 2)))",
),
(
"MULTILINESTRING((0 0, 0 1, 1 1, 1 0, 0 0), (10 10, 10 11, 11 11, 11 10, 10 10))",
"GEOMETRYCOLLECTION (POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0)), POLYGON ((10 10, 10 11, 11 11, 11 10, 10 10)))",
),
(
"MULTILINESTRING((0 0, 10 0), (10 0, 10 10), (10 10, 0 0))",
"GEOMETRYCOLLECTION (POLYGON ((10 0, 0 0, 10 10, 10 0)))",
),
(
"MULTIPOLYGON(((0 0, 1 0, 0 1, 0 0)), ((10 10, 11 10, 10 11, 10 10)))",
"GEOMETRYCOLLECTION (POLYGON ((0 0, 0 1, 1 0, 0 0)), POLYGON ((10 10, 10 11, 11 10, 10 10)))",
),
(
"GEOMETRYCOLLECTION(POINT(5 5), LINESTRING(0 0, 0 1, 1 1, 1 0, 0 0))",
"GEOMETRYCOLLECTION (POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0)))",
),
(
"GEOMETRYCOLLECTION(GEOMETRYCOLLECTION(LINESTRING(0 0, 10 0)), LINESTRING(10 0, 10 10), LINESTRING(10 10, 0 0))",
"GEOMETRYCOLLECTION (POLYGON ((10 0, 0 0, 10 10, 10 0)))",
),
("LINESTRING(0 0, 10 10)", "GEOMETRYCOLLECTION EMPTY"),
("POINT(0 0)", "GEOMETRYCOLLECTION EMPTY"),
("MULTIPOINT((0 0), (1 1))", "GEOMETRYCOLLECTION EMPTY"),
("LINESTRING EMPTY", "GEOMETRYCOLLECTION EMPTY"),
],
)
def test_st_polygonize(eng, geom, expected):
eng = eng.create_or_skip()
eng.assert_query_result(f"SELECT ST_Polygonize({geom_or_null(geom)})", expected)
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
@pytest.mark.parametrize(
("geom", "expected"),
[
(None, None),
("POINT EMPTY", None),
("POINT Z EMPTY", None),
("POINT M EMPTY", None),
("POINT ZM EMPTY", None),
("POINT (1 2)", None),
("POINT Z (1 2 3)", None),
("POINT M (1 2 3.2)", 3.2),
("POINT ZM (1 2 3 -4)", -4),
],
)
def test_st_m(eng, geom, expected):
eng = eng.create_or_skip()
eng.assert_query_result(f"SELECT ST_M({geom_or_null(geom)})", expected)
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
@pytest.mark.parametrize(
("geom", "expected"),
[
(None, None),
("POINT EMPTY", None),
("POINT Z EMPTY", None),
("POINT M EMPTY", None),
("POINT ZM EMPTY", None),
("POINT (1.1 2)", 1.1),
("POINT Z (1 2 3)", 1),
("POINT M (1 2 3)", 1),
("POINT ZM (1 2 3 -4)", 1),
],
)
def test_st_x(eng, geom, expected):
eng = eng.create_or_skip()
eng.assert_query_result(f"SELECT ST_X({geom_or_null(geom)})", expected)
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
@pytest.mark.parametrize(
("geom", "expected"),
[
(None, None),
("POINT EMPTY", None),
("POINT Z EMPTY", None),
("POINT M EMPTY", None),
("POINT ZM EMPTY", None),
("POINT (1 2)", 2),
("POINT Z (1 2 3)", 2),
("POINT M (1 2 3)", 2),
("POINT ZM (1 2.2 3 -4)", 2.2),
],
)
def test_st_y(eng, geom, expected):
eng = eng.create_or_skip()
eng.assert_query_result(f"SELECT ST_Y({geom_or_null(geom)})", expected)
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
@pytest.mark.parametrize(
("geom", "expected"),
[
(None, None),
("POINT EMPTY", None),
("POINT Z EMPTY", None),
("POINT M EMPTY", None),
("POINT ZM EMPTY", None),
("POINT (1 2)", None),
("POINT Z (1 2 3)", 3),
("POINT M (1 2 3)", None),
("POINT ZM (1 2 3 -4)", 3),
],
)
def test_st_z(eng, geom, expected):
eng = eng.create_or_skip()
eng.assert_query_result(f"SELECT ST_Z({geom_or_null(geom)})", expected)
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
@pytest.mark.parametrize(
("geom", "expected"),
[
(None, None),
("POINT EMPTY", None),
("POINT (1 2)", 1),
("POINT Z (1 2 3)", 1),
("POINT M (1 2 3)", 1),
("POINT ZM (1 2 3 4)", 1),
("MULTILINESTRING ((-1.1 0, 1 1), (2 2, 3 3))", -1.1),
("POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", 0),
(
"GEOMETRYCOLLECTION (POINT (0 0), POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0)), LINESTRING (0 0, 1 1))",
0,
),
(
"GEOMETRYCOLLECTION ZM (POINT ZM (1 2 3 4), LINESTRING ZM (3 4 5 6, 7 8 9 10), POLYGON ZM ((0 0 0 0, 1 0 0 0, 0 1 0 0, 0 0 0 0)))",
0,
),
(
"GEOMETRYCOLLECTION (POINT (1 2), LINESTRING (3 4, 5 6), GEOMETRYCOLLECTION (POINT (10 10)))",
1,
),
],
)
def test_st_xmin(eng, geom, expected):
eng = eng.create_or_skip()
eng.assert_query_result(f"SELECT ST_XMin({geom_or_null(geom)})", expected)
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
@pytest.mark.parametrize(
("geom", "expected"),
[
(None, None),
("POINT EMPTY", None),
("POINT (1 2)", 2),
("POINT Z (1 2 3)", 2),
("POINT M (1 2 3)", 2),
("POINT ZM (1 2 3 4)", 2),
("MULTILINESTRING ((0 0, 1 1), (2 -2.2, 3 3))", -2.2),
("POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", 0),
(
"GEOMETRYCOLLECTION (POINT (0 0), POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0)), LINESTRING (0 0, 1 1))",
0,
),
(
"GEOMETRYCOLLECTION ZM (POINT ZM (1 2 3 4), LINESTRING ZM (3 4 5 6, 7 8 9 10), POLYGON ZM ((0 0 0 0, 1 0 0 0, 0 1 0 0, 0 0 0 0)))",
0,
),
(
"GEOMETRYCOLLECTION (POINT (1 2), LINESTRING (3 4, 5 6), GEOMETRYCOLLECTION (POINT (10 10)))",
2,
),
],
)
def test_st_ymin(eng, geom, expected):
eng = eng.create_or_skip()
eng.assert_query_result(f"SELECT ST_YMin({geom_or_null(geom)})", expected)
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
@pytest.mark.parametrize(
("geom", "expected"),
[
(None, None),
("POINT EMPTY", None),
("POINT (1 2)", 1),
("POINT Z (1 2 3)", 1),
("POINT M (1 2 3)", 1),
("POINT ZM (1 2 3 4)", 1),
("MULTILINESTRING ((0 0, 1 1), (2 2, 3.3 3))", 3.3),
("POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", 1),
(
"GEOMETRYCOLLECTION (POINT (0 0), POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0)), LINESTRING (0 0, 1 1))",
1,
),
(
"GEOMETRYCOLLECTION ZM (POINT ZM (1 2 3 4), LINESTRING ZM (3 4 5 6, 7 8 9 10), POLYGON ZM ((0 0 0 0, 1 0 0 0, 0 1 0 0, 0 0 0 0)))",
7,
),
(
"GEOMETRYCOLLECTION (POINT (1 2), LINESTRING (3 4, 5 6), GEOMETRYCOLLECTION (POINT (10 10)))",
10,
),
],
)
def test_st_xmax(eng, geom, expected):
eng = eng.create_or_skip()
eng.assert_query_result(f"SELECT ST_XMax({geom_or_null(geom)})", expected)
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
@pytest.mark.parametrize(
("geom", "expected"),
[
(None, None),
("POINT EMPTY", None),
("POINT (1 2)", 2),
("POINT Z (1 2 3)", 2),
("POINT M (1 2 3)", 2),
("POINT ZM (1 2 3 4)", 2),
("MULTILINESTRING ((0 0, 1 1), (2 2, 3 3))", 3),
("POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", 1),
(
"GEOMETRYCOLLECTION (POINT (0 0), POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0)), LINESTRING (0 0, 1 1))",
1,
),
(
"GEOMETRYCOLLECTION ZM (POINT ZM (1 2 3 4), LINESTRING ZM (3 4 5 6, 7 8 9 10), POLYGON ZM ((0 0 0 0, 1 0 0 0, 0 1 0 0, 0 0 0 0)))",
8,
),
(
"GEOMETRYCOLLECTION (POINT (1 2), LINESTRING (3 4, 5 6), GEOMETRYCOLLECTION (POINT (10 10)))",
10,
),
],
)
def test_st_ymax(eng, geom, expected):
eng = eng.create_or_skip()
eng.assert_query_result(f"SELECT ST_YMax({geom_or_null(geom)})", expected)
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
@pytest.mark.parametrize(
("geom", "expected"),
[
(None, None),
("POINT EMPTY", None),
("POINT (1 2)", None),
("POINT Z (1 2 3)", 3),
("POINT M (1 2 3)", None),
("POINT ZM (1 2 3 4)", 3),
("MULTILINESTRING ((0 0, 1 1), (2 2, 3 3))", None),
("POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", None),
(
"GEOMETRYCOLLECTION (POINT (0 0), POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0)), LINESTRING (0 0, 1 1))",
None,
),
(
"GEOMETRYCOLLECTION ZM (POINT ZM (1 2 3 4), LINESTRING ZM (3 4 5 6, 7 8 9 10), POLYGON ZM ((0 0 0 0, 1 0 0 0, 0 1 0 0, 0 0 0 0)))",
0,
),
(
"GEOMETRYCOLLECTION (POINT (1 2), LINESTRING (3 4, 5 6), GEOMETRYCOLLECTION (POINT (10 10)))",
None,
),
],
)
def test_st_zmin(eng, geom, expected):
# PostGIS returns 0 instead of null for non-empty geometries that don't have a Z coordinate
if eng == PostGIS and (
expected is None and geom is not None and "EMPTY" not in geom
):
expected = 0
eng = eng.create_or_skip()
eng.assert_query_result(f"SELECT ST_ZMin({geom_or_null(geom)})", expected)
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
@pytest.mark.parametrize(
("geom", "expected"),
[
(None, None),
("POINT EMPTY", None),
("POINT (1 2)", None),
("POINT Z (1 2 3)", 3),
("POINT M (1 2 3)", None),
("POINT ZM (1 2 3 4)", 3),
("MULTILINESTRING ((0 0, 1 1), (2 2, 3 3))", None),
("POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", None),
(
"GEOMETRYCOLLECTION (POINT (0 0), POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0)), LINESTRING (0 0, 1 1))",
None,
),
(
"GEOMETRYCOLLECTION ZM (POINT ZM (1 2 3 4), LINESTRING ZM (3 4 5 6, 7 8 9 10), POLYGON ZM ((0 0 0 0, 1 0 0 0, 0 1 0 0, 0 0 0 0)))",
9,
),
(
"GEOMETRYCOLLECTION (POINT (1 2), LINESTRING (3 4, 5 6), GEOMETRYCOLLECTION (POINT (10 10)))",
None,
),
],
)
def test_st_zmax(eng, geom, expected):
# PostGIS returns 0 instead of null for non-empty geometries that don't have a Z coordinate
if eng == PostGIS and (
expected is None and geom is not None and "EMPTY" not in geom
):
expected = 0
eng = eng.create_or_skip()
eng.assert_query_result(f"SELECT ST_ZMax({geom_or_null(geom)})", expected)
# Note: PostGIS doesn't support MMin/MMax, so we only test SedonaDB
@pytest.mark.parametrize("eng", [SedonaDB])
@pytest.mark.parametrize(
("geom", "expected"),
[
(None, None),
("POINT EMPTY", None),
("POINT (1 2)", None),
("POINT Z (1 2 3)", None),
("POINT M (1 2 3)", 3),
("POINT ZM (1 2 3 4)", 4),
("MULTILINESTRING ((0 0, 1 1), (2 2, 3 3))", None),
("POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", None),
(
"GEOMETRYCOLLECTION (POINT (0 0), POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0)), LINESTRING (0 0, 1 1))",
None,
),
(
"GEOMETRYCOLLECTION ZM (POINT ZM (1 2 3 4), LINESTRING ZM (3 4 5 6, 7 8 9 10), POLYGON ZM ((0 0 0 -1.1, 1 0 0 0, 0 1 0 0, 0 0 0 0)))",
-1.1,
),
(
"GEOMETRYCOLLECTION (POINT (1 2), LINESTRING (3 4, 5 6), GEOMETRYCOLLECTION (POINT (10 10)))",
None,
),
],
)
def test_st_mmin(eng, geom, expected):
eng = eng.create_or_skip()
eng.assert_query_result(f"SELECT ST_MMin({geom_or_null(geom)})", expected)
# Note: PostGIS doesn't support MMin/MMax, so we only test SedonaDB
@pytest.mark.parametrize("eng", [SedonaDB])
@pytest.mark.parametrize(
("geom", "expected"),
[
(None, None),
("POINT EMPTY", None),
("POINT (1 2)", None),
("POINT Z (1 2 3)", None),
("POINT M (1 2 3)", 3),
("POINT ZM (1 2 3 4)", 4),
("MULTILINESTRING ((0 0, 1 1), (2 2, 3 3))", None),
("POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", None),
(
"GEOMETRYCOLLECTION (POINT (0 0), POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0)), LINESTRING (0 0, 1 1))",
None,
),
(
"GEOMETRYCOLLECTION ZM (POINT ZM (1 2 3 4), LINESTRING ZM (3 4 5 6, 7 8 9 10), POLYGON ZM ((0 0 0 0, 1 0 0 0, 0 1 0 0, 0 0 0 0)))",
10,
),
(
"GEOMETRYCOLLECTION (POINT (1 2), LINESTRING (3 4, 5 6), GEOMETRYCOLLECTION (POINT (10 10)))",
None,
),
],
)
def test_st_mmax(eng, geom, expected):
eng = eng.create_or_skip()
eng.assert_query_result(f"SELECT ST_MMax({geom_or_null(geom)})", expected)
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
@pytest.mark.parametrize(
("geom", "expected"),
[
# Already valid polygon should remain unchanged
(
"POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0))",
"POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0))",
),
# Self-intersecting polygon (bowtie) should be fixed
(
"POLYGON ((0 0, 2 2, 2 0, 0 2, 0 0))",
"MULTIPOLYGON (((0 2, 1 1, 0 0, 0 2)), ((2 0, 1 1, 2 2, 2 0)))",
),
# Polygon with incorrect ring orientation should be fixed
(
"POLYGON ((0 0, 0 3, 3 3, 3 0, 0 0), (1 1, 1 2, 2 2, 2 1, 1 1))",
"POLYGON ((0 0, 0 3, 3 3, 3 0, 0 0), (1 1, 1 2, 2 2, 2 1, 1 1))",
),
# Polygon with repeated points should be cleaned (Note: JTS 'make valid' might preserve the duplicate points in WKT but the effective geometry is valid)
(
"POLYGON ((0 0, 0 1, 0 1, 1 1, 1 0, 0 0, 0 0))",
"POLYGON ((0 0, 0 1, 0 1, 1 1, 1 0, 0 0, 0 0))",
),
# LineString that is already valid
(
"LINESTRING (0 0, 1 1, 2 2)",
"LINESTRING (0 0, 1 1, 2 2)",
),
# LineString with repeated points should be simplified
(
"LINESTRING (0 0, 0 0, 1 1, 1 1, 2 2)",
"LINESTRING (0 0, 0 0, 1 1, 1 1, 2 2)",
),
# MultiPolygon with invalid components (two bowtie polygons)
(
"MULTIPOLYGON (((0 0, 1 1, 1 0, 0 1, 0 0)), ((2 2, 3 3, 3 2, 2 3, 2 2)))",
"MULTIPOLYGON (((0.5 0.5, 0 0, 0 1, 0.5 0.5)), ((0.5 0.5, 1 1, 1 0, 0.5 0.5)), ((2.5 2.5, 2 2, 2 3, 2.5 2.5)), ((2.5 2.5, 3 3, 3 2, 2.5 2.5)))",
),
# Point geometry (always valid)
(
"POINT (1 1)",
"POINT (1 1)",
),
# GeometryCollection with mixed valid/invalid geometries
(
"GEOMETRYCOLLECTION (POINT (1 1), POLYGON ((0 0, 2 2, 2 0, 0 2, 0 0)))",
"GEOMETRYCOLLECTION (POINT (1 1), MULTIPOLYGON (((0 2, 1 1, 0 0, 0 2)), ((2 0, 1 1, 2 2, 2 0))))",
),
# Empty geometry
(
"POLYGON EMPTY",
"POLYGON EMPTY",
),
# Polygon with spike (almost self-intersecting)
(
"POLYGON ((0 0, 3 0, 3 3, 2 1, 1 3, 0 3, 0 0))",
"POLYGON ((0 0, 3 0, 3 3, 2 1, 1 3, 0 3, 0 0))",
),
],
)
def test_st_makevalid(eng, geom, expected):
eng = eng.create_or_skip()
eng.assert_query_result(
f"SELECT ST_MakeValid({geom_or_null(geom)})",
expected,
)
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
@pytest.mark.parametrize(
("geom", "expected"),
[
(None, None),
(
"POINT (1 1)",
float("inf"),
),
(
"LINESTRING (0 0, 1 1, 2 2)",
1.4142135623730951,
),
(
"LINESTRING (5 107, 54 84, 101 100)",
49.64876634922564,
),
(
"LINESTRING(0 0,0 0,1 1,1 1,2 2)",
1.4142135623730951,
),
(
"POLYGON((0 0,0 3,3 3,3 0,0 0),(1 1,1 2,2 2,2 1,1 1))",
1.0,
),
(
"POLYGON((0 0,0 1,0 1,1 1,1 0,0 0,0 0))",
1.0,
),
(
"POLYGON EMPTY",
float("inf"),
),
(
"POLYGON((0 0,3 0,3 3,2 1,1 3,0 3,0 0))",
1.0,
),
(
"MULTIPOINT (10 40, 40 30)",
31.622776601683793,
),
(
"MULTIPOINT ((10 10), (20 20), (30 30))",
14.142135623730951,
),
(
"MULTILINESTRING ((10 10, 20 20), (30 30, 40 40))",
14.142135623730951,
),
(
"MULTILINESTRING ((5 5, 10 5, 10 15), (20 20, 25 10))",
5.0,
),
(
"MULTIPOLYGON(((26 125, 26 200, 126 200, 126 125, 26 125 ),( 51 150, 101 150, 76 175, 51 150 )),(( 151 100, 151 200, 176 175, 151 100 )))",
25.0,
),
(
"GEOMETRYCOLLECTION(POINT(1 1),MULTIPOLYGON(((0 2,1 1,0 0,0 2)),((2 0,1 1,2 2,2 0))))",
1.0,
),
],
)
def test_st_minimum_clearance(eng, geom, expected):
eng = eng.create_or_skip()
eng.assert_query_result(
f"SELECT ST_MinimumClearance({geom_or_null(geom)})",
expected,
)
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
@pytest.mark.parametrize(
("geom", "expected"),
[
(
None,
None,
),
(
"POLYGON ((0 0, 1 0, 1 1, 0.5 3.2e-4, 0 0))",
"LINESTRING (0.5 0.00032, 0.5 0)",
),
(
"MULTIPOLYGON(((26 125, 26 200, 126 200, 126 125, 26 125 ),( 51 150, 101 150, 76 175, 51 150 )),(( 151 100, 151 200, 176 175, 151 100 )))",
"LINESTRING (76 175, 76 150)",
),
(
"LINESTRING (5 107, 54 84, 101 100)",
"LINESTRING (54 84, 101 100)",
),
(
"POLYGON((0 0,0 3,3 3,3 0,0 0),(1 1,1 2,2 2,2 1,1 1))",
"LINESTRING (1 1, 1 2)",
),
(
"POLYGON((0 0,0 1,0 1,1 1,1 0,0 0,0 0))",
"LINESTRING (0 0, 0 1)",
),
(
"LINESTRING (0 0, 1 1, 2 2)",
"LINESTRING (0 0, 1 1)",
),
(
"MULTIPOLYGON(((0.5 0.5,0 0,0 1,0.5 0.5)),((0.5 0.5,1 1,1 0,0.5 0.5)),((2.5 2.5,2 2,2 3,2.5 2.5)),((2.5 2.5,3.5 3.5,3.5 1.5,2.5 2.5)))",
"LINESTRING (2.5 2.5, 2 2.5)",
),
(
"POINT (1 1)",
"LINESTRING EMPTY",
),
(
"GEOMETRYCOLLECTION(POINT(1 1),MULTIPOLYGON(((0 2,1 1,0 0,0 2)),((2 0,1 1,2 2,2 0))))",
"LINESTRING (1 1, 2 1)",
),
(
"POLYGON EMPTY",
"LINESTRING EMPTY",
),
(
"POLYGON((0 0,3 0,3 3,2 1,1 3,0 3,0 0))",
"LINESTRING (1 3, 0 3)",
),
],
)
def test_st_minimum_clearance_line(eng, geom, expected):
eng = eng.create_or_skip()
eng.assert_query_result(
f"SELECT ST_MinimumClearanceLine({geom_or_null(geom)})",
expected,
)
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
@pytest.mark.parametrize(
("geom", "expected"),
[
(None, None),
("POINT (0 0)", "Valid Geometry"),
("POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0))", "Valid Geometry"),
("POLYGON ((0 0, 1 1, 0 1, 1 0, 0 0))", "Self-intersection%"),
("Polygon((0 0, 2 0, 1 1, 2 2, 0 2, 1 1, 0 0))", "Ring Self-intersection%"),
],
)
def test_st_isvalidreason(eng, geom, expected):
eng = eng.create_or_skip()
if expected is not None and "%" in str(expected):
query = f"SELECT ST_IsValidReason({geom_or_null(geom)}) LIKE '{expected}'"
eng.assert_query_result(query, True)
else:
query = f"SELECT ST_IsValidReason({geom_or_null(geom)})"
eng.assert_query_result(query, expected)
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
@pytest.mark.parametrize(
("geom", "tolerance", "expected"),
[
# TODO: PostGIS fails without explicit ::GEOMETRY type cast, but casting
# doesn't work on SedonaDB yet.
# (None, 2, None),
# (None, None, None),
("LINESTRING (0 0, 1 1, 2 2)", None, None),
("LINESTRING (0 0, 1 1, 2 0, 3 1, 4 0)", 1.5, "LINESTRING (0 0, 4 0)"),
(
"LINESTRING (0 0, 1 1, 2 0, 3 1, 4 0)",
0.0,
"LINESTRING (0 0, 1 1, 2 0, 3 1, 4 0)",
),
(
"POLYGON ((0 0, 0 10, 1 11, 10 10, 10 0, 0 0))",
1.5,
"POLYGON ((0 0, 0 10, 10 10, 10 0, 0 0))",
),
("POINT (10 20)", 10.0, "POINT (10 20)"),
("LINESTRING EMPTY", 1.0, "LINESTRING EMPTY"),
("POINT EMPTY", 1.0, "POINT (nan nan)"),
("POLYGON EMPTY", 1.0, "POLYGON EMPTY"),
("LINESTRING (0 0, 0 1)", 1.0, "LINESTRING (0 0, 0 1)"),
(
"LINESTRING (0 0, 0 10, 0 51, 50 20, 30 20, 7 32)",
2.0,
"LINESTRING (0 0, 0 51, 50 20, 30 20, 7 32)",
),
(
"LINESTRING (0 0, 0 10, 0 51, 50 20, 30 20, 7 32)",
10.0,
"LINESTRING (0 0, 0 51, 50 20, 7 32)",
),
(
"LINESTRING (0 0, 0 10, 0 51, 50 20, 30 20, 7 32)",
50.0,
"LINESTRING (0 0, 7 32)",
),
("MULTIPOINT ((0 0), (1 1))", 1.0, "MULTIPOINT (0 0, 1 1)"),
(
"POLYGON ((0 0, 1 0, 2 0, 3 0, 4 0, 4 1, 4 2, 4 3, 4 4, 3 4, 2 4, 1 4, 0 4, 0 3, 0 2, 0 1, 0 0))",
1.5,
"POLYGON ((0 0, 4 0, 4 4, 0 4, 0 0))",
),
# Collapsed
("LINESTRING(0 0, 1 0, 2 0.1, 3 0)", 5.0, "LINESTRING (0 0, 3 0)"),
("LINESTRING(0 0, 0.1 0.1, 0.2 0.2)", 1.0, "LINESTRING (0 0, 0.2 0.2)"),
(
"MULTIPOINT((0 0), (0.1 0.1), (5 5))",
1.0,
"MULTIPOINT (0 0, 0.1 0.1, 5 5)",
),
(
"MULTILINESTRING((0 0, 5 0.1, 10 0), (20 20, 21 21, 22 22))",
1.0,
"MULTILINESTRING ((0 0, 10 0), (20 20, 22 22))",
),
(
"MULTIPOLYGON(((0 0, 0.1 0, 0.1 0.1, 0 0.1, 0 0)), ((10 10, 20 10, 20 20, 10 20, 10 10)))",
1.0,
"MULTIPOLYGON (((10 10, 20 10, 20 20, 10 20, 10 10)))",
),
(
"POLYGON((0 0, 0 100, 1 101, 100 100, 100 0, 0 0), (20 20, 20 80, 21 81, 80 80, 80 20, 20 20))",
10.0,
"POLYGON ((0 0, 0 100, 100 100, 100 0, 0 0), (20 20, 20 80, 80 80, 80 20, 20 20))",
),
(
"POLYGON((0 0, 0 100, 100 100, 100 0, 0 0), (40 40, 40.1 40, 40.1 40.1, 40 40.1, 40 40))",
1.0,
"POLYGON ((0 0, 0 100, 100 100, 100 0, 0 0))",
),
(
"MULTILINESTRING((0 0, 1 0.1, 2 0.2, 3 0), (10 10, 11 10, 12 10), (20 20, 21 25, 22 20))",
1.0,
"MULTILINESTRING ((0 0, 3 0), (10 10, 12 10), (20 20, 21 25, 22 20))",
),
(
"MULTIPOLYGON(((0 0, 100 0, 100 100, 0 100, 0 0)), ((200 200, 200.1 200, 200.1 200.1, 200 200.1, 200 200)))",
1.0,
"MULTIPOLYGON (((0 0, 100 0, 100 100, 0 100, 0 0)))",
),
(
"MULTIPOLYGON(((0 0, 10 0, 10 10, 0 10, 0 0)), ((20 20, 20.1 20, 20.1 20.1, 20 20.1, 20 20)), ((30 30, 40 30, 40 40, 30 40, 30 30)))",
1.0,
"MULTIPOLYGON (((0 0, 10 0, 10 10, 0 10, 0 0)), ((30 30, 40 30, 40 40, 30 40, 30 30)))",
),
(
"GEOMETRYCOLLECTION(POINT(0 0), LINESTRING(10 10, 11 10.1, 12 10), POLYGON((20 20, 30 20, 30 30, 20 30, 20 20)))",
1.0,
"GEOMETRYCOLLECTION (POINT (0 0), LINESTRING (10 10, 12 10), POLYGON ((20 20, 30 20, 30 30, 20 30, 20 20)))",
),
(
"GEOMETRYCOLLECTION(GEOMETRYCOLLECTION(POINT(0 0)), LINESTRING(10 10, 11 10.1, 12 10))",
1.0,
"GEOMETRYCOLLECTION (GEOMETRYCOLLECTION (POINT (0 0)), LINESTRING (10 10, 12 10))",
),
(
"GEOMETRYCOLLECTION(MULTIPOINT((0 0), (1 1)), MULTILINESTRING((10 10, 11 10.1, 12 10), (20 20, 21 21)))",
1.0,
"GEOMETRYCOLLECTION (MULTIPOINT (0 0, 1 1), MULTILINESTRING ((10 10, 12 10), (20 20, 21 21)))",
),
("LINESTRING(0 0, 1 0, 2 0, 3 0, 4 0, 5 0)", 0.0, "LINESTRING (0 0, 5 0)"),
("LINESTRING(0 0, 1 0.01, 2 0.02, 3 0.01, 4 0)", 0.1, "LINESTRING (0 0, 4 0)"),
(
"LINESTRING(0 0, 0.00001 0.00001, 0.00002 0.00002)",
1.0,
"LINESTRING (0 0, 0.00002 0.00002)",
),
(
"LINESTRING(0 0, 10 0, 10 10, 5 15, 0 10, 0 0)",
5.0,
"LINESTRING (0 0, 10 0, 5 15, 0 0)",
),
],
)
def test_st_simplify(eng, geom, tolerance, expected):
eng = eng.create_or_skip()
eng.assert_query_result(
f"SELECT ST_Simplify({geom_or_null(geom)}, {val_or_null(tolerance)})",
expected,
)
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
@pytest.mark.parametrize(
("geom", "tolerance", "expected"),
[
# removes intermediate point
(
"LINESTRING (0 0, 0 10, 0 51, 50 20, 30 20, 7 32)",
2,
"LINESTRING (0 0, 0 51, 50 20, 30 20, 7 32)",
),
# Short linestring preserves endpoints
(
"LINESTRING (0 0, 0 10)",
20,
"LINESTRING (0 0, 0 10)",
),
# Null handling
(None, 2, None),
(None, None, None),
("LINESTRING (0 0, 0 10)", None, None),
# Empty geometries
("LINESTRING EMPTY", 2, "LINESTRING EMPTY"),
("POINT EMPTY", 2, "POINT (nan nan)"),
("POLYGON EMPTY", 2, "POLYGON EMPTY"),
# inner ring simplified
(
"POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0), (5 5, 5 6, 6 6, 8 5, 5 5))",
20,
"POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0), (5 5, 5 6, 8 5, 5 5))",
),
# second polygon's inner ring simplified
(
"MULTIPOLYGON (((100 100, 100 130, 130 130, 130 100, 100 100)), ((0 0, 10 0, 10 10, 0 10, 0 0), (5 5, 5 6, 6 6, 8 5, 5 5)))",
20,
"MULTIPOLYGON (((100 100, 100 130, 130 130, 130 100, 100 100)), ((0 0, 10 0, 10 10, 0 10, 0 0), (5 5, 5 6, 8 5, 5 5)))",
),
],
)
def test_st_simplifypreservetopology(eng, geom, tolerance, expected):
eng = eng.create_or_skip()
eng.assert_query_result(
f"SELECT ST_SimplifyPreserveTopology({geom_or_null(geom)}, {val_or_null(tolerance)})",
expected,
)
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
@pytest.mark.parametrize(
("input", "reference", "tolerance", "expected"),
[
(
"MULTIPOLYGON(((26 125, 26 200, 126 200, 126 125, 26 125 ),( 51 150, 101 150, 76 175, 51 150 )),(( 151 100, 151 200, 176 175, 151 100 )))",
"LINESTRING (5 107, 54 84, 101 100)",
25.0 * 1.01,
"MULTIPOLYGON (((26 125, 26 200, 126 200, 126 125, 101 100, 26 125), (51 150, 101 150, 76 175, 51 150)), ((151 100, 151 200, 176 175, 151 100)))",
),
(
"MULTIPOLYGON((( 26 125, 26 200, 126 200, 126 125, 26 125 ),( 51 150, 101 150, 76 175, 51 150 )),(( 151 100, 151 200, 176 175, 151 100 )))",
"LINESTRING (5 107, 54 84, 101 100)",
25.0 * 1.25,
"MULTIPOLYGON (((5 107, 26 200, 126 200, 126 125, 101 100, 54 84, 5 107), (51 150, 101 150, 76 175, 51 150)), ((151 100, 151 200, 176 175, 151 100)))",
),
(
"LINESTRING (5 107, 54 84, 101 100)",
"MULTIPOLYGON(((26 125, 26 200, 126 200, 126 125, 26 125),(51 150, 101 150, 76 175, 51 150 )),((151 100, 151 200, 176 175, 151 100)))",
25.0 * 1.01,
"LINESTRING (5 107, 26 125, 54 84, 101 100)",
),
(
"LINESTRING (5 107, 54 84, 101 100)",
"MULTIPOLYGON(((26 125, 26 200, 126 200, 126 125, 26 125),(51 150, 101 150, 76 175, 51 150 )),((151 100, 151 200, 176 175, 151 100)))",
25.0 * 1.25,
"LINESTRING (26 125, 54 84, 101 100)",
),
(
"POINT (1.1 2.1)",
"POINT (1 2)",
0.5,
"POINT (1 2)",
), # Should snap to reference
(
"POINT (5.9 6.9)",
"POINT (6 7)",
0.5,
"POINT (6 7)",
), # Should snap to reference
(
"LINESTRING (0.9 0.9, 2.1 2.1, 4.9 4.9)",
"POINT (1 1)",
0.5,
"LINESTRING (1 1, 2.1 2.1, 4.9 4.9)", # First and last vertices snap
),
(
"LINESTRING (10.1 10.1, 12 12)",
"MULTIPOINT ((5 5), (10 10))",
0.5,
"LINESTRING (10 10, 12 12)", # First vertex snaps
),
(
"POLYGON ((0.9 0.9, 0.9 3.1, 3.1 3.1, 3.1 0.9, 0.9 0.9))",
"LINESTRING (1 1, 1 3, 3 3, 3 1, 1 1)",
0.5,
"POLYGON ((1 1, 1 3, 3 3, 3 1, 1 1))", # All vertices snap
),
(
"POLYGON ((5 5, 5 8, 8 8, 8 5, 5 5))",
"LINESTRING (4.9 4.9, 4.9 8.1, 8.1 8.1, 8.1 4.9, 4.9 4.9)",
0.5,
"POLYGON ((4.9 4.9, 4.9 8.1, 8.1 8.1, 8.1 4.9, 4.9 4.9))", # Partial snapping
),
(
"MULTILINESTRING ((0.9 0.9, 2 2), (3.1 3.1, 4 4))",
"MULTIPOINT ((1 1), (3 3))",
0.5,
"MULTILINESTRING ((1 1, 2 2), (3 3, 4 4))", # Endpoints snap
),
(
"MULTIPOINT (0.9 0.9, 2.1 2.1, 3.9 3.9)",
"LINESTRING (1 1, 2 2, 3 3, 4 4)",
0.5,
"MULTIPOINT (1 1, 2 2, 4 4)", # Points snap to line
),
(
"POINT (1.1 2.1)",
"POINT (1 2)",
0.5,
"POINT (1 2)",
), # Snaps within tolerance
(
"POINT (1.6 2.6)",
"POINT (1 2)",
0.5,
"POINT (1.6 2.6)",
), # No snap (outside tolerance)
(
"LINESTRING (0 0, 10 10)",
"POINT (5 5)",
1.0,
"LINESTRING (0 0, 5 5, 10 10)", # Vertex inserted on line
),
("POINT (5 5)", "POINT (5 5)", 0.0, "POINT (5 5)"), # Exact match, no change
(
"POLYGON ((0.9 0.9, 0.9 5.1, 5.1 5.1, 5.1 0.9, 0.9 0.9), (1.9 1.9, 1.9 4.1, 4.1 4.1, 4.1 1.9, 1.9 1.9))",
"POLYGON ((1 1, 1 5, 5 5, 5 1, 1 1), (2 2, 2 4, 4 4, 4 2, 2 2))",
0.5,
"POLYGON ((1 1, 1 5, 5 5, 5 1, 1 1), (2 2, 2 4, 4 4, 4 2, 2 2))",
),
(
"LINESTRING (0.1 0.1, 0.2 0.2, 0.3 0.3, 0.4 0.4, 0.5 0.5, 0.6 0.6, 0.7 0.7, 0.8 0.8, 0.9 0.9)",
"LINESTRING (0 0, 1 1)",
0.5,
"LINESTRING (0 0, 0.2 0.2, 0.3 0.3, 0.4 0.4, 0.5 0.5, 0.6 0.6, 0.7 0.7, 0.8 0.8, 1 1)",
),
(
"POINT (1 2)",
"POINT (3 4)",
0.5,
"POINT (1 2)",
), # No snap (outside tolerance)
],
)
def test_st_snap(eng, input, reference, tolerance, expected):
eng = eng.create_or_skip()
eng.assert_query_result(
f"SELECT ST_Snap({geom_or_null(input)}, {geom_or_null(reference)}, {val_or_null(tolerance)})",
expected,
)
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
@pytest.mark.parametrize(
("geom", "expected"),
[
(None, None),
("POINT EMPTY", 0),
("POINT Z EMPTY", 2),
("POINT M EMPTY", 1),
("POINT ZM EMPTY", 3),
("POINT Z (0 0 0)", 2),
("POINT M (0 0 0)", 1),
("POINT ZM (0 0 0 0)", 3),
("LINESTRING EMPTY", 0),
("LINESTRING Z EMPTY", 2),
("LINESTRING Z (0 0 0, 1 1 1)", 2),
("POLYGON EMPTY", 0),
("MULTIPOINT ((0 0), (1 1))", 0),
("MULTIPOINT Z ((0 0 0))", 2),
("MULTIPOINT ZM ((0 0 0 0))", 3),
("GEOMETRYCOLLECTION EMPTY", 0),
("GEOMETRYCOLLECTION (POINT Z (0 0 0))", 2),
("GEOMETRYCOLLECTION Z (POINT Z (0 0 0))", 2),
("GEOMETRYCOLLECTION (GEOMETRYCOLLECTION (POINT Z (0 0 0)))", 2),
],
)
def test_st_zmflag(eng, geom, expected):
eng = eng.create_or_skip()
eng.assert_query_result(f"SELECT ST_ZmFlag({geom_or_null(geom)})", expected)