diff --git a/composer.json b/composer.json index 9dd959a29..f52e8ca09 100755 --- a/composer.json +++ b/composer.json @@ -35,6 +35,7 @@ "phpunit/phpunit": "^9.4", "swoole/ide-helper": "4.8.0", "utopia-php/cli": "^0.11.0", - "vimeo/psalm": "4.0.1" + "vimeo/psalm": "4.0.1", + "phpbench/phpbench": "1.2.0" } } diff --git a/composer.lock b/composer.lock index 67478e81f..4e7861f7b 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "d960db719a704a1cbd556d510e3708d4", + "content-hash": "a88312360d181c46f87ff0a0eb6e7827", "packages": [ { "name": "composer/package-versions-deprecated", @@ -737,6 +737,79 @@ }, "time": "2019-12-04T15:06:13+00:00" }, + { + "name": "doctrine/annotations", + "version": "1.13.3", + "source": { + "type": "git", + "url": "https://github.com/doctrine/annotations.git", + "reference": "648b0343343565c4a056bfc8392201385e8d89f0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/648b0343343565c4a056bfc8392201385e8d89f0", + "reference": "648b0343343565c4a056bfc8392201385e8d89f0", + "shasum": "" + }, + "require": { + "doctrine/lexer": "1.*", + "ext-tokenizer": "*", + "php": "^7.1 || ^8.0", + "psr/cache": "^1 || ^2 || ^3" + }, + "require-dev": { + "doctrine/cache": "^1.11 || ^2.0", + "doctrine/coding-standard": "^6.0 || ^8.1", + "phpstan/phpstan": "^1.4.10 || ^1.8.0", + "phpunit/phpunit": "^7.5 || ^8.0 || ^9.1.5", + "symfony/cache": "^4.4 || ^5.2", + "vimeo/psalm": "^4.10" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Docblock Annotations Parser", + "homepage": "https://www.doctrine-project.org/projects/annotations.html", + "keywords": [ + "annotations", + "docblock", + "parser" + ], + "support": { + "issues": "https://github.com/doctrine/annotations/issues", + "source": "https://github.com/doctrine/annotations/tree/1.13.3" + }, + "time": "2022-07-02T10:48:51+00:00" + }, { "name": "doctrine/instantiator", "version": "1.4.1", @@ -807,18 +880,94 @@ ], "time": "2022-03-03T08:28:38+00:00" }, + { + "name": "doctrine/lexer", + "version": "1.2.3", + "source": { + "type": "git", + "url": "https://github.com/doctrine/lexer.git", + "reference": "c268e882d4dbdd85e36e4ad69e02dc284f89d229" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/c268e882d4dbdd85e36e4ad69e02dc284f89d229", + "reference": "c268e882d4dbdd85e36e4ad69e02dc284f89d229", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^9.0", + "phpstan/phpstan": "^1.3", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "vimeo/psalm": "^4.11" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Lexer\\": "lib/Doctrine/Common/Lexer" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "https://www.doctrine-project.org/projects/lexer.html", + "keywords": [ + "annotations", + "docblock", + "lexer", + "parser", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/lexer/issues", + "source": "https://github.com/doctrine/lexer/tree/1.2.3" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", + "type": "tidelift" + } + ], + "time": "2022-02-28T11:07:21+00:00" + }, { "name": "fakerphp/faker", - "version": "v1.19.0", + "version": "v1.20.0", "source": { "type": "git", "url": "https://github.com/FakerPHP/Faker.git", - "reference": "d7f08a622b3346766325488aa32ddc93ccdecc75" + "reference": "37f751c67a5372d4e26353bd9384bc03744ec77b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/d7f08a622b3346766325488aa32ddc93ccdecc75", - "reference": "d7f08a622b3346766325488aa32ddc93ccdecc75", + "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/37f751c67a5372d4e26353bd9384bc03744ec77b", + "reference": "37f751c67a5372d4e26353bd9384bc03744ec77b", "shasum": "" }, "require": { @@ -845,7 +994,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "v1.19-dev" + "dev-main": "v1.20-dev" } }, "autoload": { @@ -870,9 +1019,9 @@ ], "support": { "issues": "https://github.com/FakerPHP/Faker/issues", - "source": "https://github.com/FakerPHP/Faker/tree/v1.19.0" + "source": "https://github.com/FakerPHP/Faker/tree/v1.20.0" }, - "time": "2022-02-02T17:38:57+00:00" + "time": "2022-07-20T13:12:54+00:00" }, { "name": "felixfbecker/advanced-json-rpc", @@ -1305,6 +1454,197 @@ }, "time": "2022-02-21T01:04:05+00:00" }, + { + "name": "phpbench/container", + "version": "2.2.1", + "source": { + "type": "git", + "url": "https://github.com/phpbench/container.git", + "reference": "6d555ff7174fca13f9b1ec0b4a089ed41d0ab392" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpbench/container/zipball/6d555ff7174fca13f9b1ec0b4a089ed41d0ab392", + "reference": "6d555ff7174fca13f9b1ec0b4a089ed41d0ab392", + "shasum": "" + }, + "require": { + "psr/container": "^1.0|^2.0", + "symfony/options-resolver": "^4.2 || ^5.0 || ^6.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.16", + "phpstan/phpstan": "^0.12.52", + "phpunit/phpunit": "^8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpBench\\DependencyInjection\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniel Leech", + "email": "daniel@dantleech.com" + } + ], + "description": "Simple, configurable, service container.", + "support": { + "issues": "https://github.com/phpbench/container/issues", + "source": "https://github.com/phpbench/container/tree/2.2.1" + }, + "time": "2022-01-25T10:17:35+00:00" + }, + { + "name": "phpbench/dom", + "version": "0.3.2", + "source": { + "type": "git", + "url": "https://github.com/phpbench/dom.git", + "reference": "b013b717832ddbaadf2a40984b04bc66af9a7110" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpbench/dom/zipball/b013b717832ddbaadf2a40984b04bc66af9a7110", + "reference": "b013b717832ddbaadf2a40984b04bc66af9a7110", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "php": "^7.2||^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.18", + "phpstan/phpstan": "^0.12.83", + "phpunit/phpunit": "^8.0||^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "PhpBench\\Dom\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniel Leech", + "email": "daniel@dantleech.com" + } + ], + "description": "DOM wrapper to simplify working with the PHP DOM implementation", + "support": { + "issues": "https://github.com/phpbench/dom/issues", + "source": "https://github.com/phpbench/dom/tree/0.3.2" + }, + "time": "2021-09-24T15:26:07+00:00" + }, + { + "name": "phpbench/phpbench", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/phpbench/phpbench.git", + "reference": "3555dff668e58d25c39d287f3f1bac13a7817b4c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpbench/phpbench/zipball/3555dff668e58d25c39d287f3f1bac13a7817b4c", + "reference": "3555dff668e58d25c39d287f3f1bac13a7817b4c", + "shasum": "" + }, + "require": { + "doctrine/annotations": "^1.13", + "ext-dom": "*", + "ext-json": "*", + "ext-pcre": "*", + "ext-reflection": "*", + "ext-spl": "*", + "ext-tokenizer": "*", + "php": "^7.3 || ^8.0", + "phpbench/container": "^2.1", + "phpbench/dom": "~0.3.1", + "psr/log": "^1.1", + "seld/jsonlint": "^1.1", + "symfony/console": "^4.2 || ^5.0", + "symfony/filesystem": "^4.2 || ^5.0", + "symfony/finder": "^4.2 || ^5.0", + "symfony/options-resolver": "^4.2 || ^5.0", + "symfony/process": "^4.2 || ^5.0", + "webmozart/path-util": "^2.3" + }, + "require-dev": { + "dantleech/invoke": "^2.0", + "friendsofphp/php-cs-fixer": "^3.0", + "jangregor/phpstan-prophecy": "^0.8.1", + "phpspec/prophecy": "^1.12", + "phpstan/phpstan": "^0.12.7", + "phpunit/phpunit": "^8.5.8 || ^9.0", + "symfony/error-handler": "^5.2", + "symfony/var-dumper": "^4.0 || ^5.0" + }, + "suggest": { + "ext-xdebug": "For Xdebug profiling extension." + }, + "bin": [ + "bin/phpbench" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "files": [ + "lib/Report/Func/functions.php" + ], + "psr-4": { + "PhpBench\\": "lib/", + "PhpBench\\Extensions\\XDebug\\": "extensions/xdebug/lib/", + "PhpBench\\Extensions\\Reports\\": "extensions/reports/lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniel Leech", + "email": "daniel@dantleech.com" + } + ], + "description": "PHP Benchmarking Framework", + "support": { + "issues": "https://github.com/phpbench/phpbench/issues", + "source": "https://github.com/phpbench/phpbench/tree/1.2.0" + }, + "funding": [ + { + "url": "https://github.com/dantleech", + "type": "github" + } + ], + "time": "2021-11-06T13:52:05+00:00" + }, { "name": "phpdocumentor/reflection-common", "version": "2.2.0", @@ -1952,6 +2292,55 @@ ], "time": "2022-06-19T12:14:25+00:00" }, + { + "name": "psr/cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for caching libraries", + "keywords": [ + "cache", + "psr", + "psr-6" + ], + "support": { + "source": "https://github.com/php-fig/cache/tree/3.0.0" + }, + "time": "2021-02-03T23:26:27+00:00" + }, { "name": "psr/container", "version": "2.0.2", @@ -3019,6 +3408,70 @@ ], "time": "2020-09-28T06:39:44+00:00" }, + { + "name": "seld/jsonlint", + "version": "1.9.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/jsonlint.git", + "reference": "4211420d25eba80712bff236a98960ef68b866b7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/4211420d25eba80712bff236a98960ef68b866b7", + "reference": "4211420d25eba80712bff236a98960ef68b866b7", + "shasum": "" + }, + "require": { + "php": "^5.3 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.5", + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0 || ^8.5.13" + }, + "bin": [ + "bin/jsonlint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Seld\\JsonLint\\": "src/Seld/JsonLint/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "JSON Linter", + "keywords": [ + "json", + "linter", + "parser", + "validator" + ], + "support": { + "issues": "https://github.com/Seldaek/jsonlint/issues", + "source": "https://github.com/Seldaek/jsonlint/tree/1.9.0" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/seld/jsonlint", + "type": "tidelift" + } + ], + "time": "2022-04-01T13:37:23+00:00" + }, { "name": "swoole/ide-helper", "version": "4.8.0", @@ -3227,6 +3680,202 @@ ], "time": "2022-02-25T11:15:52+00:00" }, + { + "name": "symfony/filesystem", + "version": "v5.4.9", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "36a017fa4cce1eff1b8e8129ff53513abcef05ba" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/36a017fa4cce1eff1b8e8129ff53513abcef05ba", + "reference": "36a017fa4cce1eff1b8e8129ff53513abcef05ba", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8", + "symfony/polyfill-php80": "^1.16" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v5.4.9" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-05-20T13:55:35+00:00" + }, + { + "name": "symfony/finder", + "version": "v5.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "9b630f3427f3ebe7cd346c277a1408b00249dad9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/9b630f3427f3ebe7cd346c277a1408b00249dad9", + "reference": "9b630f3427f3ebe7cd346c277a1408b00249dad9", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/polyfill-php80": "^1.16" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v5.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-04-15T08:07:45+00:00" + }, + { + "name": "symfony/options-resolver", + "version": "v5.4.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/options-resolver.git", + "reference": "cc1147cb11af1b43f503ac18f31aa3bec213aba8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/cc1147cb11af1b43f503ac18f31aa3bec213aba8", + "reference": "cc1147cb11af1b43f503ac18f31aa3bec213aba8", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/polyfill-php73": "~1.0", + "symfony/polyfill-php80": "^1.16" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\OptionsResolver\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an improved replacement for the array_replace PHP function", + "homepage": "https://symfony.com", + "keywords": [ + "config", + "configuration", + "options" + ], + "support": { + "source": "https://github.com/symfony/options-resolver/tree/v5.4.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-01-02T09:53:40+00:00" + }, { "name": "symfony/polyfill-ctype", "version": "v1.26.0", @@ -3636,6 +4285,68 @@ ], "time": "2022-05-24T11:49:31+00:00" }, + { + "name": "symfony/process", + "version": "v5.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "597f3fff8e3e91836bb0bd38f5718b56ddbde2f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/597f3fff8e3e91836bb0bd38f5718b56ddbde2f3", + "reference": "597f3fff8e3e91836bb0bd38f5718b56ddbde2f3", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/polyfill-php80": "^1.16" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v5.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-04-08T05:07:18+00:00" + }, { "name": "symfony/service-contracts", "version": "v3.1.1", diff --git a/phpbench.json b/phpbench.json new file mode 100644 index 000000000..664605231 --- /dev/null +++ b/phpbench.json @@ -0,0 +1,4 @@ +{ + "$schema": "./vendor/phpbench/phpbench/phpbench.schema.json", + "runner.bootstrap": "vendor/autoload.php" +} diff --git a/src/Database/Query.php b/src/Database/Query.php index ccfa16543..56824925d 100644 --- a/src/Database/Query.php +++ b/src/Database/Query.php @@ -24,7 +24,6 @@ class Query const TYPE_CURSORAFTER = 'cursorAfter'; const TYPE_CURSORBEFORE = 'cursorBefore'; - protected const CHAR_ALL_QUOTES = [self::CHAR_SINGLE_QUOTE, self::CHAR_DOUBLE_QUOTE]; protected const CHAR_SINGLE_QUOTE = '\''; protected const CHAR_DOUBLE_QUOTE = '"'; protected const CHAR_COMMA = ','; @@ -178,44 +177,80 @@ public static function parse(string $filter): self $currentArrayParam = []; // We build array param here before pushing when it's ended $stack = []; // Stack of syntactical symbols + $stringStackState = false; + $arrayStackState = false; + // Loop thorough all characters for ($i = $parametersStart; $i < $paramsEnd; $i++) { $char = $filter[$i]; + $isStringStack = $stringStackState; + $isArrayStack = $arrayStackState; + // String support + escaping support if ( - (\in_array($char, static::CHAR_ALL_QUOTES)) && // Must be string indicator - $filter[$i - 1] !== '\\' - ) // Must not be escaped; first cant be + (self::isQuote($char)) && // Must be string indicator + $filter[$i - 1] !== '\\' // Must not be escaped; first cant be + ) { - if (static::isInStringStack($stack)) { + if ($isStringStack) { // Dont mix-up string symbols. Only allow the same as on start if ($char === $stack[\count($stack) - 1]) { // End of string \array_pop($stack); + + $stackCount = \count($stack); + if ($stackCount > 0) { + $lastChar = $stack[\count($stack) - 1]; + $stringStackState = $lastChar === static::CHAR_SINGLE_QUOTE || $lastChar === static::CHAR_DOUBLE_QUOTE; + + if(!$stringStackState) { + $arrayStackState = $lastChar === static::CHAR_BRACKET_START; + } + } else { + $stringStackState = false; + $arrayStackState = false; + } } // Either way, add symbol to builder - static::appendSymbol($stack, $char, $i, $filter, $currentParam); + static::appendSymbol($isStringStack, $char, $i, $filter, $currentParam); } else { // Start of string $stack[] = $char; - static::appendSymbol($stack, $char, $i, $filter, $currentParam); + $stringStackState = true; + $arrayStackState = false; + static::appendSymbol($isStringStack, $char, $i, $filter, $currentParam); } continue; } // Array support - if (!(static::isInStringStack($stack))) { + if (!($isStringStack)) { if ($char === static::CHAR_BRACKET_START) { // Start of array $stack[] = $char; + $stringStackState = false; + $arrayStackState = true; continue; } else if ($char === static::CHAR_BRACKET_END) { // End of array \array_pop($stack); + $stackCount = \count($stack); + if ($stackCount > 0) { + $lastChar = $stack[\count($stack) - 1]; + $stringStackState = $lastChar === static::CHAR_SINGLE_QUOTE || $lastChar === static::CHAR_DOUBLE_QUOTE; + + if(!$stringStackState) { + $arrayStackState = $lastChar === static::CHAR_BRACKET_START; + } + } else { + $stringStackState = false; + $arrayStackState = false; + } + if (!empty($currentParam)) { $currentArrayParam[] = $currentParam; } @@ -225,15 +260,9 @@ public static function parse(string $filter): self $currentParam = ""; continue; - } - } - - // Params separation support - if ($char === static::CHAR_COMMA) { - // Only consider it end of param if stack doesn't end with string - if (!static::isInStringStack($stack)) { + } else if ($char === static::CHAR_COMMA) { // Params separation support // If in array stack, dont merge yet, just mark it in array param builder - if (static::isInArrayStack($stack)) { + if ($isArrayStack) { $currentArrayParam[] = $currentParam; $currentParam = ""; } else { @@ -242,15 +271,16 @@ public static function parse(string $filter): self if (!empty($currentParam)) { $params[] = $currentParam; } - + $currentParam = ""; } } + continue; } } // Value, not relevant to syntax - static::appendSymbol($stack, $char, $i, $filter, $currentParam); + static::appendSymbol($isStringStack, $char, $i, $filter, $currentParam); } if (!empty($currentParam)) { @@ -272,46 +302,12 @@ public static function parse(string $filter): self $parsedParams[] = self::parseParam($param); } } + $method = static::getMethodFromAlias($method); return new Query($method, $parsedParams); } - /** - * Utility method to know if we are inside String. - * - * @param array $stack - * @return bool - */ - protected static function isInStringStack(array $stack): bool - { - if (\count($stack) > 0 && \in_array($stack[\count($stack) - 1], static::CHAR_ALL_QUOTES)) // Stack ends with string symbol ' or " - { - return true; - } - - return false; - } - - /** - * Utility method to know if we are inside Array. - * - * @param array $stack - * @return bool - */ - protected static function isInArrayStack(array $stack): bool - { - if ( - \count($stack) > 0 && // Stack is not empty - $stack[\count($stack) - 1] === static::CHAR_BRACKET_START - ) // Stack ends with array symbol - { - return true; - } - - return false; - } - /** * Utility method to only append symbol if relevant. * @@ -322,19 +318,27 @@ protected static function isInArrayStack(array $stack): bool * @param string $currentParam * @return void */ - protected static function appendSymbol(array $stack, string $char, int $index, string $filter, string &$currentParam): void + protected static function appendSymbol(bool $isStringStack, string $char, int $index, string $filter, string &$currentParam): void { $nextChar = $filter[$index + 1] ?? ''; if ( $char === '\\' && // Current char might be escaping - (\in_array($nextChar, static::CHAR_ALL_QUOTES)) // Next char must be string syntax symbol + self::isQuote($nextChar) // Next char must be string syntax symbol ) { return; } // Ignore spaces and commas outside of string - if (\in_array($char, [static::CHAR_SPACE, static::CHAR_COMMA])) { - if (static::isInStringStack($stack)) { + $canBeIgnored = false; + + if($char === static::CHAR_SPACE) { + $canBeIgnored = true; + } else if($char === static::CHAR_COMMA) { + $canBeIgnored = true; + } + + if ($canBeIgnored) { + if ($isStringStack) { $currentParam .= $char; } } else { @@ -342,6 +346,32 @@ protected static function appendSymbol(array $stack, string $char, int $index, s } } + protected static function isQuote(string $char) { + if($char === self::CHAR_SINGLE_QUOTE) { + return true; + } else if($char === self::CHAR_DOUBLE_QUOTE) { + return true; + } + + return false; + } + + protected static function isSpecialChar(string $char) { + if($char === static::CHAR_COMMA) { + return true; + } else if($char === static::CHAR_BRACKET_END) { + return true; + } else if($char === static::CHAR_BRACKET_START) { + return true; + } else if($char === static::CHAR_DOUBLE_QUOTE) { + return true; + } else if($char === static::CHAR_SINGLE_QUOTE) { + return true; + } + + return false; + } + /** * Parses param value. * @@ -351,29 +381,18 @@ protected static function appendSymbol(array $stack, string $char, int $index, s protected static function parseParam(string $param): mixed { $param = \trim($param); - - // Numeric param - if (\is_numeric($param)) { - // Cast to number - return $param + 0; - } - - // Boolean param - if ($param === 'false') { + + if ($param === 'false') { // Boolean param return false; } else if ($param === 'true') { return true; - } - - // Null param - if ($param === 'null') { + } else if ($param === 'null') { // Null param return null; - } - - // String param - if (\str_starts_with($param, static::CHAR_DOUBLE_QUOTE) || \str_starts_with($param, static::CHAR_SINGLE_QUOTE)) { - $param = substr($param, 1, -1); // Remove '' or "" - + } else if (\is_numeric($param)) { // Numeric param + // Cast to number + return $param + 0; + } else if (\str_starts_with($param, static::CHAR_DOUBLE_QUOTE) || \str_starts_with($param, static::CHAR_SINGLE_QUOTE)) { // String param + $param = \substr($param, 1, -1); // Remove '' or "" return $param; } diff --git a/src/Database/QueryV1.php b/src/Database/QueryV1.php new file mode 100644 index 000000000..4f9f224a4 --- /dev/null +++ b/src/Database/QueryV1.php @@ -0,0 +1,245 @@ +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 array + */ + public function getValues(): array + { + 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, + ]; + } + + /** + * Set attribute + * @param string $attribute + * @return QueryV1 + */ + public function setAttribute(string $attribute): self + { + $this->attribute = $attribute; + + return $this; + } + + /** + * Set operator + * @param string $operator + * @return QueryV1 + */ + public function setOperator(string $operator): self + { + $this->operator = $operator; + + return $this; + } + + /** + * Set operand + * @param array $values + * @return QueryV1 + */ + public function setValues(array $values): self + { + $this->values = $values; + + return $this; + } + + /** + * Check if operator is supported + * @param string $value + * @return bool + */ + public static function isOperator(string $value): bool + { + switch ($value) { + case self::TYPE_EQUAL: + case self::TYPE_NOTEQUAL: + case self::TYPE_LESSER: + case self::TYPE_LESSEREQUAL: + case self::TYPE_GREATER: + case self::TYPE_GREATEREQUAL: + case self::TYPE_CONTAINS: + case self::TYPE_SEARCH: + return true; + default: + return false; + } + } + + /** + * Parse query filter + * + * @param string $filter + * + * @return QueryV1 + * */ + public static function parse(string $filter): QueryV1 + { + $attribute = ''; + $operator = ''; + $values = []; + + // get index of open parentheses + $end = intval(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 QueryV1($attribute, $operator, $values); + } + + /** + * Get attribute key-value from query expression + * $expression: string with format 'operator(value)' + * + * @param string $expression + * + * @return 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 + $operator = mb_substr($expression, 0, $start); + + //grab everything inside parentheses + $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]; + } +} diff --git a/tests/Database/Benchmark/QueryBench.php b/tests/Database/Benchmark/QueryBench.php new file mode 100644 index 000000000..bf5460cd4 --- /dev/null +++ b/tests/Database/Benchmark/QueryBench.php @@ -0,0 +1,44 @@ + ['attributes' => str_repeat('"' . $this->generateRandomString() . '",', 1)]; + yield '2 Attributes' => ['attributes' => str_repeat('"' . $this->generateRandomString() . '",', 2)]; + yield '4 Attributes' => ['attributes' => str_repeat('"' . $this->generateRandomString() . '",', 4)]; + yield '8 Attributes' => ['attributes' => str_repeat('"' . $this->generateRandomString() . '",', 8)]; + yield '16 Attributes' => ['attributes' => str_repeat('"' . $this->generateRandomString() . '",', 16)]; + yield '32 Attributes' => ['attributes' => str_repeat('"' . $this->generateRandomString() . '",', 32)]; + yield '64 Attributes' => ['attributes' => str_repeat('"' . $this->generateRandomString() . '",', 64)]; + yield '128 Attributes' => ['attributes' => str_repeat('"' . $this->generateRandomString() . '",', 128)]; + } + + protected function generateRandomString(int $length = 10): string + { + return substr(str_shuffle(str_repeat($x = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', ceil($length / strlen($x)))), 1, $length); + } + + #[Bench\Revs(1000)] + #[Bench\Iterations(10)] + #[Bench\ParamProviders('provideAttributes')] + public function benchV1(array $params) + { + QueryV1::parse("actors.equal({$params['attributes']})"); + } + + #[Bench\Revs(1000)] + #[Bench\Iterations(10)] + #[Bench\ParamProviders('provideAttributes')] + public function benchV2(array $params) + { + Query::parse("equal('actors', [{$params['attributes']}])"); + } +}