Skip to content

Commit 745b269

Browse files
feat(#440): add support for functions to use with the LTREE data type (#440)
1 parent b543ee8 commit 745b269

File tree

26 files changed

+1085
-1
lines changed

26 files changed

+1085
-1
lines changed

docs/AVAILABLE-FUNCTIONS-AND-OPERATORS.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ Complete documentation for mathematical operations and utility functions.
4949
- **[Mathematical and Utility Functions](MATHEMATICAL-FUNCTIONS.md)**
5050
- Includes: Mathematical functions, type conversion functions, formatting functions, utility functions
5151

52+
### **🌳 Ltree Functions**
53+
Complete documentation for PostgreSQL ltree (label tree) operations and hierarchical data processing.
54+
- **[Ltree Functions](LTREE-TYPE.md)**
55+
- Includes: Path manipulation functions, ancestor/descendant operations, type conversion functions
56+
5257
## 🚀 Quick Reference
5358

5459
### Most Commonly Used Functions
@@ -83,6 +88,13 @@ Complete documentation for mathematical operations and utility functions.
8388
- `ROUND` - Round numeric values
8489
- `RANDOM` - Generate random numbers
8590

91+
**Ltree Operations:** ([Complete documentation](LTREE-TYPE.md))
92+
- `SUBLTREE` - Extract subpath from ltree
93+
- `SUBPATH` - Extract subpath with offset and length
94+
- `NLEVEL` - Get number of labels in path
95+
- `INDEX` - Find position of ltree in another ltree
96+
- `LCA` - Find longest common ancestor
97+
8698
## 📋 Summary of Available Function Categories
8799

88100
### **Array & JSON Functions**
@@ -111,6 +123,11 @@ Complete documentation for mathematical operations and utility functions.
111123
- **Aggregation**: Array and JSON aggregation functions
112124
- **Utility Functions**: Random numbers, rounding, type casting
113125

126+
### **Ltree Functions**
127+
- **Path Operations**: Extract subpaths, manipulate hierarchical paths
128+
- **Ancestor Operations**: Find common ancestors, calculate path levels
129+
- **Type Conversion**: Convert between ltree and text types
130+
114131
### **Operators**
115132
- **Array Operators**: Contains, overlaps, element testing
116133
- **Spatial Operators**: Bounding box and distance operations
@@ -125,6 +142,7 @@ Complete documentation for mathematical operations and utility functions.
125142
4. **JSON functions** support both JSON and JSONB data types → [Array and JSON Functions](ARRAY-AND-JSON-FUNCTIONS.md)
126143
5. **Range functions** provide efficient storage and querying for value ranges → [Range Types](RANGE-TYPES.md)
127144
6. **Mathematical functions** work with numeric types and return appropriate precision → [Mathematical Functions](MATHEMATICAL-FUNCTIONS.md)
145+
7. **Ltree functions** provide efficient hierarchical data operations and path manipulation → [Ltree Functions](LTREE-TYPE.md)
128146

129147
---
130148

docs/LTREE-TYPE.md

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,3 +219,158 @@ final readonly class MyEntityOnFlushListener
219219
}
220220
}
221221
```
222+
223+
## Ltree Functions
224+
225+
This library provides DQL functions for all PostgreSQL ltree operations. These functions allow you to work with ltree data directly in your Doctrine queries.
226+
227+
### Path Manipulation Functions
228+
229+
#### `SUBLTREE(ltree, start, end)`
230+
Extracts a subpath from an ltree from position `start` to position `end-1` (counting from 0).
231+
232+
```php
233+
// DQL
234+
$dql = "SELECT SUBLTREE(e.path, 1, 2) FROM Entity e";
235+
// SQL: subltree(e.path, 1, 2)
236+
// Example: subltree('Top.Child1.Child2', 1, 2) → 'Child1'
237+
```
238+
239+
#### `SUBPATH(ltree, offset, len)`
240+
Extracts a subpath starting at position `offset` with length `len`. Supports negative values.
241+
242+
```php
243+
// DQL
244+
$dql = "SELECT SUBPATH(e.path, 0, 2) FROM Entity e";
245+
// SQL: subpath(e.path, 0, 2)
246+
// Example: subpath('Top.Child1.Child2', 0, 2) → 'Top.Child1'
247+
248+
// With negative offset
249+
$dql = "SELECT SUBPATH(e.path, -2) FROM Entity e";
250+
// SQL: subpath(e.path, -2)
251+
// Example: subpath('Top.Child1.Child2', -2) → 'Child1.Child2'
252+
```
253+
254+
#### `SUBPATH(ltree, offset)`
255+
Extracts a subpath starting at position `offset` extending to the end of the path.
256+
257+
```php
258+
// DQL
259+
$dql = "SELECT SUBPATH(e.path, 1) FROM Entity e";
260+
// SQL: subpath(e.path, 1)
261+
// Example: subpath('Top.Child1.Child2', 1) → 'Child1.Child2'
262+
```
263+
264+
### Path Information Functions
265+
266+
#### `NLEVEL(ltree)`
267+
Returns the number of labels in the path.
268+
269+
```php
270+
// DQL
271+
$dql = "SELECT NLEVEL(e.path) FROM Entity e";
272+
// SQL: nlevel(e.path)
273+
// Example: nlevel('Top.Child1.Child2') → 3
274+
```
275+
276+
#### `INDEX(a, b)`
277+
Returns the position of the first occurrence of `b` in `a`, or -1 if not found.
278+
279+
```php
280+
// DQL
281+
$dql = "SELECT INDEX(e.path, 'Child1') FROM Entity e";
282+
// SQL: index(e.path, 'Child1')
283+
// Example: index('Top.Child1.Child2', 'Child1') → 1
284+
```
285+
286+
#### `INDEX(a, b, offset)`
287+
Returns the position of the first occurrence of `b` in `a` starting at position `offset`.
288+
289+
```php
290+
// DQL
291+
$dql = "SELECT INDEX(e.path, 'Child1', 1) FROM Entity e";
292+
// SQL: index(e.path, 'Child1', 1)
293+
// Example: index('Top.Child1.Child2', 'Child1', 1) → 1
294+
```
295+
296+
### Ancestor Functions
297+
298+
#### `LCA(ltree1, ltree2, ...)`
299+
Computes the longest common ancestor of multiple paths (up to 8 arguments supported).
300+
301+
```php
302+
// DQL
303+
$dql = "SELECT LCA(e.path1, e.path2, e.path3) FROM Entity e";
304+
// SQL: lca(e.path1, e.path2, e.path3)
305+
// Example: lca('Top.Child1.Child2', 'Top.Child1', 'Top.Child2.Child3') → 'Top'
306+
```
307+
308+
### Type Conversion Functions
309+
310+
#### `TEXT2LTREE(text)`
311+
Casts text to ltree.
312+
313+
```php
314+
// DQL
315+
$dql = "SELECT TEXT2LTREE('Top.Child1.Child2') FROM Entity e";
316+
// SQL: text2ltree('Top.Child1.Child2')
317+
// Example: text2ltree('Top.Child1.Child2') → 'Top.Child1.Child2'::ltree
318+
```
319+
320+
#### `LTREE2TEXT(ltree)`
321+
Casts ltree to text.
322+
323+
```php
324+
// DQL
325+
$dql = "SELECT LTREE2TEXT(e.path) FROM Entity e";
326+
// SQL: ltree2text(e.path)
327+
// Example: ltree2text('Top.Child1.Child2'::ltree) → 'Top.Child1.Child2'
328+
```
329+
330+
### Usage Examples
331+
332+
#### Finding Ancestors and Descendants
333+
334+
```php
335+
// Find all descendants of a specific path
336+
$dql = "SELECT e FROM Entity e WHERE e.path <@ TEXT2LTREE('Top.Child1')";
337+
338+
// Find all ancestors of a specific path
339+
$dql = "SELECT e FROM Entity e WHERE TEXT2LTREE('Top.Child1') <@ e.path";
340+
341+
// Find the longest common ancestor of multiple entities
342+
$dql = "SELECT LCA(e1.path, e2.path) FROM Entity e1, Entity e2 WHERE e1.id = 1 AND e2.id = 2";
343+
```
344+
345+
#### Path Analysis
346+
347+
```php
348+
// Get the depth of a path
349+
$dql = "SELECT NLEVEL(e.path) FROM Entity e";
350+
351+
// Extract the parent path (everything except the last label)
352+
$dql = "SELECT SUBPATH(e.path, 0, NLEVEL(e.path) - 1) FROM Entity e";
353+
354+
// Extract the root label
355+
$dql = "SELECT SUBPATH(e.path, 0, 1) FROM Entity e";
356+
```
357+
358+
#### Path Manipulation
359+
360+
```php
361+
// Find entities at a specific level
362+
$dql = "SELECT e FROM Entity e WHERE NLEVEL(e.path) = 2";
363+
364+
// Find entities with a specific parent
365+
$dql = "SELECT e FROM Entity e WHERE SUBPATH(e.path, 0, NLEVEL(e.path) - 1) = 'Top.Child1'";
366+
367+
// Find entities that contain a specific label
368+
$dql = "SELECT e FROM Entity e WHERE INDEX(e.path, 'Child1') >= 0";
369+
```
370+
371+
### Performance Considerations
372+
373+
- Use GiST or GIN indexes on ltree columns for optimal performance
374+
- The `@>` and `<@` operators work automatically with ltree types
375+
- Consider using `SUBPATH` with negative offsets for efficient parent path extraction
376+
- `LCA` function is efficient for finding common ancestors in hierarchical data
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Fixtures\MartinGeorgiev\Doctrine\Entity;
6+
7+
use Doctrine\ORM\Mapping as ORM;
8+
9+
#[ORM\Entity()]
10+
class ContainsLtrees extends Entity
11+
{
12+
#[ORM\Column(type: 'ltree')]
13+
public string $ltree1;
14+
15+
#[ORM\Column(type: 'ltree')]
16+
public string $ltree2;
17+
18+
#[ORM\Column(type: 'ltree')]
19+
public string $ltree3;
20+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Ltree;
6+
7+
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\BaseVariadicFunction;
8+
9+
/**
10+
* Implementation of PostgreSQL index function for ltree.
11+
*
12+
* Returns position of first occurrence of b in a, or -1 if not found.
13+
* The search starts at position offset; negative offset means start -offset labels from the end of the path.
14+
*
15+
* @see https://www.postgresql.org/docs/17/ltree.html#LTREE-FUNCTIONS
16+
* @since 3.5
17+
*
18+
* @author Martin Georgiev <martin.georgiev@gmail.com>
19+
*
20+
* @example Using it in DQL: "SELECT INDEX(e.path, 'Child1') FROM Entity e"
21+
* Returns integer, position of first occurrence or -1 if not found.
22+
*/
23+
class Index extends BaseVariadicFunction
24+
{
25+
protected function getNodeMappingPattern(): array
26+
{
27+
return [
28+
'StringPrimary,StringPrimary,SimpleArithmeticExpression',
29+
'StringPrimary,StringPrimary',
30+
];
31+
}
32+
33+
protected function getFunctionName(): string
34+
{
35+
return 'index';
36+
}
37+
38+
protected function getMinArgumentCount(): int
39+
{
40+
return 2;
41+
}
42+
43+
protected function getMaxArgumentCount(): int
44+
{
45+
return 3;
46+
}
47+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Ltree;
6+
7+
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\BaseVariadicFunction;
8+
9+
/**
10+
* Implementation of PostgreSQL lca function.
11+
*
12+
* Computes longest common ancestor of paths (up to 8 arguments are supported).
13+
*
14+
* @see https://www.postgresql.org/docs/17/ltree.html#LTREE-FUNCTIONS
15+
* @since 3.5
16+
*
17+
* @author Martin Georgiev <martin.georgiev@gmail.com>
18+
*
19+
* @example Using it in DQL: "SELECT LCA(e.path1, e.path2) FROM Entity e"
20+
* Returns ltree, longest common ancestor of paths.
21+
*/
22+
class Lca extends BaseVariadicFunction
23+
{
24+
protected function getNodeMappingPattern(): array
25+
{
26+
return ['StringPrimary'];
27+
}
28+
29+
protected function getFunctionName(): string
30+
{
31+
return 'lca';
32+
}
33+
34+
protected function getMinArgumentCount(): int
35+
{
36+
return 2;
37+
}
38+
39+
protected function getMaxArgumentCount(): int
40+
{
41+
return 8;
42+
}
43+
}
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\Ltree;
6+
7+
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\BaseFunction;
8+
9+
/**
10+
* Implementation of PostgreSQL ltree2text function.
11+
*
12+
* Casts ltree to text.
13+
*
14+
* @see https://www.postgresql.org/docs/17/ltree.html#LTREE-FUNCTIONS
15+
* @since 3.5
16+
*
17+
* @author Martin Georgiev <martin.georgiev@gmail.com>
18+
*
19+
* @example Using it in DQL: "SELECT LTREE2TEXT(e.path) FROM Entity e"
20+
* Returns text, converted from ltree.
21+
*/
22+
class Ltree2text extends BaseFunction
23+
{
24+
protected function customizeFunction(): void
25+
{
26+
$this->setFunctionPrototype('ltree2text(%s)');
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\Ltree;
6+
7+
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\BaseFunction;
8+
9+
/**
10+
* Implementation of PostgreSQL nlevel function.
11+
*
12+
* Returns number of labels in path.
13+
*
14+
* @see https://www.postgresql.org/docs/17/ltree.html#LTREE-FUNCTIONS
15+
* @since 3.5
16+
*
17+
* @author Martin Georgiev <martin.georgiev@gmail.com>
18+
*
19+
* @example Using it in DQL: "SELECT NLEVEL(e.path) FROM Entity e"
20+
* Returns integer, number of labels in path.
21+
*/
22+
class Nlevel extends BaseFunction
23+
{
24+
protected function customizeFunction(): void
25+
{
26+
$this->setFunctionPrototype('nlevel(%s)');
27+
$this->addNodeMapping('StringPrimary');
28+
}
29+
}

0 commit comments

Comments
 (0)