Skip to content

Commit edfb576

Browse files
committed
Merge pull request #22 from moufmouf/1.1
Adding support for CASE WHEN... constructs
2 parents f00cd75 + a10a005 commit edfb576

File tree

6 files changed

+291
-17
lines changed

6 files changed

+291
-17
lines changed

src/SQLParser/Node/CaseOperation.php

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
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 CASE ... END statement.
14+
*
15+
* @author David Négrier <[email protected]>
16+
*/
17+
class CaseOperation implements NodeInterface
18+
{
19+
private $operation;
20+
21+
public function getOperation()
22+
{
23+
return $this->operation;
24+
}
25+
26+
/**
27+
* Sets the operation.
28+
*
29+
* @Important
30+
*
31+
* @param NodeInterface|NodeInterface[]|string $operation
32+
*/
33+
public function setOperation($operation)
34+
{
35+
$this->operation = $operation;
36+
}
37+
38+
/**
39+
* Returns a Mouf instance descriptor describing this object.
40+
*
41+
* @param MoufManager $moufManager
42+
*
43+
* @return MoufInstanceDescriptor
44+
*/
45+
public function toInstanceDescriptor(MoufManager $moufManager)
46+
{
47+
$instanceDescriptor = $moufManager->createInstance(get_called_class());
48+
$instanceDescriptor->getProperty('operation')->setValue(NodeFactory::nodeToInstanceDescriptor($this->operation, $moufManager));
49+
50+
return $instanceDescriptor;
51+
}
52+
53+
/**
54+
* Renders the object as a SQL string.
55+
*
56+
* @param Connection $dbConnection
57+
* @param array $parameters
58+
* @param number $indent
59+
* @param int $conditionsMode
60+
*
61+
* @return string
62+
*/
63+
public function toSql(array $parameters = array(), Connection $dbConnection = null, $indent = 0, $conditionsMode = self::CONDITION_APPLY)
64+
{
65+
66+
$sql = 'CASE '.NodeFactory::toSql($this->operation, $dbConnection, $parameters, ' ', false, $indent, $conditionsMode).' END';
67+
68+
return $sql;
69+
}
70+
71+
/**
72+
* Walks the tree of nodes, calling the visitor passed in parameter.
73+
*
74+
* @param VisitorInterface $visitor
75+
*/
76+
public function walk(VisitorInterface $visitor) {
77+
$node = $this;
78+
$result = $visitor->enterNode($node);
79+
if ($result instanceof NodeInterface) {
80+
$node = $result;
81+
}
82+
if ($result !== NodeTraverser::DONT_TRAVERSE_CHILDREN) {
83+
$result2 = $this->operation->walk($visitor);
84+
if ($result2 === NodeTraverser::REMOVE_NODE) {
85+
return NodeTraverser::REMOVE_NODE;
86+
} elseif ($result2 instanceof NodeInterface) {
87+
$this->operation = $result2;
88+
}
89+
}
90+
return $visitor->leaveNode($node);
91+
}
92+
}

src/SQLParser/Node/ElseOperation.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
namespace SQLParser\Node;
4+
5+
/**
6+
* This class represents a ELSE (in a CASE WHEN ... THEN ... ELSE ... END construct)
7+
*
8+
* @author David Négrier <[email protected]>
9+
*/
10+
class ElseOperation extends AbstractTwoOperandsOperator
11+
{
12+
/**
13+
* Returns the symbol for this operator.
14+
*
15+
* @return string
16+
*/
17+
protected function getOperatorSymbol()
18+
{
19+
return 'ELSE';
20+
}
21+
}

src/SQLParser/Node/NodeFactory.php

Lines changed: 72 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -293,24 +293,42 @@ public static function toObject(array $desc)
293293

294294
return $expr;
295295
case ExpressionType::RESERVED:
296-
$res = new Reserved();
297-
$res->setBaseExpression($desc['base_expr']);
296+
if (in_array(strtoupper($desc['base_expr']), ['CASE', 'WHEN', 'THEN', 'ELSE', 'END'])) {
297+
$operator = new Operator();
298+
$operator->setValue($desc['base_expr']);
299+
// Debug:
300+
unset($desc['base_expr']);
301+
unset($desc['expr_type']);
302+
if (!empty($desc['sub_tree'])) {
303+
throw new \InvalidArgumentException('Unexpected operator with subtree: '.var_export($desc['sub_tree'], true));
304+
}
305+
unset($desc['sub_tree']);
306+
if (!empty($desc)) {
307+
throw new \InvalidArgumentException('Unexpected parameters in exception: '.var_export($desc, true));
308+
}
298309

299-
if ($desc['expr_type'] == ExpressionType::BRACKET_EXPRESSION) {
300-
$res->setBrackets(true);
301-
}
310+
return $operator;
311+
} else {
302312

303-
// Debug:
304-
unset($desc['base_expr']);
305-
unset($desc['expr_type']);
306-
unset($desc['sub_tree']);
307-
unset($desc['alias']);
308-
unset($desc['direction']);
309-
if (!empty($desc)) {
310-
throw new \InvalidArgumentException('Unexpected parameters in exception: '.var_export($desc, true));
311-
}
313+
$res = new Reserved();
314+
$res->setBaseExpression($desc['base_expr']);
315+
316+
if ($desc['expr_type'] == ExpressionType::BRACKET_EXPRESSION) {
317+
$res->setBrackets(true);
318+
}
312319

313-
return $res;
320+
// Debug:
321+
unset($desc['base_expr']);
322+
unset($desc['expr_type']);
323+
unset($desc['sub_tree']);
324+
unset($desc['alias']);
325+
unset($desc['direction']);
326+
if (!empty($desc)) {
327+
throw new \InvalidArgumentException('Unexpected parameters in exception: '.var_export($desc, true));
328+
}
329+
330+
return $res;
331+
}
314332
case ExpressionType::USER_VARIABLE:
315333
case ExpressionType::SESSION_VARIABLE:
316334
case ExpressionType::GLOBAL_VARIABLE:
@@ -402,7 +420,10 @@ private static function buildFromSubtree($subTree)
402420
array('|'),
403421
array('=' /*(comparison)*/, '<=>', '>=', '>', '<=', '<', '<>', '!=', 'IS', 'LIKE', 'REGEXP', 'IN', 'IS NOT', 'NOT IN'),
404422
array('AND_FROM_BETWEEN'),
405-
array('BETWEEN', 'CASE', 'WHEN', 'THEN', 'ELSE'),
423+
array('THEN'),
424+
array('WHEN'),
425+
array('ELSE'),
426+
array('BETWEEN', 'CASE', 'END'),
406427
array('NOT'),
407428
array('&&', 'AND'),
408429
array('XOR'),
@@ -441,6 +462,8 @@ private static function buildFromSubtree($subTree)
441462
'||' => 'SQLParser\Node\OrOp',
442463
'OR' => 'SQLParser\Node\OrOp',
443464
'XOR' => 'SQLParser\Node\XorOp',
465+
'THEN' => 'SQLParser\Node\Then',
466+
'ELSE' => 'SQLParser\Node\ElseOperation'
444467
);
445468

446469
/**
@@ -543,8 +566,13 @@ public static function simplify($nodes)
543566
$tmpOperators = $selectedOperators;
544567
$nextOperator = array_shift($tmpOperators);
545568

569+
$isSelectedOperatorFirst = null;
570+
546571
foreach ($nodes as $node) {
547572
if ($node === $nextOperator) {
573+
if ($isSelectedOperatorFirst === null) {
574+
$isSelectedOperatorFirst = true;
575+
}
548576
// Let's apply the "simplify" method on the operand before storing it.
549577
//$operands[] = self::simplify($operand);
550578
$simple = self::simplify($operand);
@@ -557,6 +585,9 @@ public static function simplify($nodes)
557585
$operand = array();
558586
$nextOperator = array_shift($tmpOperators);
559587
} else {
588+
if ($isSelectedOperatorFirst === null) {
589+
$isSelectedOperatorFirst = false;
590+
}
560591
$operand[] = $node;
561592
}
562593
}
@@ -587,7 +618,6 @@ public static function simplify($nodes)
587618
array('INTERVAL'),
588619
array('BINARY', 'COLLATE'),
589620
array('!'),
590-
array('BETWEEN', 'CASE', 'WHEN', 'THEN', 'ELSE'),
591621
array('NOT'),
592622
*/
593623

@@ -625,6 +655,31 @@ public static function simplify($nodes)
625655
$instance->setMaxValueOperand($maxOperand);
626656

627657
return $instance;
658+
} elseif ($operation === 'WHEN') {
659+
660+
$instance = new WhenConditions();
661+
662+
if (!$isSelectedOperatorFirst) {
663+
$value = array_shift($operands);
664+
$instance->setValue($value);
665+
}
666+
$instance->setOperands($operands);
667+
668+
return $instance;
669+
} elseif ($operation === 'CASE') {
670+
$innerOperation = array_shift($operands);
671+
672+
if (!empty($operands)) {
673+
throw new MagicQueryException('A CASE statement should contain only a ThenConditions or a ElseOperand object.');
674+
}
675+
676+
$instance = new CaseOperation();
677+
$instance->setOperation($innerOperation);
678+
return $instance;
679+
} elseif ($operation === 'END') {
680+
// Simply bypass the END operation. We already have a CASE matching node:
681+
$caseOperation = array_shift($operands);
682+
return $caseOperation;
628683
} else {
629684
$instance = new Operation();
630685
$instance->setOperatorSymbol($operation);

src/SQLParser/Node/Then.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
namespace SQLParser\Node;
4+
5+
/**
6+
* This class represents a THEN (in a CASE WHEN ... THEN ... END construct)
7+
*
8+
* @author David Négrier <[email protected]>
9+
*/
10+
class Then extends AbstractTwoOperandsOperator
11+
{
12+
/**
13+
* Returns the symbol for this operator.
14+
*
15+
* @return string
16+
*/
17+
protected function getOperatorSymbol()
18+
{
19+
return 'THEN';
20+
}
21+
}

src/SQLParser/Node/WhenConditions.php

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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 set of ... WHEN ... THEN ... construct (inside a CASE).
14+
*
15+
* @author David Négrier <[email protected]>
16+
*/
17+
class WhenConditions extends AbstractManyInstancesOperator
18+
{
19+
20+
private $value;
21+
22+
public function getValue()
23+
{
24+
return $this->value;
25+
}
26+
27+
/**
28+
* Sets the value.
29+
*
30+
* @Important
31+
*
32+
* @param NodeInterface|NodeInterface[]|string $value
33+
*/
34+
public function setValue($value)
35+
{
36+
$this->value = $value;
37+
}
38+
39+
/**
40+
* Renders the object as a SQL string.
41+
*
42+
* @param Connection $dbConnection
43+
* @param array $parameters
44+
* @param number $indent
45+
* @param int $conditionsMode
46+
*
47+
* @return string
48+
*/
49+
public function toSql(array $parameters = array(), Connection $dbConnection = null, $indent = 0, $conditionsMode = self::CONDITION_APPLY)
50+
{
51+
$fullSql = '';
52+
53+
if ($this->value) {
54+
$fullSql = NodeFactory::toSql($this->value, $dbConnection, $parameters, ' ', false, $indent, $conditionsMode);
55+
}
56+
57+
foreach ($this->getOperands() as $operand) {
58+
$sql = NodeFactory::toSql($operand, $dbConnection, $parameters, ' ', false, $indent, $conditionsMode);
59+
if ($sql != null) {
60+
$fullSql .= "\n".str_repeat(' ', $indent).'WHEN '.$sql;
61+
}
62+
}
63+
64+
return $fullSql;
65+
}
66+
67+
/**
68+
* Returns the symbol for this operator.
69+
*
70+
* @return string
71+
*/
72+
protected function getOperatorSymbol()
73+
{
74+
return 'WHEN';
75+
}
76+
}

tests/Mouf/Database/MagicQueryTest.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,15 @@ public function testStandardSelect()
8888
// Tests parameters with a ! (to force NULL values)
8989
$sql = 'SELECT * FROM users WHERE status = :status!';
9090
$this->assertEquals("SELECT * FROM users WHERE status = null", self::simplifySql($magicQuery->build($sql, ['status' => null])));
91+
92+
// Test CASE WHERE
93+
$sql = "SELECT CASE WHEN status = 'on' THEN '1' WHEN status = 'off' THEN '0' ELSE '-1' END AS my_case FROM users";
94+
$this->assertEquals("SELECT CASE WHEN status = 'on' THEN '1' WHEN status = 'off' THEN '0' ELSE '-1' END AS my_case FROM users", self::simplifySql($magicQuery->build($sql)));
95+
96+
// Test CASE WHERE like SWITCH CASE
97+
$sql = "SELECT CASE status WHEN 'on' THEN '1' WHEN 'off' THEN '0' ELSE '-1' END AS my_case FROM users";
98+
$this->assertEquals("SELECT CASE status WHEN 'on' THEN '1' WHEN 'off' THEN '0' ELSE '-1' END AS my_case FROM users", self::simplifySql($magicQuery->build($sql)));
99+
91100
}
92101

93102
public function testWithCache() {

0 commit comments

Comments
 (0)