Skip to content

Commit 1fe36a2

Browse files
authored
feature: postgresql - condition builder (#2113)
* feature: postgresql - condition builder * fix: missing dsl definitions
1 parent a52cb01 commit 1fe36a2

File tree

10 files changed

+510
-6
lines changed

10 files changed

+510
-6
lines changed

documentation/components/libs/postgresql.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,10 @@ echo $query->toSQL();
238238
RETURNING
239239
- [Merge Query Builder](/documentation/components/libs/postgresql/merge-query-builder.md) - MERGE (SQL:2008 upsert)
240240

241+
**Query Building Utilities**
242+
243+
- [Condition Builder](/documentation/components/libs/postgresql/condition-builder.md) - Build WHERE conditions incrementally with fluent API
244+
241245
**Schema Management (DDL)**
242246

243247
- [Table Query Builder](/documentation/components/libs/postgresql/table-query-builder.md) - CREATE/ALTER/DROP TABLE
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
# Condition Builder
2+
3+
- [⬅️ Back](/documentation/components/libs/postgresql.md)
4+
5+
[TOC]
6+
7+
The Condition Builder provides a fluent API for building WHERE conditions incrementally. It's particularly useful when conditions need to be constructed dynamically based on runtime logic, such as building filter queries from user input.
8+
9+
## Basic Usage
10+
11+
```php
12+
<?php
13+
14+
use function Flow\PostgreSql\DSL\{
15+
conditions, select, table, col, eq, gt, param, literal
16+
};
17+
18+
// Create a condition builder and add conditions fluently
19+
$conditions = conditions()
20+
->and(eq(col('status'), literal('active')))
21+
->and(gt(col('age'), literal(18)));
22+
23+
// Use in a query
24+
$query = select()->from(table('users'))->where($conditions);
25+
26+
echo $query->toSQL();
27+
// SELECT * FROM users WHERE status = 'active' AND age > 18
28+
```
29+
30+
## Building Conditions Incrementally
31+
32+
The real power of `ConditionBuilder` is building conditions based on runtime logic:
33+
34+
```php
35+
<?php
36+
37+
use function Flow\PostgreSql\DSL\{
38+
conditions, select, table, col, eq, gte, like, param
39+
};
40+
41+
function buildUserQuery(array $filters): SelectFinalStep
42+
{
43+
$conditions = conditions();
44+
45+
if (array_key_exists('status', $filters)) {
46+
$conditions = $conditions->and(eq(col('status'), param($filters['status'])));
47+
}
48+
49+
if (array_key_exists('min_age', $filters)) {
50+
$conditions = $conditions->and(gte(col('age'), param($filters['min_age'])));
51+
}
52+
53+
if (array_key_exists('email_domain', $filters)) {
54+
$conditions = $conditions->and(like(col('email'), param('%@' . $filters['email_domain'])));
55+
}
56+
57+
$query = select()->from(table('users'));
58+
59+
// Only add WHERE clause if we have conditions
60+
if (!$conditions->isEmpty()) {
61+
$query = $query->where($conditions);
62+
}
63+
64+
return $query;
65+
}
66+
67+
// Usage
68+
$query = buildUserQuery(['status' => 'active', 'min_age' => 21]);
69+
echo $query->toSQL();
70+
// SELECT * FROM users WHERE status = $1 AND age >= $2
71+
```
72+
73+
## Nested Conditions
74+
75+
Build complex nested conditions with OR groups inside AND:
76+
77+
```php
78+
<?php
79+
80+
use function Flow\PostgreSql\DSL\{
81+
conditions, select, table, col, eq, gt, literal
82+
};
83+
84+
// WHERE status = 'active' AND (role = 'admin' OR role = 'moderator') AND age > 18
85+
$conditions = conditions()
86+
->and(eq(col('status'), literal('active')))
87+
->and(
88+
conditions()
89+
->or(eq(col('role'), literal('admin')))
90+
->or(eq(col('role'), literal('moderator')))
91+
)
92+
->and(gt(col('age'), literal(18)));
93+
94+
$query = select()->from(table('users'))->where($conditions);
95+
96+
echo $query->toSQL();
97+
// SELECT * FROM users WHERE status = 'active' AND (role = 'admin' OR role = 'moderator') AND age > 18
98+
```
99+
100+
## Immutability
101+
102+
`ConditionBuilder` is immutable - each `and()` or `or()` call returns a new builder instance:
103+
104+
```php
105+
<?php
106+
107+
use function Flow\PostgreSql\DSL\{conditions, eq, gt, col, literal};
108+
109+
$base = conditions()->and(eq(col('active'), literal(true)));
110+
111+
// Each branch creates a new builder
112+
$withRole = $base->and(eq(col('role'), literal('admin')));
113+
$withAge = $base->and(gt(col('age'), literal(18)));
114+
115+
// $base is unchanged
116+
```
117+
118+
## Empty Builder Behavior
119+
120+
- `isEmpty()` returns `true` when no conditions have been added
121+
- `getCondition()` returns `null` for empty builders
122+
- Passing an empty builder to `where()` throws `InvalidBuilderStateException`
123+
- Empty nested builders are ignored (no-op) when combined with `and()` or `or()`
124+
125+
```php
126+
<?php
127+
128+
use function Flow\PostgreSql\DSL\{conditions, eq, col, literal};
129+
130+
$conditions = conditions();
131+
132+
// Safe pattern: check before using
133+
if (!$conditions->isEmpty()) {
134+
$query = $query->where($conditions);
135+
}
136+
137+
// Empty nested builders are safely ignored
138+
$conditions = conditions()
139+
->and(eq(col('status'), literal('active')))
140+
->and(conditions()); // Empty conditions is ignored
141+
142+
// Result: only "status = 'active'" condition
143+
```
144+
145+
## Method Reference
146+
147+
| Method | Description |
148+
|--------|-------------|
149+
| `conditions()` | DSL function - creates a new empty `ConditionBuilder` |
150+
| `and(Condition\|ConditionBuilder $condition)` | Add a condition with AND logic |
151+
| `or(Condition\|ConditionBuilder $condition)` | Add a condition with OR logic |
152+
| `getCondition()` | Returns the built `Condition` or `null` if empty |
153+
| `isEmpty()` | Returns `true` if no conditions have been added |
154+
155+
## Comparison with cond_and/cond_or
156+
157+
For static conditions known at build time, `cond_and()` and `cond_or()` are simpler:
158+
159+
```php
160+
// Static conditions - use cond_and/cond_or
161+
$query = select()
162+
->from(table('users'))
163+
->where(cond_and(
164+
eq(col('active'), literal(true)),
165+
gt(col('age'), literal(18))
166+
));
167+
168+
// Dynamic conditions - use ConditionBuilder
169+
$conditions = conditions();
170+
foreach ($filters as $filter) {
171+
$conditions = $conditions->and($filter->toCondition());
172+
}
173+
```

documentation/components/libs/postgresql/select-query-builder.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@ echo $query->toSQL();
107107
// SELECT * FROM users WHERE email LIKE '%@example.com'
108108
```
109109

110+
> **Tip:** For building WHERE conditions dynamically based on runtime logic (e.g., user filters), see [Condition Builder](/documentation/components/libs/postgresql/condition-builder.md).
111+
110112
## JOINs
111113

112114
```php

src/lib/postgresql/src/Flow/PostgreSql/DSL/functions.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
Comparison,
4747
ComparisonOperator,
4848
Condition,
49+
ConditionBuilder,
4950
Exists,
5051
In,
5152
IsDistinctFrom,
@@ -1346,6 +1347,29 @@ function all_sub_select(Expression $left, ComparisonOperator $operator, SelectFi
13461347
return new All($left, $operator, $node);
13471348
}
13481349

1350+
/**
1351+
* Create a condition builder for fluent condition composition.
1352+
*
1353+
* This builder allows incremental condition building with a fluent API:
1354+
*
1355+
* ```php
1356+
* $builder = conditions();
1357+
*
1358+
* if ($hasFilter) {
1359+
* $builder = $builder->and(eq(col('status'), literal('active')));
1360+
* }
1361+
*
1362+
* if (!$builder->isEmpty()) {
1363+
* $query = select()->from(table('users'))->where($builder);
1364+
* }
1365+
* ```
1366+
*/
1367+
#[DocumentationDSL(module: Module::PG_QUERY, type: DSLType::HELPER)]
1368+
function conditions() : ConditionBuilder
1369+
{
1370+
return ConditionBuilder::create();
1371+
}
1372+
13491373
/**
13501374
* Combine conditions with AND.
13511375
*
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Flow\PostgreSql\QueryBuilder\Condition;
6+
7+
final readonly class ConditionBuilder
8+
{
9+
private function __construct(
10+
private ?Condition $condition = null,
11+
) {
12+
}
13+
14+
public static function create() : self
15+
{
16+
return new self();
17+
}
18+
19+
public function and(Condition|self $condition) : self
20+
{
21+
$resolved = $condition instanceof self ? $condition->condition : $condition;
22+
23+
if ($resolved === null) {
24+
return $this;
25+
}
26+
27+
if ($this->condition === null) {
28+
return new self($resolved);
29+
}
30+
31+
return new self($this->condition->and($resolved));
32+
}
33+
34+
public function getCondition() : ?Condition
35+
{
36+
return $this->condition;
37+
}
38+
39+
public function isEmpty() : bool
40+
{
41+
return $this->condition === null;
42+
}
43+
44+
public function or(Condition|self $condition) : self
45+
{
46+
$resolved = $condition instanceof self ? $condition->condition : $condition;
47+
48+
if ($resolved === null) {
49+
return $this;
50+
}
51+
52+
if ($this->condition === null) {
53+
return new self($resolved);
54+
}
55+
56+
return new self($this->condition->or($resolved));
57+
}
58+
}

src/lib/postgresql/src/Flow/PostgreSql/QueryBuilder/Exception/InvalidBuilderStateException.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@
66

77
final class InvalidBuilderStateException extends \InvalidArgumentException
88
{
9+
public static function emptyConditionBuilder() : self
10+
{
11+
return new self('Cannot use empty ConditionBuilder in where clause, use isEmpty() to check before passing to where()');
12+
}
13+
914
public static function mutuallyExclusiveOptions(string $option1, string $option2) : self
1015
{
1116
return new self(\sprintf(

src/lib/postgresql/src/Flow/PostgreSql/QueryBuilder/Select/SelectBuilder.php

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
use Flow\PostgreSql\Protobuf\AST\{LimitOption, Node, ResTarget, SelectStmt as ProtobufSelectStmt};
88
use Flow\PostgreSql\QueryBuilder\AstToSql;
99
use Flow\PostgreSql\QueryBuilder\Clause\{LockingClause, OrderBy, WindowDefinition, WithClause};
10-
use Flow\PostgreSql\QueryBuilder\Condition\{Condition, ConditionFactory};
11-
use Flow\PostgreSql\QueryBuilder\Exception\InvalidAstException;
10+
use Flow\PostgreSql\QueryBuilder\Condition\{Condition, ConditionBuilder, ConditionFactory};
11+
use Flow\PostgreSql\QueryBuilder\Exception\{InvalidAstException, InvalidBuilderStateException};
1212
use Flow\PostgreSql\QueryBuilder\Expression\{AliasedExpression, Expression, ExpressionFactory, Literal};
1313
use Flow\PostgreSql\QueryBuilder\Table\{AliasedTable, DerivedTable, JoinType, JoinedTable, Table, TableFunction, TableReference};
1414

@@ -773,8 +773,18 @@ public function unionAll(SelectFinalStep $other) : SelectOrderByStep
773773
);
774774
}
775775

776-
public function where(Condition $condition) : SelectGroupByStep
776+
public function where(Condition|ConditionBuilder $condition) : SelectGroupByStep
777777
{
778+
if ($condition instanceof ConditionBuilder) {
779+
$resolved = $condition->getCondition();
780+
781+
if ($resolved === null) {
782+
throw InvalidBuilderStateException::emptyConditionBuilder();
783+
}
784+
785+
$condition = $resolved;
786+
}
787+
778788
return new self(
779789
with: $this->with,
780790
selectList: $this->selectList,

src/lib/postgresql/src/Flow/PostgreSql/QueryBuilder/Select/SelectWhereStep.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44

55
namespace Flow\PostgreSql\QueryBuilder\Select;
66

7-
use Flow\PostgreSql\QueryBuilder\Condition\Condition;
7+
use Flow\PostgreSql\QueryBuilder\Condition\{Condition, ConditionBuilder};
88

99
interface SelectWhereStep extends SelectGroupByStep
1010
{
11-
public function where(Condition $condition) : SelectGroupByStep;
11+
public function where(Condition|ConditionBuilder $condition) : SelectGroupByStep;
1212
}

0 commit comments

Comments
 (0)