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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions docs/AVAILABLE-FUNCTIONS-AND-OPERATORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ Complete documentation for mathematical operations and utility functions.
- **[Mathematical and Utility Functions](MATHEMATICAL-FUNCTIONS.md)**
- Includes: Mathematical functions, type conversion functions, formatting functions, utility functions

### **🌳 Ltree Functions**
Complete documentation for PostgreSQL ltree (label tree) operations and hierarchical data processing.
- **[Ltree Functions](LTREE-TYPE.md)**
- Includes: Path manipulation functions, ancestor/descendant operations, type conversion functions

## 🚀 Quick Reference

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

**Ltree Operations:** ([Complete documentation](LTREE-TYPE.md))
- `SUBLTREE` - Extract subpath from ltree
- `SUBPATH` - Extract subpath with offset and length
- `NLEVEL` - Get number of labels in path
- `INDEX` - Find position of ltree in another ltree
- `LCA` - Find longest common ancestor

## 📋 Summary of Available Function Categories

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

### **Ltree Functions**
- **Path Operations**: Extract subpaths, manipulate hierarchical paths
- **Ancestor Operations**: Find common ancestors, calculate path levels
- **Type Conversion**: Convert between ltree and text types

### **Operators**
- **Array Operators**: Contains, overlaps, element testing
- **Spatial Operators**: Bounding box and distance operations
Expand All @@ -125,6 +142,7 @@ Complete documentation for mathematical operations and utility functions.
4. **JSON functions** support both JSON and JSONB data types → [Array and JSON Functions](ARRAY-AND-JSON-FUNCTIONS.md)
5. **Range functions** provide efficient storage and querying for value ranges → [Range Types](RANGE-TYPES.md)
6. **Mathematical functions** work with numeric types and return appropriate precision → [Mathematical Functions](MATHEMATICAL-FUNCTIONS.md)
7. **Ltree functions** provide efficient hierarchical data operations and path manipulation → [Ltree Functions](LTREE-TYPE.md)

---

Expand Down
155 changes: 155 additions & 0 deletions docs/LTREE-TYPE.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,3 +219,158 @@ final readonly class MyEntityOnFlushListener
}
}
```

## Ltree Functions

This library provides DQL functions for all PostgreSQL ltree operations. These functions allow you to work with ltree data directly in your Doctrine queries.

### Path Manipulation Functions

#### `SUBLTREE(ltree, start, end)`
Extracts a subpath from an ltree from position `start` to position `end-1` (counting from 0).

```php
// DQL
$dql = "SELECT SUBLTREE(e.path, 1, 2) FROM Entity e";
// SQL: subltree(e.path, 1, 2)
// Example: subltree('Top.Child1.Child2', 1, 2) → 'Child1'
```

#### `SUBPATH(ltree, offset, len)`
Extracts a subpath starting at position `offset` with length `len`. Supports negative values.

```php
// DQL
$dql = "SELECT SUBPATH(e.path, 0, 2) FROM Entity e";
// SQL: subpath(e.path, 0, 2)
// Example: subpath('Top.Child1.Child2', 0, 2) → 'Top.Child1'

// With negative offset
$dql = "SELECT SUBPATH(e.path, -2) FROM Entity e";
// SQL: subpath(e.path, -2)
// Example: subpath('Top.Child1.Child2', -2) → 'Child1.Child2'
```

#### `SUBPATH(ltree, offset)`
Extracts a subpath starting at position `offset` extending to the end of the path.

```php
// DQL
$dql = "SELECT SUBPATH(e.path, 1) FROM Entity e";
// SQL: subpath(e.path, 1)
// Example: subpath('Top.Child1.Child2', 1) → 'Child1.Child2'
```

### Path Information Functions

#### `NLEVEL(ltree)`
Returns the number of labels in the path.

```php
// DQL
$dql = "SELECT NLEVEL(e.path) FROM Entity e";
// SQL: nlevel(e.path)
// Example: nlevel('Top.Child1.Child2') → 3
```

#### `INDEX(a, b)`
Returns the position of the first occurrence of `b` in `a`, or -1 if not found.

```php
// DQL
$dql = "SELECT INDEX(e.path, 'Child1') FROM Entity e";
// SQL: index(e.path, 'Child1')
// Example: index('Top.Child1.Child2', 'Child1') → 1
```

#### `INDEX(a, b, offset)`
Returns the position of the first occurrence of `b` in `a` starting at position `offset`.

```php
// DQL
$dql = "SELECT INDEX(e.path, 'Child1', 1) FROM Entity e";
// SQL: index(e.path, 'Child1', 1)
// Example: index('Top.Child1.Child2', 'Child1', 1) → 1
```

### Ancestor Functions

#### `LCA(ltree1, ltree2, ...)`
Computes the longest common ancestor of multiple paths (up to 8 arguments supported).

```php
// DQL
$dql = "SELECT LCA(e.path1, e.path2, e.path3) FROM Entity e";
// SQL: lca(e.path1, e.path2, e.path3)
// Example: lca('Top.Child1.Child2', 'Top.Child1', 'Top.Child2.Child3') → 'Top'
```

### Type Conversion Functions

#### `TEXT2LTREE(text)`
Casts text to ltree.

```php
// DQL
$dql = "SELECT TEXT2LTREE('Top.Child1.Child2') FROM Entity e";
// SQL: text2ltree('Top.Child1.Child2')
// Example: text2ltree('Top.Child1.Child2') → 'Top.Child1.Child2'::ltree
```

#### `LTREE2TEXT(ltree)`
Casts ltree to text.

```php
// DQL
$dql = "SELECT LTREE2TEXT(e.path) FROM Entity e";
// SQL: ltree2text(e.path)
// Example: ltree2text('Top.Child1.Child2'::ltree) → 'Top.Child1.Child2'
```

### Usage Examples

#### Finding Ancestors and Descendants

```php
// Find all descendants of a specific path
$dql = "SELECT e FROM Entity e WHERE e.path <@ TEXT2LTREE('Top.Child1')";

// Find all ancestors of a specific path
$dql = "SELECT e FROM Entity e WHERE TEXT2LTREE('Top.Child1') <@ e.path";

// Find the longest common ancestor of multiple entities
$dql = "SELECT LCA(e1.path, e2.path) FROM Entity e1, Entity e2 WHERE e1.id = 1 AND e2.id = 2";
```

#### Path Analysis

```php
// Get the depth of a path
$dql = "SELECT NLEVEL(e.path) FROM Entity e";

// Extract the parent path (everything except the last label)
$dql = "SELECT SUBPATH(e.path, 0, NLEVEL(e.path) - 1) FROM Entity e";

// Extract the root label
$dql = "SELECT SUBPATH(e.path, 0, 1) FROM Entity e";
```

#### Path Manipulation

```php
// Find entities at a specific level
$dql = "SELECT e FROM Entity e WHERE NLEVEL(e.path) = 2";

// Find entities with a specific parent
$dql = "SELECT e FROM Entity e WHERE SUBPATH(e.path, 0, NLEVEL(e.path) - 1) = 'Top.Child1'";

// Find entities that contain a specific label
$dql = "SELECT e FROM Entity e WHERE INDEX(e.path, 'Child1') >= 0";
```

### Performance Considerations

- Use GiST or GIN indexes on ltree columns for optimal performance
- The `@>` and `<@` operators work automatically with ltree types
- Consider using `SUBPATH` with negative offsets for efficient parent path extraction
- `LCA` function is efficient for finding common ancestors in hierarchical data
20 changes: 20 additions & 0 deletions fixtures/MartinGeorgiev/Doctrine/Entity/ContainsLtrees.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace Fixtures\MartinGeorgiev\Doctrine\Entity;

use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity()]
class ContainsLtrees extends Entity
{
#[ORM\Column(type: 'ltree')]
public string $ltree1;

#[ORM\Column(type: 'ltree')]
public string $ltree2;

#[ORM\Column(type: 'ltree')]
public string $ltree3;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

declare(strict_types=1);

namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Ltree;

use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\BaseVariadicFunction;

/**
* Implementation of PostgreSQL index function for ltree.
*
* Returns position of first occurrence of b in a, or -1 if not found.
* The search starts at position offset; negative offset means start -offset labels from the end of the path.
*
* @see https://www.postgresql.org/docs/17/ltree.html#LTREE-FUNCTIONS
* @since 3.5
*
* @author Martin Georgiev <martin.georgiev@gmail.com>
*
* @example Using it in DQL: "SELECT INDEX(e.path, 'Child1') FROM Entity e"
* Returns integer, position of first occurrence or -1 if not found.
*/
class Index extends BaseVariadicFunction
{
protected function getNodeMappingPattern(): array
{
return [
'StringPrimary,StringPrimary,SimpleArithmeticExpression',
'StringPrimary,StringPrimary',
];
}

protected function getFunctionName(): string
{
return 'index';
}

protected function getMinArgumentCount(): int
{
return 2;
}

protected function getMaxArgumentCount(): int
{
return 3;
}
}
43 changes: 43 additions & 0 deletions src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/Ltree/Lca.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

declare(strict_types=1);

namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Ltree;

use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\BaseVariadicFunction;

/**
* Implementation of PostgreSQL lca function.
*
* Computes longest common ancestor of paths (up to 8 arguments are supported).
*
* @see https://www.postgresql.org/docs/17/ltree.html#LTREE-FUNCTIONS
* @since 3.5
*
* @author Martin Georgiev <martin.georgiev@gmail.com>
*
* @example Using it in DQL: "SELECT LCA(e.path1, e.path2) FROM Entity e"
* Returns ltree, longest common ancestor of paths.
*/
class Lca extends BaseVariadicFunction
{
protected function getNodeMappingPattern(): array
{
return ['StringPrimary'];
}

protected function getFunctionName(): string
{
return 'lca';
}

protected function getMinArgumentCount(): int
{
return 2;
}

protected function getMaxArgumentCount(): int
{
return 8;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

declare(strict_types=1);

namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Ltree;

use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\BaseFunction;

/**
* Implementation of PostgreSQL ltree2text function.
*
* Casts ltree to text.
*
* @see https://www.postgresql.org/docs/17/ltree.html#LTREE-FUNCTIONS
* @since 3.5
*
* @author Martin Georgiev <martin.georgiev@gmail.com>
*
* @example Using it in DQL: "SELECT LTREE2TEXT(e.path) FROM Entity e"
* Returns text, converted from ltree.
*/
class Ltree2text extends BaseFunction
{
protected function customizeFunction(): void
{
$this->setFunctionPrototype('ltree2text(%s)');
$this->addNodeMapping('StringPrimary');
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

declare(strict_types=1);

namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Ltree;

use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\BaseFunction;

/**
* Implementation of PostgreSQL nlevel function.
*
* Returns number of labels in path.
*
* @see https://www.postgresql.org/docs/17/ltree.html#LTREE-FUNCTIONS
* @since 3.5
*
* @author Martin Georgiev <martin.georgiev@gmail.com>
*
* @example Using it in DQL: "SELECT NLEVEL(e.path) FROM Entity e"
* Returns integer, number of labels in path.
*/
class Nlevel extends BaseFunction
{
protected function customizeFunction(): void
{
$this->setFunctionPrototype('nlevel(%s)');
$this->addNodeMapping('StringPrimary');
}
}
Loading
Loading