Skip to content

Commit 7c13842

Browse files
authored
Cast datetimes and add 'not between' operator (#22) /ht @sgjackman & @wpanec-uno
* Added 'not_between' operator support to the join supporting parser * Fixed datetimes so that they are cast correctly from Carbon instances. * Ensure that all the code paths are tested
1 parent 2544861 commit 7c13842

File tree

8 files changed

+195
-22
lines changed

8 files changed

+195
-22
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
vendor/**
2+
.idea/
3+
composer.lock

src/QueryBuilderParser/JoinSupportingQueryBuilderParser.php

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,13 @@ private function buildRequireArrayQuery($subclause, Builder $query)
164164
}
165165

166166
$query->whereBetween($subclause['to_value_column'], $subclause['value']);
167+
} elseif ($subclause['operator'] == 'NOT BETWEEN') {
168+
if (count($subclause['value']) !== 2) {
169+
throw new QBParseException($subclause['to_value_column'].
170+
' should be an array with only two items.');
171+
}
172+
173+
$query->whereNotBetween($subclause['to_value_column'], $subclause['value']);
167174
}
168175

169176
return $query;
@@ -192,9 +199,9 @@ private function buildRequireNotArrayQuery($subclause, Builder $query)
192199
*/
193200
private function buildSubclauseWithNull($subclause, Builder $query, $isNotNull = false)
194201
{
195-
if ($isNotNull === true) {
196-
return $query->whereNotNull($subclause['to_value_column']);
197-
}
202+
if ($isNotNull === true) {
203+
return $query->whereNotNull($subclause['to_value_column']);
204+
}
198205

199206
return $query->whereNull($subclause['to_value_column']);
200207
}

src/QueryBuilderParser/QBPFunctions.php

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
use \Illuminate\Database\Query\Builder;
55
use \stdClass;
6+
use \Carbon\Carbon;
67

78
trait QBPFunctions
89
{
@@ -21,6 +22,7 @@ abstract protected function checkRuleCorrect(stdClass $rule);
2122
'greater' => array ('accept_values' => true, 'apply_to' => ['number', 'datetime']),
2223
'greater_or_equal' => array ('accept_values' => true, 'apply_to' => ['number', 'datetime']),
2324
'between' => array ('accept_values' => true, 'apply_to' => ['number', 'datetime']),
25+
'not_between' => array ('accept_values' => true, 'apply_to' => ['number', 'datetime']),
2426
'begins_with' => array ('accept_values' => true, 'apply_to' => ['string']),
2527
'not_begins_with' => array ('accept_values' => true, 'apply_to' => ['string']),
2628
'contains' => array ('accept_values' => true, 'apply_to' => ['string']),
@@ -43,6 +45,7 @@ abstract protected function checkRuleCorrect(stdClass $rule);
4345
'greater' => array ('operator' => '>'),
4446
'greater_or_equal' => array ('operator' => '>='),
4547
'between' => array ('operator' => 'BETWEEN'),
48+
'not_between' => array ('operator' => 'NOT BETWEEN'),
4649
'begins_with' => array ('operator' => 'LIKE', 'prepend' => '%'),
4750
'not_begins_with' => array ('operator' => 'NOT LIKE', 'prepend' => '%'),
4851
'contains' => array ('operator' => 'LIKE', 'append' => '%', 'prepend' => '%'),
@@ -56,7 +59,7 @@ abstract protected function checkRuleCorrect(stdClass $rule);
5659
);
5760

5861
protected $needs_array = array(
59-
'IN', 'NOT IN', 'BETWEEN',
62+
'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN',
6063
);
6164

6265
/**
@@ -157,6 +160,24 @@ protected function convertArrayToFlatValue($field, $value)
157160
return $value[0];
158161
}
159162

163+
/**
164+
* Convert a Datetime field to Carbon items to be used for comparisons.
165+
*
166+
* @param $value
167+
* @return \Carbon\Carbon
168+
* @throws QBParseException
169+
*/
170+
protected function convertDatetimeToCarbon($value)
171+
{
172+
if (is_array($value)) {
173+
return array_map(function ($v) {
174+
return new Carbon($v);
175+
}, $value);
176+
}
177+
178+
return new Carbon($value);
179+
}
180+
160181
/**
161182
* Append or prepend a string to the query if required.
162183
*
@@ -253,8 +274,8 @@ protected function makeQueryWhenArray(Builder $query, stdClass $rule, array $sql
253274
{
254275
if ($sqlOperator['operator'] == 'IN' || $sqlOperator['operator'] == 'NOT IN') {
255276
return $this->makeArrayQueryIn($query, $rule, $sqlOperator['operator'], $value, $condition);
256-
} elseif ($sqlOperator['operator'] == 'BETWEEN') {
257-
return $this->makeArrayQueryBetween($query, $rule, $value, $condition);
277+
} elseif ($sqlOperator['operator'] == 'BETWEEN' || $sqlOperator['operator'] == 'NOT BETWEEN') {
278+
return $this->makeArrayQueryBetween($query, $rule, $sqlOperator['operator'], $value, $condition);
258279
}
259280

260281
throw new QBParseException('makeQueryWhenArray could not return a value');
@@ -266,9 +287,9 @@ protected function makeQueryWhenArray(Builder $query, stdClass $rule, array $sql
266287
* @param Builder $query
267288
* @param stdClass $rule
268289
* @param array $sqlOperator
269-
* @param array $value
270290
* @param string $condition
271291
*
292+
* @throws QBParseException when SQL operator is !null
272293
* @return Builder
273294
*/
274295
protected function makeQueryWhenNull(Builder $query, stdClass $rule, array $sqlOperator, $condition)
@@ -304,22 +325,27 @@ private function makeArrayQueryIn(Builder $query, stdClass $rule, $operator, arr
304325

305326

306327
/**
307-
* makeArrayQueryBetween, when the query is an IN or NOT IN...
328+
* makeArrayQueryBetween, when the query is a BETWEEN or NOT BETWEEN...
308329
*
309330
* @see makeQueryWhenArray
310331
* @param Builder $query
311332
* @param stdClass $rule
333+
* @param string operator the SQL operator used. [BETWEEN|NOT BETWEEN]
312334
* @param array $value
313335
* @param string $condition
314336
* @throws QBParseException when more then two items given for the between
315337
* @return Builder
316338
*/
317-
private function makeArrayQueryBetween(Builder $query, stdClass $rule, array $value, $condition)
339+
private function makeArrayQueryBetween(Builder $query, stdClass $rule, $operator, array $value, $condition)
318340
{
319341
if (count($value) !== 2) {
320342
throw new QBParseException("{$rule->field} should be an array with only two items.");
321343
}
322344

345+
if ( $operator == 'NOT BETWEEN' ) {
346+
return $query->whereNotBetween( $rule->field, $value, $condition );
347+
}
348+
323349
return $query->whereBetween($rule->field, $value, $condition);
324350
}
325351
}

src/QueryBuilderParser/QueryBuilderParser.php

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace timgws;
44

5+
use \Carbon\Carbon;
56
use \stdClass;
67
use \Illuminate\Database\Query\Builder;
78
use \timgws\QBParseException;
@@ -53,9 +54,9 @@ public function parse($json, Builder $querybuilder)
5354
/**
5455
* Called by parse, loops through all the rules to find out if nested or not.
5556
*
56-
* @param array $rules
57+
* @param array $rules
5758
* @param Builder $querybuilder
58-
* @param string $queryCondition
59+
* @param string $queryCondition
5960
*
6061
* @throws QBParseException
6162
*
@@ -111,7 +112,7 @@ protected function createNestedQuery(Builder $querybuilder, stdClass $rule, $con
111112

112113
$condition = $this->validateCondition($condition);
113114

114-
return $querybuilder->whereNested(function($query) use (&$rule, &$querybuilder, &$condition) {
115+
return $querybuilder->whereNested(function ($query) use (&$rule, &$querybuilder, &$condition) {
115116
foreach ($rule->rules as $loopRule) {
116117
$function = 'makeQuery';
117118

@@ -184,6 +185,13 @@ protected function getCorrectValue($operator, stdClass $rule, $value)
184185

185186
$value = $this->enforceArrayOrString($requireArray, $value, $field);
186187

188+
/*
189+
* Turn datetime into Carbon object so that it works with "between" operators etc.
190+
*/
191+
if ($rule->type == 'date') {
192+
$value = $this->convertDatetimeToCarbon($value);
193+
}
194+
187195
return $this->appendOperatorIfRequired($requireArray, $value, $sqlOperator);
188196
}
189197

@@ -195,9 +203,9 @@ protected function getCorrectValue($operator, stdClass $rule, $value)
195203
* Make sure that all the correct fields are in the rule object then add the expression to
196204
* the query that was given by the user to the QueryBuilder.
197205
*
198-
* @param Builder $query
206+
* @param Builder $query
199207
* @param stdClass $rule
200-
* @param string $queryCondition and/or...
208+
* @param string $queryCondition and/or...
201209
*
202210
* @throws QBParseException
203211
*
@@ -223,10 +231,10 @@ protected function makeQuery(Builder $query, stdClass $rule, $queryCondition = '
223231
* (This used to be part of makeQuery, where the name made sense, but I pulled it
224232
* out to reduce some duplicated code inside JoinSupportingQueryBuilder)
225233
*
226-
* @param Builder $query
234+
* @param Builder $query
227235
* @param stdClass $rule
228-
* @param mixed $value the value that needs to be queried in the database.
229-
* @param string $queryCondition and/or...
236+
* @param mixed $value the value that needs to be queried in the database.
237+
* @param string $queryCondition and/or...
230238
* @return Builder
231239
*/
232240
protected function convertIncomingQBtoQuery(Builder $query, stdClass $rule, $value, $queryCondition = 'AND')
@@ -241,7 +249,7 @@ protected function convertIncomingQBtoQuery(Builder $query, stdClass $rule, $val
241249

242250
if ($this->operatorRequiresArray($operator)) {
243251
return $this->makeQueryWhenArray($query, $rule, $sqlOperator, $value, $condition);
244-
} elseif($this->operatorIsNull($operator)) {
252+
} elseif ($this->operatorIsNull($operator)) {
245253
return $this->makeQueryWhenNull($query, $rule, $sqlOperator, $condition);
246254
}
247255

tests/CommonQueryBuilderTests.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ protected function getParserUnderTest($fields = null)
5252
return new QueryBuilderParser($fields);
5353
}
5454

55+
/**
56+
* @return Builder
57+
*/
5558
protected function createQueryBuilder()
5659
{
5760
$pdo = new \PDO('sqlite::memory:');

tests/JoinSupportingQueryBuilderParserTest.php

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,19 @@ public function testJoinBetween()
133133
$builder->toSql());
134134
}
135135

136+
public function testJoinNotBetween()
137+
{
138+
$json = '{"condition":"AND","rules":[{"id":"join1","field":"join1","type":"text","input":"select","operator":"not_between","value":["a","b"]}]}';
139+
140+
$builder = $this->createQueryBuilder();
141+
142+
$parser = $this->getParserUnderTest();
143+
$test = $parser->parse($json, $builder);
144+
145+
$this->assertEquals('select * where exists (select 1 from `subtable` where subtable.s_col = master.m_col and `s_value` not between ? and ?)',
146+
$builder->toSql());
147+
}
148+
136149
public function testJoinNotExistsBetween()
137150
{
138151
$json = '{"condition":"AND","rules":[{"id":"join2","field":"join2","type":"text","input":"select","operator":"between","value":["a","b"]}]}';
@@ -182,14 +195,36 @@ public function testJoinIsNotNull()
182195
*/
183196
public function testJoinNotExistsBetweenWithThreeItems()
184197
{
185-
$json = '{"condition":"AND","rules":[{"id":"join2","field":"join2","type":"text","input":"select","operator":"between","value":["a","b","c"]}]}';
198+
$this->_testJoinNotExistsBetweenWithThreeItems(false);
199+
}
200+
201+
/**
202+
* @expectedException timgws\QBParseException
203+
* @expectedExceptionMessage s2_value should be an array with only two items.
204+
*
205+
* @throws \timgws\QBParseException
206+
*/
207+
public function testJoinNotExistsNotBetweenWithThreeItems()
208+
{
209+
$this->_testJoinNotExistsBetweenWithThreeItems(true);
210+
}
211+
212+
/**
213+
* @see testJoinNotExistsBetweenWithThreeItems()
214+
* @see testJoinNotExistsNotBetweenWithThreeItems()
215+
*/
216+
private function _testJoinNotExistsBetweenWithThreeItems($not_between = false)
217+
{
218+
$json_operator = ($not_between ? 'not_' : '') . 'between';
219+
$sql_operator = ($not_between ? 'not ' : '') . 'between';
220+
$json = '{"condition":"AND","rules":[{"id":"join2","field":"join2","type":"text","input":"select","operator":"'. $json_operator . '","value":["a","b","c"]}]}';
186221

187222
$builder = $this->createQueryBuilder();
188223

189224
$parser = $this->getParserUnderTest();
190225
$parser->parse($json, $builder);
191226

192-
$this->assertEquals('select * where not exists (select 1 from `subtable2` where subtable2.s2_col = master2.m2_col and `s2_value` between ? and ?)',
227+
$this->assertEquals('select * where not exists (select 1 from `subtable2` where subtable2.s2_col = master2.m2_col and `s2_value` ' . $sql_operator . ' ? and ?)',
193228
$builder->toSql());
194229
}
195230

@@ -251,4 +286,46 @@ public function testCategoryIn()
251286

252287
$this->assertEquals('select * where `price` < ? and (`category` in (?, ?))', $builder->toSql());
253288
}
289+
290+
/**
291+
* Test for #21 (Cast datetimes and add 'not between' operator)
292+
*/
293+
public function testDateBetween()
294+
{
295+
$incoming = '{ "condition": "AND", "rules": [ { "id": "dollar_amount", "field": "dollar_amount", "type": "double", "input": "number", "operator": "less", "value": "546" }, { "id": "needed_by_date", "field": "needed_by_date", "type": "date", "input": "text", "operator": "between", "value": [ "10/22/2017", "10/28/2017" ] } ], "not": false, "valid": true }';
296+
$builder = $this->createQueryBuilder();
297+
$qb = $this->getParserUnderTest();
298+
299+
$qb->parse($incoming, $builder);
300+
301+
$this->assertEquals('select * where `dollar_amount` < ? and `needed_by_date` between ? and ?', $builder->toSql());
302+
303+
$bindings = $builder->getBindings();
304+
$this->assertCount(3, $bindings);
305+
$this->assertEquals('546', $bindings[0]);
306+
$this->assertInstanceOf("Carbon\\Carbon", $bindings[1]);
307+
$this->assertInstanceOf("Carbon\\Carbon", $bindings[2]);
308+
}
309+
310+
/**
311+
* Test for #21 (Cast datetimes and add 'not between' operator)
312+
*/
313+
public function testDateNotBetween()
314+
{
315+
$incoming = '{ "condition": "AND", "rules": [ { "id": "needed_by_date", "field": "needed_by_date", "type": "date", "input": "text", "operator": "not_between", "value": [ "10/22/2017", "10/28/2017" ] } ], "not": false, "valid": true }';
316+
$builder = $this->createQueryBuilder();
317+
$qb = $this->getParserUnderTest();
318+
319+
$qb->parse($incoming, $builder);
320+
321+
$this->assertEquals('select * where `needed_by_date` not between ? and ?', $builder->toSql());
322+
323+
$bindings = $builder->getBindings();
324+
$this->assertCount(2, $bindings);
325+
$this->assertInstanceOf("Carbon\\Carbon", $bindings[0]);
326+
$this->assertInstanceOf("Carbon\\Carbon", $bindings[1]);
327+
$this->assertEquals(2017, $bindings[0]->year);
328+
$this->assertEquals(22, $bindings[0]->day);
329+
$this->assertEquals(28, $bindings[1]->day);
330+
}
254331
}

tests/QBPFunctionsTest.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<?php
22
namespace timgws\test;
33

4+
use Carbon\Carbon;
45
use timgws\QBParseException;
56

67
/**
@@ -52,4 +53,31 @@ public function testOperatorNotValidForNull()
5253
$builder, $rule->rules[1], array('operator' => 'CONTAINS'), array('AND'), 'AND'
5354
]);
5455
}
56+
57+
public function testDate()
58+
{
59+
$method = self::getMethod('convertDatetimeToCarbon');
60+
61+
$qb = $this->getParserUnderTest();
62+
63+
/** @var Carbon $carbonDate */
64+
$carbonDate = $method->invokeArgs($qb, ['2010-12-11']);
65+
66+
$this->assertEquals('2010', $carbonDate->year);
67+
$this->assertEquals('12', $carbonDate->month);
68+
}
69+
70+
public function testDateArray()
71+
{
72+
$method = self::getMethod('convertDatetimeToCarbon');
73+
74+
$qb = $this->getParserUnderTest();
75+
76+
/** @var Carbon[] $carbonDate */
77+
$carbonDates = $method->invokeArgs($qb, [['2010-12-11', '2001-01-02']]);
78+
79+
$this->assertCount(2, $carbonDates);
80+
$this->assertEquals('2010', $carbonDates[0]->year);
81+
$this->assertEquals('2001', $carbonDates[1]->year);
82+
}
5583
}

0 commit comments

Comments
 (0)