-
Notifications
You must be signed in to change notification settings - Fork 54
Feat - add custom filter parser #3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 46 commits
Commits
Show all changes
47 commits
Select commit
Hold shift + click to select a range
3cf58ce
Add function to parse query expressions
kodumbeats 514bf70
Add method to parse custom filter queries
kodumbeats 80eec64
Add tests for parsing queries
kodumbeats 69e6bc7
Add test for parsing query expressions
kodumbeats cdf5267
Refactor query into standalone class
kodumbeats 3f75885
Add getters for object properties
kodumbeats 2a8a5b3
Add method to get all query details
kodumbeats f763ba4
Add return typing
kodumbeats 06f02f7
Clean up code
kodumbeats e01fcc4
Call parseExpression as a static method
kodumbeats 65a2166
Add tests for query class
kodumbeats 1bb848d
Refactor parsing code into query class and tests
kodumbeats e802153
Add type hinting
kodumbeats 83a09a3
Merge branch 'v0' into v0
kodumbeats c18b485
Add types to constructor params
kodumbeats b2f6603
Rename operand to value
kodumbeats f581824
Change to protected method
kodumbeats fe6becb
Use bracket syntax for switch
kodumbeats 1c6e60c
Treat query expression as kv pair
kodumbeats 179a0d5
Create query validator
kodumbeats 8ada10e
Remove references to old var
kodumbeats 8934ce7
Revert "Change to protected method"
kodumbeats 38b02cf
Revert "Treat query expression as kv pair"
kodumbeats c2b9dc2
Use value instead of operand
kodumbeats 8c8afb3
Merge branch 'v0' of github.com:kodumbeats/database into v0
kodumbeats 7071923
Clarify intended behavior for parseExpression()
kodumbeats 0ec83e0
Add and fix query tests
kodumbeats e3f1efb
Constructor expects array of $values by default
kodumbeats 0659fe8
Properly scope parseExpression
kodumbeats 6b3bcd2
Typecast expression values and handle comma-separated values
kodumbeats a1b472d
Test with integer param
kodumbeats e74da85
Test for typecasted bool expressions
kodumbeats f7e86b6
Test multiple query values
kodumbeats a14a730
Cleanup for readability
kodumbeats 631ecfd
Reserve query validator for separate PR
kodumbeats 4d6189b
Only consider periods oudside parentheses
kodumbeats 1f4c64f
Test for float values
kodumbeats fa28286
Handle null as value
kodumbeats 2ec2fc8
Refactor elseifs into switch
kodumbeats ab2a6b7
Use single array_map to trim whitespace and typecast
kodumbeats b01d9dd
Remove unneeded switch case
kodumbeats 7db5c8f
Trim whitespace and remove escape slashes
kodumbeats 40cd42c
Test for whitespace and escaped character
kodumbeats 53cf8dc
Use strrpos to find last occurrence of parenthesis
kodumbeats ba852c2
Correct comments
kodumbeats 87dd5b3
Test constructor function
kodumbeats 1522284
Properly handle whitespace between values
kodumbeats File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,175 @@ | ||
| <?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) { | ||
|
|
||
| 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 and whitespace | ||
| return trim($value, '\'" '); | ||
| } | ||
|
|
||
| }, $values); | ||
|
|
||
| return [$operator, $values]; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,122 @@ | ||
| <?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 ")'); | ||
|
|
||
| $this->assertEquals('actors', $query->getAttribute()); | ||
| $this->assertEquals('notContains', $query->getOperator()); | ||
| $this->assertContains('Johnny Depp', $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']); | ||
| } | ||
|
|
||
| } | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.