Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
3cf58ce
Add function to parse query expressions
kodumbeats Apr 22, 2021
514bf70
Add method to parse custom filter queries
kodumbeats Apr 22, 2021
80eec64
Add tests for parsing queries
kodumbeats Apr 22, 2021
69e6bc7
Add test for parsing query expressions
kodumbeats Apr 22, 2021
cdf5267
Refactor query into standalone class
kodumbeats Apr 23, 2021
3f75885
Add getters for object properties
kodumbeats Apr 23, 2021
2a8a5b3
Add method to get all query details
kodumbeats Apr 23, 2021
f763ba4
Add return typing
kodumbeats Apr 23, 2021
06f02f7
Clean up code
kodumbeats Apr 23, 2021
e01fcc4
Call parseExpression as a static method
kodumbeats Apr 23, 2021
65a2166
Add tests for query class
kodumbeats Apr 23, 2021
1bb848d
Refactor parsing code into query class and tests
kodumbeats Apr 23, 2021
e802153
Add type hinting
kodumbeats Apr 23, 2021
83a09a3
Merge branch 'v0' into v0
kodumbeats Apr 23, 2021
c18b485
Add types to constructor params
kodumbeats Apr 23, 2021
b2f6603
Rename operand to value
kodumbeats Apr 23, 2021
f581824
Change to protected method
kodumbeats Apr 23, 2021
fe6becb
Use bracket syntax for switch
kodumbeats Apr 23, 2021
1c6e60c
Treat query expression as kv pair
kodumbeats Apr 23, 2021
179a0d5
Create query validator
kodumbeats Apr 23, 2021
8ada10e
Remove references to old var
kodumbeats Apr 23, 2021
8934ce7
Revert "Change to protected method"
kodumbeats Apr 23, 2021
38b02cf
Revert "Treat query expression as kv pair"
kodumbeats Apr 23, 2021
c2b9dc2
Use value instead of operand
kodumbeats Apr 23, 2021
8c8afb3
Merge branch 'v0' of github.com:kodumbeats/database into v0
kodumbeats Apr 23, 2021
7071923
Clarify intended behavior for parseExpression()
kodumbeats Apr 23, 2021
0ec83e0
Add and fix query tests
kodumbeats Apr 23, 2021
e3f1efb
Constructor expects array of $values by default
kodumbeats Apr 23, 2021
0659fe8
Properly scope parseExpression
kodumbeats Apr 23, 2021
6b3bcd2
Typecast expression values and handle comma-separated values
kodumbeats Apr 26, 2021
a1b472d
Test with integer param
kodumbeats Apr 26, 2021
e74da85
Test for typecasted bool expressions
kodumbeats Apr 26, 2021
f7e86b6
Test multiple query values
kodumbeats Apr 26, 2021
a14a730
Cleanup for readability
kodumbeats Apr 26, 2021
631ecfd
Reserve query validator for separate PR
kodumbeats Apr 26, 2021
4d6189b
Only consider periods oudside parentheses
kodumbeats Apr 26, 2021
1f4c64f
Test for float values
kodumbeats Apr 26, 2021
fa28286
Handle null as value
kodumbeats Apr 26, 2021
2ec2fc8
Refactor elseifs into switch
kodumbeats Apr 26, 2021
ab2a6b7
Use single array_map to trim whitespace and typecast
kodumbeats Apr 26, 2021
b01d9dd
Remove unneeded switch case
kodumbeats Apr 26, 2021
7db5c8f
Trim whitespace and remove escape slashes
kodumbeats Apr 26, 2021
40cd42c
Test for whitespace and escaped character
kodumbeats Apr 26, 2021
53cf8dc
Use strrpos to find last occurrence of parenthesis
kodumbeats Apr 26, 2021
ba852c2
Correct comments
kodumbeats Apr 26, 2021
87dd5b3
Test constructor function
kodumbeats Apr 26, 2021
1522284
Properly handle whitespace between values
kodumbeats Apr 26, 2021
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
179 changes: 179 additions & 0 deletions src/Database/Query.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
<?php

namespace Utopia\Database;

class Query
{
/**
* @var string
*/
protected $attribute = '';

/**
* @var string
*/
protected $operator = '';

/**
* @var (mixed)[]
*/
protected $values;

/**
* Construct.
*
* Construct a new query object
*
* @param string $attribute
* @param string $operator
* @param array $values
*/
public function __construct(string $attribute, string $operator, array $values)
{
$this->attribute = $attribute;
$this->operator = $operator;
$this->values = $values;
}

/**
* Get attribute
*
* @return string
*/
public function getAttribute(): string
{
return $this->attribute;
}

/**
* Get operator
*
* @return string
*/
public function getOperator(): string
{
return $this->operator;
}

/**
* Get operand
*
* @return mixed
*/
public function getValues()
{
return $this->values;
}

/**
* Get all query details as array
*
* @return array
*/
public function getQuery(): array
{
return [
'attribute' => $this->attribute,
'operator' => $this->operator,
'values' => $this->values,
];
}

/**
* Parse query filter
*
* @param string $filter
*
* @return Query
* */
public static function parse(string $filter): Query
{
// get index of open parentheses
$end = mb_strpos($filter, '(');
// count stanzas by only counting '.' that come before open parentheses
$stanzas = mb_substr_count(mb_substr($filter, 0, $end), ".") + 1;

// TODO@kodumbeats handle relations between collections, e.g. if($stanzas > 2)
switch ($stanzas) {
case 2:
// use limit param to ignore '.' in $expression
$input = explode('.', $filter, $stanzas);
$attribute = $input[0];
$expression = $input[1];
[$operator, $values] = self::parseExpression($expression);
break;
}

return new Query($attribute, $operator, $values);
}

/**
* Get attribute key-value from query expression
* $expression: string with format 'operator(value)'
*
* @param string $expression
*
* @return (string|array)[]
*/
protected static function parseExpression(string $expression): array
{
//find location of parentheses in expression

/** @var int */
$start = mb_strpos($expression, '(');
/** @var int */
$end = mb_strrpos($expression, ')');

//extract the query method

/** @var string */
$operator = mb_substr($expression, 0, $start);

//grab everything inside parentheses

/** @var mixed */
$value = mb_substr($expression,
($start + 1), /* exclude open paren*/
($end - $start - 1) /* exclude closed paren*/
);

// Explode comma-separated values

$values = explode(',', $value);

// Cast $value type

$values = array_map(function ($value) {

// Trim whitespace from around $value

$value = trim($value);

switch (true) {
// type casted to int or float by "+" operator
case is_numeric($value):
return $value + 0;

// since (bool)"false" returns true, check bools manually
case $value === 'true':
return true;

case $value === 'false':
return false;

// need special case to cast (null) as null, not string
case $value === 'null':
return null;

default:
// strip escape characters
$value = stripslashes($value);
// trim leading and tailing quotes
return trim($value, '\'"');
}

}, $values);

return [$operator, $values];
}
}
1 change: 0 additions & 1 deletion tests/Database/Base.php
Original file line number Diff line number Diff line change
Expand Up @@ -1564,7 +1564,6 @@ public function decodeTest()
$this->assertEquals('1', '1');
}


/**
* @depends testCreateDocument
*/
Expand Down
124 changes: 124 additions & 0 deletions tests/Database/QueryTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
<?php

namespace Utopia\Tests;

use Utopia\Database\Query;
use PHPUnit\Framework\TestCase;

class QueryTest extends TestCase
{

public function setUp(): void
{
}

public function tearDown(): void
{
}

public function testCreate(): void
{
$query = new Query('title', 'equal', ['Iron Man']);

$this->assertEquals('title', $query->getAttribute());
$this->assertEquals('equal', $query->getOperator());
$this->assertContains('Iron Man', $query->getValues());
}

public function testParse()
{
$query = Query::parse('title.equal("Iron Man")');

$this->assertEquals('title', $query->getAttribute());
$this->assertEquals('equal', $query->getOperator());
$this->assertContains('Iron Man', $query->getValues());

$query = Query::parse('year.lesser(2001)');

$this->assertEquals('year', $query->getAttribute());
$this->assertEquals('lesser', $query->getOperator());
$this->assertContains(2001, $query->getValues());

$query = Query::parse('published.equal(true)');

$this->assertEquals('published', $query->getAttribute());
$this->assertEquals('equal', $query->getOperator());
$this->assertContains(true, $query->getValues());

$query = Query::parse('published.equal(false)');

$this->assertEquals('published', $query->getAttribute());
$this->assertEquals('equal', $query->getOperator());
$this->assertContains(false, $query->getValues());

$query = Query::parse('actors.notContains( " Johnny Depp ", " Brad Pitt" , "Al Pacino")');

$this->assertEquals('actors', $query->getAttribute());
$this->assertEquals('notContains', $query->getOperator());
$this->assertContains(' Johnny Depp ', $query->getValues());
$this->assertContains(' Brad Pitt', $query->getValues());
$this->assertContains('Al Pacino', $query->getValues());

$query = Query::parse('actors.equal("Brad Pitt", "Johnny Depp")');

$this->assertEquals('actors', $query->getAttribute());
$this->assertEquals('equal', $query->getOperator());
$this->assertContains('Brad Pitt', $query->getValues());
$this->assertContains('Johnny Depp', $query->getValues());

$query = Query::parse('writers.contains("Tim O\'Reilly")');

$this->assertEquals('writers', $query->getAttribute());
$this->assertEquals('contains', $query->getOperator());
$this->assertContains("Tim O'Reilly", $query->getValues());

$query = Query::parse('score.greater(8.5)');

$this->assertEquals('score', $query->getAttribute());
$this->assertEquals('greater', $query->getOperator());
$this->assertContains(8.5, $query->getValues());

$query = Query::parse('director.notEqual("null")');

$this->assertEquals('director', $query->getAttribute());
$this->assertEquals('notEqual', $query->getOperator());
$this->assertContains('null', $query->getValues());

$query = Query::parse('director.notEqual(null)');

$this->assertEquals('director', $query->getAttribute());
$this->assertEquals('notEqual', $query->getOperator());
$this->assertContains(null, $query->getValues());
}

public function testGetAttribute()
{
$query = Query::parse('title.equal("Iron Man")');

$this->assertEquals('title', $query->getAttribute());
}

public function testGetOperator()
{
$query = Query::parse('title.equal("Iron Man")');

$this->assertEquals('equal', $query->getOperator());
}

public function testGetValue()
{
$query = Query::parse('title.equal("Iron Man")');

$this->assertContains('Iron Man', $query->getValues());
}

public function testGetQuery()
{
$query = Query::parse('title.equal("Iron Man")')->getQuery();

$this->assertEquals('title', $query['attribute']);
$this->assertEquals('equal', $query['operator']);
$this->assertContains('Iron Man', $query['values']);
}

}