Skip to content

Commit 9887d04

Browse files
committed
Merge pull request #13 from moufmouf/1.1
Adding support for BETWEEN filter. Fixes #11
2 parents 2fa2af9 + 03baa82 commit 9887d04

File tree

5 files changed

+303
-15
lines changed

5 files changed

+303
-15
lines changed

doc/discard_unused_parameters.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,17 @@ $result2 = $magicQuery->build($sql, []);
2424
// The whole WHERE condition disappeared because it is not needed anymore!
2525
```
2626

27+
Magic-parameters can also collapse a BETWEEN filter into a simple `>=` or `<=` filter:
28+
29+
```php
30+
$sql = "SELECT * FROM products WHERE status BETWEEN :lowerStatus AND :upperStatus";
31+
32+
// Let's pass only the "lowerStatus" parameter
33+
$result = $magicQuery->build($sql, [ "lowerStatus" => 2 ]);
34+
// $result = SELECT * FROM products WHERE status >= 2
35+
// See? The BETWEEN filter was transformed in a >= filter because we did not provide a higher limit.
36+
```
37+
2738
###Why should I care?
2839

2940
Because it is **the most efficient way to deal with queries that can have a variable number of parameters**!

src/SQLParser/Node/Between.php

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
<?php
2+
3+
namespace SQLParser\Node;
4+
5+
use Mouf\Utils\Common\ConditionInterface\ConditionTrait;
6+
use Doctrine\DBAL\Connection;
7+
use Mouf\MoufManager;
8+
use Mouf\MoufInstanceDescriptor;
9+
use SQLParser\Node\Traverser\NodeTraverser;
10+
use SQLParser\Node\Traverser\VisitorInterface;
11+
12+
/**
13+
* This class represents a BETWEEN operation.
14+
*
15+
* @author David Négrier <[email protected]>
16+
*/
17+
class Between implements NodeInterface
18+
{
19+
private $leftOperand;
20+
21+
public function getLeftOperand()
22+
{
23+
return $this->leftOperand;
24+
}
25+
26+
/**
27+
* Sets the leftOperand.
28+
*
29+
* @Important
30+
*
31+
* @param NodeInterface|NodeInterface[]|string $leftOperand
32+
*/
33+
public function setLeftOperand($leftOperand)
34+
{
35+
$this->leftOperand = $leftOperand;
36+
}
37+
38+
/**
39+
* @var string|NodeInterface|NodeInterface[]
40+
*/
41+
private $minValueOperand;
42+
43+
/**
44+
* @var string|NodeInterface|NodeInterface[]
45+
*/
46+
private $maxValueOperand;
47+
48+
/**
49+
* @return NodeInterface|NodeInterface[]|string
50+
*/
51+
public function getMinValueOperand()
52+
{
53+
return $this->minValueOperand;
54+
}
55+
56+
/**
57+
* @param NodeInterface|NodeInterface[]|string $minValueOperand
58+
*/
59+
public function setMinValueOperand($minValueOperand)
60+
{
61+
$this->minValueOperand = $minValueOperand;
62+
}
63+
64+
/**
65+
* @return NodeInterface|NodeInterface[]|string
66+
*/
67+
public function getMaxValueOperand()
68+
{
69+
return $this->maxValueOperand;
70+
}
71+
72+
/**
73+
* @param NodeInterface|NodeInterface[]|string $maxValueOperand
74+
*/
75+
public function setMaxValueOperand($maxValueOperand)
76+
{
77+
$this->maxValueOperand = $maxValueOperand;
78+
}
79+
80+
/**
81+
* @var ConditionInterface
82+
*/
83+
protected $minValueCondition;
84+
85+
/**
86+
* Sets the condition.
87+
*
88+
* @Important IfSet
89+
* @param ConditionInterface $minValueCondition
90+
*/
91+
public function setMinValueCondition(ConditionInterface $minValueCondition = null) {
92+
$this->minValueCondition = $minValueCondition;
93+
}
94+
95+
/**
96+
* @var ConditionInterface
97+
*/
98+
protected $maxValueCondition;
99+
100+
/**
101+
* Sets the condition.
102+
*
103+
* @Important IfSet
104+
* @param ConditionInterface $maxValueCondition
105+
*/
106+
public function setMaxValueCondition(ConditionInterface $maxValueCondition = null) {
107+
$this->maxValueCondition = $maxValueCondition;
108+
}
109+
110+
111+
/**
112+
* Returns a Mouf instance descriptor describing this object.
113+
*
114+
* @param MoufManager $moufManager
115+
*
116+
* @return MoufInstanceDescriptor
117+
*/
118+
public function toInstanceDescriptor(MoufManager $moufManager)
119+
{
120+
$instanceDescriptor = $moufManager->createInstance(get_called_class());
121+
$instanceDescriptor->getProperty('leftOperand')->setValue(NodeFactory::nodeToInstanceDescriptor($this->leftOperand, $moufManager));
122+
$instanceDescriptor->getProperty('minValueOperand')->setValue(NodeFactory::nodeToInstanceDescriptor($this->minValueOperand, $moufManager));
123+
$instanceDescriptor->getProperty('maxValueOperand')->setValue(NodeFactory::nodeToInstanceDescriptor($this->maxValueOperand, $moufManager));
124+
125+
if ($this->minValueOperand instanceof Parameter) {
126+
// Let's add a condition on the parameter.
127+
$conditionDescriptor = $moufManager->createInstance('Mouf\\Database\\QueryWriter\\Condition\\ParamAvailableCondition');
128+
$conditionDescriptor->getProperty('parameterName')->setValue($this->minValueOperand->getName());
129+
$instanceDescriptor->getProperty('minValueCondition')->setValue($conditionDescriptor);
130+
}
131+
132+
if ($this->maxValueOperand instanceof Parameter) {
133+
// Let's add a condition on the parameter.
134+
$conditionDescriptor = $moufManager->createInstance('Mouf\\Database\\QueryWriter\\Condition\\ParamAvailableCondition');
135+
$conditionDescriptor->getProperty('parameterName')->setValue($this->maxValueOperand->getName());
136+
$instanceDescriptor->getProperty('maxValueCondition')->setValue($conditionDescriptor);
137+
}
138+
139+
return $instanceDescriptor;
140+
}
141+
142+
/**
143+
* Renders the object as a SQL string.
144+
*
145+
* @param Connection $dbConnection
146+
* @param array $parameters
147+
* @param number $indent
148+
* @param int $conditionsMode
149+
*
150+
* @return string
151+
*/
152+
public function toSql(array $parameters = array(), Connection $dbConnection = null, $indent = 0, $conditionsMode = self::CONDITION_APPLY)
153+
{
154+
$minBypass = false;
155+
$maxBypass = false;
156+
157+
if ($conditionsMode == self::CONDITION_GUESS) {
158+
if ($this->minValueOperand instanceof Parameter) {
159+
if ($this->minValueOperand->isDiscardedOnNull() && !isset($parameters[$this->minValueOperand->getName()])) {
160+
$minBypass = true;
161+
}
162+
}
163+
164+
if ($this->maxValueOperand instanceof Parameter) {
165+
if ($this->maxValueOperand->isDiscardedOnNull() && !isset($parameters[$this->maxValueOperand->getName()])) {
166+
$maxBypass = true;
167+
}
168+
}
169+
} elseif ($conditionsMode == self::CONDITION_IGNORE) {
170+
$minBypass = false;
171+
$maxBypass = false;
172+
} else {
173+
if ($this->minValueCondition && !$this->minValueCondition->isOk($parameters)) {
174+
$minBypass = true;
175+
}
176+
if ($this->maxValueCondition && !$this->maxValueCondition->isOk($parameters)) {
177+
$maxBypass = true;
178+
}
179+
}
180+
181+
if (!$minBypass && !$maxBypass) {
182+
$sql = NodeFactory::toSql($this->leftOperand, $dbConnection, $parameters, ' ', false, $indent, $conditionsMode);
183+
$sql .= ' BETWEEN ';
184+
$sql .= NodeFactory::toSql($this->minValueOperand, $dbConnection, $parameters, ' ', false, $indent, $conditionsMode);
185+
$sql .= ' AND ';
186+
$sql .= NodeFactory::toSql($this->maxValueOperand, $dbConnection, $parameters, ' ', false, $indent, $conditionsMode);
187+
} elseif (!$minBypass && $maxBypass) {
188+
$sql = NodeFactory::toSql($this->leftOperand, $dbConnection, $parameters, ' ', false, $indent, $conditionsMode);
189+
$sql .= ' >= ';
190+
$sql .= NodeFactory::toSql($this->minValueOperand, $dbConnection, $parameters, ' ', false, $indent, $conditionsMode);
191+
} elseif ($minBypass && !$maxBypass) {
192+
$sql = NodeFactory::toSql($this->leftOperand, $dbConnection, $parameters, ' ', false, $indent, $conditionsMode);
193+
$sql .= ' <= ';
194+
$sql .= NodeFactory::toSql($this->maxValueOperand, $dbConnection, $parameters, ' ', false, $indent, $conditionsMode);
195+
} else {
196+
$sql = null;
197+
}
198+
199+
return $sql;
200+
}
201+
202+
/**
203+
* Walks the tree of nodes, calling the visitor passed in parameter.
204+
*
205+
* @param VisitorInterface $visitor
206+
*/
207+
public function walk(VisitorInterface $visitor) {
208+
$node = $this;
209+
$result = $visitor->enterNode($node);
210+
if ($result instanceof NodeInterface) {
211+
$node = $result;
212+
}
213+
if ($result !== NodeTraverser::DONT_TRAVERSE_CHILDREN) {
214+
$result2 = $this->leftOperand->walk($visitor);
215+
if ($result2 === NodeTraverser::REMOVE_NODE) {
216+
return NodeTraverser::REMOVE_NODE;
217+
} elseif ($result2 instanceof NodeInterface) {
218+
$this->leftOperand = $result2;
219+
}
220+
221+
$result2 = $this->minValueOperand->walk($visitor);
222+
if ($result2 === NodeTraverser::REMOVE_NODE) {
223+
return NodeTraverser::REMOVE_NODE;
224+
} elseif ($result2 instanceof NodeInterface) {
225+
$this->minValueOperand = $result2;
226+
}
227+
228+
$result2 = $this->maxValueOperand->walk($visitor);
229+
if ($result2 === NodeTraverser::REMOVE_NODE) {
230+
return NodeTraverser::REMOVE_NODE;
231+
} elseif ($result2 instanceof NodeInterface) {
232+
$this->maxValueOperand = $result2;
233+
}
234+
}
235+
return $visitor->leaveNode($node);
236+
}
237+
}

src/SQLParser/Node/NodeFactory.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333

3434
namespace SQLParser\Node;
3535

36+
use Mouf\Database\MagicQueryException;
3637
use SQLParser\SqlRenderInterface;
3738
use Doctrine\DBAL\Connection;
3839
use Mouf\MoufManager;
@@ -335,6 +336,7 @@ private static function buildFromSubtree($subTree)
335336
array('&'),
336337
array('|'),
337338
array('=' /*(comparison)*/, '<=>', '>=', '>', '<=', '<', '<>', '!=', 'IS', 'LIKE', 'REGEXP', 'IN', 'IS NOT', 'NOT IN'),
339+
array('AND_FROM_BETWEEN'),
338340
array('BETWEEN', 'CASE', 'WHEN', 'THEN', 'ELSE'),
339341
array('NOT'),
340342
array('&&', 'AND'),
@@ -534,6 +536,23 @@ public static function simplify($nodes)
534536
$instance = new self::$OPERATOR_TO_CLASS[$operation]();
535537
$instance->setOperands($operands);
536538

539+
return $instance;
540+
} elseif ($operation === 'BETWEEN') {
541+
$leftOperand = array_shift($operands);
542+
$rightOperand = array_shift($operands);
543+
if (!$rightOperand instanceof Operation || $rightOperand->getOperatorSymbol() !== 'AND_FROM_BETWEEN') {
544+
throw new MagicQueryException('Missing AND in BETWEEN filter.');
545+
}
546+
547+
$innerOperands = $rightOperand->getOperands();
548+
$minOperand = array_shift($innerOperands);
549+
$maxOperand = array_shift($innerOperands);
550+
551+
$instance = new Between();
552+
$instance->setLeftOperand($leftOperand);
553+
$instance->setMinValueOperand($minOperand);
554+
$instance->setMaxValueOperand($maxOperand);
555+
537556
return $instance;
538557
} else {
539558
$instance = new Operation();

src/SQLParser/Query/StatementFactory.php

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
namespace SQLParser\Query;
3535

3636
use SQLParser\Node\NodeFactory;
37+
use SQLParser\Node\Operator;
3738

3839
/**
3940
* This class has the ability to create instances implementing NodeInterface based on a descriptive array.
@@ -66,40 +67,30 @@ public static function toObject(array $desc)
6667
}
6768

6869
if (isset($desc['FROM'])) {
69-
$from = array_map(function ($item) {
70-
return NodeFactory::toObject($item);
71-
}, $desc['FROM']);
70+
$from = self::mapArrayToNodeObjectList($desc['FROM']);
7271
$select->setFrom($from);
7372
}
7473

7574
if (isset($desc['WHERE'])) {
76-
$where = array_map(function ($item) {
77-
return NodeFactory::toObject($item);
78-
}, $desc['WHERE']);
75+
$where = self::mapArrayToNodeObjectList($desc['WHERE']);
7976
$where = NodeFactory::simplify($where);
8077
$select->setWhere($where);
8178
}
8279

8380
if (isset($desc['GROUP'])) {
84-
$group = array_map(function ($item) {
85-
return NodeFactory::toObject($item);
86-
}, $desc['GROUP']);
81+
$group = self::mapArrayToNodeObjectList($desc['GROUP']);
8782
$group = NodeFactory::simplify($group);
8883
$select->setGroup($group);
8984
}
9085

9186
if (isset($desc['HAVING'])) {
92-
$having = array_map(function ($item) {
93-
return NodeFactory::toObject($item);
94-
}, $desc['HAVING']);
87+
$having = self::mapArrayToNodeObjectList($desc['HAVING']);
9588
$having = NodeFactory::simplify($having);
9689
$select->setHaving($having);
9790
}
9891

9992
if (isset($desc['ORDER'])) {
100-
$order = array_map(function ($item) {
101-
return NodeFactory::toObject($item);
102-
}, $desc['ORDER']);
93+
$order = self::mapArrayToNodeObjectList($desc['ORDER']);
10394
$order = NodeFactory::simplify($order);
10495
$select->setOrder($order);
10596
}
@@ -109,4 +100,28 @@ public static function toObject(array $desc)
109100
throw new \BadMethodCallException('Unknown query');
110101
}
111102
}
103+
104+
/**
105+
* @param array $items An array of objects represented as SQLParser arrays.
106+
*/
107+
private static function mapArrayToNodeObjectList(array $items) {
108+
$list = [];
109+
110+
$nextAndPartOfBetween = false;
111+
112+
// Special case, let's replace the AND of a between with a ANDBETWEEN object.
113+
foreach ($items as $item) {
114+
$obj = NodeFactory::toObject($item);
115+
if ($obj instanceof Operator) {
116+
if ($obj->getValue() == 'BETWEEN') {
117+
$nextAndPartOfBetween = true;
118+
} elseif ($nextAndPartOfBetween && $obj->getValue() == 'AND') {
119+
$nextAndPartOfBetween = false;
120+
$obj->setValue('AND_FROM_BETWEEN');
121+
}
122+
}
123+
$list[] = $obj;
124+
}
125+
return $list;
126+
}
112127
}

tests/Mouf/Database/MagicQueryTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ public function testStandardSelect()
2626
$sql = 'SELECT * FROM users WHERE status in :status';
2727
$this->assertEquals("SELECT * FROM users WHERE status IN ('2','4')", self::simplifySql($magicQuery->build($sql, ['status' => [2,4]])));
2828

29+
$sql = 'SELECT * FROM myTable where someField BETWEEN :value1 AND :value2';
30+
$this->assertEquals("SELECT * FROM myTable WHERE someField BETWEEN '2' AND '4'", self::simplifySql($magicQuery->build($sql, ['value1' => 2, 'value2' => 4])));
31+
$this->assertEquals("SELECT * FROM myTable WHERE someField >= '2'", self::simplifySql($magicQuery->build($sql, ['value1' => 2])));
32+
$this->assertEquals("SELECT * FROM myTable WHERE someField <= '4'", self::simplifySql($magicQuery->build($sql, ['value2' => 4])));
33+
$this->assertEquals("SELECT * FROM myTable", self::simplifySql($magicQuery->build($sql, [])));
34+
2935
// Triggers an "expression"
3036
// TODO: find why it fails!
3137
//$sql = 'SELECT * FROM (users) WHERE name LIKE :name';

0 commit comments

Comments
 (0)