Skip to content

Commit 5b5583b

Browse files
feat(#305): add support for PostGIS operators (#430)
1 parent 9a0a5ef commit 5b5583b

File tree

61 files changed

+2237
-14
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+2237
-14
lines changed

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,11 +74,16 @@ This package provides comprehensive Doctrine support for PostgreSQL features:
7474
- **Range Operations**
7575
- Containment checks (in PHP value objects and for DQL queries with `@>` and `<@`)
7676
- Overlaps (`&&`)
77+
- **PostGIS Spatial Operations**
78+
- Bounding box relationships (`<<`, `>>`, `&<`, `&>`, `|&>`, `&<|`, `<<|`, `|>>`)
79+
- Spatial containment (`@`, `~`)
80+
- Distance calculations (`<->`, `<#>`, `<<->>`, `<<#>>`, `|=|`)
81+
- N-dimensional operations (`&&&`)
7782

7883
### Functions
7984
- **Text Search**
8085
- Full text search (`to_tsvector`, `to_tsquery`)
81-
- Pattern matching (`ILIKE`, `SIMILAR TO`)
86+
- Pattern matching (`ilike`, `similar to`)
8287
- Regular expressions
8388
- **Array Functions**
8489
- Array aggregation (`array_agg`)

docs/AVAILABLE-FUNCTIONS-AND-OPERATORS.md

Lines changed: 97 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,26 @@
1-
# Available operators
1+
# Available Operators
22

3-
| PostgreSQL operator | Register for DQL as | Implemented by
3+
## Operator Conflicts and Usage Notes
4+
5+
**⚠️ Important**: Some PostgreSQL operators have multiple meanings depending on the data types involved. This library provides specific DQL function names to avoid conflicts:
6+
7+
| Operator | Array/JSON Usage | Spatial Usage | Text/Pattern Usage |
8+
|---|---|---|---|
9+
| `@>` | `CONTAINS` (arrays contain elements) | Works automatically with geometry/geography | N/A |
10+
| `<@` | `IS_CONTAINED_BY` (element in array) | Works automatically with geometry/geography | N/A |
11+
| `@` | N/A | `SPATIAL_CONTAINED_BY` (bounding box contained) | N/A |
12+
| `~` | N/A | `SPATIAL_CONTAINS` (bounding box contains) | `REGEXP` (text pattern matching) |
13+
| `&&` | `OVERLAPS` (arrays/ranges overlap) | Works automatically with geometry/geography | N/A |
14+
15+
**Usage Guidelines:**
16+
- **Arrays/JSON**: Use `CONTAINS`, `IS_CONTAINED_BY`, `OVERLAPS` for array and JSON operations
17+
- **Spatial**: Use `SPATIAL_CONTAINS`, `SPATIAL_CONTAINED_BY` for explicit spatial bounding box operations
18+
- **Text**: Use `REGEXP`, `IREGEXP` for pattern matching
19+
- **Boolean operators**: All spatial operators return boolean values and **shall be used with `= TRUE` or `= FALSE` in DQL**
20+
21+
## General Operators
22+
23+
| PostgreSQL operator | Register for DQL as | Implemented by |
424
|---|---|---|
525
| @> | CONTAINS | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Contains` |
626
| <@ | IS_CONTAINED_BY | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\IsContainedBy` |
@@ -24,9 +44,80 @@
2444
| @@ | TSMATCH | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Tsmatch` |
2545
| \|\| | STRCONCAT | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\StrConcat` |
2646

27-
# Available functions
47+
## PostGIS Spatial Operators
48+
49+
**⚠️ Important**: Some operators have dual meanings for different data types. Use the specific DQL function names to avoid conflicts:
50+
51+
- **`@`**: Use `CONTAINS` for arrays/JSON, `SPATIAL_CONTAINED_BY` for geometry/geography
52+
- **`~`**: Use `REGEXP` for text patterns, `SPATIAL_CONTAINS` for geometry/geography
53+
- **`&&`**: Use `OVERLAPS` for arrays/JSON, spatial overlaps work automatically with geometry/geography
54+
55+
**📝 Compatibility Notes**:
56+
- Most bounding box operators work primarily with **geometry** types
57+
- **Geography** types have limited operator support (mainly `&&`, `<->`, `<@>`)
58+
- **3D/n-dimensional operators** may require explicit type casting: `ST_GeomFromText('POINT Z(0 0 0)')`
59+
- Some advanced operators (`&&&`, `<<#>>`) may not be available in all PostGIS versions
60+
61+
### Bounding Box Operators
62+
63+
These operators work with geometry and geography bounding boxes. All return boolean values and **shall be used with `= TRUE` or `= FALSE` in DQL**.
64+
65+
| PostgreSQL operator | Register for DQL as | Description | Implemented by |
66+
|---|---|---|---|
67+
| &< | 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` |
68+
| &> | 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` |
69+
| << | STRICTLY_LEFT | Returns TRUE if A's bounding box is strictly to the left of B's | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\StrictlyLeft` |
70+
| >> | STRICTLY_RIGHT | Returns TRUE if A's bounding box is strictly to the right of B's | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\StrictlyRight` |
71+
| @ | SPATIAL_CONTAINED_BY | Returns TRUE if A's bounding box is contained by B's (**spatial version**) | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\SpatialContainedBy` |
72+
| ~ | SPATIAL_CONTAINS | Returns TRUE if A's bounding box contains B's (**spatial version**) | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\SpatialContains` |
73+
| ~= | SPATIAL_SAME | Returns TRUE if A's bounding box is the same as B's | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\SpatialSame` |
74+
| \|&> | OVERLAPS_ABOVE | Returns TRUE if A's bounding box overlaps or is above B's | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\OverlapsAbove` |
75+
| \|>> | STRICTLY_ABOVE | Returns TRUE if A's bounding box is strictly above B's | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\StrictlyAbove` |
76+
| &<\| | OVERLAPS_BELOW | Returns TRUE if A's bounding box overlaps or is below B's | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\OverlapsBelow` |
77+
| <<\| | STRICTLY_BELOW | Returns TRUE if A's bounding box is strictly below B's | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\StrictlyBelow` |
78+
| &&& | 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` |
79+
80+
**Usage Examples:**
81+
```sql
82+
-- Find geometries to the left of a reference point
83+
SELECT e FROM Entity e WHERE STRICTLY_LEFT(e.geometry, 'POINT(0 0)') = TRUE
84+
85+
-- Find overlapping polygons
86+
SELECT e FROM Entity e WHERE SPATIAL_CONTAINS(e.polygon, e.point) = TRUE
87+
88+
-- 3D spatial relationships
89+
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
90+
```
91+
92+
### Distance Operators
93+
94+
These operators calculate distances between geometries. All return numeric values.
95+
96+
| PostgreSQL operator | Register for DQL as | Description | Implemented by |
97+
|---|---|---|---|
98+
| <-> | GEOMETRY_DISTANCE | Returns the 2D distance between A and B geometries | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\GeometryDistance` |
99+
| <@> | DISTANCE | Returns distance between points (legacy operator) | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Distance` |
100+
| \|=\| | TRAJECTORY_DISTANCE | Returns distance between trajectories at closest point of approach | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\TrajectoryDistance` |
101+
| <#> | BOUNDING_BOX_DISTANCE | Returns the 2D distance between A and B bounding boxes | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\BoundingBoxDistance` |
102+
| <<->> | ND_CENTROID_DISTANCE | Returns n-D distance between centroids of bounding boxes | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\NDimensionalCentroidDistance` |
103+
| <<#>> | ND_BOUNDING_BOX_DISTANCE | Returns the n-D distance between A and B bounding boxes | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\NDimensionalBoundingBoxDistance` |
104+
105+
**Usage Examples:**
106+
```sql
107+
-- Find nearest geometries
108+
SELECT e, GEOMETRY_DISTANCE(e.geometry, 'POINT(0 0)') as distance
109+
FROM Entity e ORDER BY distance LIMIT 10
110+
111+
-- Bounding box distance for index optimization
112+
SELECT e FROM Entity e WHERE BOUNDING_BOX_DISTANCE(e.geometry, 'POINT(0 0)') < 1000
113+
114+
-- 3D distance calculations
115+
SELECT ND_CENTROID_DISTANCE(e.geometry3d1, e.geometry3d2) as distance FROM Entity e
116+
```
117+
118+
# Available Functions
28119

29-
| PostgreSQL functions | Register for DQL as | Implemented by
120+
| PostgreSQL functions | Register for DQL as | Implemented by |
30121
|---|---|---|
31122
| all | ALL_OF | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\All` |
32123
| any | ANY_OF | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Any` |
@@ -134,9 +225,9 @@
134225
| width_bucket | WIDTH_BUCKET | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\WidthBucket` |
135226

136227

137-
# Bonus helpers
228+
# Bonus Helpers
138229

139-
| PostgreSQL functions | Register for DQL as | Implemented by
230+
| PostgreSQL functions | Register for DQL as | Implemented by |
140231
|---|---|---|
141232
| array | ARRAY | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Arr` |
142233
| value = ANY(list of values) | IN_ARRAY | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\InArray` |

docs/USE-CASES-AND-EXAMPLES.md

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,9 +178,87 @@ POLYGONZM((...)) => POLYGON ZM((...))
178178
POINT Z (1 2 3) => POINT Z(1 2 3)
179179
```
180180

181+
### Using PostGIS Spatial Operators in DQL
182+
183+
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.
184+
185+
#### Bounding Box Spatial Relationships
186+
187+
```sql
188+
-- Find geometries to the left of a reference point
189+
SELECT e FROM Entity e WHERE STRICTLY_LEFT(e.geometry, 'POINT(0 0)') = TRUE
190+
191+
-- Find geometries that spatially contain a point (bounding box level)
192+
SELECT e FROM Entity e WHERE SPATIAL_CONTAINS(e.polygon, 'POINT(1 1)') = TRUE
193+
194+
-- Find geometries contained within a bounding box
195+
SELECT e FROM Entity e WHERE SPATIAL_CONTAINED_BY(e.geometry, 'POLYGON((0 0, 10 10, 20 20, 0 0))') = TRUE
196+
197+
-- Check if two geometries have the same bounding box
198+
SELECT e FROM Entity e WHERE SPATIAL_SAME(e.geometry1, e.geometry2) = TRUE
199+
200+
-- Vertical relationships
201+
SELECT e FROM Entity e WHERE STRICTLY_ABOVE(e.geometry, 'LINESTRING(0 0, 5 0)') = TRUE
202+
SELECT e FROM Entity e WHERE OVERLAPS_BELOW(e.geometry, 'POLYGON((0 5, 5 5, 5 10, 0 10, 0 5))') = TRUE
203+
204+
-- 3D spatial relationships
205+
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
206+
```
207+
208+
#### Distance-Based Queries
209+
210+
```sql
211+
-- Find the 10 nearest geometries to a point
212+
SELECT e, GEOMETRY_DISTANCE(e.geometry, 'POINT(0 0)') as distance
213+
FROM Entity e
214+
ORDER BY distance
215+
LIMIT 10
216+
217+
-- Find geometries within a specific distance (using bounding box distance for performance)
218+
SELECT e FROM Entity e WHERE BOUNDING_BOX_DISTANCE(e.geometry, 'POINT(0 0)') < 1000
219+
220+
-- Calculate trajectory distances (for linestrings with measure values)
221+
SELECT TRAJECTORY_DISTANCE(e.trajectory1, e.trajectory2) as closest_approach
222+
FROM Entity e
223+
WHERE e.trajectory1 IS NOT NULL
224+
225+
-- 3D distance calculations
226+
SELECT e, ND_CENTROID_DISTANCE(e.geometry3d1, e.geometry3d2) as distance3d
227+
FROM Entity e
228+
WHERE ND_BOUNDING_BOX_DISTANCE(e.geometry3d1, e.geometry3d2) < 500
229+
```
230+
231+
#### Operator Conflicts and Best Practices
232+
233+
Some operators have different meanings for different data types. Use specific function names to avoid conflicts:
234+
235+
```sql
236+
-- ✅ CORRECT: Use specific function names
237+
SELECT e FROM Entity e WHERE CONTAINS(e.tags, ARRAY['tag1']) = TRUE -- Array containment
238+
SELECT e FROM Entity e WHERE SPATIAL_CONTAINS(e.polygon, e.point) = TRUE -- Spatial containment
239+
SELECT e FROM Entity e WHERE REGEXP(e.text, 'pattern') = TRUE -- Text pattern matching
240+
241+
-- ❌ AVOID: Ambiguous usage that might conflict
242+
-- The @ and ~ operators have different meanings for arrays vs spatial data
243+
```
244+
245+
#### Performance Tips
246+
247+
```sql
248+
-- Use bounding box operators for initial filtering (they use spatial indexes)
249+
SELECT e FROM Entity e
250+
WHERE OVERLAPS(e.geometry, 'POLYGON((0 0, 10 10, 20 20, 0 0))') = TRUE
251+
AND ST_Intersects(e.geometry, 'POLYGON((0 0, 10 10, 20 20, 0 0))') -- Exact check
252+
253+
-- Use distance operators for nearest neighbor queries
254+
SELECT e FROM Entity e
255+
ORDER BY GEOMETRY_DISTANCE(e.geometry, 'POINT(0 0)')
256+
LIMIT 10
257+
```
258+
181259
For multi-item arrays, see [GEOMETRY-ARRAYS.md](./GEOMETRY-ARRAYS.md) for Doctrine DQL limitations and the suggested workarounds.
182260

183-
The library provides DBAL type support for PostGIS `geometry` and `geography` columns. Example usage:
261+
The library provides DBAL type support for PostGIS `geometry` and `geography` types. Example usage:
184262

185263
```sql
186264
CREATE TABLE places (
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Fixtures\MartinGeorgiev\Doctrine\Entity;
6+
7+
use Doctrine\ORM\Mapping as ORM;
8+
use MartinGeorgiev\Doctrine\DBAL\Types\ValueObject\WktSpatialData;
9+
10+
#[ORM\Entity()]
11+
class ContainsGeometries extends Entity
12+
{
13+
#[ORM\Column(type: 'geometry')]
14+
public WktSpatialData $geometry1;
15+
16+
#[ORM\Column(type: 'geometry')]
17+
public WktSpatialData $geometry2;
18+
19+
#[ORM\Column(type: 'geography')]
20+
public WktSpatialData $geography1;
21+
22+
#[ORM\Column(type: 'geography')]
23+
public WktSpatialData $geography2;
24+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions;
6+
7+
/**
8+
* Implementation of PostGIS 2D bounding box distance operator (using <#>).
9+
*
10+
* Returns the 2D distance between A and B bounding boxes.
11+
* This is useful for index-based distance queries.
12+
*
13+
* @see https://postgis.net/docs/reference.html#Operators_Distance
14+
* @since 3.5
15+
*
16+
* @author Martin Georgiev <[email protected]>
17+
*
18+
* @example Using it in DQL: "SELECT BOUNDING_BOX_DISTANCE(g1.geometry, g2.geometry) FROM Entity g1, Entity g2"
19+
* Returns numeric distance value.
20+
*/
21+
class BoundingBoxDistance extends BaseFunction
22+
{
23+
protected function customizeFunction(): void
24+
{
25+
$this->setFunctionPrototype('(%s <#> %s)');
26+
$this->addNodeMapping('StringPrimary');
27+
$this->addNodeMapping('StringPrimary');
28+
}
29+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions;
6+
7+
/**
8+
* Implementation of PostGIS 2D distance between geometries operator (using <->).
9+
*
10+
* Returns the 2D distance between A and B geometries.
11+
* This differs from BoundingBoxDistance (using <#>), which measures distance between
12+
* bounding boxes only. The <-> operator is commonly used for KNN nearest-neighbor
13+
* ordering, and on PostgreSQL 9.5+ with PostGIS 2.2+ it returns true geometry distance;
14+
* on older stacks it behaved as a centroid-of-bounding-box distance approximation.
15+
*
16+
* @see https://postgis.net/docs/reference.html#Operators
17+
* @see https://postgis.net/docs/geometry_distance_knn.html
18+
* @since 3.5
19+
*
20+
* @author Martin Georgiev <[email protected]>
21+
*
22+
* @example Using it in DQL: "SELECT GEOMETRY_DISTANCE(g1.geometry, g2.geometry) FROM Entity g1, Entity g2"
23+
* Returns numeric distance value.
24+
*/
25+
class GeometryDistance extends BaseFunction
26+
{
27+
protected function customizeFunction(): void
28+
{
29+
$this->setFunctionPrototype('(%s <-> %s)');
30+
$this->addNodeMapping('StringPrimary');
31+
$this->addNodeMapping('StringPrimary');
32+
}
33+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions;
6+
7+
/**
8+
* Implementation of PostGIS n-D bounding box distance operator (using <<#>>).
9+
*
10+
* Returns the n-D distance between A and B bounding boxes.
11+
* This operator works with multi-dimensional geometries.
12+
*
13+
* @see https://postgis.net/docs/reference.html#Operators_Distance
14+
* @since 3.5
15+
*
16+
* @author Martin Georgiev <[email protected]>
17+
*
18+
* @example Using it in DQL: "SELECT ND_BOUNDING_BOX_DISTANCE(g1.geometry, g2.geometry) FROM Entity g1, Entity g2"
19+
* Returns numeric distance value.
20+
*/
21+
class NDimensionalBoundingBoxDistance extends BaseFunction
22+
{
23+
protected function customizeFunction(): void
24+
{
25+
$this->setFunctionPrototype('(%s <<#>> %s)');
26+
$this->addNodeMapping('StringPrimary');
27+
$this->addNodeMapping('StringPrimary');
28+
}
29+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions;
6+
7+
/**
8+
* Implementation of PostGIS n-D centroid distance operator (using <<->>).
9+
*
10+
* Returns the n-D distance between the centroids of A and B bounding boxes.
11+
* This operator works with multi-dimensional geometries.
12+
*
13+
* @see https://postgis.net/docs/reference.html#Operators_Distance
14+
* @since 3.5
15+
*
16+
* @author Martin Georgiev <[email protected]>
17+
*
18+
* @example Using it in DQL: "SELECT ND_CENTROID_DISTANCE(g1.geometry, g2.geometry) FROM Entity g1, Entity g2"
19+
* Returns numeric distance value.
20+
*/
21+
class NDimensionalCentroidDistance extends BaseFunction
22+
{
23+
protected function customizeFunction(): void
24+
{
25+
$this->setFunctionPrototype('(%s <<->> %s)');
26+
$this->addNodeMapping('StringPrimary');
27+
$this->addNodeMapping('StringPrimary');
28+
}
29+
}

0 commit comments

Comments
 (0)