diff --git a/README.md b/README.md index 678bae70..3fb9581a 100644 --- a/README.md +++ b/README.md @@ -74,11 +74,16 @@ This package provides comprehensive Doctrine support for PostgreSQL features: - **Range Operations** - Containment checks (in PHP value objects and for DQL queries with `@>` and `<@`) - Overlaps (`&&`) +- **PostGIS Spatial Operations** + - Bounding box relationships (`<<`, `>>`, `&<`, `&>`, `|&>`, `&<|`, `<<|`, `|>>`) + - Spatial containment (`@`, `~`) + - Distance calculations (`<->`, `<#>`, `<<->>`, `<<#>>`, `|=|`) + - N-dimensional operations (`&&&`) ### Functions - **Text Search** - Full text search (`to_tsvector`, `to_tsquery`) - - Pattern matching (`ILIKE`, `SIMILAR TO`) + - Pattern matching (`ilike`, `similar to`) - Regular expressions - **Array Functions** - Array aggregation (`array_agg`) diff --git a/docs/AVAILABLE-FUNCTIONS-AND-OPERATORS.md b/docs/AVAILABLE-FUNCTIONS-AND-OPERATORS.md index ad9962eb..c5a493db 100644 --- a/docs/AVAILABLE-FUNCTIONS-AND-OPERATORS.md +++ b/docs/AVAILABLE-FUNCTIONS-AND-OPERATORS.md @@ -1,6 +1,26 @@ -# Available operators +# Available Operators -| PostgreSQL operator | Register for DQL as | Implemented by +## Operator Conflicts and Usage Notes + +**⚠️ Important**: Some PostgreSQL operators have multiple meanings depending on the data types involved. This library provides specific DQL function names to avoid conflicts: + +| Operator | Array/JSON Usage | Spatial Usage | Text/Pattern Usage | +|---|---|---|---| +| `@>` | `CONTAINS` (arrays contain elements) | Works automatically with geometry/geography | N/A | +| `<@` | `IS_CONTAINED_BY` (element in array) | Works automatically with geometry/geography | N/A | +| `@` | N/A | `SPATIAL_CONTAINED_BY` (bounding box contained) | N/A | +| `~` | N/A | `SPATIAL_CONTAINS` (bounding box contains) | `REGEXP` (text pattern matching) | +| `&&` | `OVERLAPS` (arrays/ranges overlap) | Works automatically with geometry/geography | N/A | + +**Usage Guidelines:** +- **Arrays/JSON**: Use `CONTAINS`, `IS_CONTAINED_BY`, `OVERLAPS` for array and JSON operations +- **Spatial**: Use `SPATIAL_CONTAINS`, `SPATIAL_CONTAINED_BY` for explicit spatial bounding box operations +- **Text**: Use `REGEXP`, `IREGEXP` for pattern matching +- **Boolean operators**: All spatial operators return boolean values and **shall be used with `= TRUE` or `= FALSE` in DQL** + +## General Operators + +| PostgreSQL operator | Register for DQL as | Implemented by | |---|---|---| | @> | CONTAINS | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Contains` | | <@ | IS_CONTAINED_BY | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\IsContainedBy` | @@ -24,9 +44,80 @@ | @@ | TSMATCH | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Tsmatch` | | \|\| | STRCONCAT | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\StrConcat` | -# Available functions +## PostGIS Spatial Operators + +**⚠️ Important**: Some operators have dual meanings for different data types. Use the specific DQL function names to avoid conflicts: + +- **`@`**: Use `CONTAINS` for arrays/JSON, `SPATIAL_CONTAINED_BY` for geometry/geography +- **`~`**: Use `REGEXP` for text patterns, `SPATIAL_CONTAINS` for geometry/geography +- **`&&`**: Use `OVERLAPS` for arrays/JSON, spatial overlaps work automatically with geometry/geography + +**📝 Compatibility Notes**: +- Most bounding box operators work primarily with **geometry** types +- **Geography** types have limited operator support (mainly `&&`, `<->`, `<@>`) +- **3D/n-dimensional operators** may require explicit type casting: `ST_GeomFromText('POINT Z(0 0 0)')` +- Some advanced operators (`&&&`, `<<#>>`) may not be available in all PostGIS versions + +### Bounding Box Operators + +These operators work with geometry and geography bounding boxes. All return boolean values and **shall be used with `= TRUE` or `= FALSE` in DQL**. + +| PostgreSQL operator | Register for DQL as | Description | Implemented by | +|---|---|---|---| +| &< | OVERLAPS_LEFT | Returns TRUE if A's bounding box overlaps or is to the left of B's | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\OverlapsLeft` | +| &> | OVERLAPS_RIGHT | Returns TRUE if A's bounding box overlaps or is to the right of B's | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\OverlapsRight` | +| << | STRICTLY_LEFT | Returns TRUE if A's bounding box is strictly to the left of B's | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\StrictlyLeft` | +| >> | STRICTLY_RIGHT | Returns TRUE if A's bounding box is strictly to the right of B's | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\StrictlyRight` | +| @ | SPATIAL_CONTAINED_BY | Returns TRUE if A's bounding box is contained by B's (**spatial version**) | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\SpatialContainedBy` | +| ~ | SPATIAL_CONTAINS | Returns TRUE if A's bounding box contains B's (**spatial version**) | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\SpatialContains` | +| ~= | SPATIAL_SAME | Returns TRUE if A's bounding box is the same as B's | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\SpatialSame` | +| \|&> | OVERLAPS_ABOVE | Returns TRUE if A's bounding box overlaps or is above B's | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\OverlapsAbove` | +| \|>> | STRICTLY_ABOVE | Returns TRUE if A's bounding box is strictly above B's | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\StrictlyAbove` | +| &<\| | OVERLAPS_BELOW | Returns TRUE if A's bounding box overlaps or is below B's | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\OverlapsBelow` | +| <<\| | STRICTLY_BELOW | Returns TRUE if A's bounding box is strictly below B's | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\StrictlyBelow` | +| &&& | ND_OVERLAPS | Returns TRUE if A's n-D bounding box intersects B's n-D bounding box | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\NDimensionalOverlaps` | + +**Usage Examples:** +```sql +-- Find geometries to the left of a reference point +SELECT e FROM Entity e WHERE STRICTLY_LEFT(e.geometry, 'POINT(0 0)') = TRUE + +-- Find overlapping polygons +SELECT e FROM Entity e WHERE SPATIAL_CONTAINS(e.polygon, e.point) = TRUE + +-- 3D spatial relationships +SELECT e FROM Entity e WHERE ND_OVERLAPS(e.geometry3d, 'POLYGON Z((0 0 0, 1 1 1, 2 2 2, 0 0 0))') = TRUE +``` + +### Distance Operators + +These operators calculate distances between geometries. All return numeric values. + +| PostgreSQL operator | Register for DQL as | Description | Implemented by | +|---|---|---|---| +| <-> | GEOMETRY_DISTANCE | Returns the 2D distance between A and B geometries | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\GeometryDistance` | +| <@> | DISTANCE | Returns distance between points (legacy operator) | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Distance` | +| \|=\| | TRAJECTORY_DISTANCE | Returns distance between trajectories at closest point of approach | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\TrajectoryDistance` | +| <#> | BOUNDING_BOX_DISTANCE | Returns the 2D distance between A and B bounding boxes | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\BoundingBoxDistance` | +| <<->> | ND_CENTROID_DISTANCE | Returns n-D distance between centroids of bounding boxes | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\NDimensionalCentroidDistance` | +| <<#>> | ND_BOUNDING_BOX_DISTANCE | Returns the n-D distance between A and B bounding boxes | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\NDimensionalBoundingBoxDistance` | + +**Usage Examples:** +```sql +-- Find nearest geometries +SELECT e, GEOMETRY_DISTANCE(e.geometry, 'POINT(0 0)') as distance +FROM Entity e ORDER BY distance LIMIT 10 + +-- Bounding box distance for index optimization +SELECT e FROM Entity e WHERE BOUNDING_BOX_DISTANCE(e.geometry, 'POINT(0 0)') < 1000 + +-- 3D distance calculations +SELECT ND_CENTROID_DISTANCE(e.geometry3d1, e.geometry3d2) as distance FROM Entity e +``` + +# Available Functions -| PostgreSQL functions | Register for DQL as | Implemented by +| PostgreSQL functions | Register for DQL as | Implemented by | |---|---|---| | all | ALL_OF | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\All` | | any | ANY_OF | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Any` | @@ -134,9 +225,9 @@ | width_bucket | WIDTH_BUCKET | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\WidthBucket` | -# Bonus helpers +# Bonus Helpers -| PostgreSQL functions | Register for DQL as | Implemented by +| PostgreSQL functions | Register for DQL as | Implemented by | |---|---|---| | array | ARRAY | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Arr` | | value = ANY(list of values) | IN_ARRAY | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\InArray` | diff --git a/docs/USE-CASES-AND-EXAMPLES.md b/docs/USE-CASES-AND-EXAMPLES.md index 7fa63975..358f196e 100644 --- a/docs/USE-CASES-AND-EXAMPLES.md +++ b/docs/USE-CASES-AND-EXAMPLES.md @@ -178,9 +178,87 @@ POLYGONZM((...)) => POLYGON ZM((...)) POINT Z (1 2 3) => POINT Z(1 2 3) ``` +### Using PostGIS Spatial Operators in DQL + +PostGIS spatial operators allow you to perform spatial queries using bounding box relationships and distance calculations. **Important**: All spatial operators return boolean values and shall be used with `= TRUE` or `= FALSE` in DQL. + +#### Bounding Box Spatial Relationships + +```sql +-- Find geometries to the left of a reference point +SELECT e FROM Entity e WHERE STRICTLY_LEFT(e.geometry, 'POINT(0 0)') = TRUE + +-- Find geometries that spatially contain a point (bounding box level) +SELECT e FROM Entity e WHERE SPATIAL_CONTAINS(e.polygon, 'POINT(1 1)') = TRUE + +-- Find geometries contained within a bounding box +SELECT e FROM Entity e WHERE SPATIAL_CONTAINED_BY(e.geometry, 'POLYGON((0 0, 10 10, 20 20, 0 0))') = TRUE + +-- Check if two geometries have the same bounding box +SELECT e FROM Entity e WHERE SPATIAL_SAME(e.geometry1, e.geometry2) = TRUE + +-- Vertical relationships +SELECT e FROM Entity e WHERE STRICTLY_ABOVE(e.geometry, 'LINESTRING(0 0, 5 0)') = TRUE +SELECT e FROM Entity e WHERE OVERLAPS_BELOW(e.geometry, 'POLYGON((0 5, 5 5, 5 10, 0 10, 0 5))') = TRUE + +-- 3D spatial relationships +SELECT e FROM Entity e WHERE ND_OVERLAPS(e.geometry3d, 'POLYGON Z((0 0 0, 1 1 1, 2 2 2, 0 0 0))') = TRUE +``` + +#### Distance-Based Queries + +```sql +-- Find the 10 nearest geometries to a point +SELECT e, GEOMETRY_DISTANCE(e.geometry, 'POINT(0 0)') as distance +FROM Entity e +ORDER BY distance +LIMIT 10 + +-- Find geometries within a specific distance (using bounding box distance for performance) +SELECT e FROM Entity e WHERE BOUNDING_BOX_DISTANCE(e.geometry, 'POINT(0 0)') < 1000 + +-- Calculate trajectory distances (for linestrings with measure values) +SELECT TRAJECTORY_DISTANCE(e.trajectory1, e.trajectory2) as closest_approach +FROM Entity e +WHERE e.trajectory1 IS NOT NULL + +-- 3D distance calculations +SELECT e, ND_CENTROID_DISTANCE(e.geometry3d1, e.geometry3d2) as distance3d +FROM Entity e +WHERE ND_BOUNDING_BOX_DISTANCE(e.geometry3d1, e.geometry3d2) < 500 +``` + +#### Operator Conflicts and Best Practices + +Some operators have different meanings for different data types. Use specific function names to avoid conflicts: + +```sql +-- ✅ CORRECT: Use specific function names +SELECT e FROM Entity e WHERE CONTAINS(e.tags, ARRAY['tag1']) = TRUE -- Array containment +SELECT e FROM Entity e WHERE SPATIAL_CONTAINS(e.polygon, e.point) = TRUE -- Spatial containment +SELECT e FROM Entity e WHERE REGEXP(e.text, 'pattern') = TRUE -- Text pattern matching + +-- ❌ AVOID: Ambiguous usage that might conflict +-- The @ and ~ operators have different meanings for arrays vs spatial data +``` + +#### Performance Tips + +```sql +-- Use bounding box operators for initial filtering (they use spatial indexes) +SELECT e FROM Entity e +WHERE OVERLAPS(e.geometry, 'POLYGON((0 0, 10 10, 20 20, 0 0))') = TRUE + AND ST_Intersects(e.geometry, 'POLYGON((0 0, 10 10, 20 20, 0 0))') -- Exact check + +-- Use distance operators for nearest neighbor queries +SELECT e FROM Entity e +ORDER BY GEOMETRY_DISTANCE(e.geometry, 'POINT(0 0)') +LIMIT 10 +``` + For multi-item arrays, see [GEOMETRY-ARRAYS.md](./GEOMETRY-ARRAYS.md) for Doctrine DQL limitations and the suggested workarounds. -The library provides DBAL type support for PostGIS `geometry` and `geography` columns. Example usage: +The library provides DBAL type support for PostGIS `geometry` and `geography` types. Example usage: ```sql CREATE TABLE places ( diff --git a/fixtures/MartinGeorgiev/Doctrine/Entity/ContainsGeometries.php b/fixtures/MartinGeorgiev/Doctrine/Entity/ContainsGeometries.php new file mode 100644 index 00000000..4821581e --- /dev/null +++ b/fixtures/MartinGeorgiev/Doctrine/Entity/ContainsGeometries.php @@ -0,0 +1,24 @@ +). + * + * Returns the 2D distance between A and B bounding boxes. + * This is useful for index-based distance queries. + * + * @see https://postgis.net/docs/reference.html#Operators_Distance + * @since 3.5 + * + * @author Martin Georgiev + * + * @example Using it in DQL: "SELECT BOUNDING_BOX_DISTANCE(g1.geometry, g2.geometry) FROM Entity g1, Entity g2" + * Returns numeric distance value. + */ +class BoundingBoxDistance extends BaseFunction +{ + protected function customizeFunction(): void + { + $this->setFunctionPrototype('(%s <#> %s)'); + $this->addNodeMapping('StringPrimary'); + $this->addNodeMapping('StringPrimary'); + } +} diff --git a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/GeometryDistance.php b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/GeometryDistance.php new file mode 100644 index 00000000..7858c6ac --- /dev/null +++ b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/GeometryDistance.php @@ -0,0 +1,33 @@ +). + * + * Returns the 2D distance between A and B geometries. + * This differs from BoundingBoxDistance (using <#>), which measures distance between + * bounding boxes only. The <-> operator is commonly used for KNN nearest-neighbor + * ordering, and on PostgreSQL 9.5+ with PostGIS 2.2+ it returns true geometry distance; + * on older stacks it behaved as a centroid-of-bounding-box distance approximation. + * + * @see https://postgis.net/docs/reference.html#Operators + * @see https://postgis.net/docs/geometry_distance_knn.html + * @since 3.5 + * + * @author Martin Georgiev + * + * @example Using it in DQL: "SELECT GEOMETRY_DISTANCE(g1.geometry, g2.geometry) FROM Entity g1, Entity g2" + * Returns numeric distance value. + */ +class GeometryDistance extends BaseFunction +{ + protected function customizeFunction(): void + { + $this->setFunctionPrototype('(%s <-> %s)'); + $this->addNodeMapping('StringPrimary'); + $this->addNodeMapping('StringPrimary'); + } +} diff --git a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/NDimensionalBoundingBoxDistance.php b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/NDimensionalBoundingBoxDistance.php new file mode 100644 index 00000000..e1b8b277 --- /dev/null +++ b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/NDimensionalBoundingBoxDistance.php @@ -0,0 +1,29 @@ +>). + * + * Returns the n-D distance between A and B bounding boxes. + * This operator works with multi-dimensional geometries. + * + * @see https://postgis.net/docs/reference.html#Operators_Distance + * @since 3.5 + * + * @author Martin Georgiev + * + * @example Using it in DQL: "SELECT ND_BOUNDING_BOX_DISTANCE(g1.geometry, g2.geometry) FROM Entity g1, Entity g2" + * Returns numeric distance value. + */ +class NDimensionalBoundingBoxDistance extends BaseFunction +{ + protected function customizeFunction(): void + { + $this->setFunctionPrototype('(%s <<#>> %s)'); + $this->addNodeMapping('StringPrimary'); + $this->addNodeMapping('StringPrimary'); + } +} diff --git a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/NDimensionalCentroidDistance.php b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/NDimensionalCentroidDistance.php new file mode 100644 index 00000000..5117abe5 --- /dev/null +++ b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/NDimensionalCentroidDistance.php @@ -0,0 +1,29 @@ +>). + * + * Returns the n-D distance between the centroids of A and B bounding boxes. + * This operator works with multi-dimensional geometries. + * + * @see https://postgis.net/docs/reference.html#Operators_Distance + * @since 3.5 + * + * @author Martin Georgiev + * + * @example Using it in DQL: "SELECT ND_CENTROID_DISTANCE(g1.geometry, g2.geometry) FROM Entity g1, Entity g2" + * Returns numeric distance value. + */ +class NDimensionalCentroidDistance extends BaseFunction +{ + protected function customizeFunction(): void + { + $this->setFunctionPrototype('(%s <<->> %s)'); + $this->addNodeMapping('StringPrimary'); + $this->addNodeMapping('StringPrimary'); + } +} diff --git a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/NDimensionalOverlaps.php b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/NDimensionalOverlaps.php new file mode 100644 index 00000000..4a83d385 --- /dev/null +++ b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/NDimensionalOverlaps.php @@ -0,0 +1,29 @@ + + * + * @example Using it in DQL with boolean comparison: "WHERE ND_OVERLAPS(g1.geometry, g2.geometry) = TRUE" + * Returns boolean, must be used with "= TRUE" or "= FALSE" in DQL. + */ +class NDimensionalOverlaps extends BaseFunction +{ + protected function customizeFunction(): void + { + $this->setFunctionPrototype('(%s &&& %s)'); + $this->addNodeMapping('StringPrimary'); + $this->addNodeMapping('StringPrimary'); + } +} diff --git a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/OverlapsAbove.php b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/OverlapsAbove.php new file mode 100644 index 00000000..9e080ff2 --- /dev/null +++ b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/OverlapsAbove.php @@ -0,0 +1,28 @@ +). + * + * Returns TRUE if A's bounding box overlaps or is above B's. + * + * @see https://postgis.net/docs/reference.html#Operators_Geometry + * @since 3.5 + * + * @author Martin Georgiev + * + * @example Using it in DQL with boolean comparison: "WHERE OVERLAPS_ABOVE(g1.geometry, g2.geometry) = TRUE" + * Returns boolean, must be used with "= TRUE" or "= FALSE" in DQL. + */ +class OverlapsAbove extends BaseFunction +{ + protected function customizeFunction(): void + { + $this->setFunctionPrototype('(%s |&> %s)'); + $this->addNodeMapping('StringPrimary'); + $this->addNodeMapping('StringPrimary'); + } +} diff --git a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/OverlapsBelow.php b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/OverlapsBelow.php new file mode 100644 index 00000000..724072b0 --- /dev/null +++ b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/OverlapsBelow.php @@ -0,0 +1,28 @@ + + * + * @example Using it in DQL with boolean comparison: "WHERE OVERLAPS_BELOW(g1.geometry, g2.geometry) = TRUE" + * Returns boolean, must be used with "= TRUE" or "= FALSE" in DQL. + */ +class OverlapsBelow extends BaseFunction +{ + protected function customizeFunction(): void + { + $this->setFunctionPrototype('(%s &<| %s)'); + $this->addNodeMapping('StringPrimary'); + $this->addNodeMapping('StringPrimary'); + } +} diff --git a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/OverlapsLeft.php b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/OverlapsLeft.php new file mode 100644 index 00000000..24088c27 --- /dev/null +++ b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/OverlapsLeft.php @@ -0,0 +1,28 @@ + + * + * @example Using it in DQL with boolean comparison: "WHERE OVERLAPS_LEFT(g1.geometry, g2.geometry) = TRUE" + * Returns boolean, must be used with "= TRUE" or "= FALSE" in DQL. + */ +class OverlapsLeft extends BaseFunction +{ + protected function customizeFunction(): void + { + $this->setFunctionPrototype('(%s &< %s)'); + $this->addNodeMapping('StringPrimary'); + $this->addNodeMapping('StringPrimary'); + } +} diff --git a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/OverlapsRight.php b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/OverlapsRight.php new file mode 100644 index 00000000..b94b89a9 --- /dev/null +++ b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/OverlapsRight.php @@ -0,0 +1,28 @@ +). + * + * Returns TRUE if A's bounding box overlaps or is to the right of B's. + * + * @see https://postgis.net/docs/reference.html#Operators_Geometry + * @since 3.5 + * + * @author Martin Georgiev + * + * @example Using it in DQL with boolean comparison: "WHERE OVERLAPS_RIGHT(g1.geometry, g2.geometry) = TRUE" + * Returns boolean, must be used with "= TRUE" or "= FALSE" in DQL. + */ +class OverlapsRight extends BaseFunction +{ + protected function customizeFunction(): void + { + $this->setFunctionPrototype('(%s &> %s)'); + $this->addNodeMapping('StringPrimary'); + $this->addNodeMapping('StringPrimary'); + } +} diff --git a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/SpatialContainedBy.php b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/SpatialContainedBy.php new file mode 100644 index 00000000..436aaf54 --- /dev/null +++ b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/SpatialContainedBy.php @@ -0,0 +1,29 @@ + + * + * @example Using it in DQL with boolean comparison: "WHERE SPATIAL_CONTAINED_BY(g1.geometry, g2.geometry) = TRUE" + * Returns boolean, must be used with "= TRUE" or "= FALSE" in DQL. + */ +class SpatialContainedBy extends BaseFunction +{ + protected function customizeFunction(): void + { + $this->setFunctionPrototype('(%s @ %s)'); + $this->addNodeMapping('StringPrimary'); + $this->addNodeMapping('StringPrimary'); + } +} diff --git a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/SpatialContains.php b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/SpatialContains.php new file mode 100644 index 00000000..961386b5 --- /dev/null +++ b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/SpatialContains.php @@ -0,0 +1,29 @@ + + * + * @example Using it in DQL with boolean comparison: "WHERE SPATIAL_CONTAINS(g1.geometry, g2.geometry) = TRUE" + * Returns boolean, must be used with "= TRUE" or "= FALSE" in DQL. + */ +class SpatialContains extends BaseFunction +{ + protected function customizeFunction(): void + { + $this->setFunctionPrototype('(%s ~ %s)'); + $this->addNodeMapping('StringPrimary'); + $this->addNodeMapping('StringPrimary'); + } +} diff --git a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/SpatialSame.php b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/SpatialSame.php new file mode 100644 index 00000000..2fde3202 --- /dev/null +++ b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/SpatialSame.php @@ -0,0 +1,28 @@ + + * + * @example Using it in DQL with boolean comparison: "WHERE SPATIAL_SAME(g1.geometry, g2.geometry) = TRUE" + * Returns boolean, must be used with "= TRUE" or "= FALSE" in DQL. + */ +class SpatialSame extends BaseFunction +{ + protected function customizeFunction(): void + { + $this->setFunctionPrototype('(%s ~= %s)'); + $this->addNodeMapping('StringPrimary'); + $this->addNodeMapping('StringPrimary'); + } +} diff --git a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/StrictlyAbove.php b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/StrictlyAbove.php new file mode 100644 index 00000000..87fe177e --- /dev/null +++ b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/StrictlyAbove.php @@ -0,0 +1,28 @@ +>). + * + * Returns TRUE if A's bounding box is strictly above B's. + * + * @see https://postgis.net/docs/reference.html#Operators_Geometry + * @since 3.5 + * + * @author Martin Georgiev + * + * @example Using it in DQL with boolean comparison: "WHERE STRICTLY_ABOVE(g1.geometry, g2.geometry) = TRUE" + * Returns boolean, must be used with "= TRUE" or "= FALSE" in DQL. + */ +class StrictlyAbove extends BaseFunction +{ + protected function customizeFunction(): void + { + $this->setFunctionPrototype('(%s |>> %s)'); + $this->addNodeMapping('StringPrimary'); + $this->addNodeMapping('StringPrimary'); + } +} diff --git a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/StrictlyBelow.php b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/StrictlyBelow.php new file mode 100644 index 00000000..1a85522e --- /dev/null +++ b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/StrictlyBelow.php @@ -0,0 +1,28 @@ + + * + * @example Using it in DQL with boolean comparison: "WHERE STRICTLY_BELOW(g1.geometry, g2.geometry) = TRUE" + * Returns boolean, must be used with "= TRUE" or "= FALSE" in DQL. + */ +class StrictlyBelow extends BaseFunction +{ + protected function customizeFunction(): void + { + $this->setFunctionPrototype('(%s <<| %s)'); + $this->addNodeMapping('StringPrimary'); + $this->addNodeMapping('StringPrimary'); + } +} diff --git a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/StrictlyLeft.php b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/StrictlyLeft.php new file mode 100644 index 00000000..35172ad7 --- /dev/null +++ b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/StrictlyLeft.php @@ -0,0 +1,28 @@ + + * + * @example Using it in DQL with boolean comparison: "WHERE STRICTLY_LEFT(g1.geometry, g2.geometry) = TRUE" + * Returns boolean, must be used with "= TRUE" or "= FALSE" in DQL. + */ +class StrictlyLeft extends BaseFunction +{ + protected function customizeFunction(): void + { + $this->setFunctionPrototype('(%s << %s)'); + $this->addNodeMapping('StringPrimary'); + $this->addNodeMapping('StringPrimary'); + } +} diff --git a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/StrictlyRight.php b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/StrictlyRight.php new file mode 100644 index 00000000..30a659db --- /dev/null +++ b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/StrictlyRight.php @@ -0,0 +1,28 @@ +>). + * + * Returns TRUE if A's bounding box is strictly to the right of B's. + * + * @see https://postgis.net/docs/reference.html#Operators_Geometry + * @since 3.5 + * + * @author Martin Georgiev + * + * @example Using it in DQL with boolean comparison: "WHERE STRICTLY_RIGHT(g1.geometry, g2.geometry) = TRUE" + * Returns boolean, must be used with "= TRUE" or "= FALSE" in DQL. + */ +class StrictlyRight extends BaseFunction +{ + protected function customizeFunction(): void + { + $this->setFunctionPrototype('(%s >> %s)'); + $this->addNodeMapping('StringPrimary'); + $this->addNodeMapping('StringPrimary'); + } +} diff --git a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/TrajectoryDistance.php b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/TrajectoryDistance.php new file mode 100644 index 00000000..a7e3ab98 --- /dev/null +++ b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/TrajectoryDistance.php @@ -0,0 +1,29 @@ + + * + * @example Using it in DQL: "SELECT TRAJECTORY_DISTANCE(t1.trajectory, t2.trajectory) FROM Entity t1, Entity t2" + * Returns numeric distance value. + */ +class TrajectoryDistance extends BaseFunction +{ + protected function customizeFunction(): void + { + $this->setFunctionPrototype('(%s |=| %s)'); + $this->addNodeMapping('StringPrimary'); + $this->addNodeMapping('StringPrimary'); + } +} diff --git a/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/BoundingBoxDistanceTest.php b/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/BoundingBoxDistanceTest.php new file mode 100644 index 00000000..7744c4e6 --- /dev/null +++ b/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/BoundingBoxDistanceTest.php @@ -0,0 +1,64 @@ + BoundingBoxDistance::class, + ]; + } + + #[Test] + public function returns_zero_when_comparing_identical_point_geometries(): void + { + $dql = "SELECT BOUNDING_BOX_DISTANCE('POINT(1 1)', 'POINT(1 1)') as distance + FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsGeometries g + WHERE g.id = 1"; + + $result = $this->executeDqlQuery($dql); + $this->assertEquals(0, $result[0]['distance']); + } + + #[Test] + public function returns_zero_when_polygon_bounding_boxes_overlap(): void + { + $dql = "SELECT BOUNDING_BOX_DISTANCE('POLYGON((0 0, 2 0, 2 2, 0 2, 0 0))', 'POLYGON((1 1, 3 1, 3 3, 1 3, 1 1))') as distance + FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsGeometries g + WHERE g.id = 1"; + + $result = $this->executeDqlQuery($dql); + $this->assertEquals(0, $result[0]['distance']); + } + + #[Test] + public function can_calculate_euclidean_distance_between_separated_points(): void + { + $dql = "SELECT BOUNDING_BOX_DISTANCE('POINT(0 0)', 'POINT(3 4)') as distance + FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsGeometries g + WHERE g.id = 1"; + + $result = $this->executeDqlQuery($dql); + $this->assertEquals(5.0, $result[0]['distance']); + } + + #[Test] + public function can_calculate_distance_between_non_overlapping_polygons(): void + { + $dql = "SELECT BOUNDING_BOX_DISTANCE('POLYGON((0 0, 1 0, 1 1, 0 1, 0 0))', 'POLYGON((3 3, 4 3, 4 4, 3 4, 3 3))') as distance + FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsGeometries g + WHERE g.id = 1"; + + $result = $this->executeDqlQuery($dql); + $this->assertIsNumeric($result[0]['distance']); + $this->assertGreaterThan(0, $result[0]['distance']); + $this->assertEqualsWithDelta(2.83, $result[0]['distance'], 0.1, 'Distance should be approximately sqrt((3-1)² + (3-1)²) = sqrt(8) ≈ 2.83 between separated polygon bounding boxes'); + } +} diff --git a/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/ExpTest.php b/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/ExpTest.php index 5db8afe9..18fc3891 100644 --- a/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/ExpTest.php +++ b/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/ExpTest.php @@ -17,7 +17,7 @@ protected function getStringFunctions(): array } #[Test] - public function exp(): void + public function can_calculate_exponential_of_one_to_euler_constant(): void { $dql = 'SELECT EXP(1) as result FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsNumerics t WHERE t.id = 1'; $result = $this->executeDqlQuery($dql); @@ -25,7 +25,7 @@ public function exp(): void } #[Test] - public function exp_with_entity_property(): void + public function can_calculate_exponential_of_entity_decimal_value(): void { $dql = 'SELECT EXP(n.decimal1) as result FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsNumerics n WHERE n.id = 1'; $result = $this->executeDqlQuery($dql); diff --git a/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/GeometryDistanceTest.php b/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/GeometryDistanceTest.php new file mode 100644 index 00000000..49006fc8 --- /dev/null +++ b/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/GeometryDistanceTest.php @@ -0,0 +1,54 @@ + GeometryDistance::class, + ]; + } + + #[Test] + public function can_calculate_euclidean_distance_between_geometric_points(): void + { + $dql = 'SELECT GEOMETRY_DISTANCE(g.geometry1, g.geometry2) as distance + FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsGeometries g + WHERE g.id = 1'; + + $result = $this->executeDqlQuery($dql); + $this->assertIsNumeric($result[0]['distance']); + $this->assertGreaterThan(0, $result[0]['distance']); + $this->assertEqualsWithDelta(1.414, $result[0]['distance'], 0.01, 'Euclidean distance between points (0,0) and (1,1) should be sqrt(2) ≈ 1.414'); + } + + #[Test] + public function returns_zero_when_comparing_identical_geometries(): void + { + $dql = 'SELECT GEOMETRY_DISTANCE(g.geometry1, g.geometry1) as distance + FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsGeometries g + WHERE g.id = 1'; + + $result = $this->executeDqlQuery($dql); + $this->assertEquals(0, $result[0]['distance']); + } + + #[Test] + public function can_calculate_spherical_distance_between_geographical_coordinates(): void + { + $dql = 'SELECT GEOMETRY_DISTANCE(g.geography1, g.geography2) as distance + FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsGeometries g + WHERE g.id = 1'; + + $result = $this->executeDqlQuery($dql); + $this->assertIsNumeric($result[0]['distance']); + $this->assertGreaterThan(1000000, $result[0]['distance'], 'Spherical distance between Lisbon and London geographical coordinates should be greater than 1 million meters'); + } +} diff --git a/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/LnTest.php b/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/LnTest.php index ebc9133d..b1d592a5 100644 --- a/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/LnTest.php +++ b/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/LnTest.php @@ -17,7 +17,7 @@ protected function getStringFunctions(): array } #[Test] - public function ln(): void + public function can_calculate_natural_logarithm_of_euler_constant(): void { $dql = 'SELECT LN(2.718281828459) as result FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsNumerics t WHERE t.id = 1'; $result = $this->executeDqlQuery($dql); @@ -25,7 +25,7 @@ public function ln(): void } #[Test] - public function ln_with_entity_property(): void + public function can_calculate_natural_logarithm_of_entity_decimal_value(): void { $dql = 'SELECT LN(n.decimal1) as result FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsNumerics n WHERE n.id = 1'; $result = $this->executeDqlQuery($dql); diff --git a/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/LogTest.php b/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/LogTest.php index e681b1cd..85d4288a 100644 --- a/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/LogTest.php +++ b/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/LogTest.php @@ -17,7 +17,7 @@ protected function getStringFunctions(): array } #[Test] - public function log(): void + public function can_calculate_base_ten_logarithm_of_hundred(): void { $dql = 'SELECT LOG(10, 100) as result FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsNumerics t WHERE t.id = 1'; $result = $this->executeDqlQuery($dql); @@ -25,7 +25,7 @@ public function log(): void } #[Test] - public function log_with_entity_property(): void + public function can_calculate_base_ten_logarithm_of_entity_decimal_value(): void { $dql = 'SELECT LOG(10, n.decimal1) as result FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsNumerics n WHERE n.id = 1'; $result = $this->executeDqlQuery($dql); diff --git a/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/NDimensionalCentroidDistanceTest.php b/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/NDimensionalCentroidDistanceTest.php new file mode 100644 index 00000000..f232fd40 --- /dev/null +++ b/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/NDimensionalCentroidDistanceTest.php @@ -0,0 +1,53 @@ + NDimensionalCentroidDistance::class, + ]; + } + + #[Test] + public function can_calculate_distance_between_geometry_centroids(): void + { + $dql = 'SELECT ND_CENTROID_DISTANCE(g.geometry1, g.geometry2) as distance + FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsGeometries g + WHERE g.id = 1'; + + $result = $this->executeDqlQuery($dql); + $this->assertIsNumeric($result[0]['distance']); + $this->assertGreaterThan(0, $result[0]['distance']); + } + + #[Test] + public function can_calculate_distance_between_geometry_and_literal_point(): void + { + $dql = "SELECT ND_CENTROID_DISTANCE(g.geometry1, 'POINT(3 3)') as distance + FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsGeometries g + WHERE g.id = 1"; + + $result = $this->executeDqlQuery($dql); + $this->assertIsNumeric($result[0]['distance']); + $this->assertGreaterThan(0, $result[0]['distance']); + } + + #[Test] + public function returns_zero_when_comparing_identical_geometries(): void + { + $dql = 'SELECT ND_CENTROID_DISTANCE(g.geometry1, g.geometry1) as distance + FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsGeometries g + WHERE g.id = 1'; + + $result = $this->executeDqlQuery($dql); + $this->assertEquals(0, $result[0]['distance']); + } +} diff --git a/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/NDimensionalOverlapsTest.php b/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/NDimensionalOverlapsTest.php new file mode 100644 index 00000000..13301c4b --- /dev/null +++ b/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/NDimensionalOverlapsTest.php @@ -0,0 +1,40 @@ + NDimensionalOverlaps::class, + ]; + } + + #[Test] + public function returns_true_when_2d_polygons_have_overlapping_bounding_boxes(): void + { + $dql = 'SELECT ND_OVERLAPS(g.geometry1, g.geometry2) as result + FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsGeometries g + WHERE g.id = 2'; + + $result = $this->executeDqlQuery($dql); + $this->assertTrue($result[0]['result']); + } + + #[Test] + public function returns_true_when_comparing_identical_geometries(): void + { + $dql = 'SELECT ND_OVERLAPS(g.geometry1, g.geometry1) as result + FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsGeometries g + WHERE g.id = 1'; + + $result = $this->executeDqlQuery($dql); + $this->assertTrue($result[0]['result']); + } +} diff --git a/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/OverlapsAboveTest.php b/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/OverlapsAboveTest.php new file mode 100644 index 00000000..48ed1adc --- /dev/null +++ b/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/OverlapsAboveTest.php @@ -0,0 +1,51 @@ + OverlapsAbove::class, + ]; + } + + #[Test] + public function returns_false_when_polygons_are_at_same_vertical_level(): void + { + $dql = 'SELECT OVERLAPS_ABOVE(g.geometry1, g.geometry2) as result + FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsGeometries g + WHERE g.id = 2'; + + $result = $this->executeDqlQuery($dql); + $this->assertFalse($result[0]['result']); + } + + #[Test] + public function returns_true_when_geometry_is_positioned_above_literal_point(): void + { + $dql = "SELECT OVERLAPS_ABOVE(g.geometry1, 'POINT(0 -1)') as result + FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsGeometries g + WHERE g.id = 1"; + + $result = $this->executeDqlQuery($dql); + $this->assertTrue($result[0]['result']); + } + + #[Test] + public function returns_true_when_second_polygon_overlaps_or_is_above_first(): void + { + $dql = 'SELECT OVERLAPS_ABOVE(g.geometry2, g.geometry1) as result + FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsGeometries g + WHERE g.id = 2'; + + $result = $this->executeDqlQuery($dql); + $this->assertTrue($result[0]['result']); + } +} diff --git a/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/OverlapsBelowTest.php b/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/OverlapsBelowTest.php new file mode 100644 index 00000000..107ca253 --- /dev/null +++ b/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/OverlapsBelowTest.php @@ -0,0 +1,51 @@ + OverlapsBelow::class, + ]; + } + + #[Test] + public function returns_true_when_first_polygon_overlaps_or_is_below_second(): void + { + $dql = 'SELECT OVERLAPS_BELOW(g.geometry1, g.geometry2) as result + FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsGeometries g + WHERE g.id = 2'; + + $result = $this->executeDqlQuery($dql); + $this->assertTrue($result[0]['result']); + } + + #[Test] + public function returns_true_when_geometry_is_positioned_below_literal_point(): void + { + $dql = "SELECT OVERLAPS_BELOW(g.geometry1, 'POINT(0 2)') as result + FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsGeometries g + WHERE g.id = 1"; + + $result = $this->executeDqlQuery($dql); + $this->assertTrue($result[0]['result']); + } + + #[Test] + public function returns_true_when_comparing_overlapping_polygons(): void + { + $dql = 'SELECT OVERLAPS_BELOW(g.geometry1, g.geometry2) as result + FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsGeometries g + WHERE g.id = 2'; + + $result = $this->executeDqlQuery($dql); + $this->assertTrue($result[0]['result']); + } +} diff --git a/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/OverlapsLeftTest.php b/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/OverlapsLeftTest.php new file mode 100644 index 00000000..9b6f2603 --- /dev/null +++ b/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/OverlapsLeftTest.php @@ -0,0 +1,62 @@ + OverlapsLeft::class, + ]; + } + + #[Test] + public function returns_true_when_first_point_is_positioned_left_of_second(): void + { + $dql = 'SELECT OVERLAPS_LEFT(g.geometry1, g.geometry2) as result + FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsGeometries g + WHERE g.id = 1'; + + $result = $this->executeDqlQuery($dql); + $this->assertTrue($result[0]['result']); + } + + #[Test] + public function returns_false_when_geometry_positions_are_reversed(): void + { + $dql = 'SELECT OVERLAPS_LEFT(g.geometry2, g.geometry1) as result + FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsGeometries g + WHERE g.id = 1'; + + $result = $this->executeDqlQuery($dql); + $this->assertFalse($result[0]['result']); + } + + #[Test] + public function returns_true_when_first_polygon_overlaps_or_is_left_of_second(): void + { + $dql = 'SELECT OVERLAPS_LEFT(g.geometry1, g.geometry2) as result + FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsGeometries g + WHERE g.id = 2'; + + $result = $this->executeDqlQuery($dql); + $this->assertTrue($result[0]['result']); + } + + #[Test] + public function returns_true_when_comparing_identical_geometries(): void + { + $dql = 'SELECT OVERLAPS_LEFT(g.geometry1, g.geometry1) as result + FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsGeometries g + WHERE g.id = 1'; + + $result = $this->executeDqlQuery($dql); + $this->assertTrue($result[0]['result']); + } +} diff --git a/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/OverlapsRightTest.php b/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/OverlapsRightTest.php new file mode 100644 index 00000000..c5c4dbe0 --- /dev/null +++ b/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/OverlapsRightTest.php @@ -0,0 +1,62 @@ + OverlapsRight::class, + ]; + } + + #[Test] + public function returns_false_when_left_geometry_is_to_the_left(): void + { + $dql = 'SELECT OVERLAPS_RIGHT(g.geometry1, g.geometry2) as result + FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsGeometries g + WHERE g.id = 1'; + + $result = $this->executeDqlQuery($dql); + $this->assertFalse($result[0]['result']); + } + + #[Test] + public function returns_true_when_second_point_is_positioned_right_of_first(): void + { + $dql = 'SELECT OVERLAPS_RIGHT(g.geometry2, g.geometry1) as result + FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsGeometries g + WHERE g.id = 1'; + + $result = $this->executeDqlQuery($dql); + $this->assertTrue($result[0]['result']); + } + + #[Test] + public function returns_false_when_polygons_overlap_without_right_positioning(): void + { + $dql = 'SELECT OVERLAPS_RIGHT(g.geometry1, g.geometry2) as result + FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsGeometries g + WHERE g.id = 2'; + + $result = $this->executeDqlQuery($dql); + $this->assertFalse($result[0]['result']); + } + + #[Test] + public function returns_true_when_comparing_identical_geometries(): void + { + $dql = 'SELECT OVERLAPS_RIGHT(g.geometry1, g.geometry1) as result + FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsGeometries g + WHERE g.id = 1'; + + $result = $this->executeDqlQuery($dql); + $this->assertTrue($result[0]['result']); + } +} diff --git a/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/SpatialContainedByTest.php b/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/SpatialContainedByTest.php new file mode 100644 index 00000000..07deed42 --- /dev/null +++ b/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/SpatialContainedByTest.php @@ -0,0 +1,51 @@ + SpatialContainedBy::class, + ]; + } + + #[Test] + public function returns_false_when_first_polygon_is_not_contained_by_overlapping_second(): void + { + $dql = 'SELECT SPATIAL_CONTAINED_BY(g.geometry1, g.geometry2) as result + FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsGeometries g + WHERE g.id = 2'; + + $result = $this->executeDqlQuery($dql); + $this->assertFalse($result[0]['result']); + } + + #[Test] + public function returns_false_when_comparing_separate_point_geometries(): void + { + $dql = 'SELECT SPATIAL_CONTAINED_BY(g.geometry1, g.geometry2) as result + FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsGeometries g + WHERE g.id = 1'; + + $result = $this->executeDqlQuery($dql); + $this->assertFalse($result[0]['result']); + } + + #[Test] + public function returns_true_when_comparing_identical_geometries(): void + { + $dql = 'SELECT SPATIAL_CONTAINED_BY(g.geometry1, g.geometry1) as result + FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsGeometries g + WHERE g.id = 1'; + + $result = $this->executeDqlQuery($dql); + $this->assertTrue($result[0]['result']); + } +} diff --git a/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/SpatialContainsTest.php b/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/SpatialContainsTest.php new file mode 100644 index 00000000..f4f209e0 --- /dev/null +++ b/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/SpatialContainsTest.php @@ -0,0 +1,51 @@ + SpatialContains::class, + ]; + } + + #[Test] + public function returns_false_when_comparing_separate_point_geometries(): void + { + $dql = 'SELECT SPATIAL_CONTAINS(g.geometry1, g.geometry2) as result + FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsGeometries g + WHERE g.id = 1'; + + $result = $this->executeDqlQuery($dql); + $this->assertFalse($result[0]['result']); + } + + #[Test] + public function returns_false_when_first_polygon_does_not_fully_contain_second(): void + { + $dql = 'SELECT SPATIAL_CONTAINS(g.geometry1, g.geometry2) as result + FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsGeometries g + WHERE g.id = 2'; + + $result = $this->executeDqlQuery($dql); + $this->assertFalse($result[0]['result']); + } + + #[Test] + public function returns_true_when_comparing_identical_geometries(): void + { + $dql = 'SELECT SPATIAL_CONTAINS(g.geometry1, g.geometry1) as result + FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsGeometries g + WHERE g.id = 1'; + + $result = $this->executeDqlQuery($dql); + $this->assertTrue($result[0]['result']); + } +} diff --git a/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/SpatialOperatorTestCase.php b/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/SpatialOperatorTestCase.php new file mode 100644 index 00000000..9a00a0d4 --- /dev/null +++ b/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/SpatialOperatorTestCase.php @@ -0,0 +1,80 @@ +createTestTableForSpatialFixture(); + $this->insertTestDataForSpatialFixture(); + } + + protected function createTestTableForSpatialFixture(): void + { + $tableName = 'containsgeometries'; + + $this->createTestSchema(); + $this->dropTestTableIfItExists($tableName); + + $fullTableName = \sprintf('%s.%s', self::DATABASE_SCHEMA, $tableName); + $sql = \sprintf(' + CREATE TABLE %s ( + id SERIAL PRIMARY KEY, + geometry1 GEOMETRY, + geometry2 GEOMETRY, + geography1 GEOGRAPHY, + geography2 GEOGRAPHY + ) + ', $fullTableName); + + $this->connection->executeStatement($sql); + } + + protected function insertTestDataForSpatialFixture(): void + { + $tableName = 'containsgeometries'; + $fullTableName = \sprintf('%s.%s', self::DATABASE_SCHEMA, $tableName); + + // Insert test data with various spatial relationships + $testData = [ + [ + 'geometry1' => 'POINT(0 0)', + 'geometry2' => 'POINT(1 1)', + 'geography1' => 'SRID=4326;POINT(-9.1393 38.7223)', + 'geography2' => 'SRID=4326;POINT(-0.1276 51.5074)', + ], + [ + 'geometry1' => 'POLYGON((0 0, 0 2, 2 2, 2 0, 0 0))', + 'geometry2' => 'POLYGON((1 1, 1 3, 3 3, 3 1, 1 1))', + 'geography1' => 'SRID=4326;POLYGON((-9.2 38.6, -9.2 38.8, -9.0 38.8, -9.0 38.6, -9.2 38.6))', // Lisbon area + 'geography2' => 'SRID=4326;POLYGON((-0.2 51.4, -0.2 51.6, 0.0 51.6, 0.0 51.4, -0.2 51.4))', // London area + ], + [ + 'geometry1' => 'LINESTRING(0 0, 1 1, 2 2)', + 'geometry2' => 'LINESTRING(3 3, 4 4, 5 5)', + 'geography1' => 'SRID=4326;LINESTRING(-9.1393 38.7223, -9.1293 38.7323)', // Lisbon area + 'geography2' => 'SRID=4326;LINESTRING(-0.1276 51.5074, -0.1176 51.5174)', // London area + ], + ]; + + foreach ($testData as $row) { + $sql = \sprintf(' + INSERT INTO %s (geometry1, geometry2, geography1, geography2) + VALUES (ST_GeomFromText(?), ST_GeomFromText(?), ST_GeogFromText(?), ST_GeogFromText(?)) + ', $fullTableName); + + $this->connection->executeStatement($sql, [ + $row['geometry1'], + $row['geometry2'], + $row['geography1'], + $row['geography2'], + ]); + } + } +} diff --git a/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/SpatialSameTest.php b/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/SpatialSameTest.php new file mode 100644 index 00000000..7380fdd9 --- /dev/null +++ b/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/SpatialSameTest.php @@ -0,0 +1,51 @@ + SpatialSame::class, + ]; + } + + #[Test] + public function returns_true_when_comparing_identical_geometries(): void + { + $dql = 'SELECT SPATIAL_SAME(g.geometry1, g.geometry1) as result + FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsGeometries g + WHERE g.id = 1'; + + $result = $this->executeDqlQuery($dql); + $this->assertTrue($result[0]['result']); + } + + #[Test] + public function returns_false_when_comparing_different_point_geometries(): void + { + $dql = 'SELECT SPATIAL_SAME(g.geometry1, g.geometry2) as result + FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsGeometries g + WHERE g.id = 1'; + + $result = $this->executeDqlQuery($dql); + $this->assertFalse($result[0]['result']); + } + + #[Test] + public function returns_false_when_comparing_overlapping_polygons(): void + { + $dql = 'SELECT SPATIAL_SAME(g.geometry1, g.geometry2) as result + FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsGeometries g + WHERE g.id = 2'; + + $result = $this->executeDqlQuery($dql); + $this->assertFalse($result[0]['result']); + } +} diff --git a/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/StrictlyAboveTest.php b/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/StrictlyAboveTest.php new file mode 100644 index 00000000..9c444971 --- /dev/null +++ b/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/StrictlyAboveTest.php @@ -0,0 +1,66 @@ + StrictlyAbove::class, + ]; + } + + #[Test] + public function strictly_above_returns_false_with_overlapping_polygons(): void + { + // Overlapping polygons are not strictly above each other + $dql = 'SELECT STRICTLY_ABOVE(g.geometry1, g.geometry2) as result + FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsGeometries g + WHERE g.id = 2'; + + $result = $this->executeDqlQuery($dql); + $this->assertFalse($result[0]['result'], 'Overlapping polygons should not be strictly above each other'); + } + + #[Test] + public function strictly_above_returns_false_with_points_at_same_level(): void + { + // POINT(0 0) is not strictly above POINT(1 1) - they are at similar Y coordinates + $dql = 'SELECT STRICTLY_ABOVE(g.geometry1, g.geometry2) as result + FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsGeometries g + WHERE g.id = 1'; + + $result = $this->executeDqlQuery($dql); + $this->assertFalse($result[0]['result'], 'POINT(0 0) should not be strictly above POINT(1 1)'); + } + + #[Test] + public function strictly_above_returns_true_when_geometry_is_higher(): void + { + // Test with linestrings where second has higher Y coordinates + $dql = 'SELECT STRICTLY_ABOVE(g.geometry2, g.geometry1) as result + FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsGeometries g + WHERE g.id = 3'; + + $result = $this->executeDqlQuery($dql); + $this->assertTrue($result[0]['result'], 'Higher linestring should be strictly above lower linestring'); + } + + #[Test] + public function strictly_above_returns_false_with_identical_geometries(): void + { + // Identical geometries are not strictly above each other + $dql = 'SELECT STRICTLY_ABOVE(g.geometry1, g.geometry1) as result + FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsGeometries g + WHERE g.id = 1'; + + $result = $this->executeDqlQuery($dql); + $this->assertFalse($result[0]['result'], 'Identical geometries should not be strictly above each other'); + } +} diff --git a/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/StrictlyBelowTest.php b/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/StrictlyBelowTest.php new file mode 100644 index 00000000..6ba9abd0 --- /dev/null +++ b/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/StrictlyBelowTest.php @@ -0,0 +1,66 @@ + StrictlyBelow::class, + ]; + } + + #[Test] + public function strictly_below_returns_false_with_overlapping_polygons(): void + { + // Overlapping polygons are not strictly below each other + $dql = 'SELECT STRICTLY_BELOW(g.geometry1, g.geometry2) as result + FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsGeometries g + WHERE g.id = 2'; + + $result = $this->executeDqlQuery($dql); + $this->assertFalse($result[0]['result'], 'Overlapping polygons should not be strictly below each other'); + } + + #[Test] + public function strictly_below_returns_true_when_geometry_is_lower(): void + { + // POINT(0 0) should be strictly below POINT(1 1) + $dql = 'SELECT STRICTLY_BELOW(g.geometry1, g.geometry2) as result + FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsGeometries g + WHERE g.id = 1'; + + $result = $this->executeDqlQuery($dql); + $this->assertTrue($result[0]['result'], 'POINT(0 0) should be strictly below POINT(1 1)'); + } + + #[Test] + public function strictly_below_returns_true_with_linestrings(): void + { + // First linestring should be strictly below second linestring + $dql = 'SELECT STRICTLY_BELOW(g.geometry1, g.geometry2) as result + FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsGeometries g + WHERE g.id = 3'; + + $result = $this->executeDqlQuery($dql); + $this->assertTrue($result[0]['result'], 'Lower linestring should be strictly below higher linestring'); + } + + #[Test] + public function strictly_below_returns_false_with_identical_geometries(): void + { + // Identical geometries are not strictly below each other + $dql = 'SELECT STRICTLY_BELOW(g.geometry1, g.geometry1) as result + FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsGeometries g + WHERE g.id = 1'; + + $result = $this->executeDqlQuery($dql); + $this->assertFalse($result[0]['result'], 'Identical geometries should not be strictly below each other'); + } +} diff --git a/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/StrictlyLeftTest.php b/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/StrictlyLeftTest.php new file mode 100644 index 00000000..51b1dc58 --- /dev/null +++ b/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/StrictlyLeftTest.php @@ -0,0 +1,62 @@ + StrictlyLeft::class, + ]; + } + + #[Test] + public function returns_true_when_left_geometry_is_strictly_to_the_left(): void + { + $dql = 'SELECT STRICTLY_LEFT(g.geometry1, g.geometry2) as result + FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsGeometries g + WHERE g.id = 1'; + + $result = $this->executeDqlQuery($dql); + $this->assertTrue($result[0]['result']); + } + + #[Test] + public function returns_false_when_geometries_overlap(): void + { + $dql = 'SELECT STRICTLY_LEFT(g.geometry1, g.geometry2) as result + FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsGeometries g + WHERE g.id = 2'; + + $result = $this->executeDqlQuery($dql); + $this->assertFalse($result[0]['result']); + } + + #[Test] + public function returns_false_when_left_geometry_is_to_the_right(): void + { + $dql = 'SELECT STRICTLY_LEFT(g.geometry2, g.geometry1) as result + FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsGeometries g + WHERE g.id = 1'; + + $result = $this->executeDqlQuery($dql); + $this->assertFalse($result[0]['result']); + } + + #[Test] + public function returns_false_with_identical_geometries(): void + { + $dql = 'SELECT STRICTLY_LEFT(g.geometry1, g.geometry1) as result + FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsGeometries g + WHERE g.id = 1'; + + $result = $this->executeDqlQuery($dql); + $this->assertFalse($result[0]['result']); + } +} diff --git a/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/StrictlyRightTest.php b/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/StrictlyRightTest.php new file mode 100644 index 00000000..49e8270c --- /dev/null +++ b/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/StrictlyRightTest.php @@ -0,0 +1,51 @@ + StrictlyRight::class, + ]; + } + + #[Test] + public function returns_false_when_first_point_is_not_strictly_to_the_right(): void + { + $dql = 'SELECT STRICTLY_RIGHT(g.geometry1, g.geometry2) as result + FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsGeometries g + WHERE g.id = 1'; + + $result = $this->executeDqlQuery($dql); + $this->assertFalse($result[0]['result']); + } + + #[Test] + public function returns_true_when_geometry_is_positioned_right_of_literal_point(): void + { + $dql = "SELECT STRICTLY_RIGHT(g.geometry1, 'POINT(-5 -5)') as result + FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsGeometries g + WHERE g.id = 1"; + + $result = $this->executeDqlQuery($dql); + $this->assertTrue($result[0]['result']); + } + + #[Test] + public function returns_true_when_higher_linestring_is_strictly_to_the_right_of_lower_onetrajetoryds(): void + { + $dql = 'SELECT STRICTLY_RIGHT(g.geometry2, g.geometry1) as result + FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsGeometries g + WHERE g.id = 3'; + + $result = $this->executeDqlQuery($dql); + $this->assertTrue($result[0]['result']); + } +} diff --git a/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/TrajectoryDistanceTest.php b/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/TrajectoryDistanceTest.php new file mode 100644 index 00000000..a405f3c0 --- /dev/null +++ b/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/TrajectoryDistanceTest.php @@ -0,0 +1,53 @@ + TrajectoryDistance::class, + ]; + } + + #[Test] + public function can_calculate_distance_between_measured_linestring_trajectories(): void + { + $dql = "SELECT TRAJECTORY_DISTANCE('LINESTRING M(0 0 1, 1 1 2)', 'LINESTRING M(2 2 1, 3 3 2)') as distance + FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsGeometries g + WHERE g.id = 1"; + + $result = $this->executeDqlQuery($dql); + $this->assertIsNumeric($result[0]['distance']); + $this->assertGreaterThanOrEqual(0, $result[0]['distance']); + } + + #[Test] + public function returns_zero_when_comparing_identical_trajectories(): void + { + $dql = "SELECT TRAJECTORY_DISTANCE('LINESTRING M(0 0 1, 1 1 2)', 'LINESTRING M(0 0 1, 1 1 2)') as distance + FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsGeometries g + WHERE g.id = 1"; + + $result = $this->executeDqlQuery($dql); + $this->assertEquals(0, $result[0]['distance']); + } + + #[Test] + public function can_calculate_distance_between_complex_measured_trajectories(): void + { + $dql = "SELECT TRAJECTORY_DISTANCE('LINESTRING M(0 0 0, 5 5 10)', 'LINESTRING M(1 1 0, 6 6 10)') as distance + FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsGeometries g + WHERE g.id = 1"; + + $result = $this->executeDqlQuery($dql); + $this->assertIsNumeric($result[0]['distance']); + $this->assertGreaterThanOrEqual(0, $result[0]['distance']); + } +} diff --git a/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/BoundingBoxDistanceTest.php b/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/BoundingBoxDistanceTest.php new file mode 100644 index 00000000..01241195 --- /dev/null +++ b/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/BoundingBoxDistanceTest.php @@ -0,0 +1,32 @@ + BoundingBoxDistance::class, + ]; + } + + protected function getExpectedSqlStatements(): array + { + return [ + 'calculates bounding box distance between geometries' => 'SELECT (c0_.geometry1 <#> c0_.geometry2) AS sclr_0 FROM ContainsGeometries c0_', + ]; + } + + protected function getDqlStatements(): array + { + return [ + 'calculates bounding box distance between geometries' => \sprintf('SELECT BOUNDING_BOX_DISTANCE(e.geometry1, e.geometry2) FROM %s e', ContainsGeometries::class), + ]; + } +} diff --git a/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/ContainsTest.php b/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/ContainsTest.php index 99de16a5..de5fed01 100644 --- a/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/ContainsTest.php +++ b/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/ContainsTest.php @@ -5,6 +5,7 @@ namespace Tests\Unit\MartinGeorgiev\Doctrine\ORM\Query\AST\Functions; use Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsArrays; +use Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsGeometries; use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Contains; class ContainsTest extends TestCase @@ -23,6 +24,8 @@ protected function getExpectedSqlStatements(): array 'contains array of strings' => "SELECT (c0_.textArray @> '{\"foo\",\"bar\"}') AS sclr_0 FROM ContainsArrays c0_", 'contains single element' => "SELECT (c0_.textArray @> '{42}') AS sclr_0 FROM ContainsArrays c0_", 'contains using parameter' => 'SELECT (c0_.textArray @> ?) AS sclr_0 FROM ContainsArrays c0_', + 'contains with spatial geometries' => 'SELECT (c0_.geometry1 @> c0_.geometry2) AS sclr_0 FROM ContainsGeometries c0_', + 'contains with spatial literal' => "SELECT (c0_.geometry1 @> 'POINT(1 2)') AS sclr_0 FROM ContainsGeometries c0_", ]; } @@ -33,6 +36,8 @@ protected function getDqlStatements(): array 'contains array of strings' => \sprintf("SELECT CONTAINS(e.textArray, '{\"foo\",\"bar\"}') FROM %s e", ContainsArrays::class), 'contains single element' => \sprintf("SELECT CONTAINS(e.textArray, '{42}') FROM %s e", ContainsArrays::class), 'contains using parameter' => \sprintf('SELECT CONTAINS(e.textArray, :parameter) FROM %s e', ContainsArrays::class), + 'contains with spatial geometries' => \sprintf('SELECT CONTAINS(e.geometry1, e.geometry2) FROM %s e', ContainsGeometries::class), + 'contains with spatial literal' => \sprintf("SELECT CONTAINS(e.geometry1, 'POINT(1 2)') FROM %s e", ContainsGeometries::class), ]; } } diff --git a/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/GeometryDistanceTest.php b/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/GeometryDistanceTest.php new file mode 100644 index 00000000..fbb6ac51 --- /dev/null +++ b/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/GeometryDistanceTest.php @@ -0,0 +1,34 @@ + GeometryDistance::class, + ]; + } + + protected function getExpectedSqlStatements(): array + { + return [ + 'calculates distance between geometries' => 'SELECT (c0_.geometry1 <-> c0_.geometry2) AS sclr_0 FROM ContainsGeometries c0_', + 'calculates distance between geometry and literal' => "SELECT (c0_.geometry1 <-> 'POINT(1 2)') AS sclr_0 FROM ContainsGeometries c0_", + ]; + } + + protected function getDqlStatements(): array + { + return [ + 'calculates distance between geometries' => \sprintf('SELECT GEOMETRY_DISTANCE(e.geometry1, e.geometry2) FROM %s e', ContainsGeometries::class), + 'calculates distance between geometry and literal' => \sprintf("SELECT GEOMETRY_DISTANCE(e.geometry1, 'POINT(1 2)') FROM %s e", ContainsGeometries::class), + ]; + } +} diff --git a/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/IsContainedByTest.php b/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/IsContainedByTest.php index 909084c8..a5bf2ce8 100644 --- a/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/IsContainedByTest.php +++ b/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/IsContainedByTest.php @@ -5,6 +5,7 @@ namespace Tests\Unit\MartinGeorgiev\Doctrine\ORM\Query\AST\Functions; use Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsArrays; +use Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsGeometries; use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\IsContainedBy; class IsContainedByTest extends TestCase @@ -20,6 +21,8 @@ protected function getExpectedSqlStatements(): array { return [ 'checks if left array is contained by right array' => "SELECT (c0_.textArray <@ '{681,1185,1878}') AS sclr_0 FROM ContainsArrays c0_", + 'checks if geometry is contained by another' => 'SELECT (c0_.geometry1 <@ c0_.geometry2) AS sclr_0 FROM ContainsGeometries c0_', + 'checks if geometry is contained by literal' => "SELECT (c0_.geometry1 <@ 'POLYGON((0 0, 2 2, 4 4, 0 0))') AS sclr_0 FROM ContainsGeometries c0_", ]; } @@ -27,6 +30,8 @@ protected function getDqlStatements(): array { return [ 'checks if left array is contained by right array' => \sprintf("SELECT IS_CONTAINED_BY(e.textArray, '{681,1185,1878}') FROM %s e", ContainsArrays::class), + 'checks if geometry is contained by another' => \sprintf('SELECT IS_CONTAINED_BY(e.geometry1, e.geometry2) FROM %s e', ContainsGeometries::class), + 'checks if geometry is contained by literal' => \sprintf("SELECT IS_CONTAINED_BY(e.geometry1, 'POLYGON((0 0, 2 2, 4 4, 0 0))') FROM %s e", ContainsGeometries::class), ]; } } diff --git a/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/NDimensionalBoundingBoxDistanceTest.php b/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/NDimensionalBoundingBoxDistanceTest.php new file mode 100644 index 00000000..5ed20bc3 --- /dev/null +++ b/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/NDimensionalBoundingBoxDistanceTest.php @@ -0,0 +1,32 @@ + NDimensionalBoundingBoxDistance::class, + ]; + } + + protected function getExpectedSqlStatements(): array + { + return [ + 'calculates n-dimensional bounding box distance' => 'SELECT (c0_.geometry1 <<#>> c0_.geometry2) AS sclr_0 FROM ContainsGeometries c0_', + ]; + } + + protected function getDqlStatements(): array + { + return [ + 'calculates n-dimensional bounding box distance' => \sprintf('SELECT ND_BOUNDING_BOX_DISTANCE(e.geometry1, e.geometry2) FROM %s e', ContainsGeometries::class), + ]; + } +} diff --git a/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/NDimensionalCentroidDistanceTest.php b/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/NDimensionalCentroidDistanceTest.php new file mode 100644 index 00000000..b9082fcd --- /dev/null +++ b/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/NDimensionalCentroidDistanceTest.php @@ -0,0 +1,32 @@ + NDimensionalCentroidDistance::class, + ]; + } + + protected function getExpectedSqlStatements(): array + { + return [ + 'calculates n-dimensional centroid distance' => 'SELECT (c0_.geometry1 <<->> c0_.geometry2) AS sclr_0 FROM ContainsGeometries c0_', + ]; + } + + protected function getDqlStatements(): array + { + return [ + 'calculates n-dimensional centroid distance' => \sprintf('SELECT ND_CENTROID_DISTANCE(e.geometry1, e.geometry2) FROM %s e', ContainsGeometries::class), + ]; + } +} diff --git a/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/NDimensionalOverlapsTest.php b/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/NDimensionalOverlapsTest.php new file mode 100644 index 00000000..7bfedcfb --- /dev/null +++ b/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/NDimensionalOverlapsTest.php @@ -0,0 +1,34 @@ + NDimensionalOverlaps::class, + ]; + } + + protected function getExpectedSqlStatements(): array + { + return [ + 'checks if geometries overlap in n-dimensions' => 'SELECT (c0_.geometry1 &&& c0_.geometry2) AS sclr_0 FROM ContainsGeometries c0_', + 'checks if geometry overlaps literal in n-dimensions' => "SELECT (c0_.geometry1 &&& 'POINT Z(1 2 3)') AS sclr_0 FROM ContainsGeometries c0_", + ]; + } + + protected function getDqlStatements(): array + { + return [ + 'checks if geometries overlap in n-dimensions' => \sprintf('SELECT ND_OVERLAPS(e.geometry1, e.geometry2) FROM %s e', ContainsGeometries::class), + 'checks if geometry overlaps literal in n-dimensions' => \sprintf("SELECT ND_OVERLAPS(e.geometry1, 'POINT Z(1 2 3)') FROM %s e", ContainsGeometries::class), + ]; + } +} diff --git a/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/OverlapsAboveTest.php b/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/OverlapsAboveTest.php new file mode 100644 index 00000000..205fdc42 --- /dev/null +++ b/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/OverlapsAboveTest.php @@ -0,0 +1,32 @@ + OverlapsAbove::class, + ]; + } + + protected function getExpectedSqlStatements(): array + { + return [ + 'checks if geometry overlaps or is above' => 'SELECT (c0_.geometry1 |&> c0_.geometry2) AS sclr_0 FROM ContainsGeometries c0_', + ]; + } + + protected function getDqlStatements(): array + { + return [ + 'checks if geometry overlaps or is above' => \sprintf('SELECT OVERLAPS_ABOVE(e.geometry1, e.geometry2) FROM %s e', ContainsGeometries::class), + ]; + } +} diff --git a/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/OverlapsBelowTest.php b/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/OverlapsBelowTest.php new file mode 100644 index 00000000..ca84001a --- /dev/null +++ b/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/OverlapsBelowTest.php @@ -0,0 +1,32 @@ + OverlapsBelow::class, + ]; + } + + protected function getExpectedSqlStatements(): array + { + return [ + 'checks if geometry overlaps or is below' => 'SELECT (c0_.geometry1 &<| c0_.geometry2) AS sclr_0 FROM ContainsGeometries c0_', + ]; + } + + protected function getDqlStatements(): array + { + return [ + 'checks if geometry overlaps or is below' => \sprintf('SELECT OVERLAPS_BELOW(e.geometry1, e.geometry2) FROM %s e', ContainsGeometries::class), + ]; + } +} diff --git a/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/OverlapsLeftTest.php b/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/OverlapsLeftTest.php new file mode 100644 index 00000000..73a927c4 --- /dev/null +++ b/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/OverlapsLeftTest.php @@ -0,0 +1,34 @@ + OverlapsLeft::class, + ]; + } + + protected function getExpectedSqlStatements(): array + { + return [ + 'checks if geometry overlaps or is to the left' => 'SELECT (c0_.geometry1 &< c0_.geometry2) AS sclr_0 FROM ContainsGeometries c0_', + 'checks if geometry overlaps or is to the left of literal' => "SELECT (c0_.geometry1 &< 'POINT(1 2)') AS sclr_0 FROM ContainsGeometries c0_", + ]; + } + + protected function getDqlStatements(): array + { + return [ + 'checks if geometry overlaps or is to the left' => \sprintf('SELECT OVERLAPS_LEFT(e.geometry1, e.geometry2) FROM %s e', ContainsGeometries::class), + 'checks if geometry overlaps or is to the left of literal' => \sprintf("SELECT OVERLAPS_LEFT(e.geometry1, 'POINT(1 2)') FROM %s e", ContainsGeometries::class), + ]; + } +} diff --git a/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/OverlapsRightTest.php b/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/OverlapsRightTest.php new file mode 100644 index 00000000..dd0912bd --- /dev/null +++ b/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/OverlapsRightTest.php @@ -0,0 +1,32 @@ + OverlapsRight::class, + ]; + } + + protected function getExpectedSqlStatements(): array + { + return [ + 'checks if geometry overlaps or is to the right' => 'SELECT (c0_.geometry1 &> c0_.geometry2) AS sclr_0 FROM ContainsGeometries c0_', + ]; + } + + protected function getDqlStatements(): array + { + return [ + 'checks if geometry overlaps or is to the right' => \sprintf('SELECT OVERLAPS_RIGHT(e.geometry1, e.geometry2) FROM %s e', ContainsGeometries::class), + ]; + } +} diff --git a/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/OverlapsTest.php b/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/OverlapsTest.php index b0af3a93..ffd77743 100644 --- a/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/OverlapsTest.php +++ b/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/OverlapsTest.php @@ -5,6 +5,7 @@ namespace Tests\Unit\MartinGeorgiev\Doctrine\ORM\Query\AST\Functions; use Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsArrays; +use Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsGeometries; use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Overlaps; class OverlapsTest extends TestCase @@ -20,6 +21,8 @@ protected function getExpectedSqlStatements(): array { return [ 'checks if arrays have overlapping elements' => "SELECT (c0_.textArray && '{681,1185,1878}') AS sclr_0 FROM ContainsArrays c0_", + 'checks if geometries overlap' => 'SELECT (c0_.geometry1 && c0_.geometry2) AS sclr_0 FROM ContainsGeometries c0_', + 'checks if geometry overlaps literal' => "SELECT (c0_.geometry1 && 'POLYGON((0 0, 1 1, 2 2, 0 0))') AS sclr_0 FROM ContainsGeometries c0_", ]; } @@ -27,6 +30,8 @@ protected function getDqlStatements(): array { return [ 'checks if arrays have overlapping elements' => \sprintf("SELECT OVERLAPS(e.textArray, '{681,1185,1878}') FROM %s e", ContainsArrays::class), + 'checks if geometries overlap' => \sprintf('SELECT OVERLAPS(e.geometry1, e.geometry2) FROM %s e', ContainsGeometries::class), + 'checks if geometry overlaps literal' => \sprintf("SELECT OVERLAPS(e.geometry1, 'POLYGON((0 0, 1 1, 2 2, 0 0))') FROM %s e", ContainsGeometries::class), ]; } } diff --git a/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/SpatialContainedByTest.php b/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/SpatialContainedByTest.php new file mode 100644 index 00000000..2db82dac --- /dev/null +++ b/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/SpatialContainedByTest.php @@ -0,0 +1,32 @@ + SpatialContainedBy::class, + ]; + } + + protected function getExpectedSqlStatements(): array + { + return [ + 'checks if geometry is spatially contained by another' => 'SELECT (c0_.geometry1 @ c0_.geometry2) AS sclr_0 FROM ContainsGeometries c0_', + ]; + } + + protected function getDqlStatements(): array + { + return [ + 'checks if geometry is spatially contained by another' => \sprintf('SELECT SPATIAL_CONTAINED_BY(e.geometry1, e.geometry2) FROM %s e', ContainsGeometries::class), + ]; + } +} diff --git a/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/SpatialContainsTest.php b/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/SpatialContainsTest.php new file mode 100644 index 00000000..111067c7 --- /dev/null +++ b/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/SpatialContainsTest.php @@ -0,0 +1,34 @@ + SpatialContains::class, + ]; + } + + protected function getExpectedSqlStatements(): array + { + return [ + 'checks if geometry spatially contains another' => 'SELECT (c0_.geometry1 ~ c0_.geometry2) AS sclr_0 FROM ContainsGeometries c0_', + 'checks if geometry spatially contains literal' => "SELECT (c0_.geometry1 ~ 'POINT(1 2)') AS sclr_0 FROM ContainsGeometries c0_", + ]; + } + + protected function getDqlStatements(): array + { + return [ + 'checks if geometry spatially contains another' => \sprintf('SELECT SPATIAL_CONTAINS(e.geometry1, e.geometry2) FROM %s e', ContainsGeometries::class), + 'checks if geometry spatially contains literal' => \sprintf("SELECT SPATIAL_CONTAINS(e.geometry1, 'POINT(1 2)') FROM %s e", ContainsGeometries::class), + ]; + } +} diff --git a/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/SpatialSameTest.php b/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/SpatialSameTest.php new file mode 100644 index 00000000..70d2b1bc --- /dev/null +++ b/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/SpatialSameTest.php @@ -0,0 +1,32 @@ + SpatialSame::class, + ]; + } + + protected function getExpectedSqlStatements(): array + { + return [ + 'checks if geometries have same bounding box' => 'SELECT (c0_.geometry1 ~= c0_.geometry2) AS sclr_0 FROM ContainsGeometries c0_', + ]; + } + + protected function getDqlStatements(): array + { + return [ + 'checks if geometries have same bounding box' => \sprintf('SELECT SPATIAL_SAME(e.geometry1, e.geometry2) FROM %s e', ContainsGeometries::class), + ]; + } +} diff --git a/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/StrictlyAboveTest.php b/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/StrictlyAboveTest.php new file mode 100644 index 00000000..4c1166ce --- /dev/null +++ b/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/StrictlyAboveTest.php @@ -0,0 +1,32 @@ + StrictlyAbove::class, + ]; + } + + protected function getExpectedSqlStatements(): array + { + return [ + 'checks if geometry is strictly above' => 'SELECT (c0_.geometry1 |>> c0_.geometry2) AS sclr_0 FROM ContainsGeometries c0_', + ]; + } + + protected function getDqlStatements(): array + { + return [ + 'checks if geometry is strictly above' => \sprintf('SELECT STRICTLY_ABOVE(e.geometry1, e.geometry2) FROM %s e', ContainsGeometries::class), + ]; + } +} diff --git a/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/StrictlyBelowTest.php b/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/StrictlyBelowTest.php new file mode 100644 index 00000000..5edbd7ff --- /dev/null +++ b/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/StrictlyBelowTest.php @@ -0,0 +1,32 @@ + StrictlyBelow::class, + ]; + } + + protected function getExpectedSqlStatements(): array + { + return [ + 'checks if geometry is strictly below' => 'SELECT (c0_.geometry1 <<| c0_.geometry2) AS sclr_0 FROM ContainsGeometries c0_', + ]; + } + + protected function getDqlStatements(): array + { + return [ + 'checks if geometry is strictly below' => \sprintf('SELECT STRICTLY_BELOW(e.geometry1, e.geometry2) FROM %s e', ContainsGeometries::class), + ]; + } +} diff --git a/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/StrictlyLeftTest.php b/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/StrictlyLeftTest.php new file mode 100644 index 00000000..0503b863 --- /dev/null +++ b/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/StrictlyLeftTest.php @@ -0,0 +1,34 @@ + StrictlyLeft::class, + ]; + } + + protected function getExpectedSqlStatements(): array + { + return [ + 'checks if geometry is strictly to the left' => 'SELECT (c0_.geometry1 << c0_.geometry2) AS sclr_0 FROM ContainsGeometries c0_', + 'checks if geometry is strictly to the left of literal' => "SELECT (c0_.geometry1 << 'POINT(1 2)') AS sclr_0 FROM ContainsGeometries c0_", + ]; + } + + protected function getDqlStatements(): array + { + return [ + 'checks if geometry is strictly to the left' => \sprintf('SELECT STRICTLY_LEFT(e.geometry1, e.geometry2) FROM %s e', ContainsGeometries::class), + 'checks if geometry is strictly to the left of literal' => \sprintf("SELECT STRICTLY_LEFT(e.geometry1, 'POINT(1 2)') FROM %s e", ContainsGeometries::class), + ]; + } +} diff --git a/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/StrictlyRightTest.php b/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/StrictlyRightTest.php new file mode 100644 index 00000000..2db75b2b --- /dev/null +++ b/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/StrictlyRightTest.php @@ -0,0 +1,32 @@ + StrictlyRight::class, + ]; + } + + protected function getExpectedSqlStatements(): array + { + return [ + 'checks if geometry is strictly to the right' => 'SELECT (c0_.geometry1 >> c0_.geometry2) AS sclr_0 FROM ContainsGeometries c0_', + ]; + } + + protected function getDqlStatements(): array + { + return [ + 'checks if geometry is strictly to the right' => \sprintf('SELECT STRICTLY_RIGHT(e.geometry1, e.geometry2) FROM %s e', ContainsGeometries::class), + ]; + } +} diff --git a/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/TrajectoryDistanceTest.php b/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/TrajectoryDistanceTest.php new file mode 100644 index 00000000..c47ce2d7 --- /dev/null +++ b/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/TrajectoryDistanceTest.php @@ -0,0 +1,32 @@ + TrajectoryDistance::class, + ]; + } + + protected function getExpectedSqlStatements(): array + { + return [ + 'calculates trajectory distance between geometries' => 'SELECT (c0_.geometry1 |=| c0_.geometry2) AS sclr_0 FROM ContainsGeometries c0_', + ]; + } + + protected function getDqlStatements(): array + { + return [ + 'calculates trajectory distance between geometries' => \sprintf('SELECT TRAJECTORY_DISTANCE(e.geometry1, e.geometry2) FROM %s e', ContainsGeometries::class), + ]; + } +}