diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a6ad250..705018f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,8 +1,6 @@ name: "PHQL CI" on: - schedule: - - cron: '0 2 * * *' # Daily at 02:00 runs only on default branch push: paths-ignore: - '**.md' @@ -17,6 +15,50 @@ concurrency: cancel-in-progress: ${{ github.ref != 'refs/heads/master' }} jobs: + # PHP CodeSniffer inspection + phpcs: + name: "Validate code style" + + permissions: + contents: read + + runs-on: ubuntu-latest + + strategy: + fail-fast: true + matrix: + php: + - '8.1' + - '8.2' + - '8.3' + - '8.4' + - '8.5' + + steps: + - uses: actions/checkout@v6 + + - name: "Setup PHP" + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: ${{ env.EXTENSIONS }} + ini-values: apc.enable_cli=on, session.save_path=/tmp + tools: pecl + env: + COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PHALCON_PATH: ext + + - name: "Install development dependencies with Composer" + uses: "ramsey/composer-install@v3" + with: + composer-options: "--prefer-dist" + + - name: "PHPCS" + run: composer cs + + - name: "PHPStan" + run: composer analyze + tests: name: PHP ${{ matrix.php }} runs-on: ubuntu-latest @@ -60,6 +102,7 @@ jobs: coverage: name: Code Coverage + needs: tests runs-on: ubuntu-latest permissions: @@ -71,7 +114,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: '8.3' + php-version: '8.5' extensions: mbstring coverage: xdebug tools: composer:v2 @@ -79,12 +122,12 @@ jobs: COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Install dependencies - uses: ramsey/composer-install@v3 + uses: ramsey/composer-install@v4 with: composer-options: "--prefer-dist" - name: Run tests with coverage - run: vendor/bin/phpunit --coverage-clover coverage.xml + run: vendor/bin/phpunit -c phpunit.xml --coverage-clover coverage.xml - name: Upload coverage to Codecov uses: codecov/codecov-action@v5 diff --git a/.gitignore b/.gitignore index 3256564..e2f5912 100644 --- a/.gitignore +++ b/.gitignore @@ -7,5 +7,6 @@ phql.log # PHPUnit .phpunit.result.cache/ +tests/_output/ coverage/ coverage.xml diff --git a/composer.json b/composer.json index 839847d..8c19b0a 100644 --- a/composer.json +++ b/composer.json @@ -1,15 +1,26 @@ { "name": "phalcon/phql", "description": "Phalcon Query Language (PHQL)", + "type": "library", "keywords": [ "php", "framework", + "phalcon", "query", - "sql" + "sql", + "phql" ], + "homepage": "https://phalcon.io", "license": "MIT", + "authors": [ + { + "name": "Phalcon Team", + "email": "team@phalcon.io", + "homepage": "https://phalcon.io" + } + ], "require": { - "php": ">=8.1", + "php": ">=8.1 <9.0", "ext-mbstring": "*" }, "suggest": { @@ -17,7 +28,10 @@ }, "require-dev": { "phpstan/phpstan": "^2.0", - "phpunit/phpunit": "^10.5" + "phpunit/phpunit": "^10.5", + "pds/skeleton": "^1.0", + "pds/composer-script-names": "^1.0", + "squizlabs/php_codesniffer": "^4.0" }, "autoload": { "psr-4": { @@ -29,12 +43,20 @@ }, "autoload-dev": { "psr-4": { - "Phalcon\\Phql\\Tests\\": "tests/", - "Phalcon\\Phql\\Tests\\Unit\\": "tests/unit/" + "Phalcon\\Phql\\Tests\\": "tests/" } }, + "minimum-stability": "stable", + "prefer-stable": true, "scripts": { + "cs": "vendor/bin/phpcs --standard=phpcs.xml", + "cs-fix": "vendor/bin/phpcbf --standard=phpcs.xml", + "analyze": "vendor/bin/phpstan analyse -c phpstan.neon --memory-limit 1024M", "test": "vendor/bin/phpunit -c phpunit.xml --fail-on-all-issues", "test-coverage": "vendor/bin/phpunit -c phpunit.xml --coverage-html tests/_output/coverage/" + }, + "support": { + "issues": "https://github.com/phalcon/phql/issues", + "source": "https://github.com/phalcon/phql" } } diff --git a/composer.lock b/composer.lock index 6a56b5f..8545330 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": "3449d9f432f7ba89aa4794432fe52259", + "content-hash": "e76f5860ff8d0931244d183985f3a478", "packages": [], "packages-dev": [ { @@ -125,6 +125,68 @@ }, "time": "2025-12-06T11:56:16+00:00" }, + { + "name": "pds/composer-script-names", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-pds/composer-script-names.git", + "reference": "e6a78aaeaee7cef82a995718ab2f5d0445ef8073" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-pds/composer-script-names/zipball/e6a78aaeaee7cef82a995718ab2f5d0445ef8073", + "reference": "e6a78aaeaee7cef82a995718ab2f5d0445ef8073", + "shasum": "" + }, + "type": "standard", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "CC-BY-SA-4.0" + ], + "description": "Standard for Composer script names.", + "homepage": "https://github.com/php-pds/composer-script-names", + "support": { + "issues": "https://github.com/php-pds/composer-script-names/issues", + "source": "https://github.com/php-pds/composer-script-names/tree/1.0.0" + }, + "time": "2023-04-06T13:42:16+00:00" + }, + { + "name": "pds/skeleton", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-pds/skeleton.git", + "reference": "95e476e5d629eadacbd721c5a9553e537514a231" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-pds/skeleton/zipball/95e476e5d629eadacbd721c5a9553e537514a231", + "reference": "95e476e5d629eadacbd721c5a9553e537514a231", + "shasum": "" + }, + "bin": [ + "bin/pds-skeleton" + ], + "type": "standard", + "autoload": { + "psr-4": { + "Pds\\Skeleton\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "CC-BY-SA-4.0" + ], + "description": "Standard for PHP package skeletons.", + "homepage": "https://github.com/php-pds/skeleton", + "support": { + "issues": "https://github.com/php-pds/skeleton/issues", + "source": "https://github.com/php-pds/skeleton/tree/1.x" + }, + "time": "2017-01-25T23:30:41+00:00" + }, { "name": "phar-io/manifest", "version": "2.0.4", @@ -1679,6 +1741,85 @@ ], "time": "2023-02-07T11:34:05+00:00" }, + { + "name": "squizlabs/php_codesniffer", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", + "reference": "0525c73950de35ded110cffafb9892946d7771b5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/0525c73950de35ded110cffafb9892946d7771b5", + "reference": "0525c73950de35ded110cffafb9892946d7771b5", + "shasum": "" + }, + "require": { + "ext-simplexml": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": ">=7.2.0" + }, + "require-dev": { + "phpunit/phpunit": "^8.4.0 || ^9.3.4 || ^10.5.32 || 11.3.3 - 11.5.28 || ^11.5.31" + }, + "bin": [ + "bin/phpcbf", + "bin/phpcs" + ], + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Greg Sherwood", + "role": "Former lead" + }, + { + "name": "Juliette Reinders Folmer", + "role": "Current lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer/graphs/contributors" + } + ], + "description": "PHP_CodeSniffer tokenizes PHP files and detects violations of a defined set of coding standards.", + "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer", + "keywords": [ + "phpcs", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/PHPCSStandards/PHP_CodeSniffer/issues", + "security": "https://github.com/PHPCSStandards/PHP_CodeSniffer/security/policy", + "source": "https://github.com/PHPCSStandards/PHP_CodeSniffer", + "wiki": "https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki" + }, + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2025-11-10T16:43:36+00:00" + }, { "name": "theseer/tokenizer", "version": "1.3.1", @@ -1733,10 +1874,10 @@ "aliases": [], "minimum-stability": "stable", "stability-flags": {}, - "prefer-stable": false, + "prefer-stable": true, "prefer-lowest": false, "platform": { - "php": ">=8.1", + "php": ">=8.1 <9.0", "ext-mbstring": "*" }, "platform-dev": {}, diff --git a/phpcs.xml b/phpcs.xml new file mode 100644 index 0000000..8828afe --- /dev/null +++ b/phpcs.xml @@ -0,0 +1,12 @@ + + + Phalcon Coding Standards + + + + + + + src + tests/unit + diff --git a/phpunit.xml b/phpunit.xml index c217093..614d18c 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -15,7 +15,9 @@ src - resources + + resources/files/parser.php + diff --git a/resources/docker/develop/Dockerfile b/resources/docker/develop/Dockerfile index 5a68a02..9578c2b 100644 --- a/resources/docker/develop/Dockerfile +++ b/resources/docker/develop/Dockerfile @@ -44,7 +44,7 @@ RUN groupadd -g "${GID}" "${GROUP}" \ && rm -rf /tmp/* /var/tmp/* \ && find /var/cache/apt/archives /var/lib/apt/lists -not -name lock -type f -delete \ && find /var/cache -type f -delete \ - && find /var/log -type f -delete \ + && find /var/log -type f -delete COPY resources/docker/develop/.bashrc /home/${USER}/.bashrc COPY resources/docker/develop/extra.ini /usr/local/etc/php/conf.d/ diff --git a/resources/files/parser.php b/resources/files/parser.php index bac010d..172f571 100644 --- a/resources/files/parser.php +++ b/resources/files/parser.php @@ -31,9 +31,9 @@ class phql_Parser { - protected array $output = []; + protected mixed $output = []; - public function getOutput(): array + public function getOutput(): mixed { return $this->output; } @@ -3913,7 +3913,7 @@ private function yy_reduce(int $yyruleno): void */ case 0: $yygotominor = $this->yystack[$this->yyidx + 0]->minor; - //ZVAL_ZVAL($status->ret, $this->yystack[$this->yyidx + 0]->minor, 1, 1); + $this->status->setAst($this->yystack[$this->yyidx + 0]->minor); break; case 1: case 2: @@ -3971,9 +3971,13 @@ private function yy_reduce(int $yyruleno): void $this->yy_destructor(30, $this->yystack[$this->yyidx + 0]->minor); break; case 9: - case 20: - case 27: - case 38: + // distinct_all ::= (no DISTINCT or ALL keyword) - no distinct + $yygotominor = null; + break; + case 135: + // distinct_or_null ::= (no DISTINCT keyword in function call) + $yygotominor = null; + break; case 69: case 71: case 78: @@ -3981,10 +3985,18 @@ private function yy_reduce(int $yyruleno): void case 85: case 89: case 91: - case 135: case 137: + // Empty optional clause (where, limit, order, group, having, for_update, etc.) + // Matches cphalcon ZVAL_UNDEF — must be null so phql_ret_*_statement skips adding the key + $yygotominor = null; + break; + case 20: + case 27: $yygotominor = []; break; + case 38: + $yygotominor = null; + break; case 10: case 17: case 41: @@ -4010,13 +4022,13 @@ private function yy_reduce(int $yyruleno): void break; case 12: case 140: - phql_ret_column_item($yygotominor, Opcode::PHQL_T_STARALL); + phql_ret_column_item($yygotominor, Opcode::STARALL->value); $this->yy_destructor(18, $this->yystack[$this->yyidx + 0]->minor); break; case 13: phql_ret_column_item( $yygotominor, - Opcode::PHQL_T_DOMAINALL, + Opcode::DOMAINALL->value, null, $this->yystack[$this->yyidx + -2]->minor, ); @@ -4026,7 +4038,7 @@ private function yy_reduce(int $yyruleno): void case 14: phql_ret_column_item( $yygotominor, - Opcode::PHQL_T_EXPR, + Opcode::EXPR->value, $this->yystack[$this->yyidx + -2]->minor, null, $this->yystack[$this->yyidx + 0]->minor @@ -4036,7 +4048,7 @@ private function yy_reduce(int $yyruleno): void case 15: phql_ret_column_item( $yygotominor, - Opcode::PHQL_T_EXPR, + Opcode::EXPR->value, $this->yystack[$this->yyidx + -1]->minor, null, $this->yystack[$this->yyidx + 0]->minor @@ -4045,7 +4057,7 @@ private function yy_reduce(int $yyruleno): void case 16: phql_ret_column_item( $yygotominor, - Opcode::PHQL_T_EXPR, + Opcode::EXPR->value, $this->yystack[$this->yyidx + 0]->minor, ); break; @@ -4077,50 +4089,50 @@ private function yy_reduce(int $yyruleno): void phql_ret_qualified_name($yygotominor, null, null, $this->yystack[$this->yyidx + 0]->minor); break; case 28: - $yygotominor = Opcode::PHQL_T_INNERJOIN; + $yygotominor = Opcode::INNERJOIN->value; $this->yy_destructor(34, $this->yystack[$this->yyidx + -1]->minor); $this->yy_destructor(35, $this->yystack[$this->yyidx + 0]->minor); break; case 29: - $yygotominor = Opcode::PHQL_T_CROSSJOIN; + $yygotominor = Opcode::CROSSJOIN->value; $this->yy_destructor(36, $this->yystack[$this->yyidx + -1]->minor); $this->yy_destructor(35, $this->yystack[$this->yyidx + 0]->minor); break; case 30: - $yygotominor = Opcode::PHQL_T_LEFTJOIN; + $yygotominor = Opcode::LEFTJOIN->value; $this->yy_destructor(37, $this->yystack[$this->yyidx + -2]->minor); $this->yy_destructor(38, $this->yystack[$this->yyidx + -1]->minor); $this->yy_destructor(35, $this->yystack[$this->yyidx + 0]->minor); break; case 31: - $yygotominor = Opcode::PHQL_T_LEFTJOIN; + $yygotominor = Opcode::LEFTJOIN->value; $this->yy_destructor(37, $this->yystack[$this->yyidx + -1]->minor); $this->yy_destructor(35, $this->yystack[$this->yyidx + 0]->minor); break; case 32: - $yygotominor = Opcode::PHQL_T_RIGHTJOIN; + $yygotominor = Opcode::RIGHTJOIN->value; $this->yy_destructor(39, $this->yystack[$this->yyidx + -2]->minor); $this->yy_destructor(38, $this->yystack[$this->yyidx + -1]->minor); $this->yy_destructor(35, $this->yystack[$this->yyidx + 0]->minor); break; case 33: - $yygotominor = Opcode::PHQL_T_RIGHTJOIN; + $yygotominor = Opcode::RIGHTJOIN->value; $this->yy_destructor(39, $this->yystack[$this->yyidx + -1]->minor); $this->yy_destructor(35, $this->yystack[$this->yyidx + 0]->minor); break; case 34: - $yygotominor = Opcode::PHQL_T_FULLJOIN; + $yygotominor = Opcode::FULLJOIN->value; $this->yy_destructor(40, $this->yystack[$this->yyidx + -2]->minor); $this->yy_destructor(38, $this->yystack[$this->yyidx + -1]->minor); $this->yy_destructor(35, $this->yystack[$this->yyidx + 0]->minor); break; case 35: - $yygotominor = Opcode::PHQL_T_FULLJOIN; + $yygotominor = Opcode::FULLJOIN->value; $this->yy_destructor(40, $this->yystack[$this->yyidx + -1]->minor); $this->yy_destructor(35, $this->yystack[$this->yyidx + 0]->minor); break; case 36: - $yygotominor = Opcode::PHQL_T_INNERJOIN; + $yygotominor = Opcode::INNERJOIN->value; $this->yy_destructor(35, $this->yystack[$this->yyidx + 0]->minor); break; case 37: @@ -4286,11 +4298,11 @@ private function yy_reduce(int $yyruleno): void phql_ret_order_item($yygotominor, $this->yystack[$this->yyidx + 0]->minor); break; case 75: - phql_ret_order_item($yygotominor, $this->yystack[$this->yyidx + -1]->minor, Opcode::PHQL_T_ASC); + phql_ret_order_item($yygotominor, $this->yystack[$this->yyidx + -1]->minor, Opcode::ASC->value); $this->yy_destructor(54, $this->yystack[$this->yyidx + 0]->minor); break; case 76: - phql_ret_order_item($yygotominor, $this->yystack[$this->yyidx + -1]->minor, Opcode::PHQL_T_DESC); + phql_ret_order_item($yygotominor, $this->yystack[$this->yyidx + -1]->minor, Opcode::DESC->value); $this->yy_destructor(55, $this->yystack[$this->yyidx + 0]->minor); break; case 77: @@ -4334,7 +4346,7 @@ private function yy_reduce(int $yyruleno): void case 150: phql_ret_literal_zval( $yygotominor, - Opcode::PHQL_T_INTEGER, + Opcode::INTEGER->value, $this->yystack[$this->yyidx + 0]->minor ); break; @@ -4342,7 +4354,7 @@ private function yy_reduce(int $yyruleno): void case 151: phql_ret_literal_zval( $yygotominor, - Opcode::PHQL_T_HINTEGER, + Opcode::HINTEGER->value, $this->yystack[$this->yyidx + 0]->minor ); break; @@ -4350,7 +4362,7 @@ private function yy_reduce(int $yyruleno): void case 157: phql_ret_placeholder_zval( $yygotominor, - Opcode::PHQL_T_NPLACEHOLDER, + Opcode::NPLACEHOLDER->value, $this->yystack[$this->yyidx + 0]->minor ); break; @@ -4358,7 +4370,7 @@ private function yy_reduce(int $yyruleno): void case 158: phql_ret_placeholder_zval( $yygotominor, - Opcode::PHQL_T_SPLACEHOLDER, + Opcode::SPLACEHOLDER->value, $this->yystack[$this->yyidx + 0]->minor ); break; @@ -4366,18 +4378,18 @@ private function yy_reduce(int $yyruleno): void case 159: phql_ret_placeholder_zval( $yygotominor, - Opcode::PHQL_T_BPLACEHOLDER, + Opcode::BPLACEHOLDER->value, $this->yystack[$this->yyidx + 0]->minor ); break; case 97: - phql_ret_expr($yygotominor, Opcode::PHQL_T_MINUS, null, $this->yystack[$this->yyidx + 0]->minor); + phql_ret_expr($yygotominor, Opcode::MINUS->value, null, $this->yystack[$this->yyidx + 0]->minor); $this->yy_destructor(21, $this->yystack[$this->yyidx + -1]->minor); break; case 98: phql_ret_expr( $yygotominor, - Opcode::PHQL_T_SUB, + Opcode::SUB->value, $this->yystack[$this->yyidx + -2]->minor, $this->yystack[$this->yyidx + 0]->minor ); @@ -4386,7 +4398,7 @@ private function yy_reduce(int $yyruleno): void case 99: phql_ret_expr( $yygotominor, - Opcode::PHQL_T_ADD, + Opcode::ADD->value, $this->yystack[$this->yyidx + -2]->minor, $this->yystack[$this->yyidx + 0]->minor ); @@ -4395,7 +4407,7 @@ private function yy_reduce(int $yyruleno): void case 100: phql_ret_expr( $yygotominor, - Opcode::PHQL_T_MUL, + Opcode::MUL->value, $this->yystack[$this->yyidx + -2]->minor, $this->yystack[$this->yyidx + 0]->minor ); @@ -4404,7 +4416,7 @@ private function yy_reduce(int $yyruleno): void case 101: phql_ret_expr( $yygotominor, - Opcode::PHQL_T_DIV, + Opcode::DIV->value, $this->yystack[$this->yyidx + -2]->minor, $this->yystack[$this->yyidx + 0]->minor ); @@ -4413,7 +4425,7 @@ private function yy_reduce(int $yyruleno): void case 102: phql_ret_expr( $yygotominor, - Opcode::PHQL_T_MOD, + Opcode::MOD->value, $this->yystack[$this->yyidx + -2]->minor, $this->yystack[$this->yyidx + 0]->minor ); @@ -4422,7 +4434,7 @@ private function yy_reduce(int $yyruleno): void case 103: phql_ret_expr( $yygotominor, - Opcode::PHQL_T_AND, + Opcode::AND->value, $this->yystack[$this->yyidx + -2]->minor, $this->yystack[$this->yyidx + 0]->minor ); @@ -4431,7 +4443,7 @@ private function yy_reduce(int $yyruleno): void case 104: phql_ret_expr( $yygotominor, - Opcode::PHQL_T_OR, + Opcode::OR->value, $this->yystack[$this->yyidx + -2]->minor, $this->yystack[$this->yyidx + 0]->minor ); @@ -4440,7 +4452,7 @@ private function yy_reduce(int $yyruleno): void case 105: phql_ret_expr( $yygotominor, - Opcode::PHQL_T_BITWISE_AND, + Opcode::BITWISE_AND->value, $this->yystack[$this->yyidx + -2]->minor, $this->yystack[$this->yyidx + 0]->minor ); @@ -4449,7 +4461,7 @@ private function yy_reduce(int $yyruleno): void case 106: phql_ret_expr( $yygotominor, - Opcode::PHQL_T_BITWISE_OR, + Opcode::BITWISE_OR->value, $this->yystack[$this->yyidx + -2]->minor, $this->yystack[$this->yyidx + 0]->minor ); @@ -4458,7 +4470,7 @@ private function yy_reduce(int $yyruleno): void case 107: phql_ret_expr( $yygotominor, - Opcode::PHQL_T_BITWISE_XOR, + Opcode::BITWISE_XOR->value, $this->yystack[$this->yyidx + -2]->minor, $this->yystack[$this->yyidx + 0]->minor ); @@ -4467,7 +4479,7 @@ private function yy_reduce(int $yyruleno): void case 108: phql_ret_expr( $yygotominor, - Opcode::PHQL_T_EQUALS, + Opcode::EQUALS->value, $this->yystack[$this->yyidx + -2]->minor, $this->yystack[$this->yyidx + 0]->minor ); @@ -4476,7 +4488,7 @@ private function yy_reduce(int $yyruleno): void case 109: phql_ret_expr( $yygotominor, - Opcode::PHQL_T_NOTEQUALS, + Opcode::NOTEQUALS->value, $this->yystack[$this->yyidx + -2]->minor, $this->yystack[$this->yyidx + 0]->minor ); @@ -4485,7 +4497,7 @@ private function yy_reduce(int $yyruleno): void case 110: phql_ret_expr( $yygotominor, - Opcode::PHQL_T_LESS, + Opcode::LESS->value, $this->yystack[$this->yyidx + -2]->minor, $this->yystack[$this->yyidx + 0]->minor ); @@ -4494,7 +4506,7 @@ private function yy_reduce(int $yyruleno): void case 111: phql_ret_expr( $yygotominor, - Opcode::PHQL_T_GREATER, + Opcode::GREATER->value, $this->yystack[$this->yyidx + -2]->minor, $this->yystack[$this->yyidx + 0]->minor ); @@ -4503,7 +4515,7 @@ private function yy_reduce(int $yyruleno): void case 112: phql_ret_expr( $yygotominor, - Opcode::PHQL_T_GREATEREQUAL, + Opcode::GREATEREQUAL->value, $this->yystack[$this->yyidx + -2]->minor, $this->yystack[$this->yyidx + 0]->minor ); @@ -4512,7 +4524,7 @@ private function yy_reduce(int $yyruleno): void case 113: phql_ret_expr( $yygotominor, - Opcode::PHQL_T_LESSEQUAL, + Opcode::LESSEQUAL->value, $this->yystack[$this->yyidx + -2]->minor, $this->yystack[$this->yyidx + 0]->minor ); @@ -4521,7 +4533,7 @@ private function yy_reduce(int $yyruleno): void case 114: phql_ret_expr( $yygotominor, - Opcode::PHQL_T_LIKE, + Opcode::LIKE->value, $this->yystack[$this->yyidx + -2]->minor, $this->yystack[$this->yyidx + 0]->minor ); @@ -4530,7 +4542,7 @@ private function yy_reduce(int $yyruleno): void case 115: phql_ret_expr( $yygotominor, - Opcode::PHQL_T_NLIKE, + Opcode::NLIKE->value, $this->yystack[$this->yyidx + -3]->minor, $this->yystack[$this->yyidx + 0]->minor ); @@ -4540,7 +4552,7 @@ private function yy_reduce(int $yyruleno): void case 116: phql_ret_expr( $yygotominor, - Opcode::PHQL_T_ILIKE, + Opcode::ILIKE->value, $this->yystack[$this->yyidx + -2]->minor, $this->yystack[$this->yyidx + 0]->minor ); @@ -4549,7 +4561,7 @@ private function yy_reduce(int $yyruleno): void case 117: phql_ret_expr( $yygotominor, - Opcode::PHQL_T_NILIKE, + Opcode::NILIKE->value, $this->yystack[$this->yyidx + -3]->minor, $this->yystack[$this->yyidx + 0]->minor ); @@ -4560,7 +4572,7 @@ private function yy_reduce(int $yyruleno): void case 121: phql_ret_expr( $yygotominor, - Opcode::PHQL_T_IN, + Opcode::IN->value, $this->yystack[$this->yyidx + -4]->minor, $this->yystack[$this->yyidx + -1]->minor ); @@ -4572,7 +4584,7 @@ private function yy_reduce(int $yyruleno): void case 122: phql_ret_expr( $yygotominor, - Opcode::PHQL_T_NOTIN, + Opcode::NOTIN->value, $this->yystack[$this->yyidx + -5]->minor, $this->yystack[$this->yyidx + -1]->minor ); @@ -4584,7 +4596,7 @@ private function yy_reduce(int $yyruleno): void case 120: phql_ret_expr( $yygotominor, - Opcode::PHQL_T_SUBQUERY, + Opcode::SUBQUERY->value, $this->yystack[$this->yyidx + -1]->minor, null ); @@ -4592,7 +4604,7 @@ private function yy_reduce(int $yyruleno): void $this->yy_destructor(46, $this->yystack[$this->yyidx + 0]->minor); break; case 123: - phql_ret_expr($yygotominor, Opcode::PHQL_T_EXISTS, null, $this->yystack[$this->yyidx + -1]->minor); + phql_ret_expr($yygotominor, Opcode::EXISTS->value, null, $this->yystack[$this->yyidx + -1]->minor); $this->yy_destructor(66, $this->yystack[$this->yyidx + -3]->minor); $this->yy_destructor(45, $this->yystack[$this->yyidx + -2]->minor); $this->yy_destructor(46, $this->yystack[$this->yyidx + 0]->minor); @@ -4600,7 +4612,7 @@ private function yy_reduce(int $yyruleno): void case 124: phql_ret_expr( $yygotominor, - Opcode::PHQL_T_AGAINST, + Opcode::AGAINST->value, $this->yystack[$this->yyidx + -2]->minor, $this->yystack[$this->yyidx + 0]->minor ); @@ -4611,7 +4623,7 @@ private function yy_reduce(int $yyruleno): void phql_ret_raw_qualified_name($qualified, $this->yystack[$this->yyidx + -1]->minor); phql_ret_expr( $yygotominor, - Opcode::PHQL_T_CAST, + Opcode::CAST->value, $this->yystack[$this->yyidx + -3]->minor, $qualified ); @@ -4625,7 +4637,7 @@ private function yy_reduce(int $yyruleno): void phql_ret_raw_qualified_name($qualified, $this->yystack[$this->yyidx + -1]->minor, null); phql_ret_expr( $yygotominor, - Opcode::PHQL_T_CONVERT, + Opcode::CONVERT->value, $this->yystack[$this->yyidx + -3]->minor, $qualified ); @@ -4637,7 +4649,7 @@ private function yy_reduce(int $yyruleno): void case 127: phql_ret_expr( $yygotominor, - Opcode::PHQL_T_CASE, + Opcode::CASE->value, $this->yystack[$this->yyidx + -2]->minor, $this->yystack[$this->yyidx + -1]->minor ); @@ -4647,7 +4659,7 @@ private function yy_reduce(int $yyruleno): void case 130: phql_ret_expr( $yygotominor, - Opcode::PHQL_T_WHEN, + Opcode::WHEN->value, $this->yystack[$this->yyidx + -2]->minor, $this->yystack[$this->yyidx + 0]->minor ); @@ -4655,7 +4667,7 @@ private function yy_reduce(int $yyruleno): void $this->yy_destructor(73, $this->yystack[$this->yyidx + -1]->minor); break; case 131: - phql_ret_expr($yygotominor, Opcode::PHQL_T_ELSE, $this->yystack[$this->yyidx + 0]->minor, null); + phql_ret_expr($yygotominor, Opcode::ELSE->value, $this->yystack[$this->yyidx + 0]->minor, null); $this->yy_destructor(74, $this->yystack[$this->yyidx + -1]->minor); break; case 133: @@ -4669,18 +4681,19 @@ private function yy_reduce(int $yyruleno): void $this->yy_destructor(46, $this->yystack[$this->yyidx + 0]->minor); break; case 134: - $yygotominor = 0; + // distinct_or_null ::= DISTINCT (DISTINCT keyword present in function call) + $yygotominor = true; $this->yy_destructor(29, $this->yystack[$this->yyidx + 0]->minor); break; case 142: - phql_ret_expr($yygotominor, Opcode::PHQL_T_ISNULL, $this->yystack[$this->yyidx + -2]->minor, null); + phql_ret_expr($yygotominor, Opcode::ISNULL->value, $this->yystack[$this->yyidx + -2]->minor, null); $this->yy_destructor(22, $this->yystack[$this->yyidx + -1]->minor); $this->yy_destructor(75, $this->yystack[$this->yyidx + 0]->minor); break; case 143: phql_ret_expr( $yygotominor, - Opcode::PHQL_T_ISNOTNULL, + Opcode::ISNOTNULL->value, $this->yystack[$this->yyidx + -3]->minor, null ); @@ -4691,7 +4704,7 @@ private function yy_reduce(int $yyruleno): void case 144: phql_ret_expr( $yygotominor, - Opcode::PHQL_T_BETWEEN, + Opcode::BETWEEN->value, $this->yystack[$this->yyidx + -2]->minor, $this->yystack[$this->yyidx + 0]->minor ); @@ -4700,20 +4713,20 @@ private function yy_reduce(int $yyruleno): void case 145: phql_ret_expr( $yygotominor, - Opcode::PHQL_T_BETWEEN_NOT, + Opcode::BETWEEN_NOT->value, $this->yystack[$this->yyidx + -2]->minor, $this->yystack[$this->yyidx + 0]->minor ); $this->yy_destructor(3, $this->yystack[$this->yyidx + -1]->minor); break; case 146: - phql_ret_expr($yygotominor, Opcode::PHQL_T_NOT, null, $this->yystack[$this->yyidx + 0]->minor); + phql_ret_expr($yygotominor, Opcode::NOT->value, null, $this->yystack[$this->yyidx + 0]->minor); $this->yy_destructor(24, $this->yystack[$this->yyidx + -1]->minor); break; case 147: phql_ret_expr( $yygotominor, - Opcode::PHQL_T_BITWISE_NOT, + Opcode::BITWISE_NOT->value, null, $this->yystack[$this->yyidx + 0]->minor ); @@ -4722,7 +4735,7 @@ private function yy_reduce(int $yyruleno): void case 148: phql_ret_expr( $yygotominor, - Opcode::PHQL_T_ENCLOSED, + Opcode::ENCLOSED->value, $this->yystack[$this->yyidx + -1]->minor, null ); @@ -4730,19 +4743,19 @@ private function yy_reduce(int $yyruleno): void $this->yy_destructor(46, $this->yystack[$this->yyidx + 0]->minor); break; case 152: - phql_ret_literal_zval($yygotominor, Opcode::PHQL_T_STRING, $this->yystack[$this->yyidx + 0]->minor); + phql_ret_literal_zval($yygotominor, Opcode::STRING->value, $this->yystack[$this->yyidx + 0]->minor); break; case 153: - phql_ret_literal_zval($yygotominor, Opcode::PHQL_T_DOUBLE, $this->yystack[$this->yyidx + 0]->minor); + phql_ret_literal_zval($yygotominor, Opcode::DOUBLE->value, $this->yystack[$this->yyidx + 0]->minor); break; case 154: - phql_ret_literal_zval($yygotominor, Opcode::PHQL_T_NULL); + phql_ret_literal_zval($yygotominor, Opcode::NULL->value); $this->yy_destructor(75, $this->yystack[$this->yyidx + 0]->minor); break; case 155: #line 920 "c/parser.php.lemon" { - phql_ret_literal_zval($yygotominor, Opcode::PHQL_T_TRUE, null); + phql_ret_literal_zval($yygotominor, Opcode::TRUE->value, null); $this->yy_destructor(78, $this->yystack[$this->yyidx + 0]->minor); } #line 2086 "c/parser.php.php" @@ -4750,7 +4763,7 @@ private function yy_reduce(int $yyruleno): void case 156: #line 924 "c/parser.php.lemon" { - phql_ret_literal_zval($yygotominor, Opcode::PHQL_T_FALSE, null); + phql_ret_literal_zval($yygotominor, Opcode::FALSE->value, null); $this->yy_destructor(79, $this->yystack[$this->yyidx + 0]->minor); } #line 2094 "c/parser.php.php" @@ -4848,8 +4861,8 @@ private function yy_syntax_error() if ($this->status->getState()->getStartLength()) { if ($active_token) { - if (in_array($active_token->getOpcode(), $tokens)) { - $token_name = array_search($active_token->getOpcode(), $tokens); + if (in_array($active_token->value, $tokens)) { + $token_name = array_search($active_token->value, $tokens); } } @@ -4858,11 +4871,11 @@ private function yy_syntax_error() } if ($near_length > 0) { - if ($this->status->getToken()->getValue()) { + if ($this->status->getToken()->value) { $this->status->setSyntaxError(sprintf( "Syntax error, unexpected token %s(%s), near to '%s', when parsing: %s", $token_name, - $this->status->getToken()->getValue(), + $this->status->getToken()->value, $this->status->getState()->getStart(), $this->status->getState()->getRawBuffer(), )); @@ -4875,12 +4888,12 @@ private function yy_syntax_error() )); } } else { - if ($active_token != Opcode::PHQL_T_IGNORE) { - if ($this->status->getToken()->getValue()) { + if ($active_token !== Opcode::IGNORE) { + if ($this->status->getToken()->value) { $this->status->setSyntaxError(sprintf( "Syntax error, unexpected token %s(%s), at the end of query, when parsing: %s", $token_name, - $this->status->getToken()->getValue(), + $this->status->getToken()->value, $this->status->getState()->getRawBuffer(), )); } else { @@ -4924,20 +4937,21 @@ class phql_yyStackEntry function phql_ret_insert_statement(&$ret, $Q, $F, $V): void { $ret = [ - "type" => Opcode::PHQL_T_INSERT, + "type" => Opcode::INSERT->value, "qualifiedName" => $Q, - "values" => $V, ]; if ($F !== null) { $ret["fields"] = $F; } + + $ret["values"] = $V; } function phql_ret_select_statement(&$ret, $S, $W, $O, $G, $H, $L, $F): void { $ret = [ - "type" => Opcode::PHQL_T_SELECT, + "type" => Opcode::SELECT->value, "select" => $S, ]; @@ -4995,7 +5009,7 @@ function phql_ret_literal_zval(&$ret, int $type, ?Token $T = null): array { $ret = ['type' => $type]; if ($T !== null) { - $ret['value'] = $T->getValue(); + $ret['value'] = $T->value; } return $ret; @@ -5040,11 +5054,11 @@ function phql_ret_column_item( } if ($identifierColumn !== null) { - $ret['column'] = $identifierColumn->getValue(); + $ret['column'] = $identifierColumn->value; } if ($alias !== null) { - $ret['alias'] = $alias->getValue(); + $ret['alias'] = $alias->value; } return $ret; @@ -5083,25 +5097,25 @@ function phql_ret_qualified_name( ): void { $ret = [ - 'type' => Opcode::PHQL_T_QUALIFIED, + 'type' => Opcode::QUALIFIED->value, ]; if ($nsAlias !== null) { - $ret['ns-alias'] = $nsAlias->getValue(); + $ret['ns-alias'] = $nsAlias->value; } /* if (B) phql_add_assoc_stringl(..., "domain", ...) */ if ($domain !== null) { - $ret['domain'] = $domain->getValue(); + $ret['domain'] = $domain->value; } - $ret['name'] = $name->getValue(); + $ret['name'] = $name->value; } function phql_ret_update_statement(array &$ret, $update, $where = null, $limit = null): void { $ret = []; - $ret['type'] = defined('PHQL_T_UPDATE') ? PHQL_T_UPDATE : 0; + $ret['type'] = Opcode::UPDATE->value; $ret['update'] = $update; if ($where !== null) { @@ -5130,7 +5144,7 @@ function phql_ret_update_item(array &$ret, $column, $expr): void function phql_ret_delete_statement(array &$ret, $delete, $where = null, $limit = null): void { $ret = []; - $ret['type'] = defined('PHQL_T_DELETE') ? PHQL_T_DELETE : 0; + $ret['type'] = Opcode::DELETE->value; $ret['delete'] = $delete; if ($where !== null) { @@ -5156,7 +5170,7 @@ function phql_ret_assoc_name(array &$ret, $qualifiedName, $alias = null, $with = ]; if ($alias !== null) { - $ret['alias'] = $alias instanceof Token ? $alias->getValue() : $alias; + $ret['alias'] = $alias instanceof Token ? $alias->value : $alias; } if ($with !== null) { @@ -5188,29 +5202,29 @@ function phql_ret_placeholder_zval(array &$ret, int $type, ?Token $value = null) { $ret = []; $ret['type'] = $type; - $ret['value'] = $value->getValue() ?? null; + $ret['value'] = $value->value ?? null; } -function phql_ret_raw_qualified_name(array &$ret, string $tokenA, ?string $tokenB = null): void +function phql_ret_raw_qualified_name(array &$ret, Token $tokenA, ?Token $tokenB = null): void { $ret = []; - $ret['type'] = defined('PHQL_T_RAW_QUALIFIED') ? PHQL_T_RAW_QUALIFIED : 0; + $ret['type'] = Opcode::RAW_QUALIFIED->value; if ($tokenB !== null) { /* Two-part qualified name: domain + name */ - $ret['domain'] = $tokenA; // equivalent to phql_add_assoc_stringl(..., "domain", A->token, ...) - $ret['name'] = $tokenB; // equivalent to phql_add_assoc_stringl(..., "name", B->token, ...) + $ret['domain'] = $tokenA->value; + $ret['name'] = $tokenB->value; } else { /* Single-part name */ - $ret['name'] = $tokenA; + $ret['name'] = $tokenA->value; } } function phql_ret_func_call(array &$ret, $name, $arguments = null, $distinct = null): void { $ret = []; - $ret['type'] = defined('PHQL_T_FCALL') ? PHQL_T_FCALL : 0; - $ret['name'] = $name instanceof Token ? $name->getValue() : $name; + $ret['type'] = Opcode::FCALL->value; + $ret['name'] = $name instanceof Token ? $name->value : $name; if ($arguments !== null) { $ret['arguments'] = $arguments; diff --git a/src/Enum.php b/src/Enum.php deleted file mode 100644 index 5b14eb0..0000000 --- a/src/Enum.php +++ /dev/null @@ -1,138 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE.txt - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Phalcon\Phql; - -class Enum -{ - /** - * PHQL - */ - public const PHQL_SCANNER_RETCODE_EOF = -1; - public const PHQL_SCANNER_RETCODE_ERR = -2; - public const PHQL_SCANNER_RETCODE_IMPOSSIBLE = -3; - public const PHQL_T_ADD = '+'; - - /* Literals & Identifiers */ - public const PHQL_T_AGAINST = 276; - public const PHQL_T_ALL = 338; - public const PHQL_T_AND = 266; - public const PHQL_T_AS = 305; - public const PHQL_T_ASC = 327; - - /* Operators */ - public const PHQL_T_BETWEEN = 331; - public const PHQL_T_BETWEEN_NOT = 332; - public const PHQL_T_BITWISE_AND = '&'; - public const PHQL_T_BITWISE_NOT = '~'; - public const PHQL_T_BITWISE_OR = '|'; - public const PHQL_T_BITWISE_XOR = '^'; - public const PHQL_T_BPLACEHOLDER = 277; - public const PHQL_T_BY = 311; - public const PHQL_T_CASE = 409; - public const PHQL_T_CAST = 333; - public const PHQL_T_COLON = ':'; - public const PHQL_T_COMMA = 269; - public const PHQL_T_CONVERT = 336; - public const PHQL_T_CROSS = 324; - public const PHQL_T_CROSSJOIN = 363; - public const PHQL_T_DELETE = 303; - public const PHQL_T_DESC = 328; - public const PHQL_T_DISTINCT = 330; - public const PHQL_T_DIV = '/'; - public const PHQL_T_DOMAINALL = 353; - public const PHQL_T_DOT = '.'; - public const PHQL_T_DOUBLE = 259; - public const PHQL_T_ELSE = 411; - public const PHQL_T_ENCLOSED = 356; - public const PHQL_T_END = 412; - public const PHQL_T_EQUALS = '='; - public const PHQL_T_EXISTS = 408; - public const PHQL_T_EXPR = 354; - public const PHQL_T_FALSE = 335; - /** Special Tokens */ - public const PHQL_T_FCALL = 350; - public const PHQL_T_FOR = 339; - public const PHQL_T_FROM = 304; - public const PHQL_T_FULL = 325; - public const PHQL_T_FULLJOIN = 364; - public const PHQL_T_GREATER = '>'; - public const PHQL_T_GREATEREQUAL = 272; - public const PHQL_T_GROUP = 313; - public const PHQL_T_HAVING = 314; - public const PHQL_T_HINTEGER = 414; - public const PHQL_T_IDENTIFIER = 265; - public const PHQL_T_IGNORE = 257; - public const PHQL_T_ILIKE = 275; - public const PHQL_T_IN = 315; - public const PHQL_T_INNER = 317; - public const PHQL_T_INNERJOIN = 360; - public const PHQL_T_INSERT = 306; - public const PHQL_T_INTEGER = 258; - public const PHQL_T_INTO = 307; - public const PHQL_T_IS = 321; - public const PHQL_T_ISNOTNULL = 366; - public const PHQL_T_ISNULL = 365; - public const PHQL_T_JOIN = 318; - public const PHQL_T_LEFT = 319; - public const PHQL_T_LEFTJOIN = 361; - public const PHQL_T_LESS = '<'; - public const PHQL_T_LESSEQUAL = 271; - public const PHQL_T_LIKE = 268; - public const PHQL_T_LIMIT = 312; - public const PHQL_T_MINUS = 367; - public const PHQL_T_MOD = '%'; - public const PHQL_T_MUL = '*'; - public const PHQL_T_NILIKE = 357; - public const PHQL_T_NLIKE = 351; - public const PHQL_T_NOT = '!'; - public const PHQL_T_NOTEQUALS = 270; - public const PHQL_T_NOTIN = 323; - /** Placeholders */ - public const PHQL_T_NPLACEHOLDER = 273; - public const PHQL_T_NULL = 322; - public const PHQL_T_OFFSET = 329; - public const PHQL_T_ON = 316; - public const PHQL_T_OR = 267; - public const PHQL_T_ORDER = 310; - public const PHQL_T_OUTER = 326; - public const PHQL_T_PARENTHESES_CLOSE = ')'; - public const PHQL_T_PARENTHESES_OPEN = '('; - public const PHQL_T_QUALIFIED = 355; - public const PHQL_T_RAW_QUALIFIED = 358; - public const PHQL_T_RIGHT = 320; - public const PHQL_T_RIGHTJOIN = 362; - public const PHQL_T_SELECT = 309; - public const PHQL_T_SET = 301; - public const PHQL_T_SPLACEHOLDER = 274; - public const PHQL_T_STARALL = 352; - public const PHQL_T_STRING = 260; - public const PHQL_T_SUB = '-'; - public const PHQL_T_SUBQUERY = 407; - public const PHQL_T_THEN = 413; - public const PHQL_T_TRUE = 334; - public const PHQL_T_TS_AND = 403; - public const PHQL_T_TS_CONTAINS_ANOTHER = 405; - public const PHQL_T_TS_CONTAINS_IN = 406; - /** Postgresql Text Search Operators */ - public const PHQL_T_TS_MATCHES = 401; - public const PHQL_T_TS_NEGATE = 404; - public const PHQL_T_TS_OR = 402; - /** Reserved words */ - public const PHQL_T_UPDATE = 300; - public const PHQL_T_USING = 337; - public const PHQL_T_VALUES = 308; - public const PHQL_T_WHEN = 410; - public const PHQL_T_WHERE = 302; - public const PHQL_T_WITH = 415; -} diff --git a/src/Parser.php b/src/Parser.php index 233e752..70f173c 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -4,423 +4,226 @@ namespace Phalcon\Phql; +use Phalcon\Phql\Parser\Status; use Phalcon\Phql\Scanner\Opcode; use Phalcon\Phql\Scanner\Scanner; +use Phalcon\Phql\Scanner\ScannerStatus; use Phalcon\Phql\Scanner\State; use Phalcon\Phql\Scanner\Token; -use RuntimeException; -use stdClass; /** * Orchestrates the PHQL lexer and parser, equivalent to * phql_internal_parse_phql() in base.c. */ -class Parser +final class Parser { - private bool $enableLiterals; - - public function __construct(bool $enableLiterals = true) - { - $this->enableLiterals = $enableLiterals; - } + private bool $enableLiterals = true; /** * Parse a PHQL string and return the AST array. * - * @throws RuntimeException on syntax or scanner error + * @return array + * @throws Exception on syntax or scanner error */ public function parse(string $phql): array { if ($phql === '') { - throw new RuntimeException('PHQL statement cannot be NULL'); + throw new Exception('PHQL statement cannot be NULL'); } - $state = new State($phql); - $token = new Token(); - $scanner = new Scanner($state, $token); - - $parserObject = new \phql_Parser(); - - // Status object mirrors phql_parser_status in C - $status = new stdClass(); - $status->status = \phql_Parser::PHQL_PARSING_OK; - $status->scanner_state = $state; - $status->ret = null; - $status->syntax_error = null; - $status->token = $token; - $status->enable_literals = $this->enableLiterals; - $status->phql = $phql; - $status->phql_length = mb_strlen($phql); - - $parserObject->status = $status; - - $errorMsg = null; - $failed = false; - - while (($scannerStatus = $scanner->scanForToken()) >= 0) { - // Equivalent to: state->start_length = (phql + phql_length - state->start) - $state->startLength = $status->phql_length - $state->getCursor(); - - $state->activeToken = $token->opcode; - - switch ($token->opcode) { - case Opcode::PHQL_T_IGNORE: - break; - - case Opcode::PHQL_T_ADD: - $parserObject->phql_(\phql_Parser::PHQL_PLUS); - break; - case Opcode::PHQL_T_SUB: - $parserObject->phql_(\phql_Parser::PHQL_MINUS); - break; - case Opcode::PHQL_T_MUL: - $parserObject->phql_(\phql_Parser::PHQL_TIMES); - break; - case Opcode::PHQL_T_DIV: - $parserObject->phql_(\phql_Parser::PHQL_DIVIDE); - break; - case Opcode::PHQL_T_MOD: - $parserObject->phql_(\phql_Parser::PHQL_MOD); - break; - case Opcode::PHQL_T_AND: - $parserObject->phql_(\phql_Parser::PHQL_AND); - break; - case Opcode::PHQL_T_OR: - $parserObject->phql_(\phql_Parser::PHQL_OR); - break; - case Opcode::PHQL_T_EQUALS: - $parserObject->phql_(\phql_Parser::PHQL_EQUALS); - break; - case Opcode::PHQL_T_NOTEQUALS: - $parserObject->phql_(\phql_Parser::PHQL_NOTEQUALS); - break; - case Opcode::PHQL_T_LESS: - $parserObject->phql_(\phql_Parser::PHQL_LESS); - break; - case Opcode::PHQL_T_GREATER: - $parserObject->phql_(\phql_Parser::PHQL_GREATER); - break; - case Opcode::PHQL_T_GREATEREQUAL: - $parserObject->phql_(\phql_Parser::PHQL_GREATEREQUAL); - break; - case Opcode::PHQL_T_LESSEQUAL: - $parserObject->phql_(\phql_Parser::PHQL_LESSEQUAL); - break; - - case Opcode::PHQL_T_IDENTIFIER: - $parserObject->phql_(\phql_Parser::PHQL_IDENTIFIER, $this->makeParserToken($token)); - break; - - case Opcode::PHQL_T_DOT: - $parserObject->phql_(\phql_Parser::PHQL_DOT); - break; - case Opcode::PHQL_T_COMMA: - $parserObject->phql_(\phql_Parser::PHQL_COMMA); - break; - - case Opcode::PHQL_T_PARENTHESES_OPEN: - $parserObject->phql_(\phql_Parser::PHQL_PARENTHESES_OPEN); - break; - case Opcode::PHQL_T_PARENTHESES_CLOSE: - $parserObject->phql_(\phql_Parser::PHQL_PARENTHESES_CLOSE); - break; - - case Opcode::PHQL_T_LIKE: - $parserObject->phql_(\phql_Parser::PHQL_LIKE); - break; - case Opcode::PHQL_T_ILIKE: - $parserObject->phql_(\phql_Parser::PHQL_ILIKE); - break; - case Opcode::PHQL_T_NOT: - $parserObject->phql_(\phql_Parser::PHQL_NOT); - break; - case Opcode::PHQL_T_BITWISE_AND: - $parserObject->phql_(\phql_Parser::PHQL_BITWISE_AND); - break; - case Opcode::PHQL_T_BITWISE_OR: - $parserObject->phql_(\phql_Parser::PHQL_BITWISE_OR); - break; - case Opcode::PHQL_T_BITWISE_NOT: - $parserObject->phql_(\phql_Parser::PHQL_BITWISE_NOT); - break; - case Opcode::PHQL_T_BITWISE_XOR: - $parserObject->phql_(\phql_Parser::PHQL_BITWISE_XOR); - break; - case Opcode::PHQL_T_AGAINST: - $parserObject->phql_(\phql_Parser::PHQL_AGAINST); - break; - case Opcode::PHQL_T_CASE: - $parserObject->phql_(\phql_Parser::PHQL_CASE); - break; - case Opcode::PHQL_T_WHEN: - $parserObject->phql_(\phql_Parser::PHQL_WHEN); - break; - case Opcode::PHQL_T_THEN: - $parserObject->phql_(\phql_Parser::PHQL_THEN); - break; - case Opcode::PHQL_T_END: - $parserObject->phql_(\phql_Parser::PHQL_END); - break; - case Opcode::PHQL_T_ELSE: - $parserObject->phql_(\phql_Parser::PHQL_ELSE); - break; - case Opcode::PHQL_T_FOR: - $parserObject->phql_(\phql_Parser::PHQL_FOR); - break; - case Opcode::PHQL_T_WITH: - $parserObject->phql_(\phql_Parser::PHQL_WITH); - break; - - case Opcode::PHQL_T_INTEGER: - if ($this->enableLiterals) { - $parserObject->phql_(\phql_Parser::PHQL_INTEGER, $this->makeParserToken($token)); - } else { - $errorMsg = 'Literals are disabled in PHQL statements'; - $status->status = \phql_Parser::PHQL_PARSING_FAILED; - } - break; - case Opcode::PHQL_T_DOUBLE: - if ($this->enableLiterals) { - $parserObject->phql_(\phql_Parser::PHQL_DOUBLE, $this->makeParserToken($token)); - } else { - $errorMsg = 'Literals are disabled in PHQL statements'; - $status->status = \phql_Parser::PHQL_PARSING_FAILED; - } - break; - case Opcode::PHQL_T_STRING: - if ($this->enableLiterals) { - $parserObject->phql_(\phql_Parser::PHQL_STRING, $this->makeParserToken($token)); - } else { - $errorMsg = 'Literals are disabled in PHQL statements'; - $status->status = \phql_Parser::PHQL_PARSING_FAILED; - } - break; - case Opcode::PHQL_T_TRUE: - if ($this->enableLiterals) { - $parserObject->phql_(\phql_Parser::PHQL_TRUE); - } else { - $errorMsg = 'Literals are disabled in PHQL statements'; - $status->status = \phql_Parser::PHQL_PARSING_FAILED; - } - break; - case Opcode::PHQL_T_FALSE: - if ($this->enableLiterals) { - $parserObject->phql_(\phql_Parser::PHQL_FALSE); - } else { - $errorMsg = 'Literals are disabled in PHQL statements'; - $status->status = \phql_Parser::PHQL_PARSING_FAILED; - } - break; - case Opcode::PHQL_T_HINTEGER: - if ($this->enableLiterals) { - $parserObject->phql_(\phql_Parser::PHQL_HINTEGER, $this->makeParserToken($token)); - } else { - $errorMsg = 'Literals are disabled in PHQL statements'; - $status->status = \phql_Parser::PHQL_PARSING_FAILED; - } - break; - - case Opcode::PHQL_T_NPLACEHOLDER: - $parserObject->phql_(\phql_Parser::PHQL_NPLACEHOLDER, $this->makeParserToken($token)); - break; - case Opcode::PHQL_T_SPLACEHOLDER: - $parserObject->phql_(\phql_Parser::PHQL_SPLACEHOLDER, $this->makeParserToken($token)); - break; - case Opcode::PHQL_T_BPLACEHOLDER: - $parserObject->phql_(\phql_Parser::PHQL_BPLACEHOLDER, $this->makeParserToken($token)); - break; - - case Opcode::PHQL_T_FROM: - $parserObject->phql_(\phql_Parser::PHQL_FROM); - break; - case Opcode::PHQL_T_UPDATE: - $parserObject->phql_(\phql_Parser::PHQL_UPDATE); - break; - case Opcode::PHQL_T_SET: - $parserObject->phql_(\phql_Parser::PHQL_SET); - break; - case Opcode::PHQL_T_WHERE: - $parserObject->phql_(\phql_Parser::PHQL_WHERE); - break; - case Opcode::PHQL_T_DELETE: - $parserObject->phql_(\phql_Parser::PHQL_DELETE); - break; - case Opcode::PHQL_T_INSERT: - $parserObject->phql_(\phql_Parser::PHQL_INSERT); - break; - case Opcode::PHQL_T_INTO: - $parserObject->phql_(\phql_Parser::PHQL_INTO); - break; - case Opcode::PHQL_T_VALUES: - $parserObject->phql_(\phql_Parser::PHQL_VALUES); - break; - case Opcode::PHQL_T_SELECT: - $parserObject->phql_(\phql_Parser::PHQL_SELECT); - break; - case Opcode::PHQL_T_AS: - $parserObject->phql_(\phql_Parser::PHQL_AS); - break; - case Opcode::PHQL_T_ORDER: - $parserObject->phql_(\phql_Parser::PHQL_ORDER); - break; - case Opcode::PHQL_T_BY: - $parserObject->phql_(\phql_Parser::PHQL_BY); - break; - case Opcode::PHQL_T_LIMIT: - $parserObject->phql_(\phql_Parser::PHQL_LIMIT); - break; - case Opcode::PHQL_T_OFFSET: - $parserObject->phql_(\phql_Parser::PHQL_OFFSET); - break; - case Opcode::PHQL_T_GROUP: - $parserObject->phql_(\phql_Parser::PHQL_GROUP); - break; - case Opcode::PHQL_T_HAVING: - $parserObject->phql_(\phql_Parser::PHQL_HAVING); - break; - case Opcode::PHQL_T_ASC: - $parserObject->phql_(\phql_Parser::PHQL_ASC); - break; - case Opcode::PHQL_T_DESC: - $parserObject->phql_(\phql_Parser::PHQL_DESC); - break; - case Opcode::PHQL_T_IN: - $parserObject->phql_(\phql_Parser::PHQL_IN); - break; - case Opcode::PHQL_T_ON: - $parserObject->phql_(\phql_Parser::PHQL_ON); - break; - case Opcode::PHQL_T_INNER: - $parserObject->phql_(\phql_Parser::PHQL_INNER); - break; - case Opcode::PHQL_T_JOIN: - $parserObject->phql_(\phql_Parser::PHQL_JOIN); - break; - case Opcode::PHQL_T_LEFT: - $parserObject->phql_(\phql_Parser::PHQL_LEFT); - break; - case Opcode::PHQL_T_RIGHT: - $parserObject->phql_(\phql_Parser::PHQL_RIGHT); - break; - case Opcode::PHQL_T_CROSS: - $parserObject->phql_(\phql_Parser::PHQL_CROSS); - break; - case Opcode::PHQL_T_FULL: - $parserObject->phql_(\phql_Parser::PHQL_FULL); - break; - case Opcode::PHQL_T_OUTER: - $parserObject->phql_(\phql_Parser::PHQL_OUTER); - break; - case Opcode::PHQL_T_IS: - $parserObject->phql_(\phql_Parser::PHQL_IS); - break; - case Opcode::PHQL_T_NULL: - $parserObject->phql_(\phql_Parser::PHQL_NULL); - break; - case Opcode::PHQL_T_BETWEEN: - $parserObject->phql_(\phql_Parser::PHQL_BETWEEN); - break; - case Opcode::PHQL_T_BETWEEN_NOT: - $parserObject->phql_(\phql_Parser::PHQL_BETWEEN_NOT); - break; - case Opcode::PHQL_T_DISTINCT: - $parserObject->phql_(\phql_Parser::PHQL_DISTINCT); - break; - case Opcode::PHQL_T_ALL: - $parserObject->phql_(\phql_Parser::PHQL_ALL); - break; - case Opcode::PHQL_T_CAST: - $parserObject->phql_(\phql_Parser::PHQL_CAST); - break; - case Opcode::PHQL_T_CONVERT: - $parserObject->phql_(\phql_Parser::PHQL_CONVERT); - break; - case Opcode::PHQL_T_USING: - $parserObject->phql_(\phql_Parser::PHQL_USING); - break; - case Opcode::PHQL_T_EXISTS: - $parserObject->phql_(\phql_Parser::PHQL_EXISTS); - break; - - default: - $status->status = \phql_Parser::PHQL_PARSING_FAILED; - $errorMsg = sprintf('Scanner: Unknown opcode %d', $token->opcode); - break; - } - - if ($status->status !== \phql_Parser::PHQL_PARSING_OK) { - $failed = true; + $state = new State($phql); + $scanner = new Scanner($state); + $token = $scanner->getToken(); + $status = new Status($state); + $parserObject = new \phql_Parser($status); + + $status->setToken($token); + $status->setEnableLiterals($this->enableLiterals); + + $errorMessage = null; + $parseFailed = false; + + while (($scannerStatus = $scanner->scanForToken()) === ScannerStatus::OK) { + $state->setStartLength(mb_strlen($phql) - $state->getCursor()); + $state->setActiveToken($scanner->getToken()->opcode); + + match ($scanner->getToken()->opcode) { + Opcode::IGNORE => null, + + Opcode::ADD => $parserObject->phql_(\phql_Parser::PHQL_PLUS), + Opcode::SUB => $parserObject->phql_(\phql_Parser::PHQL_MINUS), + Opcode::MUL => $parserObject->phql_(\phql_Parser::PHQL_TIMES), + Opcode::DIV => $parserObject->phql_(\phql_Parser::PHQL_DIVIDE), + Opcode::MOD => $parserObject->phql_(\phql_Parser::PHQL_MOD), + Opcode::AND => $parserObject->phql_(\phql_Parser::PHQL_AND), + Opcode::OR => $parserObject->phql_(\phql_Parser::PHQL_OR), + Opcode::EQUALS => $parserObject->phql_(\phql_Parser::PHQL_EQUALS), + Opcode::NOTEQUALS => $parserObject->phql_(\phql_Parser::PHQL_NOTEQUALS), + Opcode::LESS => $parserObject->phql_(\phql_Parser::PHQL_LESS), + Opcode::GREATER => $parserObject->phql_(\phql_Parser::PHQL_GREATER), + Opcode::GREATEREQUAL => $parserObject->phql_(\phql_Parser::PHQL_GREATEREQUAL), + Opcode::LESSEQUAL => $parserObject->phql_(\phql_Parser::PHQL_LESSEQUAL), + Opcode::DOT => $parserObject->phql_(\phql_Parser::PHQL_DOT), + Opcode::COMMA => $parserObject->phql_(\phql_Parser::PHQL_COMMA), + Opcode::PARENTHESES_OPEN => $parserObject->phql_(\phql_Parser::PHQL_PARENTHESES_OPEN), + Opcode::PARENTHESES_CLOSE => $parserObject->phql_(\phql_Parser::PHQL_PARENTHESES_CLOSE), + Opcode::LIKE => $parserObject->phql_(\phql_Parser::PHQL_LIKE), + Opcode::ILIKE => $parserObject->phql_(\phql_Parser::PHQL_ILIKE), + Opcode::NOT => $parserObject->phql_(\phql_Parser::PHQL_NOT), + Opcode::BITWISE_AND => $parserObject->phql_(\phql_Parser::PHQL_BITWISE_AND), + Opcode::BITWISE_OR => $parserObject->phql_(\phql_Parser::PHQL_BITWISE_OR), + Opcode::BITWISE_NOT => $parserObject->phql_(\phql_Parser::PHQL_BITWISE_NOT), + Opcode::BITWISE_XOR => $parserObject->phql_(\phql_Parser::PHQL_BITWISE_XOR), + Opcode::AGAINST => $parserObject->phql_(\phql_Parser::PHQL_AGAINST), + Opcode::CASE => $parserObject->phql_(\phql_Parser::PHQL_CASE), + Opcode::WHEN => $parserObject->phql_(\phql_Parser::PHQL_WHEN), + Opcode::THEN => $parserObject->phql_(\phql_Parser::PHQL_THEN), + Opcode::END => $parserObject->phql_(\phql_Parser::PHQL_END), + Opcode::ELSE => $parserObject->phql_(\phql_Parser::PHQL_ELSE), + Opcode::FOR => $parserObject->phql_(\phql_Parser::PHQL_FOR), + Opcode::WITH => $parserObject->phql_(\phql_Parser::PHQL_WITH), + Opcode::FROM => $parserObject->phql_(\phql_Parser::PHQL_FROM), + Opcode::UPDATE => $parserObject->phql_(\phql_Parser::PHQL_UPDATE), + Opcode::SET => $parserObject->phql_(\phql_Parser::PHQL_SET), + Opcode::WHERE => $parserObject->phql_(\phql_Parser::PHQL_WHERE), + Opcode::DELETE => $parserObject->phql_(\phql_Parser::PHQL_DELETE), + Opcode::INSERT => $parserObject->phql_(\phql_Parser::PHQL_INSERT), + Opcode::INTO => $parserObject->phql_(\phql_Parser::PHQL_INTO), + Opcode::VALUES => $parserObject->phql_(\phql_Parser::PHQL_VALUES), + Opcode::SELECT => $parserObject->phql_(\phql_Parser::PHQL_SELECT), + Opcode::AS => $parserObject->phql_(\phql_Parser::PHQL_AS), + Opcode::ORDER => $parserObject->phql_(\phql_Parser::PHQL_ORDER), + Opcode::BY => $parserObject->phql_(\phql_Parser::PHQL_BY), + Opcode::LIMIT => $parserObject->phql_(\phql_Parser::PHQL_LIMIT), + Opcode::OFFSET => $parserObject->phql_(\phql_Parser::PHQL_OFFSET), + Opcode::GROUP => $parserObject->phql_(\phql_Parser::PHQL_GROUP), + Opcode::HAVING => $parserObject->phql_(\phql_Parser::PHQL_HAVING), + Opcode::ASC => $parserObject->phql_(\phql_Parser::PHQL_ASC), + Opcode::DESC => $parserObject->phql_(\phql_Parser::PHQL_DESC), + Opcode::IN => $parserObject->phql_(\phql_Parser::PHQL_IN), + Opcode::ON => $parserObject->phql_(\phql_Parser::PHQL_ON), + Opcode::INNER => $parserObject->phql_(\phql_Parser::PHQL_INNER), + Opcode::JOIN => $parserObject->phql_(\phql_Parser::PHQL_JOIN), + Opcode::LEFT => $parserObject->phql_(\phql_Parser::PHQL_LEFT), + Opcode::RIGHT => $parserObject->phql_(\phql_Parser::PHQL_RIGHT), + Opcode::CROSS => $parserObject->phql_(\phql_Parser::PHQL_CROSS), + Opcode::FULL => $parserObject->phql_(\phql_Parser::PHQL_FULL), + Opcode::OUTER => $parserObject->phql_(\phql_Parser::PHQL_OUTER), + Opcode::IS => $parserObject->phql_(\phql_Parser::PHQL_IS), + Opcode::NULL => $parserObject->phql_(\phql_Parser::PHQL_NULL), + Opcode::BETWEEN => $parserObject->phql_(\phql_Parser::PHQL_BETWEEN), + Opcode::BETWEEN_NOT => $parserObject->phql_(\phql_Parser::PHQL_BETWEEN_NOT), + Opcode::DISTINCT => $parserObject->phql_(\phql_Parser::PHQL_DISTINCT), + Opcode::ALL => $parserObject->phql_(\phql_Parser::PHQL_ALL), + Opcode::CAST => $parserObject->phql_(\phql_Parser::PHQL_CAST), + Opcode::CONVERT => $parserObject->phql_(\phql_Parser::PHQL_CONVERT), + Opcode::USING => $parserObject->phql_(\phql_Parser::PHQL_USING), + Opcode::EXISTS => $parserObject->phql_(\phql_Parser::PHQL_EXISTS), + + Opcode::IDENTIFIER => $parserObject->phql_( + \phql_Parser::PHQL_IDENTIFIER, + $this->makeParserToken($scanner->getToken()) + ), + Opcode::NPLACEHOLDER => $parserObject->phql_( + \phql_Parser::PHQL_NPLACEHOLDER, + $this->makeParserToken($scanner->getToken()) + ), + Opcode::SPLACEHOLDER => $parserObject->phql_( + \phql_Parser::PHQL_SPLACEHOLDER, + $this->makeParserToken($scanner->getToken()) + ), + Opcode::BPLACEHOLDER => $parserObject->phql_( + \phql_Parser::PHQL_BPLACEHOLDER, + $this->makeParserToken($scanner->getToken()) + ), + + Opcode::INTEGER => $this->enableLiterals + ? $parserObject->phql_(\phql_Parser::PHQL_INTEGER, $this->makeParserToken($scanner->getToken())) + : $this->handleLiteralsDisabled($status), + Opcode::DOUBLE => $this->enableLiterals + ? $parserObject->phql_(\phql_Parser::PHQL_DOUBLE, $this->makeParserToken($scanner->getToken())) + : $this->handleLiteralsDisabled($status), + Opcode::STRING => $this->enableLiterals + ? $parserObject->phql_(\phql_Parser::PHQL_STRING, $this->makeParserToken($scanner->getToken())) + : $this->handleLiteralsDisabled($status), + Opcode::HINTEGER => $this->enableLiterals + ? $parserObject->phql_(\phql_Parser::PHQL_HINTEGER, $this->makeParserToken($scanner->getToken())) + : $this->handleLiteralsDisabled($status), + Opcode::TRUE => $this->enableLiterals + ? $parserObject->phql_(\phql_Parser::PHQL_TRUE) + : $this->handleLiteralsDisabled($status), + Opcode::FALSE => $this->enableLiterals + ? $parserObject->phql_(\phql_Parser::PHQL_FALSE) + : $this->handleLiteralsDisabled($status), + + default => $this->handleUnknownOpcode($scanner->getToken()->opcode, $status), + }; + + if ($status->getStatus() !== Status::PHQL_PARSING_OK) { + $parseFailed = true; break; } } - if (!$failed) { + if (!$parseFailed) { if ( - $scannerStatus === Scanner::PHQL_SCANNER_RETCODE_ERR - || $scannerStatus === Scanner::PHQL_SCANNER_RETCODE_IMPOSSIBLE + $scannerStatus === ScannerStatus::ERR + || $scannerStatus === ScannerStatus::IMPOSSIBLE ) { - if ($errorMsg === null) { - $errorMsg = $this->buildScannerErrorMsg($status, $phql); - } - $failed = true; + $errorMessage = $this->buildScannerErrorMessage($status, $phql); + $parseFailed = true; } else { - // Signal EOF to the parser $parserObject->phql_(0); } } - $state->activeToken = 0; + $state->setActiveToken(null); - if ($status->status !== \phql_Parser::PHQL_PARSING_OK) { - $failed = true; - if ($status->syntax_error !== null && $errorMsg === null) { - $errorMsg = $status->syntax_error; + if ($status->getStatus() !== Status::PHQL_PARSING_OK) { + $parseFailed = true; + if ($status->getSyntaxError() !== null && $errorMessage === null) { + $errorMessage = $status->getSyntaxError(); } } - if ($failed) { - throw new RuntimeException($errorMsg ?? 'Unknown PHQL parsing error'); + if ($parseFailed) { + throw new Exception($errorMessage ?? 'Unknown PHQL parsing error'); } - if (!is_array($status->ret)) { - throw new RuntimeException('PHQL parsing produced no result'); + /** @var array|null $ast */ + $ast = $status->getAst(); + if (!is_array($ast)) { + throw new Exception('PHQL parsing produced no result'); // @codeCoverageIgnore } - return $status->ret; + return $ast; } - /** - * Wrap a scanner Token into the lightweight token object the parser expects. - * Mirrors phql_parse_with_token() in base.c. - */ - private function makeParserToken(Token $token): stdClass + public function setEnableLiterals(bool $enable): static { - $pt = new stdClass(); - $pt->opcode = $token->opcode; - $pt->token = $token->value; - $pt->token_len = $token->len; - $pt->free_flag = 1; + $this->enableLiterals = $enable; - return $pt; + return $this; } /** * Mirrors phql_scanner_error_msg() in base.c. */ - private function buildScannerErrorMsg(stdClass $status, string $phql): string + private function buildScannerErrorMessage(Status $status, string $phql): string { - $state = $status->scanner_state; + $state = $status->getState(); + $phqlLength = mb_strlen($phql); - if ($state->getStart() !== null && $state->startLength > 0) { + if ($state->getStart() !== null && $state->getStartLength() > 0) { $startStr = substr($phql, $state->getCursor()); - if ($state->startLength > 16) { + if ($state->getStartLength() > 16) { $errorPart = substr($startStr, 0, 16); return sprintf( "Scanning error before '%s...' when parsing: %s (%d)", $errorPart, $phql, - $status->phql_length + $phqlLength ); } @@ -428,10 +231,39 @@ private function buildScannerErrorMsg(stdClass $status, string $phql): string "Scanning error before '%s' when parsing: %s (%d)", $startStr, $phql, - $status->phql_length + $phqlLength ); } - return 'Scanning error near to EOF'; + return 'Scanning error near to EOF'; // @codeCoverageIgnore + } + + private function handleLiteralsDisabled(Status $status): void + { + $status->setSyntaxError('Literals are disabled in PHQL statements'); + $status->setStatus(Status::PHQL_PARSING_FAILED); + } + + private function handleUnknownOpcode(?Opcode $opcode, Status $status): void + { + $status->setStatus(Status::PHQL_PARSING_FAILED); + + throw new Exception( + sprintf('Scanner: Unknown opcode %d', $opcode->value ?? 0) + ); + } + + /** + * Snapshot the current scanner token into a new Token instance so the + * parser stack holds stable values (the scanner reuses its token object). + * Mirrors phql_parse_with_token() in base.c. + */ + private function makeParserToken(Token $token): Token + { + return new Token( + $token->opcode, + $token->value, + $token->length, + ); } } diff --git a/src/Parser/Parser.php b/src/Parser/Parser.php deleted file mode 100644 index 1e3add2..0000000 --- a/src/Parser/Parser.php +++ /dev/null @@ -1,414 +0,0 @@ -debug) { - $debug = fopen($this->debugFile, 'w+'); - } - - $codeLength = strlen($phql); - $parserState = new State($phql); - $parserStatus = new Status($parserState); - $parserStatus->setEnableLiterals(true); - $scanner = new Scanner($parserStatus->getState()); - - $parser = new phql_Parser($parserStatus); - $parser->phql_Trace($debug); - - $state = $parserStatus->getState(); - while (0 <= $scannerStatus = $scanner->scanForToken()) { - $this->token = $scanner->getToken(); - $parserStatus->setToken($this->token); - $state->setStartLength($codeLength - $state->getCursor()); - - $opcode = $this->token->getOpcode(); - $state->setActiveToken($this->token); - - switch ($opcode) { - case Opcode::PHQL_T_IGNORE: - break; - - case Opcode::PHQL_T_ADD: - $parser->phql_(phql_Parser::PHQL_PLUS); - break; - - case Opcode::PHQL_T_SUB: - $parser->phql_(phql_Parser::PHQL_MINUS); - break; - - case Opcode::PHQL_T_MUL: - $parser->phql_(phql_Parser::PHQL_TIMES); - break; - - case Opcode::PHQL_T_DIV: - $parser->phql_(phql_Parser::PHQL_DIVIDE); - break; - - case Opcode::PHQL_T_MOD: - $parser->phql_(phql_Parser::PHQL_MOD); - break; - - case Opcode::PHQL_T_AND: - $parser->phql_(phql_Parser::PHQL_AND); - break; - - case Opcode::PHQL_T_OR: - $parser->phql_(phql_Parser::PHQL_OR); - break; - case Opcode::PHQL_T_EQUALS: - $parser->phql_(phql_Parser::PHQL_EQUALS); - break; - case Opcode::PHQL_T_NOTEQUALS: - $parser->phql_(phql_Parser::PHQL_NOTEQUALS); - break; - case Opcode::PHQL_T_LESS: - $parser->phql_(phql_Parser::PHQL_LESS); - break; - case Opcode::PHQL_T_GREATER: - $parser->phql_(phql_Parser::PHQL_GREATER); - break; - case Opcode::PHQL_T_GREATEREQUAL: - $parser->phql_(phql_Parser::PHQL_GREATEREQUAL); - break; - case Opcode::PHQL_T_LESSEQUAL: - $parser->phql_(phql_Parser::PHQL_LESSEQUAL); - break; - case Opcode::PHQL_T_IDENTIFIER: - $this->phqlParseWithToken($parser, Opcode::PHQL_T_IDENTIFIER, phql_Parser::PHQL_IDENTIFIER); - break; - - case Opcode::PHQL_T_DOT: - $parser->phql_(phql_Parser::PHQL_DOT); - break; - case Opcode::PHQL_T_COMMA: - $parser->phql_(phql_Parser::PHQL_COMMA); - break; - - case Opcode::PHQL_T_PARENTHESES_OPEN: - $parser->phql_(phql_Parser::PHQL_PARENTHESES_OPEN); - break; - case Opcode::PHQL_T_PARENTHESES_CLOSE: - $parser->phql_(phql_Parser::PHQL_PARENTHESES_CLOSE); - break; - - case Opcode::PHQL_T_LIKE: - $parser->phql_(phql_Parser::PHQL_LIKE); - break; - case Opcode::PHQL_T_ILIKE: - $parser->phql_(phql_Parser::PHQL_ILIKE); - break; - case Opcode::PHQL_T_NOT: - $parser->phql_(phql_Parser::PHQL_NOT); - break; - case Opcode::PHQL_T_BITWISE_AND: - $parser->phql_(phql_Parser::PHQL_BITWISE_AND); - break; - case Opcode::PHQL_T_BITWISE_OR: - $parser->phql_(phql_Parser::PHQL_BITWISE_OR); - break; - case Opcode::PHQL_T_BITWISE_NOT: - $parser->phql_(phql_Parser::PHQL_BITWISE_NOT); - break; - case Opcode::PHQL_T_BITWISE_XOR: - $parser->phql_(phql_Parser::PHQL_BITWISE_XOR); - break; - case Opcode::PHQL_T_AGAINST: - $parser->phql_(phql_Parser::PHQL_AGAINST); - break; - case Opcode::PHQL_T_CASE: - $parser->phql_(phql_Parser::PHQL_CASE); - break; - case Opcode::PHQL_T_WHEN: - $parser->phql_(phql_Parser::PHQL_WHEN); - break; - case Opcode::PHQL_T_THEN: - $parser->phql_(phql_Parser::PHQL_THEN); - break; - case Opcode::PHQL_T_END: - $parser->phql_(phql_Parser::PHQL_END); - break; - case Opcode::PHQL_T_ELSE: - $parser->phql_(phql_Parser::PHQL_ELSE); - break; - case Opcode::PHQL_T_FOR: - $parser->phql_(phql_Parser::PHQL_FOR); - break; - case Opcode::PHQL_T_WITH: - $parser->phql_(phql_Parser::PHQL_WITH); - break; - - case Opcode::PHQL_T_INTEGER: - if ($parserStatus->getEnableLiterals()) { - $this->phqlParseWithToken($parser, Opcode::PHQL_T_INTEGER, phql_Parser::PHQL_INTEGER); - } else { - $parserStatus->setSyntaxError("Literals are disabled in PHQL statements"); - $parserStatus->setStatus(Status::PHQL_PARSING_FAILED); - } - break; - case Opcode::PHQL_T_DOUBLE: - if ($parserStatus->getEnableLiterals()) { - $this->phqlParseWithToken($parser, Opcode::PHQL_T_DOUBLE, phql_Parser::PHQL_DOUBLE); - } else { - $parserStatus->setSyntaxError("Literals are disabled in PHQL statements"); - $parserStatus->setStatus(Status::PHQL_PARSING_FAILED); - } - break; - case Opcode::PHQL_T_STRING: - if ($parserStatus->getEnableLiterals()) { - $this->phqlParseWithToken($parser, Opcode::PHQL_T_STRING, phql_Parser::PHQL_STRING); - } else { - $parserStatus->setSyntaxError("Literals are disabled in PHQL statements"); - $parserStatus->setStatus(Status::PHQL_PARSING_FAILED); - } - break; - case Opcode::PHQL_T_TRUE: - if ($parserStatus->getEnableLiterals()) { - $parser->phql_(phql_Parser::PHQL_TRUE); - } else { - $parserStatus->setSyntaxError("Literals are disabled in PHQL statements"); - $parserStatus->setStatus(Status::PHQL_PARSING_FAILED); - } - break; - case Opcode::PHQL_T_FALSE: - if ($parserStatus->getEnableLiterals()) { - $parser->phql_(phql_Parser::PHQL_FALSE); - } else { - $parserStatus->setSyntaxError("Literals are disabled in PHQL statements"); - $parserStatus->setStatus(Status::PHQL_PARSING_FAILED); - } - break; - case Opcode::PHQL_T_HINTEGER: - if ($parserStatus->getEnableLiterals()) { - $this->phqlParseWithToken($parser, Opcode::PHQL_T_HINTEGER, phql_Parser::PHQL_HINTEGER); - } else { - $parserStatus->setSyntaxError("Integers are disabled in PHQL statements"); - $parserStatus->setStatus(Status::PHQL_PARSING_FAILED); - } - break; - - case Opcode::PHQL_T_NPLACEHOLDER: - $this->phqlParseWithToken($parser, Opcode::PHQL_T_NPLACEHOLDER, phql_Parser::PHQL_NPLACEHOLDER); - break; - case Opcode::PHQL_T_SPLACEHOLDER: - $this->phqlParseWithToken($parser, Opcode::PHQL_T_SPLACEHOLDER, phql_Parser::PHQL_SPLACEHOLDER); - break; - case Opcode::PHQL_T_BPLACEHOLDER: - $this->phqlParseWithToken($parser, Opcode::PHQL_T_BPLACEHOLDER, phql_Parser::PHQL_BPLACEHOLDER); - break; - - case Opcode::PHQL_T_FROM: - $parser->phql_(phql_Parser::PHQL_FROM); - break; - case Opcode::PHQL_T_UPDATE: - $parser->phql_(phql_Parser::PHQL_UPDATE); - break; - case Opcode::PHQL_T_SET: - $parser->phql_(phql_Parser::PHQL_SET); - break; - case Opcode::PHQL_T_WHERE: - $parser->phql_(phql_Parser::PHQL_WHERE); - break; - case Opcode::PHQL_T_DELETE: - $parser->phql_(phql_Parser::PHQL_DELETE); - break; - case Opcode::PHQL_T_INSERT: - $parser->phql_(phql_Parser::PHQL_INSERT); - break; - case Opcode::PHQL_T_INTO: - $parser->phql_(phql_Parser::PHQL_INTO); - break; - case Opcode::PHQL_T_VALUES: - $parser->phql_(phql_Parser::PHQL_VALUES); - break; - case Opcode::PHQL_T_SELECT: - $parser->phql_(phql_Parser::PHQL_SELECT); - break; - case Opcode::PHQL_T_AS: - $parser->phql_(phql_Parser::PHQL_AS); - break; - case Opcode::PHQL_T_ORDER: - $parser->phql_(phql_Parser::PHQL_ORDER); - break; - case Opcode::PHQL_T_BY: - $parser->phql_(phql_Parser::PHQL_BY); - break; - case Opcode::PHQL_T_LIMIT: - $parser->phql_(phql_Parser::PHQL_LIMIT); - break; - case Opcode::PHQL_T_OFFSET: - $parser->phql_(phql_Parser::PHQL_OFFSET); - break; - case Opcode::PHQL_T_GROUP: - $parser->phql_(phql_Parser::PHQL_GROUP); - break; - case Opcode::PHQL_T_HAVING: - $parser->phql_(phql_Parser::PHQL_HAVING); - break; - case Opcode::PHQL_T_ASC: - $parser->phql_(phql_Parser::PHQL_ASC); - break; - case Opcode::PHQL_T_DESC: - $parser->phql_(phql_Parser::PHQL_DESC); - break; - case Opcode::PHQL_T_IN: - $parser->phql_(phql_Parser::PHQL_IN); - break; - case Opcode::PHQL_T_ON: - $parser->phql_(phql_Parser::PHQL_ON); - break; - case Opcode::PHQL_T_INNER: - $parser->phql_(phql_Parser::PHQL_INNER); - break; - case Opcode::PHQL_T_JOIN: - $parser->phql_(phql_Parser::PHQL_JOIN); - break; - case Opcode::PHQL_T_LEFT: - $parser->phql_(phql_Parser::PHQL_LEFT); - break; - case Opcode::PHQL_T_RIGHT: - $parser->phql_(phql_Parser::PHQL_RIGHT); - break; - case Opcode::PHQL_T_CROSS: - $parser->phql_(phql_Parser::PHQL_CROSS); - break; - case Opcode::PHQL_T_FULL: - $parser->phql_(phql_Parser::PHQL_FULL); - break; - case Opcode::PHQL_T_OUTER: - $parser->phql_(phql_Parser::PHQL_OUTER); - break; - case Opcode::PHQL_T_IS: - $parser->phql_(phql_Parser::PHQL_IS); - break; - case Opcode::PHQL_T_NULL: - $parser->phql_(phql_Parser::PHQL_NULL); - break; - case Opcode::PHQL_T_BETWEEN: - $parser->phql_(phql_Parser::PHQL_BETWEEN); - break; - case Opcode::PHQL_T_BETWEEN_NOT: - $parser->phql_(phql_Parser::PHQL_BETWEEN_NOT); - break; - case Opcode::PHQL_T_DISTINCT: - $parser->phql_(phql_Parser::PHQL_DISTINCT); - break; - case Opcode::PHQL_T_ALL: - $parser->phql_(phql_Parser::PHQL_ALL); - break; - case Opcode::PHQL_T_CAST: - $parser->phql_(phql_Parser::PHQL_CAST); - break; - case Opcode::PHQL_T_CONVERT: - $parser->phql_(phql_Parser::PHQL_CONVERT); - break; - case Opcode::PHQL_T_USING: - $parser->phql_(phql_Parser::PHQL_USING); - break; - case Opcode::PHQL_T_EXISTS: - $parser->phql_(phql_Parser::PHQL_EXISTS); - break; - - default: - $parserStatus->setStatus(Status::PHQL_PARSING_FAILED); - $parserStatus->setSyntaxError("Scanner: Unknown opcode %d" . $opcode); - break; - } - - if ($parserStatus->getStatus() === Status::PHQL_PARSING_FAILED) { - break; - } - - $state->setEnd($state->getStart()); - } - - if ($scannerStatus === Scanner::PHQL_SCANNER_RETCODE_ERR || $scannerStatus === Scanner::PHQL_SCANNER_RETCODE_IMPOSSIBLE) { - throw new Exception($parserStatus->getSyntaxError()); - } elseif ($scannerStatus === Scanner::PHQL_SCANNER_RETCODE_EOF) { - $parser->phql_(0); - } - - /** - * Set a unique id for the parsed ast - * / - * if (phalcon_globals_ptr->orm.cache_level >= 1) { - * if (Z_TYPE_P(&parser_status->ret) == IS_ARRAY) { - * add_assoc_long(&parser_status->ret, "id", phalcon_globals_ptr->orm.unique_cache_id++); - * } - * } - * - * ZVAL_ZVAL(*result, &parser_status->ret, 1, 1); - * - * /** - * Store the parsed definition in the cache - * / - * if (cache_level >= 0) { - * - * if (!phalcon_globals_ptr->orm.parser_cache) { - * ALLOC_HASHTABLE(phalcon_globals_ptr->orm.parser_cache); - * zend_hash_init(phalcon_globals_ptr->orm.parser_cache, 0, NULL, ZVAL_PTR_DTOR, 0); - * } - * - * Z_TRY_ADDREF_P(*result); - * - * zend_hash_index_update( - * phalcon_globals_ptr->orm.parser_cache, - * phql_key, - * result - * ); - * } - * - * } - * } - * } - */ - - $state->setStartLength(0); - $state->setActiveToken(0); - - if ($parserStatus->getStatus() !== Status::PHQL_PARSING_OK) { - throw new Exception($parserStatus->getSyntaxError()); - } - - return $parser->getOutput(); - } - - private function phqlParseWithToken( - phql_Parser $parser, - int $opcode, - int $parserCode, - ): void { - $newToken = new Token(); - $newToken->setOpcode($opcode); - $newToken->setValue($this->token->getValue()); - - $this->token = $newToken; - - $parser->phql_($parserCode, $newToken); - } -} diff --git a/src/Parser/Status.php b/src/Parser/Status.php index a2f1c09..12fae80 100644 --- a/src/Parser/Status.php +++ b/src/Parser/Status.php @@ -12,33 +12,32 @@ class Status public const PHQL_PARSING_FAILED = 0; public const PHQL_PARSING_OK = 1; - protected ?string $syntaxError = null; - - protected ?Token $token = null; - - protected bool $enableLiterals = false; + /** @var array|null $ast */ + private array|null $ast = null; + private bool $enableLiterals = false; + private ?string $syntaxError = null; + private ?Token $token = null; public function __construct( - protected State $scannerState, - protected int $status = self::PHQL_PARSING_OK, + private readonly State $scannerState, + private int $status = self::PHQL_PARSING_OK, ) { } - public function getState(): State + /** @return array|null */ + public function getAst(): array|null { - return $this->scannerState; + return $this->ast; } - public function setEnableLiterals(bool $enable): self + public function getEnableLiterals(): bool { - $this->enableLiterals = $enable; - - return $this; + return $this->enableLiterals; } - public function getEnableLiterals(): bool + public function getState(): State { - return $this->enableLiterals; + return $this->scannerState; } public function getStatus(): int @@ -56,25 +55,39 @@ public function getToken(): ?Token return $this->token; } - public function setStatus(int $status): self + /** @param array $ast */ + public function setAst(array $ast): static + { + $this->ast = $ast; + + return $this; + } + + public function setEnableLiterals(bool $enable): static + { + $this->enableLiterals = $enable; + + return $this; + } + + public function setStatus(int $status): static { - //throw new \Exception('Test'); $this->status = $status; return $this; } - public function setSyntaxError(string $syntaxError): self + public function setSyntaxError(string $syntaxError): static { $this->syntaxError = $syntaxError; return $this; } - public function setToken(Token $token): self + public function setToken(Token $token): static { $this->token = $token; return $this; } -} \ No newline at end of file +} diff --git a/src/Scanner/Opcode.php b/src/Scanner/Opcode.php index 648b666..140f618 100644 --- a/src/Scanner/Opcode.php +++ b/src/Scanner/Opcode.php @@ -4,120 +4,139 @@ namespace Phalcon\Phql\Scanner; -class Opcode +enum Opcode: int { - public const PHQL_T_ADD = '+'; + case ADD = 43; + case AGAINST = 276; + case ALL = 338; + case AND = 266; + case AS = 305; + case ASC = 327; + case BETWEEN = 331; + case BETWEEN_NOT = 332; + case BITWISE_AND = 38; + case BITWISE_NOT = 126; + case BITWISE_OR = 124; + case BITWISE_XOR = 94; + case BPLACEHOLDER = 277; + case BY = 311; + case CASE = 409; + case CAST = 333; + case COLON = 58; + case COMMA = 269; + case CONVERT = 336; + case CROSS = 324; + case CROSSJOIN = 363; + case DELETE = 303; + case DESC = 328; + case DISTINCT = 330; + case DIV = 47; + case DOMAINALL = 353; + case DOT = 46; + case DOUBLE = 259; + case ELSE = 411; + case ENCLOSED = 356; + case END = 412; + case EQUALS = 61; + case EXISTS = 408; + case EXPR = 354; + case FALSE = 335; + case FCALL = 350; + case FOR = 339; + case FROM = 304; + case FULL = 325; + case FULLJOIN = 364; + case GREATER = 62; + case GREATEREQUAL = 272; + case GROUP = 313; + case HAVING = 314; + case HINTEGER = 414; + case IDENTIFIER = 265; + case IGNORE = 257; + case ILIKE = 275; + case IN = 315; + case INNER = 317; + case INNERJOIN = 360; + case INSERT = 306; + case INTEGER = 258; + case INTO = 307; + case IS = 321; + case ISNOTNULL = 366; + case ISNULL = 365; + case JOIN = 318; + case LEFT = 319; + case LEFTJOIN = 361; + case LESS = 60; + case LESSEQUAL = 271; + case LIKE = 268; + case LIMIT = 312; + case MINUS = 367; + case MOD = 37; + case MUL = 42; + case NILIKE = 357; + case NLIKE = 351; + case NOT = 33; + case NOTEQUALS = 270; + case NOTIN = 323; + case NPLACEHOLDER = 273; + case NULL = 322; + case OFFSET = 329; + case ON = 316; + case OR = 267; + case ORDER = 310; + case OUTER = 326; + case PARENTHESES_CLOSE = 41; + case PARENTHESES_OPEN = 40; + case QUALIFIED = 355; + case RAW_QUALIFIED = 358; + case RIGHT = 320; + case RIGHTJOIN = 362; + case SELECT = 309; + case SET = 301; + case SPLACEHOLDER = 274; + case STARALL = 352; + case STRING = 260; + case SUB = 45; + case SUBQUERY = 407; + case THEN = 413; + case TRUE = 334; + case TS_AND = 403; + case TS_CONTAINS_ANOTHER = 405; + case TS_CONTAINS_IN = 406; + case TS_MATCHES = 401; + case TS_NEGATE = 404; + case TS_OR = 402; + case UPDATE = 300; + case USING = 337; + case VALUES = 308; + case WHEN = 410; + case WHERE = 302; + case WITH = 415; - /* Literals & Identifiers */ - public const PHQL_T_AGAINST = 276; - public const PHQL_T_ALL = 338; - public const PHQL_T_AND = 266; - public const PHQL_T_AS = 305; - public const PHQL_T_ASC = 327; - - /* Operators */ - public const PHQL_T_BETWEEN = 331; - public const PHQL_T_BETWEEN_NOT = 332; - public const PHQL_T_BITWISE_AND = '&'; - public const PHQL_T_BITWISE_NOT = '~'; - public const PHQL_T_BITWISE_OR = '|'; - public const PHQL_T_BITWISE_XOR = '^'; - public const PHQL_T_BPLACEHOLDER = 277; - public const PHQL_T_BY = 311; - public const PHQL_T_CASE = 409; - public const PHQL_T_CAST = 333; - public const PHQL_T_COLON = ':'; - public const PHQL_T_COMMA = 269; - public const PHQL_T_CONVERT = 336; - public const PHQL_T_CROSS = 324; - public const PHQL_T_CROSSJOIN = 363; - public const PHQL_T_DELETE = 303; - public const PHQL_T_DESC = 328; - public const PHQL_T_DISTINCT = 330; - public const PHQL_T_DIV = '/'; - public const PHQL_T_DOMAINALL = 353; - public const PHQL_T_DOT = '.'; - public const PHQL_T_DOUBLE = 259; - public const PHQL_T_ELSE = 411; - public const PHQL_T_ENCLOSED = 356; - public const PHQL_T_END = 412; - public const PHQL_T_EQUALS = '='; - public const PHQL_T_EXISTS = 408; - public const PHQL_T_EXPR = 354; - public const PHQL_T_FALSE = 335; - /** Special Tokens */ - public const PHQL_T_FCALL = 350; - public const PHQL_T_FOR = 339; - public const PHQL_T_FROM = 304; - public const PHQL_T_FULL = 325; - public const PHQL_T_FULLJOIN = 364; - public const PHQL_T_GREATER = '>'; - public const PHQL_T_GREATEREQUAL = 272; - public const PHQL_T_GROUP = 313; - public const PHQL_T_HAVING = 314; - public const PHQL_T_HINTEGER = 414; - public const PHQL_T_IDENTIFIER = 265; - public const PHQL_T_IGNORE = 257; - public const PHQL_T_ILIKE = 275; - public const PHQL_T_IN = 315; - public const PHQL_T_INNER = 317; - public const PHQL_T_INNERJOIN = 360; - public const PHQL_T_INSERT = 306; - public const PHQL_T_INTEGER = 258; - public const PHQL_T_INTO = 307; - public const PHQL_T_IS = 321; - public const PHQL_T_ISNOTNULL = 366; - public const PHQL_T_ISNULL = 365; - public const PHQL_T_JOIN = 318; - public const PHQL_T_LEFT = 319; - public const PHQL_T_LEFTJOIN = 361; - public const PHQL_T_LESS = '<'; - public const PHQL_T_LESSEQUAL = 271; - public const PHQL_T_LIKE = 268; - public const PHQL_T_LIMIT = 312; - public const PHQL_T_MINUS = 367; - public const PHQL_T_MOD = '%'; - public const PHQL_T_MUL = '*'; - public const PHQL_T_NILIKE = 357; - public const PHQL_T_NLIKE = 351; - public const PHQL_T_NOT = '!'; - public const PHQL_T_NOTEQUALS = 270; - public const PHQL_T_NOTIN = 323; - /** Placeholders */ - public const PHQL_T_NPLACEHOLDER = 273; - public const PHQL_T_NULL = 322; - public const PHQL_T_OFFSET = 329; - public const PHQL_T_ON = 316; - public const PHQL_T_OR = 267; - public const PHQL_T_ORDER = 310; - public const PHQL_T_OUTER = 326; - public const PHQL_T_PARENTHESES_CLOSE = ')'; - public const PHQL_T_PARENTHESES_OPEN = '('; - public const PHQL_T_QUALIFIED = 355; - public const PHQL_T_RAW_QUALIFIED = 358; - public const PHQL_T_RIGHT = 320; - public const PHQL_T_RIGHTJOIN = 362; - public const PHQL_T_SELECT = 309; - public const PHQL_T_SET = 301; - public const PHQL_T_SPLACEHOLDER = 274; - public const PHQL_T_STARALL = 352; - public const PHQL_T_STRING = 260; - public const PHQL_T_SUB = '-'; - public const PHQL_T_SUBQUERY = 407; - public const PHQL_T_THEN = 413; - public const PHQL_T_TRUE = 334; - public const PHQL_T_TS_AND = 403; - public const PHQL_T_TS_CONTAINS_ANOTHER = 405; - public const PHQL_T_TS_CONTAINS_IN = 406; - /** Postgresql Text Search Operators */ - public const PHQL_T_TS_MATCHES = 401; - public const PHQL_T_TS_NEGATE = 404; - public const PHQL_T_TS_OR = 402; - /** Reserved words */ - public const PHQL_T_UPDATE = 300; - public const PHQL_T_USING = 337; - public const PHQL_T_VALUES = 308; - public const PHQL_T_WHEN = 410; - public const PHQL_T_WHERE = 302; - public const PHQL_T_WITH = 415; + public function label(): string + { + return match ($this) { + self::ADD => '+', + self::BITWISE_AND => '&', + self::BITWISE_NOT => '~', + self::BITWISE_OR => '|', + self::BITWISE_XOR => '^', + self::COLON => ':', + self::DIV => '/', + self::DOT => '.', + self::EQUALS => '=', + self::GREATER => '>', + self::GREATEREQUAL => '>=', + self::LESS => '<', + self::LESSEQUAL => '<=', + self::MOD => '%', + self::MUL => '*', + self::NOT => '!', + self::NOTEQUALS => '<>', + self::PARENTHESES_CLOSE => ')', + self::PARENTHESES_OPEN => '(', + self::SUB => '-', + default => $this->name, + }; + } } diff --git a/src/Scanner/Scanner.php b/src/Scanner/Scanner.php index 6d30d85..0584986 100644 --- a/src/Scanner/Scanner.php +++ b/src/Scanner/Scanner.php @@ -4,18 +4,16 @@ namespace Phalcon\Phql\Scanner; +use Exception; +use Phalcon\Phql\Scanner\ScannerStatus; use Phalcon\Phql\Scanner\State; use Phalcon\Phql\Scanner\Token; class Scanner { - public const PHQL_SCANNER_RETCODE_EOF = -1; - public const PHQL_SCANNER_RETCODE_ERR = -2; - public const PHQL_SCANNER_RETCODE_IMPOSSIBLE = -3; - private Token $token; - public function __construct(private State $state) + public function __construct(private readonly State $state) { $this->token = new Token(); } @@ -25,26 +23,23 @@ public function getToken(): Token return $this->token; } - public function scanForToken(): int + public function scanForToken(): ScannerStatus { $yyinput = $this->state->getRawBuffer(); $yycursor = $this->state->getCursor(); if ($yycursor >= $this->state->getBufferLength()) { - return self::PHQL_SCANNER_RETCODE_EOF; + return ScannerStatus::EOF; } - $q = $yycursor; - $token = $this->token; - $token->value = null; - $token->opcode = null; - $token->len = 0; - $status = self::PHQL_SCANNER_RETCODE_IMPOSSIBLE; + $q = $yycursor; + $yymarker = $yycursor; + $status = ScannerStatus::IMPOSSIBLE; - while (self::PHQL_SCANNER_RETCODE_IMPOSSIBLE == $status) { + while (ScannerStatus::IMPOSSIBLE === $status) { if ($yycursor >= $this->state->getBufferLength()) { - return self::PHQL_SCANNER_RETCODE_EOF; + return ScannerStatus::EOF; } $yych = 0; $yyaccept = 0; @@ -55,7 +50,7 @@ public function scanForToken(): int $yych = $yyinput[$yycursor]; $yycursor += 1; switch ($yych) { - case 0x00: + case "\x00": $yystate = 1; break 2; case "\t": @@ -251,17 +246,15 @@ public function scanForToken(): int break 2; } case 1: - - $status = self::PHQL_SCANNER_RETCODE_EOF; - break; + $status = ScannerStatus::EOF; + break 2; case 2: $yystate = 3; - break 2; - case 3: - - $status = self::PHQL_SCANNER_RETCODE_ERR; break; + case 3: + $status = ScannerStatus::ERR; + break 2; case 4: $yych = $yyinput[$yycursor]; @@ -278,10 +271,9 @@ public function scanForToken(): int break 2; } case 5: - - $token->opcode = Opcode::PHQL_T_IGNORE; + $this->token = new Token(Opcode::IGNORE); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 6: $yych = $yyinput[$yycursor]; @@ -299,26 +291,24 @@ public function scanForToken(): int break 2; } case 7: - - $token->opcode = Opcode::PHQL_T_NOT; + $this->token = new Token(Opcode::NOT); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 8: $yyaccept = 0; $yymarker = $yycursor; $yych = $yyinput[$yycursor]; - if ($yych <= 0x00) { + if ($yych === "\x00") { $yystate = 3; - break 2; + break; } $yystate = 68; - break 2; + break; case 9: - - $token->opcode = Opcode::PHQL_T_MOD; + $this->token = new Token(Opcode::MOD); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 10: $yych = $yyinput[$yycursor]; @@ -332,56 +322,49 @@ public function scanForToken(): int break 2; } case 11: - - $token->opcode = Opcode::PHQL_T_BITWISE_AND; + $this->token = new Token(Opcode::BITWISE_AND); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 12: $yyaccept = 0; $yymarker = $yycursor; $yych = $yyinput[$yycursor]; - if ($yych <= 0x00) { + if ($yych === "\x00") { $yystate = 3; - break 2; + break; } $yystate = 74; - break 2; + break; case 13: - - $token->opcode = Opcode::PHQL_T_PARENTHESES_OPEN; + $this->token = new Token(Opcode::PARENTHESES_OPEN); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 14: - - $token->opcode = Opcode::PHQL_T_PARENTHESES_CLOSE; + $this->token = new Token(Opcode::PARENTHESES_CLOSE); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 15: - - $token->opcode = Opcode::PHQL_T_MUL; + $this->token = new Token(Opcode::MUL); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 16: - - $token->opcode = Opcode::PHQL_T_ADD; + $this->token = new Token(Opcode::ADD); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 17: - - $token->opcode = Opcode::PHQL_T_COMMA; + $this->token = new Token(Opcode::COMMA); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 18: - - $token->opcode = Opcode::PHQL_T_SUB; + $this->token = new Token(Opcode::SUB); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 19: $yych = $yyinput[$yycursor]; @@ -404,16 +387,14 @@ public function scanForToken(): int break 2; } case 20: - - $token->opcode = Opcode::PHQL_T_DOT; + $this->token = new Token(Opcode::DOT); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 21: - - $token->opcode = Opcode::PHQL_T_DIV; + $this->token = new Token(Opcode::DIV); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 22: $yych = $yyinput[$yycursor]; @@ -456,13 +437,10 @@ public function scanForToken(): int break 2; } case 23: - - $token->opcode = Opcode::PHQL_T_INTEGER; - $token->value = substr($yyinput, $q, $yycursor - $q); - $token->len = $yycursor - $q; + $this->token = new Token(Opcode::INTEGER, substr($yyinput, $q, $yycursor - $q), $yycursor - $q); $q = $yycursor; $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 24: $yyaccept = 1; @@ -541,10 +519,9 @@ public function scanForToken(): int break 2; } case 25: - - $token->opcode = Opcode::PHQL_T_COLON; + $this->token = new Token(Opcode::COLON); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 26: $yych = $yyinput[$yycursor]; @@ -562,16 +539,14 @@ public function scanForToken(): int break 2; } case 27: - - $token->opcode = Opcode::PHQL_T_LESS; + $this->token = new Token(Opcode::LESS); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 28: - - $token->opcode = Opcode::PHQL_T_EQUALS; + $this->token = new Token(Opcode::EQUALS); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 29: $yych = $yyinput[$yycursor]; @@ -585,10 +560,9 @@ public function scanForToken(): int break 2; } case 30: - - $token->opcode = Opcode::PHQL_T_GREATER; + $this->token = new Token(Opcode::GREATER); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 31: $yych = $yyinput[$yycursor]; @@ -653,15 +627,14 @@ public function scanForToken(): int break 2; } case 34: - - $token->value = substr($yyinput, $q, $yycursor - $q); - $token->len = $yycursor - $q; - if ($token->len > 2 && str_starts_with($token->value, "0x")) { - $token->opcode = Opcode::PHQL_T_HINTEGER; + $tokenValue = substr($yyinput, $q, $yycursor - $q); + $tokenLen = $yycursor - $q; + if ($tokenLen > 2 && str_starts_with($tokenValue, "0x")) { + $this->token = new Token(Opcode::HINTEGER, $tokenValue, $tokenLen); } else { $alpha = 0; - for ($i = 0; $i < $token->len; $i++) { - $ch = $token->value[$i]; + for ($i = 0; $i < $tokenLen; $i++) { + $ch = $tokenValue[$i]; if (!(($ch >= '0') && ($ch <= '9'))) { $alpha = 1; break; @@ -669,15 +642,15 @@ public function scanForToken(): int } if ($alpha) { - $token->opcode = Opcode::PHQL_T_IDENTIFIER; + $this->token = new Token(Opcode::IDENTIFIER, $tokenValue, $tokenLen); } else { - $token->opcode = Opcode::PHQL_T_INTEGER; + $this->token = new Token(Opcode::INTEGER, $tokenValue, $tokenLen); } } $q = $yycursor; $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 35: $yych = $yyinput[$yycursor]; @@ -800,23 +773,27 @@ public function scanForToken(): int break 2; } case 41: - - $token->opcode = Opcode::PHQL_T_IDENTIFIER; - if (($yycursor - $q) > 1) { - if ($yyinput[$q] === '\\') { - $token->value = substr($yyinput, $q + 1, $yycursor - $q - 1); - $token->len = $yycursor - $q - 1; + // Use $yymarker (PPMARKER in C re2c) as token start. + // Keyword save-point states (e.g. NOT at state 176) overwrite $yymarker, + // so identifiers like "Notes" yield the post-keyword suffix ("es"). + $tokenValue = null; + $tokenLen = 0; + if (($yycursor - $yymarker) > 1) { + if ($yyinput[$yymarker] === '\\') { + $tokenValue = substr($yyinput, $yymarker + 1, $yycursor - $yymarker - 1); + $tokenLen = $yycursor - $yymarker - 1; } else { - $token->value = substr($yyinput, $q, $yycursor - $q); - $token->len = $yycursor - $q; + $tokenValue = substr($yyinput, $yymarker, $yycursor - $yymarker); + $tokenLen = $yycursor - $yymarker; } } else { - $token->value = substr($yyinput, $q, $yycursor - $q); - $token->len = $yycursor - $q; + $tokenValue = substr($yyinput, $yymarker, $yycursor - $yymarker); + $tokenLen = $yycursor - $yymarker; } $q = $yycursor; $this->state->setCursor($yycursor); - return 0; + $this->token = new Token(Opcode::IDENTIFIER, $tokenValue, $tokenLen); + return ScannerStatus::OK; case 42: $yych = $yyinput[$yycursor]; @@ -1094,15 +1071,15 @@ public function scanForToken(): int $yymarker = $yycursor; $yych = $yyinput[$yycursor]; switch ($yych) { - case 0x00: - case 0x01: - case 0x02: - case 0x03: - case 0x04: - case 0x05: - case 0x06: - case 0x07: - case 0x08: + case "\x00": + case "\x01": + case "\x02": + case "\x03": + case "\x04": + case "\x05": + case "\x06": + case "\x07": + case "\x08": case "\t": case "\n": case "\v": @@ -1198,10 +1175,9 @@ public function scanForToken(): int break 2; } case 58: - - $token->opcode = Opcode::PHQL_T_BITWISE_XOR; + $this->token = new Token(Opcode::BITWISE_XOR); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 59: $yych = $yyinput[$yycursor]; @@ -1372,35 +1348,31 @@ public function scanForToken(): int break 2; } case 63: - - $token->opcode = Opcode::PHQL_T_BITWISE_OR; + $this->token = new Token(Opcode::BITWISE_OR); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 64: - - $token->opcode = Opcode::PHQL_T_BITWISE_NOT; + $this->token = new Token(Opcode::BITWISE_NOT); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 65: - - $token->opcode = Opcode::PHQL_T_TS_NEGATE; + $this->token = new Token(Opcode::TS_NEGATE); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 66: - - $token->opcode = Opcode::PHQL_T_NOTEQUALS; + $this->token = new Token(Opcode::NOTEQUALS); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 67: $yych = $yyinput[$yycursor]; // fall through case 68: switch ($yych) { - case 0x00: + case "\x00": $yystate = 69; break 2; case '"': @@ -1433,13 +1405,16 @@ public function scanForToken(): int break 2; } case 70: - - $token->opcode = Opcode::PHQL_T_STRING; - $token->value = substr($yyinput, $q, $yycursor - $q - 1); - $token->len = $yycursor - $q - 1; + // $yymarker points to position after the opening quote (set in state 8/12) + // $yycursor is past the closing quote; subtract 1 to exclude it + $this->token = new Token( + Opcode::STRING, + substr($yyinput, $yymarker, $yycursor - $yymarker - 1), + $yycursor - $yymarker - 1 + ); $q = $yycursor; $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 71: $yych = $yyinput[$yycursor]; @@ -1453,17 +1428,16 @@ public function scanForToken(): int break 2; } case 72: - - $token->opcode = Opcode::PHQL_T_TS_AND; + $this->token = new Token(Opcode::TS_AND); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 73: $yych = $yyinput[$yycursor]; // fall through case 74: switch ($yych) { - case 0x00: + case "\x00": $yystate = 69; break 2; case '\'': @@ -1511,13 +1485,10 @@ public function scanForToken(): int break 2; } case 77: - - $token->opcode = Opcode::PHQL_T_DOUBLE; - $token->value = substr($yyinput, $q, $yycursor - $q); - $token->len = $yycursor - $q; + $this->token = new Token(Opcode::DOUBLE, substr($yyinput, $q, $yycursor - $q), $yycursor - $q); $q = $yycursor; $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 78: $yych = $yyinput[$yycursor]; @@ -1631,17 +1602,17 @@ public function scanForToken(): int break 2; } case 80: - $token->opcode = Opcode::PHQL_T_LESSEQUAL; + $this->token = new Token(Opcode::LESSEQUAL); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 81: - $token->opcode = Opcode::PHQL_T_NOTEQUALS; + $this->token = new Token(Opcode::NOTEQUALS); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 82: - $token->opcode = Opcode::PHQL_T_GREATEREQUAL; + $this->token = new Token(Opcode::GREATEREQUAL); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 83: $yych = $yyinput[$yycursor]; switch ($yych) { @@ -1663,20 +1634,22 @@ public function scanForToken(): int break 2; } case 84: - $token->opcode = Opcode::PHQL_T_NPLACEHOLDER; - $token->value = substr($yyinput, $q, $yycursor - $q); - $token->len = $yycursor - $q; + $this->token = new Token( + Opcode::NPLACEHOLDER, + substr($yyinput, $q, $yycursor - $q), + $yycursor - $q + ); $q = $yycursor; $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 85: - $token->opcode = Opcode::PHQL_T_TS_CONTAINS_ANOTHER; + $this->token = new Token(Opcode::TS_CONTAINS_ANOTHER); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 86: - $token->opcode = Opcode::PHQL_T_TS_MATCHES; + $this->token = new Token(Opcode::TS_MATCHES); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 87: $yych = $yyinput[$yycursor]; switch ($yych) { @@ -1792,10 +1765,9 @@ public function scanForToken(): int break 2; } case 91: - - $token->opcode = Opcode::PHQL_T_AS; + $this->token = new Token(Opcode::AS); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 92: $yych = $yyinput[$yycursor]; switch ($yych) { @@ -1884,10 +1856,9 @@ public function scanForToken(): int break 2; } case 94: - - $token->opcode = Opcode::PHQL_T_BY; + $this->token = new Token(Opcode::BY); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 95: $yych = $yyinput[$yycursor]; switch ($yych) { @@ -2170,10 +2141,9 @@ public function scanForToken(): int break 2; } case 112: - - $token->opcode = Opcode::PHQL_T_IN; + $this->token = new Token(Opcode::IN); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 113: $yych = $yyinput[$yycursor]; @@ -2251,10 +2221,9 @@ public function scanForToken(): int break 2; } case 114: - - $token->opcode = Opcode::PHQL_T_IS; + $this->token = new Token(Opcode::IS); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 115: $yych = $yyinput[$yycursor]; @@ -2409,10 +2378,9 @@ public function scanForToken(): int break 2; } case 122: - - $token->opcode = Opcode::PHQL_T_ON; + $this->token = new Token(Opcode::ON); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 123: $yych = $yyinput[$yycursor]; @@ -2493,10 +2461,9 @@ public function scanForToken(): int break 2; } case 124: - - $token->opcode = Opcode::PHQL_T_OR; + $this->token = new Token(Opcode::OR); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 125: $yych = $yyinput[$yycursor]; @@ -2628,15 +2595,15 @@ public function scanForToken(): int // fall through case 136: switch ($yych) { - case 0x00: - case 0x01: - case 0x02: - case 0x03: - case 0x04: - case 0x05: - case 0x06: - case 0x07: - case 0x08: + case "\x00": + case "\x01": + case "\x02": + case "\x03": + case "\x04": + case "\x05": + case "\x06": + case "\x07": + case "\x08": case "\t": case "\n": case "\v": @@ -2680,15 +2647,15 @@ public function scanForToken(): int case 137: $yych = $yyinput[$yycursor]; switch ($yych) { - case 0x00: - case 0x01: - case 0x02: - case 0x03: - case 0x04: - case 0x05: - case 0x06: - case 0x07: - case 0x08: + case "\x00": + case "\x01": + case "\x02": + case "\x03": + case "\x04": + case "\x05": + case "\x06": + case "\x07": + case "\x08": case "\t": case "\n": case "\v": @@ -2729,15 +2696,26 @@ public function scanForToken(): int break 2; } case 138: - // fall through + // Bracket-enclosed identifier: [name] or [First Name] + // Use $yymarker (set by state 56 after '[', updated by state 193 after '\]') + // so that escaped-bracket sequences correctly yield the post-escape substring. + $this->token = new Token( + Opcode::IDENTIFIER, + substr($yyinput, $yymarker, $yycursor - $yymarker - 1), + $yycursor - $yymarker - 1 + ); + $q = $yycursor; + $this->state->setCursor($yycursor); + return ScannerStatus::OK; case 139: - - $token->opcode = Opcode::PHQL_T_IDENTIFIER; - $token->value = substr($yyinput, $q, $yycursor - $q); - $token->len = $yycursor - $q; + $this->token = new Token( + Opcode::IDENTIFIER, + substr($yyinput, $q, $yycursor - $q), + $yycursor - $q + ); $q = $yycursor; $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 140: $yych = $yyinput[$yycursor]; @@ -2819,19 +2797,20 @@ public function scanForToken(): int break 2; } case 141: - - $token->opcode = Opcode::PHQL_T_TS_OR; + $this->token = new Token(Opcode::TS_OR); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 142: - - $token->opcode = Opcode::PHQL_T_SPLACEHOLDER; - $token->value = substr($yyinput, $q, $yycursor - $q - 1); - $token->len = $yycursor - $q - 1; + // Strip leading ':' — Query.php prepends ':' when building the placeholder + $this->token = new Token( + Opcode::SPLACEHOLDER, + substr($yyinput, $q + 1, $yycursor - $q - 2), + $yycursor - $q - 2 + ); $q = $yycursor; $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 143: $yych = $yyinput[$yycursor]; @@ -2921,10 +2900,9 @@ public function scanForToken(): int break 2; } case 145: - - $token->opcode = Opcode::PHQL_T_ALL; + $this->token = new Token(Opcode::ALL); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 146: $yych = $yyinput[$yycursor]; @@ -3002,10 +2980,9 @@ public function scanForToken(): int break 2; } case 147: - - $token->opcode = Opcode::PHQL_T_AND; + $this->token = new Token(Opcode::AND); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 148: $yych = $yyinput[$yycursor]; @@ -3083,10 +3060,9 @@ public function scanForToken(): int break 2; } case 149: - - $token->opcode = Opcode::PHQL_T_ASC; + $this->token = new Token(Opcode::ASC); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 150: $yych = $yyinput[$yycursor]; @@ -3265,10 +3241,9 @@ public function scanForToken(): int break 2; } case 159: - - $token->opcode = Opcode::PHQL_T_END; + $this->token = new Token(Opcode::END); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 160: $yych = $yyinput[$yycursor]; @@ -3370,10 +3345,9 @@ public function scanForToken(): int break 2; } case 163: - - $token->opcode = Opcode::PHQL_T_FOR; + $this->token = new Token(Opcode::FOR); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 164: $yych = $yyinput[$yycursor]; @@ -3601,10 +3575,9 @@ public function scanForToken(): int break 2; } case 177: - - $token->opcode = Opcode::PHQL_T_NOT; + $this->token = new Token(Opcode::NOT); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 178: $yych = $yyinput[$yycursor]; @@ -3754,10 +3727,9 @@ public function scanForToken(): int break 2; } case 185: - - $token->opcode = Opcode::PHQL_T_SET; + $this->token = new Token(Opcode::SET); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 186: $yych = $yyinput[$yycursor]; @@ -3853,15 +3825,15 @@ public function scanForToken(): int $yymarker = $yycursor; $yych = $yyinput[$yycursor]; switch ($yych) { - case 0x00: - case 0x01: - case 0x02: - case 0x03: - case 0x04: - case 0x05: - case 0x06: - case 0x07: - case 0x08: + case "\x00": + case "\x01": + case "\x02": + case "\x03": + case "\x04": + case "\x05": + case "\x06": + case "\x07": + case "\x08": case "\t": case "\n": case "\v": @@ -3903,13 +3875,15 @@ public function scanForToken(): int break 2; } case 194: - - $token->opcode = Opcode::PHQL_T_BPLACEHOLDER; - $token->value = substr($yyinput, $q, $yycursor - $q - 1); - $token->len = $yycursor - $q - 1; + // Strip leading ':' — Query.php handles the ':' prefix separately + $this->token = new Token( + Opcode::BPLACEHOLDER, + substr($yyinput, $q + 1, $yycursor - $q - 2), + $yycursor - $q - 2 + ); $q = $yycursor; $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 195: $yych = $yyinput[$yycursor]; @@ -4011,10 +3985,9 @@ public function scanForToken(): int break 2; } case 198: - - $token->opcode = Opcode::PHQL_T_CASE; + $this->token = new Token(Opcode::CASE); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 199: $yych = $yyinput[$yycursor]; @@ -4092,10 +4065,9 @@ public function scanForToken(): int break 2; } case 200: - - $token->opcode = Opcode::PHQL_T_CAST; + $this->token = new Token(Opcode::CAST); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 201: $yych = $yyinput[$yycursor]; @@ -4209,10 +4181,9 @@ public function scanForToken(): int break 2; } case 205: - - $token->opcode = Opcode::PHQL_T_DESC; + $this->token = new Token(Opcode::DESC); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 206: $yych = $yyinput[$yycursor]; @@ -4302,10 +4273,9 @@ public function scanForToken(): int break 2; } case 208: - - $token->opcode = Opcode::PHQL_T_ELSE; + $this->token = new Token(Opcode::ELSE); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 209: $yych = $yyinput[$yycursor]; @@ -4407,10 +4377,9 @@ public function scanForToken(): int break 2; } case 212: - - $token->opcode = Opcode::PHQL_T_FROM; + $this->token = new Token(Opcode::FROM); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 213: $yych = $yyinput[$yycursor]; @@ -4488,10 +4457,9 @@ public function scanForToken(): int break 2; } case 214: - - $token->opcode = Opcode::PHQL_T_FULL; + $this->token = new Token(Opcode::FULL); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 215: $yych = $yyinput[$yycursor]; @@ -4629,10 +4597,9 @@ public function scanForToken(): int break 2; } case 221: - - $token->opcode = Opcode::PHQL_T_INTO; + $this->token = new Token(Opcode::INTO); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 222: $yych = $yyinput[$yycursor]; @@ -4710,10 +4677,9 @@ public function scanForToken(): int break 2; } case 223: - - $token->opcode = Opcode::PHQL_T_JOIN; + $this->token = new Token(Opcode::JOIN); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 224: $yych = $yyinput[$yycursor]; @@ -4791,10 +4757,9 @@ public function scanForToken(): int break 2; } case 225: - - $token->opcode = Opcode::PHQL_T_LEFT; + $this->token = new Token(Opcode::LEFT); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 226: $yych = $yyinput[$yycursor]; @@ -4872,10 +4837,9 @@ public function scanForToken(): int break 2; } case 227: - - $token->opcode = Opcode::PHQL_T_LIKE; + $this->token = new Token(Opcode::LIKE); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 228: $yych = $yyinput[$yycursor]; @@ -4977,10 +4941,9 @@ public function scanForToken(): int break 2; } case 231: - - $token->opcode = Opcode::PHQL_T_NULL; + $this->token = new Token(Opcode::NULL); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 232: $yych = $yyinput[$yycursor]; @@ -5118,10 +5081,9 @@ public function scanForToken(): int break 2; } case 238: - - $token->opcode = Opcode::PHQL_T_THEN; + $this->token = new Token(Opcode::THEN); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 239: $yych = $yyinput[$yycursor]; @@ -5199,10 +5161,9 @@ public function scanForToken(): int break 2; } case 240: - - $token->opcode = Opcode::PHQL_T_TRUE; + $this->token = new Token(Opcode::TRUE); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 241: $yych = $yyinput[$yycursor]; @@ -5316,10 +5277,9 @@ public function scanForToken(): int break 2; } case 245: - - $token->opcode = Opcode::PHQL_T_WHEN; + $this->token = new Token(Opcode::WHEN); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 246: $yych = $yyinput[$yycursor]; @@ -5409,10 +5369,9 @@ public function scanForToken(): int break 2; } case 248: - - $token->opcode = Opcode::PHQL_T_WITH; + $this->token = new Token(Opcode::WITH); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 249: $yych = $yyinput[$yycursor]; @@ -5526,10 +5485,9 @@ public function scanForToken(): int break 2; } case 253: - - $token->opcode = Opcode::PHQL_T_CROSS; + $this->token = new Token(Opcode::CROSS); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 254: $yych = $yyinput[$yycursor]; @@ -5643,10 +5601,9 @@ public function scanForToken(): int break 2; } case 258: - - $token->opcode = Opcode::PHQL_T_FALSE; + $this->token = new Token(Opcode::FALSE); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 259: $yych = $yyinput[$yycursor]; @@ -5724,10 +5681,9 @@ public function scanForToken(): int break 2; } case 260: - - $token->opcode = Opcode::PHQL_T_GROUP; + $this->token = new Token(Opcode::GROUP); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 261: $yych = $yyinput[$yycursor]; @@ -5817,10 +5773,9 @@ public function scanForToken(): int break 2; } case 263: - - $token->opcode = Opcode::PHQL_T_ILIKE; + $this->token = new Token(Opcode::ILIKE); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 264: $yych = $yyinput[$yycursor]; @@ -5898,10 +5853,9 @@ public function scanForToken(): int break 2; } case 265: - - $token->opcode = Opcode::PHQL_T_INNER; + $this->token = new Token(Opcode::INNER); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 266: $yych = $yyinput[$yycursor]; @@ -5991,10 +5945,9 @@ public function scanForToken(): int break 2; } case 268: - - $token->opcode = Opcode::PHQL_T_LIMIT; + $this->token = new Token(Opcode::LIMIT); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 269: $yych = $yyinput[$yycursor]; @@ -6096,10 +6049,9 @@ public function scanForToken(): int break 2; } case 272: - - $token->opcode = Opcode::PHQL_T_ORDER; + $this->token = new Token(Opcode::ORDER); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 273: $yych = $yyinput[$yycursor]; @@ -6177,10 +6129,9 @@ public function scanForToken(): int break 2; } case 274: - - $token->opcode = Opcode::PHQL_T_OUTER; + $this->token = new Token(Opcode::OUTER); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 275: $yych = $yyinput[$yycursor]; @@ -6258,10 +6209,9 @@ public function scanForToken(): int break 2; } case 276: - - $token->opcode = Opcode::PHQL_T_RIGHT; + $this->token = new Token(Opcode::RIGHT); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 277: $yych = $yyinput[$yycursor]; @@ -6363,10 +6313,9 @@ public function scanForToken(): int break 2; } case 280: - - $token->opcode = Opcode::PHQL_T_USING; + $this->token = new Token(Opcode::USING); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 281: $yych = $yyinput[$yycursor]; @@ -6456,10 +6405,9 @@ public function scanForToken(): int break 2; } case 283: - - $token->opcode = Opcode::PHQL_T_WHERE; + $this->token = new Token(Opcode::WHERE); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 284: $yych = $yyinput[$yycursor]; @@ -6573,10 +6521,9 @@ public function scanForToken(): int break 2; } case 288: - - $token->opcode = Opcode::PHQL_T_DELETE; + $this->token = new Token(Opcode::DELETE); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 289: $yych = $yyinput[$yycursor]; @@ -6666,10 +6613,9 @@ public function scanForToken(): int break 2; } case 291: - - $token->opcode = Opcode::PHQL_T_EXISTS; + $this->token = new Token(Opcode::EXISTS); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 292: $yych = $yyinput[$yycursor]; @@ -6747,10 +6693,9 @@ public function scanForToken(): int break 2; } case 293: - - $token->opcode = Opcode::PHQL_T_HAVING; + $this->token = new Token(Opcode::HAVING); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 294: $yych = $yyinput[$yycursor]; @@ -6828,10 +6773,9 @@ public function scanForToken(): int break 2; } case 295: - - $token->opcode = Opcode::PHQL_T_INSERT; + $this->token = new Token(Opcode::INSERT); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 296: $yych = $yyinput[$yycursor]; @@ -6921,10 +6865,9 @@ public function scanForToken(): int break 2; } case 298: - - $token->opcode = Opcode::PHQL_T_OFFSET; + $this->token = new Token(Opcode::OFFSET); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 299: $yych = $yyinput[$yycursor]; @@ -7002,10 +6945,9 @@ public function scanForToken(): int break 2; } case 300: - - $token->opcode = Opcode::PHQL_T_SELECT; + $this->token = new Token(Opcode::SELECT); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 301: $yych = $yyinput[$yycursor]; @@ -7083,10 +7025,9 @@ public function scanForToken(): int break 2; } case 302: - - $token->opcode = Opcode::PHQL_T_UPDATE; + $this->token = new Token(Opcode::UPDATE); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 303: $yych = $yyinput[$yycursor]; @@ -7164,10 +7105,9 @@ public function scanForToken(): int break 2; } case 304: - - $token->opcode = Opcode::PHQL_T_VALUES; + $this->token = new Token(Opcode::VALUES); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 305: $yych = $yyinput[$yycursor]; @@ -7245,10 +7185,9 @@ public function scanForToken(): int break 2; } case 306: - - $token->opcode = Opcode::PHQL_T_AGAINST; + $this->token = new Token(Opcode::AGAINST); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 307: $yych = $yyinput[$yycursor]; @@ -7326,10 +7265,9 @@ public function scanForToken(): int break 2; } case 308: - - $token->opcode = Opcode::PHQL_T_BETWEEN; + $this->token = new Token(Opcode::BETWEEN); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 309: $yych = $yyinput[$yycursor]; @@ -7407,10 +7345,9 @@ public function scanForToken(): int break 2; } case 310: - - $token->opcode = Opcode::PHQL_T_CONVERT; + $this->token = new Token(Opcode::CONVERT); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 311: $yych = $yyinput[$yycursor]; @@ -7512,10 +7449,9 @@ public function scanForToken(): int break 2; } case 314: - - $token->opcode = Opcode::PHQL_T_DISTINCT; + $this->token = new Token(Opcode::DISTINCT); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; case 315: $yych = $yyinput[$yycursor]; @@ -7554,10 +7490,9 @@ public function scanForToken(): int break 2; } case 318: - - $token->opcode = Opcode::PHQL_T_BETWEEN_NOT; + $this->token = new Token(Opcode::BETWEEN_NOT); $this->state->setCursor($yycursor); - return 0; + return ScannerStatus::OK; default: throw new Exception("internal lexer error"); diff --git a/src/Scanner/ScannerStatus.php b/src/Scanner/ScannerStatus.php new file mode 100644 index 0000000..74b06f7 --- /dev/null +++ b/src/Scanner/ScannerStatus.php @@ -0,0 +1,13 @@ +bufferLength = strlen($buffer); - $this->rawBuffer = $buffer . "\0"; // null terminator for look-ahead safety in scanner + $this->rawBuffer = $buffer . "\0"; $this->startLength = mb_strlen($buffer); + if ($this->startLength > 0) { - $this->setStart($buffer[0]); - $this->setEnd($buffer[0]); + $this->start = $buffer[0]; } } - public function getActiveToken(): mixed + public function getActiveToken(): ?Opcode { return $this->activeToken; } @@ -36,14 +35,14 @@ public function getBufferLength(): int return $this->bufferLength; } - public function getRawBuffer(): string + public function getCursor(): int { - return $this->rawBuffer; + return $this->cursor; } - public function getCursor(): int + public function getRawBuffer(): string { - return $this->cursor; + return $this->rawBuffer; } public function getStart(): ?string @@ -56,47 +55,33 @@ public function getStartLength(): int return $this->startLength; } - public function incrementStart(int $value = 1): self + public function incrementStart(int $value = 1): static { $this->cursor += $value; - $this->setStart($this->rawBuffer[$this->cursor] ?? null); + $this->start = $this->rawBuffer[$this->cursor] ?? null; return $this; } - public function setCursor(int $cursor): self + public function setActiveToken(?Opcode $activeToken): static { - $this->cursor = $cursor; - $this->setStart($this->rawBuffer[$this->cursor] ?? null); - - return $this; - } - - public function setEnd(?string $end): self - { - $this->end = $end; + $this->activeToken = $activeToken; return $this; } - public function setStart(?string $start): self + public function setCursor(int $cursor): static { - $this->start = $start; + $this->cursor = $cursor; + $this->start = $this->rawBuffer[$this->cursor] ?? null; return $this; } - public function setStartLength(int $startLength): self + public function setStartLength(int $startLength): static { $this->startLength = $startLength; return $this; } - - public function setActiveToken(mixed $activeToken): self - { - $this->activeToken = $activeToken; - - return $this; - } } diff --git a/src/Scanner/Token.php b/src/Scanner/Token.php index ce8d1dd..f8ac0bf 100644 --- a/src/Scanner/Token.php +++ b/src/Scanner/Token.php @@ -4,49 +4,12 @@ namespace Phalcon\Phql\Scanner; -class Token +final class Token { - protected int $length = 0; - public int $len = 0; - public mixed $opcode = null; - public mixed $value = null; - - public function getLength(): int - { - return $this->length; - } - - public function getOpcode(): mixed - { - return $this->opcode; - } - - public function getValue(): mixed - { - return $this->value; - } - - public function setLength(int $length): self - { - $this->length = $length; - - return $this; - } - - public function setOpcode(mixed $opcode): self - { - $this->opcode = $opcode; - - return $this; - } - - public function setValue(mixed $value): self - { - $this->value = $value; - if (!empty($value)) { - $this->setLength(mb_strlen($value)); - } - - return $this; + public function __construct( + public readonly ?Opcode $opcode = null, + public readonly ?string $value = null, + public readonly int $length = 0, + ) { } } diff --git a/src/Tokens.php b/src/Tokens.php deleted file mode 100644 index ea5fbbd..0000000 --- a/src/Tokens.php +++ /dev/null @@ -1,54 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE.txt - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Phalcon\Phql; - -class Tokens -{ - public static array $names = [ - 'INTEGER' => Enum::PHQL_T_INTEGER, - 'DOUBLE' => Enum::PHQL_T_DOUBLE, - 'STRING' => Enum::PHQL_T_STRING, - 'IDENTIFIER' => Enum::PHQL_T_IDENTIFIER, - 'MINUS' => Enum::PHQL_T_MINUS, - '+' => Enum::PHQL_T_ADD, - '-' => Enum::PHQL_T_SUB, - '*' => Enum::PHQL_T_MUL, - '/' => Enum::PHQL_T_DIV, - '%%' => Enum::PHQL_T_MOD, - '!' => Enum::PHQL_T_NOT, - //'~' => Enum::PHQL_T_CONCAT, - 'AND' => Enum::PHQL_T_AND, - 'OR' => Enum::PHQL_T_OR, - 'DOT' => Enum::PHQL_T_DOT, - 'COMMA' => Enum::PHQL_T_COMMA, - 'EQUALS' => Enum::PHQL_T_EQUALS, - 'NOT EQUALS' => Enum::PHQL_T_NOTEQUALS, - //'IDENTICAL' => Enum::PHQL_T_IDENTICAL, - //'NOT IDENTICAL' => Enum::PHQL_T_NOTIDENTICAL, - 'NOT' => Enum::PHQL_T_NOT, - //'RANGE' => Enum::PHQL_T_RANGE, - 'COLON' => Enum::PHQL_T_COLON, - //'QUESTION MARK' => Enum::PHQL_T_QUESTION, - '<' => Enum::PHQL_T_LESS, - '<=' => Enum::PHQL_T_LESSEQUAL, - '>' => Enum::PHQL_T_GREATER, - '>=' => Enum::PHQL_T_GREATEREQUAL, - '(' => Enum::PHQL_T_PARENTHESES_OPEN, - ')' => Enum::PHQL_T_PARENTHESES_CLOSE, - //'[' => Enum::PHQL_T_SBRACKET_OPEN, - //']' => Enum::PHQL_T_SBRACKET_CLOSE, - //'{' => Enum::PHQL_T_CBRACKET_OPEN, - //'}' => Enum::PHQL_T_CBRACKET_CLOSE, - ]; -} diff --git a/tests-old/001.phpt b/tests-old/001.phpt deleted file mode 100644 index d6d8eff..0000000 --- a/tests-old/001.phpt +++ /dev/null @@ -1,55 +0,0 @@ ---TEST-- -phql_parse_phql - Select with limit ---SKIPIF-- - ---FILE-- - ---EXPECT-- -array(4) { - ["type"]=> - int(309) - ["select"]=> - array(2) { - ["columns"]=> - array(1) { - [0]=> - array(2) { - ["type"]=> - int(353) - ["column"]=> - string(1) "r" - } - } - ["tables"]=> - array(2) { - ["qualifiedName"]=> - array(2) { - ["type"]=> - int(355) - ["name"]=> - string(6) "Robots" - } - ["alias"]=> - string(1) "r" - } - } - ["limit"]=> - array(1) { - ["number"]=> - array(2) { - ["type"]=> - int(258) - ["value"]=> - string(2) "10" - } - } - ["id"]=> - int(3) -} diff --git a/tests-old/002.phpt b/tests-old/002.phpt deleted file mode 100644 index b70403c..0000000 --- a/tests-old/002.phpt +++ /dev/null @@ -1,81 +0,0 @@ ---TEST-- -phql_parse_phql - Select with betwen ---SKIPIF-- - ---FILE-- - ---EXPECT-- -array(4) { - ["type"]=> - int(309) - ["select"]=> - array(2) { - ["columns"]=> - array(1) { - [0]=> - array(2) { - ["type"]=> - int(354) - ["column"]=> - array(2) { - ["type"]=> - int(355) - ["name"]=> - string(11) "column_name" - } - } - } - ["tables"]=> - array(1) { - ["qualifiedName"]=> - array(2) { - ["type"]=> - int(355) - ["name"]=> - string(10) "table_name" - } - } - } - ["where"]=> - array(3) { - ["type"]=> - int(331) - ["left"]=> - array(2) { - ["type"]=> - int(355) - ["name"]=> - string(11) "column_name" - } - ["right"]=> - array(3) { - ["type"]=> - int(266) - ["left"]=> - array(2) { - ["type"]=> - int(355) - ["name"]=> - string(6) "value1" - } - ["right"]=> - array(2) { - ["type"]=> - int(355) - ["name"]=> - string(6) "value2" - } - } - } - ["id"]=> - int(3) -} diff --git a/tests-old/003.phpt b/tests-old/003.phpt deleted file mode 100644 index 8e8a57d..0000000 --- a/tests-old/003.phpt +++ /dev/null @@ -1,63 +0,0 @@ ---TEST-- -phql_parse_phql - Using FQCN for source model ---SKIPIF-- - ---FILE-- - ---EXPECT-- -array(3) { - ["type"]=> - int(309) - ["select"]=> - array(2) { - ["columns"]=> - array(1) { - [0]=> - array(3) { - ["type"]=> - int(354) - ["column"]=> - array(3) { - ["type"]=> - int(350) - ["name"]=> - string(3) "AVG" - ["arguments"]=> - array(1) { - [0]=> - array(2) { - ["type"]=> - int(355) - ["name"]=> - string(9) "inv_total" - } - } - } - ["alias"]=> - string(7) "average" - } - } - ["tables"]=> - array(1) { - ["qualifiedName"]=> - array(2) { - ["type"]=> - int(355) - ["name"]=> - string(28) "Phalcon\Tests\Models\Invoices" - } - } - } - ["id"]=> - int(3) -} diff --git a/tests-old/bug14253.phpt b/tests-old/bug14253.phpt deleted file mode 100644 index 4e72c3c..0000000 --- a/tests-old/bug14253.phpt +++ /dev/null @@ -1,105 +0,0 @@ ---TEST-- -phql_parse_phql - Select with betwen ---SKIPIF-- - ---FILE-- - ---EXPECT-- -array(4) { - ["type"]=> - int(309) - ["select"]=> - array(2) { - ["columns"]=> - array(3) { - [0]=> - array(2) { - ["type"]=> - int(354) - ["column"]=> - array(2) { - ["type"]=> - int(355) - ["name"]=> - string(2) "Id" - } - } - [1]=> - array(2) { - ["type"]=> - int(354) - ["column"]=> - array(2) { - ["type"]=> - int(355) - ["name"]=> - string(11) "ProductName" - } - } - [2]=> - array(2) { - ["type"]=> - int(354) - ["column"]=> - array(2) { - ["type"]=> - int(355) - ["name"]=> - string(9) "UnitPrice" - } - } - } - ["tables"]=> - array(1) { - ["qualifiedName"]=> - array(2) { - ["type"]=> - int(355) - ["name"]=> - string(7) "Product" - } - } - } - ["where"]=> - array(3) { - ["type"]=> - int(332) - ["left"]=> - array(2) { - ["type"]=> - int(355) - ["name"]=> - string(9) "UnitPrice" - } - ["right"]=> - array(3) { - ["type"]=> - int(266) - ["left"]=> - array(2) { - ["type"]=> - int(258) - ["value"]=> - string(1) "5" - } - ["right"]=> - array(2) { - ["type"]=> - int(258) - ["value"]=> - string(3) "100" - } - } - } - ["id"]=> - int(3) -} diff --git a/tests-old/bug14535.phpt b/tests-old/bug14535.phpt deleted file mode 100644 index 6923c97..0000000 --- a/tests-old/bug14535.phpt +++ /dev/null @@ -1,72 +0,0 @@ ---TEST-- -phql_parse_phql - Using spaces in column alias ---SKIPIF-- - ---FILE-- - ---EXPECT-- -array(3) { - ["type"]=> - int(309) - ["select"]=> - array(2) { - ["columns"]=> - array(2) { - [0]=> - array(3) { - ["type"]=> - int(354) - ["column"]=> - array(3) { - ["type"]=> - int(355) - ["domain"]=> - string(6) "People" - ["name"]=> - string(9) "firstName" - } - ["alias"]=> - string(10) "First Name" - } - [1]=> - array(3) { - ["type"]=> - int(354) - ["column"]=> - array(3) { - ["type"]=> - int(355) - ["domain"]=> - string(6) "People" - ["name"]=> - string(8) "lastName" - } - ["alias"]=> - string(9) "Last Name" - } - } - ["tables"]=> - array(1) { - ["qualifiedName"]=> - array(2) { - ["type"]=> - int(355) - ["name"]=> - string(6) "People" - } - } - } - ["id"]=> - int(3) -} diff --git a/tests/unit/Parser/PhqlParserTest.php b/tests/unit/Parser/PhqlParserTest.php deleted file mode 100644 index b7b479f..0000000 --- a/tests/unit/Parser/PhqlParserTest.php +++ /dev/null @@ -1,193 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE.txt - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Phalcon\Phql\Tests\Unit\Parser; - -use Phalcon\Phql\Parser\Parser; -use Phalcon\Phql\Tests\AbstractUnitTestCase; - -final class PhqlParserTest extends AbstractUnitTestCase -{ - private Parser $parser; - - protected function setUp(): void - { - $this->parser = new Parser(); - } - - /** - * Test: Select with limit - * Original: tests-old/001.phpt - */ - public function testSelectWithLimit(): void - { - $phql = 'SELECT r.* FROM Robots r LIMIT 10'; - $result = $this->parser->parse($phql); - - $this->assertIsArray($result); - $this->assertEquals(309, $result['type']); - $this->assertArrayHasKey('select', $result); - $this->assertArrayHasKey('limit', $result); - - $this->assertArrayHasKey('columns', $result['select']); - $this->assertIsArray($result['select']['columns']); - $this->assertCount(1, $result['select']['columns']); - - $this->assertEquals(353, $result['select']['columns'][0]['type']); - $this->assertEquals('r', $result['select']['columns'][0]['column']); - - $this->assertArrayHasKey('tables', $result['select']); - $this->assertEquals('Robots', $result['select']['tables']['qualifiedName']['name']); - $this->assertEquals('r', $result['select']['tables']['alias']); - - $this->assertEquals('10', $result['limit']['number']['value']); - } - - /** - * Test: Select with BETWEEN - * Original: tests-old/002.phpt - */ - public function testSelectWithBetween(): void - { - $phql = <<parser->parse($phql); - - $this->assertIsArray($result); - $this->assertEquals(309, $result['type']); - $this->assertArrayHasKey('select', $result); - $this->assertArrayHasKey('where', $result); - - $this->assertEquals('column_name', $result['select']['columns'][0]['column']['name']); - $this->assertEquals('table_name', $result['select']['tables']['qualifiedName']['name']); - $this->assertEquals('column_name', $result['where']['left']['name']); - $this->assertEquals('value1', $result['where']['right']['left']['name']); - $this->assertEquals('value2', $result['where']['right']['right']['name']); - } - - /** - * Test: Using FQCN for source model - * Original: tests-old/003.phpt - */ - public function testUsingFQCNForSourceModel(): void - { - $phql = <<parser->parse($phql); - - $this->assertIsArray($result); - $this->assertEquals(309, $result['type']); - $this->assertArrayHasKey('select', $result); - - $this->assertEquals('AVG', $result['select']['columns'][0]['column']['name']); - $this->assertEquals('inv_total', $result['select']['columns'][0]['column']['arguments'][0]['name']); - $this->assertEquals('average', $result['select']['columns'][0]['alias']); - - $this->assertEquals('[Phalcon\\Tests\\Models\\Invoices]', $result['select']['tables']['qualifiedName']['name']); - } - - /** - * Test: Select with NOT BETWEEN - * Original: tests-old/bug14253.phpt - */ - public function testSelectWithNotBetween(): void - { - $phql = <<parser->parse($phql); - - $this->assertIsArray($result); - $this->assertEquals(309, $result['type']); - $this->assertArrayHasKey('select', $result); - $this->assertArrayHasKey('where', $result); - - $this->assertCount(3, $result['select']['columns']); - $this->assertEquals('Id', $result['select']['columns'][0]['column']['name']); - $this->assertEquals('ProductName', $result['select']['columns'][1]['column']['name']); - $this->assertEquals('UnitPrice', $result['select']['columns'][2]['column']['name']); - $this->assertEquals('Product', $result['select']['tables']['qualifiedName']['name']); - - $this->assertEquals(332, $result['where']['type']); // NOT BETWEEN type - $this->assertEquals('UnitPrice', $result['where']['left']['name']); - $this->assertEquals('5', $result['where']['right']['left']['value']); - $this->assertEquals('100', $result['where']['right']['right']['value']); - } - - /** - * Test: Using spaces in column alias - * Original: tests-old/bug14535.phpt - */ - public function testUsingSpacesInColumnAlias(): void - { - $phql = <<parser->parse($phql); - - $this->assertIsArray($result); - $this->assertEquals(309, $result['type']); - $this->assertArrayHasKey('select', $result); - - $this->assertCount(2, $result['select']['columns']); - $this->assertEquals('People', $result['select']['columns'][0]['column']['domain']); - $this->assertEquals('firstName', $result['select']['columns'][0]['column']['name']); - $this->assertEquals('[First Name]', $result['select']['columns'][0]['alias']); - - $this->assertEquals('People', $result['select']['columns'][1]['column']['domain']); - $this->assertEquals('lastName', $result['select']['columns'][1]['column']['name']); - $this->assertEquals('[Last Name]', $result['select']['columns'][1]['alias']); - - $this->assertEquals('People', $result['select']['tables']['qualifiedName']['name']); - } - - /** - * Test: Delete with WHERE conditions using AND/OR - */ - public function testDeleteWithWhereAndOr(): void - { - $phql = "DELETE FROM co_invoices " - . "WHERE inv_total > :test: " - . "AND inv_cst_id = 2 " - . "OR inv_status_flag = 3 "; - - $parser = new Parser(true); - $result = $parser->parse($phql); - - $this->assertIsArray($result); - $this->assertArrayHasKey('type', $result); - $this->assertArrayHasKey('delete', $result); - - $this->assertArrayHasKey('tables', $result['delete']); - $this->assertEquals('co_invoices', $result['delete']['tables']['qualifiedName']['name']); - - $this->assertArrayHasKey('where', $result); - } -} \ No newline at end of file diff --git a/tests/unit/Parser/StatusTest.php b/tests/unit/Parser/StatusTest.php new file mode 100644 index 0000000..64663e7 --- /dev/null +++ b/tests/unit/Parser/StatusTest.php @@ -0,0 +1,82 @@ +assertSame(Status::PHQL_PARSING_OK, $status->getStatus()); + $this->assertNull($status->getAst()); + $this->assertNull($status->getSyntaxError()); + $this->assertNull($status->getToken()); + $this->assertFalse($status->getEnableLiterals()); + } + + public function testEnableLiterals(): void + { + $state = new State('SELECT'); + $status = new Status($state); + + $status->setEnableLiterals(true); + + $this->assertTrue($status->getEnableLiterals()); + } + + public function testGetState(): void + { + $state = new State('SELECT'); + $status = new Status($state); + + $this->assertSame($state, $status->getState()); + } + + public function testNoGetRetMethod(): void + { + $state = new State('SELECT'); + $status = new Status($state); + + $this->assertFalse(method_exists($status, 'getRet')); + $this->assertFalse(method_exists($status, 'setRet')); + } + + public function testSetAndGetAst(): void + { + $state = new State('SELECT'); + $status = new Status($state); + $ast = ['type' => 309, 'select' => []]; + + $status->setAst($ast); + + $this->assertSame($ast, $status->getAst()); + } + + public function testSetStatus(): void + { + $state = new State('SELECT'); + $status = new Status($state); + + $status->setStatus(Status::PHQL_PARSING_FAILED); + + $this->assertSame(Status::PHQL_PARSING_FAILED, $status->getStatus()); + } + + public function testSetSyntaxError(): void + { + $state = new State('SELECT'); + $status = new Status($state); + + $status->setSyntaxError('Unexpected token'); + + $this->assertSame('Unexpected token', $status->getSyntaxError()); + } +} diff --git a/tests/unit/ParserTest.php b/tests/unit/ParserTest.php new file mode 100644 index 0000000..279160a --- /dev/null +++ b/tests/unit/ParserTest.php @@ -0,0 +1,135 @@ +expectException(Exception::class); + $this->expectExceptionMessage('Literals are disabled in PHQL statements'); + + (new Parser())->setEnableLiterals(false)->parse('SELECT 1.5 FROM Invoices'); + } + + public function testLiteralsDisabledBlocksFalse(): void + { + $this->expectException(Exception::class); + $this->expectExceptionMessage('Literals are disabled in PHQL statements'); + + (new Parser())->setEnableLiterals(false)->parse('SELECT FALSE FROM Invoices'); + } + + public function testLiteralsDisabledBlocksHinteger(): void + { + $this->expectException(Exception::class); + $this->expectExceptionMessage('Literals are disabled in PHQL statements'); + + (new Parser())->setEnableLiterals(false)->parse('SELECT 0xFF FROM Invoices'); + } + + public function testLiteralsDisabledBlocksInteger(): void + { + $this->expectException(Exception::class); + $this->expectExceptionMessage('Literals are disabled in PHQL statements'); + + (new Parser())->setEnableLiterals(false)->parse('SELECT 1 FROM Invoices'); + } + + public function testLiteralsDisabledBlocksString(): void + { + $this->expectException(Exception::class); + $this->expectExceptionMessage('Literals are disabled in PHQL statements'); + + (new Parser())->setEnableLiterals(false)->parse("SELECT 'hello' FROM Invoices"); + } + + public function testLiteralsDisabledBlocksTrue(): void + { + $this->expectException(Exception::class); + $this->expectExceptionMessage('Literals are disabled in PHQL statements'); + + (new Parser())->setEnableLiterals(false)->parse('SELECT TRUE FROM Invoices'); + } + + public function testLiteralsEnabledByDefault(): void + { + // Integer literal should parse without error when literals are enabled (default) + $result = (new Parser())->parse('SELECT 1 FROM Invoices'); + $this->assertIsArray($result); + } + + public function testParseEmptyStringThrows(): void + { + $this->expectException(Exception::class); + $this->expectExceptionMessage('PHQL statement cannot be NULL'); + + (new Parser())->parse(''); + } + + public function testParseSimpleSelect(): void + { + $result = (new Parser())->parse('SELECT * FROM Invoices'); + + $this->assertIsArray($result); + $this->assertSame(Opcode::SELECT->value, $result['type']); + $this->assertArrayHasKey('select', $result); + } + + public function testScannerErrorLongMessage(): void + { + $this->expectException(Exception::class); + $this->expectExceptionMessageMatches('/Scanning error before/'); + $this->expectExceptionMessageMatches('/\.\.\./'); + + (new Parser())->parse('#' . str_repeat('x', 20)); + } + + public function testScannerErrorShortMessage(): void + { + $this->expectException(Exception::class); + $this->expectExceptionMessageMatches('/Scanning error before/'); + + (new Parser())->parse('#'); + } + + public function testSetEnableLiteralsChaining(): void + { + // Should not throw — fluent chaining works + $result = (new Parser())->setEnableLiterals(true)->parse('SELECT * FROM Invoices'); + $this->assertIsArray($result); + } + + public function testSetEnableLiteralsReturnsSelf(): void + { + $parser = new Parser(); + $result = $parser->setEnableLiterals(false); + + $this->assertSame($parser, $result); + } + + public function testThrowsPhqlException(): void + { + try { + (new Parser())->parse(''); + } catch (\Throwable $e) { + $this->assertInstanceOf(Exception::class, $e); + $this->assertNotInstanceOf(\RuntimeException::class, $e); + } + } + + public function testUnknownOpcodeThrows(): void + { + $this->expectException(Exception::class); + $this->expectExceptionMessageMatches('/Unknown opcode/'); + + (new Parser())->parse('SELECT : FROM Invoices'); + } +} diff --git a/tests/unit/Phql/Delete/CombinationTest.php b/tests/unit/Phql/Delete/CombinationTest.php new file mode 100644 index 0000000..14a8664 --- /dev/null +++ b/tests/unit/Phql/Delete/CombinationTest.php @@ -0,0 +1,369 @@ + + * + * For the full copyright and license information, please view the LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Phql\Tests\Unit\Phql\Delete; + +use Phalcon\Phql\Parser; +use Phalcon\Phql\Scanner\Opcode; +use Phalcon\Phql\Tests\AbstractUnitTestCase; + +final class CombinationTest extends AbstractUnitTestCase +{ + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlDelete(): void + { + $source = "DELETE FROM Invoices"; + $expected = [ + 'type' => Opcode::DELETE->value, + 'delete' => [ + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlDeleteAliasWhereEqZero(): void + { + $source = "DELETE FROM Invoices AS i WHERE i.inv_status_flag = 0"; + $expected = [ + 'type' => Opcode::DELETE->value, + 'delete' => [ + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + 'alias' => 'i', + ], + ], + 'where' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'i', + 'name' => 'inv_status_flag', + ], + 'right' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '0', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlDeleteLimit(): void + { + $source = "DELETE FROM Invoices LIMIT 10"; + $expected = [ + 'type' => Opcode::DELETE->value, + 'delete' => [ + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'limit' => [ + 'number' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '10', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlDeleteWhereEqNum(): void + { + $source = "DELETE FROM Invoices WHERE inv_id = 1"; + $expected = [ + 'type' => Opcode::DELETE->value, + 'delete' => [ + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'where' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_id', + ], + 'right' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '1', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlDeleteWhereEqPlaceholder(): void + { + $source = "DELETE FROM Invoices WHERE inv_id = :id:"; + $expected = [ + 'type' => Opcode::DELETE->value, + 'delete' => [ + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'where' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_id', + ], + 'right' => [ + 'type' => Opcode::SPLACEHOLDER->value, + 'value' => 'id', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlDeleteWhereEqPlaceholderNumeric(): void + { + $source = "DELETE FROM Invoices WHERE inv_id = ?0"; + $expected = [ + 'type' => Opcode::DELETE->value, + 'delete' => [ + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'where' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_id', + ], + 'right' => [ + 'type' => Opcode::NPLACEHOLDER->value, + 'value' => '?0', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlDeleteWhereEqZero(): void + { + $source = "DELETE FROM Invoices WHERE inv_status_flag = 0"; + $expected = [ + 'type' => Opcode::DELETE->value, + 'delete' => [ + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'where' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_status_flag', + ], + 'right' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '0', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlDeleteWhereIn(): void + { + $source = "DELETE FROM Invoices WHERE inv_cst_id IN (1, 2, 3)"; + $expected = [ + 'type' => Opcode::DELETE->value, + 'delete' => [ + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'where' => [ + 'type' => Opcode::IN->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_cst_id', + ], + 'right' => [ + 0 => [ + 'type' => Opcode::INTEGER->value, + 'value' => '1', + ], + 1 => [ + 'type' => Opcode::INTEGER->value, + 'value' => '2', + ], + 2 => [ + 'type' => Opcode::INTEGER->value, + 'value' => '3', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlDeleteWhereIsNull(): void + { + $source = "DELETE FROM Invoices WHERE inv_created_at IS NULL"; + $expected = [ + 'type' => Opcode::DELETE->value, + 'delete' => [ + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'where' => [ + 'type' => Opcode::ISNULL->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_created_at', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlDeleteWhereLtNumLimit(): void + { + $source = "DELETE FROM Invoices WHERE inv_total < 0 LIMIT 3"; + $expected = [ + 'type' => Opcode::DELETE->value, + 'delete' => [ + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'where' => [ + 'type' => Opcode::LESS->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_total', + ], + 'right' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '0', + ], + ], + 'limit' => [ + 'number' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '3', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/unit/Phql/Insert/CombinationTest.php b/tests/unit/Phql/Insert/CombinationTest.php new file mode 100644 index 0000000..565885a --- /dev/null +++ b/tests/unit/Phql/Insert/CombinationTest.php @@ -0,0 +1,506 @@ + + * + * For the full copyright and license information, please view the LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Phql\Tests\Unit\Phql\Insert; + +use Phalcon\Phql\Parser; +use Phalcon\Phql\Scanner\Opcode; +use Phalcon\Phql\Tests\AbstractUnitTestCase; + +final class CombinationTest extends AbstractUnitTestCase +{ + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqInsertFields(): void + { + $source = "INSERT INTO Invoices " . "(inv_cst_id, inv_status_flag, inv_title, inv_total, inv_created_at) " . + "VALUES (1, 0, 'Test Invoice', 150.50, '2025-01-01 00:00:00')"; + $expected = [ + 'type' => Opcode::INSERT->value, + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + 'fields' => [ + 0 => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_cst_id', + ], + 1 => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_status_flag', + ], + 2 => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_title', + ], + 3 => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_total', + ], + 4 => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_created_at', + ], + ], + 'values' => [ + 0 => [ + 'type' => Opcode::INTEGER->value, + 'value' => '1', + ], + 1 => [ + 'type' => Opcode::INTEGER->value, + 'value' => '0', + ], + 2 => [ + 'type' => Opcode::STRING->value, + 'value' => 'Test Invoice', + ], + 3 => [ + 'type' => Opcode::DOUBLE->value, + 'value' => '150.50', + ], + 4 => [ + 'type' => Opcode::STRING->value, + 'value' => '2025-01-01 00:00:00', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlInsert(): void + { + $source = "INSERT INTO Invoices " . "VALUES (1, 1, 1, 'Test Invoice', 100.00, '2025-01-01 00:00:00')"; + $expected = [ + 'type' => Opcode::INSERT->value, + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + 'values' => [ + 0 => [ + 'type' => Opcode::INTEGER->value, + 'value' => '1', + ], + 1 => [ + 'type' => Opcode::INTEGER->value, + 'value' => '1', + ], + 2 => [ + 'type' => Opcode::INTEGER->value, + 'value' => '1', + ], + 3 => [ + 'type' => Opcode::STRING->value, + 'value' => 'Test Invoice', + ], + 4 => [ + 'type' => Opcode::DOUBLE->value, + 'value' => '100.00', + ], + 5 => [ + 'type' => Opcode::STRING->value, + 'value' => '2025-01-01 00:00:00', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-10 + */ + public function testMvcModelQueryPhqlInsertArithmeticInValues(): void + { + $source = "INSERT INTO Invoices (inv_total) VALUES (100 + 50)"; + $expected = [ + 'type' => Opcode::INSERT->value, + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + 'fields' => [ + 0 => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_total', + ], + ], + 'values' => [ + 0 => [ + 'type' => Opcode::ADD->value, + 'left' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '100', + ], + 'right' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '50', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlInsertFieldsNull(): void + { + $source = "INSERT INTO Invoices " . "(inv_title, inv_total) " . "VALUES ('Null Test', NULL)"; + $expected = [ + 'type' => Opcode::INSERT->value, + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + 'fields' => [ + 0 => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_title', + ], + 1 => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_total', + ], + ], + 'values' => [ + 0 => [ + 'type' => Opcode::STRING->value, + 'value' => 'Null Test', + ], + 1 => [ + 'type' => Opcode::NULL->value, + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlInsertFieldsPartial(): void + { + $source = "INSERT INTO Invoices (inv_title, inv_total) " . "VALUES ('Invoice A', 200.00)"; + $expected = [ + 'type' => Opcode::INSERT->value, + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + 'fields' => [ + 0 => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_title', + ], + 1 => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_total', + ], + ], + 'values' => [ + 0 => [ + 'type' => Opcode::STRING->value, + 'value' => 'Invoice A', + ], + 1 => [ + 'type' => Opcode::DOUBLE->value, + 'value' => '200.00', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlInsertFieldsPlaceholders(): void + { + $source = "INSERT INTO Invoices " . "(inv_cst_id, inv_status_flag, inv_title, inv_total) " . + "VALUES (:cstId:, :status:, :title:, :total:)"; + $expected = [ + 'type' => Opcode::INSERT->value, + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + 'fields' => [ + 0 => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_cst_id', + ], + 1 => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_status_flag', + ], + 2 => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_title', + ], + 3 => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_total', + ], + ], + 'values' => [ + 0 => [ + 'type' => Opcode::SPLACEHOLDER->value, + 'value' => 'cstId', + ], + 1 => [ + 'type' => Opcode::SPLACEHOLDER->value, + 'value' => 'status', + ], + 2 => [ + 'type' => Opcode::SPLACEHOLDER->value, + 'value' => 'title', + ], + 3 => [ + 'type' => Opcode::SPLACEHOLDER->value, + 'value' => 'total', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlInsertFieldsPlaceholdersBrackets(): void + { + $source = "INSERT INTO Invoices " . "(inv_id, inv_cst_id, inv_status_flag, inv_title, inv_total) " . + "VALUES ({id}, {cstId}, {status}, {title}, {total})"; + $expected = [ + 'type' => Opcode::INSERT->value, + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + 'fields' => [ + 0 => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_id', + ], + 1 => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_cst_id', + ], + 2 => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_status_flag', + ], + 3 => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_title', + ], + 4 => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_total', + ], + ], + 'values' => [ + 0 => [ + 'type' => Opcode::BPLACEHOLDER->value, + 'value' => 'id', + ], + 1 => [ + 'type' => Opcode::BPLACEHOLDER->value, + 'value' => 'cstId', + ], + 2 => [ + 'type' => Opcode::BPLACEHOLDER->value, + 'value' => 'status', + ], + 3 => [ + 'type' => Opcode::BPLACEHOLDER->value, + 'value' => 'title', + ], + 4 => [ + 'type' => Opcode::BPLACEHOLDER->value, + 'value' => 'total', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlInsertFieldsPlaceholdersNum(): void + { + $source = "INSERT INTO Invoices " . "(inv_cst_id, inv_title, inv_total) " . "VALUES (?0, ?1, ?2)"; + $expected = [ + 'type' => Opcode::INSERT->value, + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + 'fields' => [ + 0 => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_cst_id', + ], + 1 => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_title', + ], + 2 => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_total', + ], + ], + 'values' => [ + 0 => [ + 'type' => Opcode::NPLACEHOLDER->value, + 'value' => '?0', + ], + 1 => [ + 'type' => Opcode::NPLACEHOLDER->value, + 'value' => '?1', + ], + 2 => [ + 'type' => Opcode::NPLACEHOLDER->value, + 'value' => '?2', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlInsertFieldsTrue(): void + { + $source = "INSERT INTO Invoices (inv_title, inv_status_flag) VALUES ('New Invoice', TRUE)"; + $expected = [ + 'type' => Opcode::INSERT->value, + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + 'fields' => [ + 0 => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_title', + ], + 1 => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_status_flag', + ], + ], + 'values' => [ + 0 => [ + 'type' => Opcode::STRING->value, + 'value' => 'New Invoice', + ], + 1 => [ + 'type' => Opcode::TRUE->value, + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-10 + */ + public function testMvcModelQueryPhqlInsertFuncInValues(): void + { + $source = "INSERT INTO Invoices (inv_title, inv_total) " + . "VALUES (UPPER('test invoice'), 100.00)"; + $expected = [ + 'type' => Opcode::INSERT->value, + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + 'fields' => [ + 0 => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_title', + ], + 1 => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_total', + ], + ], + 'values' => [ + 0 => [ + 'type' => Opcode::FCALL->value, + 'name' => 'UPPER', + 'arguments' => [ + 0 => [ + 'type' => Opcode::STRING->value, + 'value' => 'test invoice', + ], + ], + ], + 1 => [ + 'type' => Opcode::DOUBLE->value, + 'value' => '100.00', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/unit/Phql/Select/AggregateTest.php b/tests/unit/Phql/Select/AggregateTest.php new file mode 100644 index 0000000..4909c30 --- /dev/null +++ b/tests/unit/Phql/Select/AggregateTest.php @@ -0,0 +1,504 @@ + + * + * For the full copyright and license information, please view the LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Phql\Tests\Unit\Phql\Select; + +use Phalcon\Phql\Parser; +use Phalcon\Phql\Scanner\Opcode; +use Phalcon\Phql\Tests\AbstractUnitTestCase; + +final class AggregateTest extends AbstractUnitTestCase +{ + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectAvgField(): void + { + $source = "SELECT AVG(inv_total) FROM Invoices"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::FCALL->value, + 'name' => 'AVG', + 'arguments' => [ + 0 => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_total', + ], + ], + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @issue 3 + * @since 2026-04-11 + */ + public function testMvcModelQueryPhqlSelectAvgFieldAliasFqcn(): void + { + $source = "SELECT AVG(inv_total) AS average " + . "FROM [Phalcon\\Tests\\Models\\Invoices]"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::FCALL->value, + 'name' => 'AVG', + 'arguments' => [ + 0 => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_total', + ], + ], + ], + 'alias' => 'average', + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Phalcon\\Tests\\Models\\Invoices', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectCount(): void + { + $source = "SELECT COUNT(*) FROM Invoices"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::FCALL->value, + 'name' => 'COUNT', + 'arguments' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectCountDistinctField(): void + { + $source = "SELECT COUNT(DISTINCT inv_cst_id) FROM Invoices"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::FCALL->value, + 'name' => 'COUNT', + 'arguments' => [ + 0 => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_cst_id', + ], + ], + 'distinct' => true, + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectCountField(): void + { + $source = "SELECT COUNT(inv_id) FROM Invoices"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::FCALL->value, + 'name' => 'COUNT', + 'arguments' => [ + 0 => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_id', + ], + ], + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectCountSumAvgMinMax(): void + { + $source = "SELECT COUNT(*), SUM(inv_total), AVG(inv_total), MIN(inv_total), MAX(inv_total) FROM Invoices"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::FCALL->value, + 'name' => 'COUNT', + 'arguments' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + ], + ], + 1 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::FCALL->value, + 'name' => 'SUM', + 'arguments' => [ + 0 => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_total', + ], + ], + ], + ], + 2 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::FCALL->value, + 'name' => 'AVG', + 'arguments' => [ + 0 => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_total', + ], + ], + ], + ], + 3 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::FCALL->value, + 'name' => 'MIN', + 'arguments' => [ + 0 => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_total', + ], + ], + ], + ], + 4 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::FCALL->value, + 'name' => 'MAX', + 'arguments' => [ + 0 => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_total', + ], + ], + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectMaxDate(): void + { + $source = "SELECT MAX(inv_created_at) FROM Invoices"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::FCALL->value, + 'name' => 'MAX', + 'arguments' => [ + 0 => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_created_at', + ], + ], + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectMaxField(): void + { + $source = "SELECT MAX(inv_total) FROM Invoices"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::FCALL->value, + 'name' => 'MAX', + 'arguments' => [ + 0 => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_total', + ], + ], + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectMinDate(): void + { + $source = "SELECT MIN(inv_created_at) FROM Invoices"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::FCALL->value, + 'name' => 'MIN', + 'arguments' => [ + 0 => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_created_at', + ], + ], + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectMinField(): void + { + $source = "SELECT MIN(inv_total) FROM Invoices"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::FCALL->value, + 'name' => 'MIN', + 'arguments' => [ + 0 => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_total', + ], + ], + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectSumField(): void + { + $source = "SELECT SUM(inv_total) FROM Invoices"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::FCALL->value, + 'name' => 'SUM', + 'arguments' => [ + 0 => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_total', + ], + ], + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/unit/Phql/Select/BasicTest.php b/tests/unit/Phql/Select/BasicTest.php new file mode 100644 index 0000000..811160f --- /dev/null +++ b/tests/unit/Phql/Select/BasicTest.php @@ -0,0 +1,335 @@ + + * + * For the full copyright and license information, please view the LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Phql\Tests\Unit\Phql\Select; + +use Phalcon\Phql\Parser; +use Phalcon\Phql\Scanner\Opcode; +use Phalcon\Phql\Tests\AbstractUnitTestCase; + +final class BasicTest extends AbstractUnitTestCase +{ + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectAll(): void + { + $source = "SELECT * FROM Invoices"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectAllAliased(): void + { + $source = "SELECT i.* FROM Invoices i"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::DOMAINALL->value, + 'column' => 'i', + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + 'alias' => 'i', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectAllTable(): void + { + $source = "SELECT Invoices.* FROM Invoices"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::DOMAINALL->value, + 'column' => 'Invoices', + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectIdStringFloat(): void + { + $source = "SELECT inv_id, inv_title, inv_total FROM Invoices"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_id', + ], + ], + 1 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_title', + ], + ], + 2 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_total', + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectInt(): void + { + $source = "SELECT inv_id FROM Invoices"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_id', + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectIntAliased(): void + { + $source = "SELECT i.inv_id FROM Invoices i"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'i', + 'name' => 'inv_id', + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + 'alias' => 'i', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectIntAliasedAs(): void + { + $source = "SELECT i.inv_id FROM Invoices AS i"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'i', + 'name' => 'inv_id', + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + 'alias' => 'i', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectIntAliasedTable(): void + { + $source = "SELECT Invoices.inv_id FROM Invoices"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'Invoices', + 'name' => 'inv_id', + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectIntString(): void + { + $source = "SELECT inv_id, inv_title FROM Invoices"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_id', + ], + ], + 1 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_title', + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/unit/Phql/Select/BetweenTest.php b/tests/unit/Phql/Select/BetweenTest.php new file mode 100644 index 0000000..3ab1275 --- /dev/null +++ b/tests/unit/Phql/Select/BetweenTest.php @@ -0,0 +1,338 @@ + + * + * For the full copyright and license information, please view the LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Phql\Tests\Unit\Phql\Select; + +use Phalcon\Phql\Parser; +use Phalcon\Phql\Scanner\Opcode; +use Phalcon\Phql\Tests\AbstractUnitTestCase; + +final class BetweenTest extends AbstractUnitTestCase +{ + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectBetweenFloat(): void + { + $source = "SELECT * " + . "FROM Invoices " + . "WHERE inv_total BETWEEN 10.00 AND 500.00"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'where' => [ + 'type' => Opcode::BETWEEN->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_total', + ], + 'right' => [ + 'type' => Opcode::AND->value, + 'left' => [ + 'type' => Opcode::DOUBLE->value, + 'value' => '10.00', + ], + 'right' => [ + 'type' => Opcode::DOUBLE->value, + 'value' => '500.00', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @issue 2 + * @since 2026-04-11 + */ + public function testMvcModelQueryPhqlSelectBetweenIdentifiers(): void + { + $source = "SELECT column_name " + . "FROM table_name " + . "WHERE column_name BETWEEN value1 AND value2"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'column_name', + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'table_name', + ], + ], + ], + 'where' => [ + 'type' => Opcode::BETWEEN->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'column_name', + ], + 'right' => [ + 'type' => Opcode::AND->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'value1', + ], + 'right' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'value2', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectBetweenInt(): void + { + $source = "SELECT * " + . "FROM Invoices " + . "WHERE inv_id BETWEEN 1 AND 100"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'where' => [ + 'type' => Opcode::BETWEEN->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_id', + ], + 'right' => [ + 'type' => Opcode::AND->value, + 'left' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '1', + ], + 'right' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '100', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectNotBetweenFloat(): void + { + $source = "SELECT * " + . "FROM Invoices " + . "WHERE inv_total NOT BETWEEN 10.00 AND 500.00"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'where' => [ + 'type' => Opcode::BETWEEN_NOT->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_total', + ], + 'right' => [ + 'type' => Opcode::AND->value, + 'left' => [ + 'type' => Opcode::DOUBLE->value, + 'value' => '10.00', + ], + 'right' => [ + 'type' => Opcode::DOUBLE->value, + 'value' => '500.00', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectNotBetweenInt(): void + { + $source = "SELECT * " + . "FROM Invoices " + . "WHERE inv_id NOT BETWEEN 1 AND 100"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'where' => [ + 'type' => Opcode::BETWEEN_NOT->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_id', + ], + 'right' => [ + 'type' => Opcode::AND->value, + 'left' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '1', + ], + 'right' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '100', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-11 + */ + public function testMvcModelQueryPhqlSelectNotBetweenIntMultipleColumns(): void + { + $source = "SELECT Id, ProductName, UnitPrice " + . "FROM Product " + . "WHERE UnitPrice NOT BETWEEN 5 AND 100"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Id', + ], + ], + 1 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'ProductName', + ], + ], + 2 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'UnitPrice', + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Product', + ], + ], + ], + 'where' => [ + 'type' => Opcode::BETWEEN_NOT->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'UnitPrice', + ], + 'right' => [ + 'type' => Opcode::AND->value, + 'left' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '5', + ], + 'right' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '100', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/unit/Phql/Select/BitwiseTest.php b/tests/unit/Phql/Select/BitwiseTest.php new file mode 100644 index 0000000..9c3eede --- /dev/null +++ b/tests/unit/Phql/Select/BitwiseTest.php @@ -0,0 +1,247 @@ + + * + * For the full copyright and license information, please view the LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Phql\Tests\Unit\Phql\Select; + +use Phalcon\Phql\Parser; +use Phalcon\Phql\Scanner\Opcode; +use Phalcon\Phql\Tests\AbstractUnitTestCase; + +final class BitwiseTest extends AbstractUnitTestCase +{ + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectBitwiseAnd(): void + { + $source = "SELECT * " + . "FROM Invoices " + . "WHERE inv_status_flag & 1 = 1"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'where' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::BITWISE_AND->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_status_flag', + ], + 'right' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '1', + ], + ], + 'right' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '1', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectBitwiseInField(): void + { + $source = "SELECT inv_status_flag & 3 AS masked " + . "FROM Invoices"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::BITWISE_AND->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_status_flag', + ], + 'right' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '3', + ], + ], + 'alias' => 'masked', + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectBitwiseNotField(): void + { + $source = "SELECT ~inv_status_flag " + . "FROM Invoices"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::BITWISE_NOT->value, + 'right' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_status_flag', + ], + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectBitwiseOr(): void + { + $source = "SELECT * " + . "FROM Invoices " + . "WHERE inv_status_flag | 2 = 3"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'where' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::BITWISE_OR->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_status_flag', + ], + 'right' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '2', + ], + ], + 'right' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '3', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectBitwiseXor(): void + { + $source = "SELECT * " + . "FROM Invoices " + . "WHERE inv_status_flag ^ 1 = 0"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'where' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::BITWISE_XOR->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_status_flag', + ], + 'right' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '1', + ], + ], + 'right' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '0', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/unit/Phql/Select/BracketsNameTest.php b/tests/unit/Phql/Select/BracketsNameTest.php new file mode 100644 index 0000000..7691378 --- /dev/null +++ b/tests/unit/Phql/Select/BracketsNameTest.php @@ -0,0 +1,105 @@ + + * + * For the full copyright and license information, please view the LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Phql\Tests\Unit\Phql\Select; + +use Phalcon\Phql\Parser; +use Phalcon\Phql\Scanner\Opcode; +use Phalcon\Phql\Tests\AbstractUnitTestCase; + +final class BracketsNameTest extends AbstractUnitTestCase +{ + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectBrackets(): void + { + $source = "SELECT [inv_id], [inv_title] FROM [Invoices]"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_id', + ], + ], + 1 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_title', + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectBracketsWhereNum(): void + { + $source = "SELECT [inv_id] FROM Invoices WHERE [inv_status_flag] = 1"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_id', + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'where' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_status_flag', + ], + 'right' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '1', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/unit/Phql/Select/BracketsWithEscapedNameTest.php b/tests/unit/Phql/Select/BracketsWithEscapedNameTest.php new file mode 100644 index 0000000..109383f --- /dev/null +++ b/tests/unit/Phql/Select/BracketsWithEscapedNameTest.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Phql\Tests\Unit\Phql\Select; + +use Phalcon\Phql\Parser; +use Phalcon\Phql\Scanner\Opcode; +use Phalcon\Phql\Tests\AbstractUnitTestCase; + +final class BracketsWithEscapedNameTest extends AbstractUnitTestCase +{ + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectBracketsEscapedNames(): void + { + $source = "SELECT [col\[0\]], [col\[1\]] FROM Items"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => '', + ], + ], + 1 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => '', + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Items', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/unit/Phql/Select/BracketsWithSpaceNameTest.php b/tests/unit/Phql/Select/BracketsWithSpaceNameTest.php new file mode 100644 index 0000000..10a7913 --- /dev/null +++ b/tests/unit/Phql/Select/BracketsWithSpaceNameTest.php @@ -0,0 +1,277 @@ + + * + * For the full copyright and license information, please view the LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Phql\Tests\Unit\Phql\Select; + +use Phalcon\Phql\Parser; +use Phalcon\Phql\Scanner\Opcode; +use Phalcon\Phql\Tests\AbstractUnitTestCase; + +final class BracketsWithSpaceNameTest extends AbstractUnitTestCase +{ + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-11 + */ + public function testMvcModelQueryPhqlSelectBracketsColumnAliasSpaced(): void + { + $source = "SELECT People.firstName AS [First Name], " + . "People.lastName AS [Last Name] " + . "FROM People"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'People', + 'name' => 'firstName', + ], + 'alias' => 'First Name', + ], + 1 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'People', + 'name' => 'lastName', + ], + 'alias' => 'Last Name', + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'People', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectBracketsFieldSpaces(): void + { + $source = "SELECT [First Name], [Last Name] FROM Contacts"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'First Name', + ], + ], + 1 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Last Name', + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Contacts', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectBracketsFieldSpacesAlias(): void + { + $source = "SELECT c.[First Name], c.[Last Name] " . "FROM Contacts AS c"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'c', + 'name' => 'First Name', + ], + ], + 1 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'c', + 'name' => 'Last Name', + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Contacts', + ], + 'alias' => 'c', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectBracketsFieldSpacesAliasWhereString(): void + { + $source = "SELECT c.[First Name] " . "FROM Contacts AS c " . "WHERE c.[Last Name] = 'Smith'"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'c', + 'name' => 'First Name', + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Contacts', + ], + 'alias' => 'c', + ], + ], + 'where' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'c', + 'name' => 'Last Name', + ], + 'right' => [ + 'type' => Opcode::STRING->value, + 'value' => 'Smith', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectBracketsFieldSpacesOrderBy(): void + { + $source = "SELECT [First Name] AS firstName, [Last Name] AS lastName " + . "FROM Contacts " + . "ORDER BY [Last Name] ASC"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'First Name', + ], + 'alias' => 'firstName', + ], + 1 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Last Name', + ], + 'alias' => 'lastName', + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Contacts', + ], + ], + ], + 'orderBy' => [ + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Last Name', + ], + 'sort' => 327, + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectBracketsTableSpaces(): void + { + $source = "SELECT * FROM [My Table]"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'My Table', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/unit/Phql/Select/CaseTest.php b/tests/unit/Phql/Select/CaseTest.php new file mode 100644 index 0000000..a4efbbe --- /dev/null +++ b/tests/unit/Phql/Select/CaseTest.php @@ -0,0 +1,228 @@ + + * + * For the full copyright and license information, please view the LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Phql\Tests\Unit\Phql\Select; + +use Phalcon\Phql\Parser; +use Phalcon\Phql\Scanner\Opcode; +use Phalcon\Phql\Tests\AbstractUnitTestCase; + +final class CaseTest extends AbstractUnitTestCase +{ + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-10 + */ + public function testMvcModelQueryPhqlSelectCaseSimple(): void + { + $source = "SELECT CASE inv_status_flag " + . "WHEN 0 THEN 'pending' " + . "WHEN 1 THEN 'paid' " + . "ELSE 'unknown' END " + . "FROM Invoices"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::CASE->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_status_flag', + ], + 'right' => [ + 0 => [ + 'type' => Opcode::WHEN->value, + 'left' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '0', + ], + 'right' => [ + 'type' => Opcode::STRING->value, + 'value' => 'pending', + ], + ], + 1 => [ + 'type' => Opcode::WHEN->value, + 'left' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '1', + ], + 'right' => [ + 'type' => Opcode::STRING->value, + 'value' => 'paid', + ], + ], + 2 => [ + 'type' => Opcode::ELSE->value, + 'left' => [ + 'type' => Opcode::STRING->value, + 'value' => 'unknown', + ], + ], + ], + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-10 + */ + public function testMvcModelQueryPhqlSelectCaseSimpleAlias(): void + { + $source = "SELECT CASE inv_status_flag " + . "WHEN 0 THEN 'pending' " + . "WHEN 1 THEN 'paid' " + . "ELSE 'unknown' END AS status " + . "FROM Invoices"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::CASE->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_status_flag', + ], + 'right' => [ + 0 => [ + 'type' => Opcode::WHEN->value, + 'left' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '0', + ], + 'right' => [ + 'type' => Opcode::STRING->value, + 'value' => 'pending', + ], + ], + 1 => [ + 'type' => Opcode::WHEN->value, + 'left' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '1', + ], + 'right' => [ + 'type' => Opcode::STRING->value, + 'value' => 'paid', + ], + ], + 2 => [ + 'type' => Opcode::ELSE->value, + 'left' => [ + 'type' => Opcode::STRING->value, + 'value' => 'unknown', + ], + ], + ], + ], + 'alias' => 'status', + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-10 + */ + public function testMvcModelQueryPhqlSelectCaseSimpleInWhere(): void + { + $source = "SELECT * FROM Invoices " + . "WHERE CASE inv_status_flag WHEN 1 THEN 1 ELSE 0 END = 1"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'where' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::CASE->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_status_flag', + ], + 'right' => [ + 0 => [ + 'type' => Opcode::WHEN->value, + 'left' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '1', + ], + 'right' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '1', + ], + ], + 1 => [ + 'type' => Opcode::ELSE->value, + 'left' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '0', + ], + ], + ], + ], + 'right' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '1', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/unit/Phql/Select/CastConvertTest.php b/tests/unit/Phql/Select/CastConvertTest.php new file mode 100644 index 0000000..80deede --- /dev/null +++ b/tests/unit/Phql/Select/CastConvertTest.php @@ -0,0 +1,141 @@ + + * + * For the full copyright and license information, please view the LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Phql\Tests\Unit\Phql\Select; + +use Phalcon\Phql\Parser; +use Phalcon\Phql\Scanner\Opcode; +use Phalcon\Phql\Tests\AbstractUnitTestCase; + +final class CastConvertTest extends AbstractUnitTestCase +{ + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectCastInt(): void + { + $source = "SELECT CAST(inv_total AS INTEGER) FROM Invoices"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::CAST->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_total', + ], + 'right' => [ + 'type' => Opcode::RAW_QUALIFIED->value, + 'name' => 'INTEGER', + ], + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectCastVarchar(): void + { + $source = "SELECT CAST(inv_id AS VARCHAR) FROM Invoices"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::CAST->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_id', + ], + 'right' => [ + 'type' => Opcode::RAW_QUALIFIED->value, + 'name' => 'VARCHAR', + ], + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectConvertUtf8(): void + { + $source = "SELECT CONVERT(inv_title USING utf8) FROM Invoices"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::CONVERT->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_title', + ], + 'right' => [ + 'type' => Opcode::RAW_QUALIFIED->value, + 'name' => 'utf8', + ], + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/unit/Phql/Select/ColumnAliasesTest.php b/tests/unit/Phql/Select/ColumnAliasesTest.php new file mode 100644 index 0000000..4348f13 --- /dev/null +++ b/tests/unit/Phql/Select/ColumnAliasesTest.php @@ -0,0 +1,142 @@ + + * + * For the full copyright and license information, please view the LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Phql\Tests\Unit\Phql\Select; + +use Phalcon\Phql\Parser; +use Phalcon\Phql\Scanner\Opcode; +use Phalcon\Phql\Tests\AbstractUnitTestCase; + +final class ColumnAliasesTest extends AbstractUnitTestCase +{ + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectAliasInt(): void + { + $source = "SELECT inv_id AS id FROM Invoices"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_id', + ], + 'alias' => 'id', + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectAliasStringFloat(): void + { + $source = "SELECT inv_title AS title, inv_total AS total FROM Invoices"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_title', + ], + 'alias' => 'title', + ], + 1 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_total', + ], + 'alias' => 'total', + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectAliasTableAlias(): void + { + $source = "SELECT i.inv_id AS id, i.inv_title title FROM Invoices AS i"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'i', + 'name' => 'inv_id', + ], + 'alias' => 'id', + ], + 1 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'i', + 'name' => 'inv_title', + ], + 'alias' => 'title', + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + 'alias' => 'i', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/unit/Phql/Select/ComplexTest.php b/tests/unit/Phql/Select/ComplexTest.php new file mode 100644 index 0000000..2fe62d5 --- /dev/null +++ b/tests/unit/Phql/Select/ComplexTest.php @@ -0,0 +1,306 @@ + + * + * For the full copyright and license information, please view the LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Phql\Tests\Unit\Phql\Select; + +use Phalcon\Phql\Parser; +use Phalcon\Phql\Scanner\Opcode; +use Phalcon\Phql\Tests\AbstractUnitTestCase; + +final class ComplexTest extends AbstractUnitTestCase +{ + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectAllWhereAndInAndBetweenOrderByLimitOffset(): void + { + $source = "SELECT * " + . "FROM Invoices " + . "WHERE inv_cst_id = :cstId: " + . "AND inv_status_flag IN (0, 1) " + . "AND inv_total BETWEEN :min: AND :max: " + . "ORDER BY inv_created_at DESC " + . "LIMIT :limit: " + . "OFFSET :offset:"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'where' => [ + 'type' => Opcode::BETWEEN->value, + 'left' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_cst_id', + ], + 'right' => [ + 'type' => Opcode::AND->value, + 'left' => [ + 'type' => Opcode::AND->value, + 'left' => [ + 'type' => Opcode::SPLACEHOLDER->value, + 'value' => 'cstId', + ], + 'right' => [ + 'type' => Opcode::IN->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_status_flag', + ], + 'right' => [ + 0 => [ + 'type' => Opcode::INTEGER->value, + 'value' => '0', + ], + 1 => [ + 'type' => Opcode::INTEGER->value, + 'value' => '1', + ], + ], + ], + ], + 'right' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_total', + ], + ], + ], + 'right' => [ + 'type' => Opcode::AND->value, + 'left' => [ + 'type' => Opcode::SPLACEHOLDER->value, + 'value' => 'min', + ], + 'right' => [ + 'type' => Opcode::SPLACEHOLDER->value, + 'value' => 'max', + ], + ], + ], + 'orderBy' => [ + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_created_at', + ], + 'sort' => 328, + ], + 'limit' => [ + 'number' => [ + 'type' => Opcode::SPLACEHOLDER->value, + 'value' => 'limit', + ], + 'offset' => [ + 'type' => Opcode::SPLACEHOLDER->value, + 'value' => 'offset', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectSumFieldWhereGroupByHavingOrderByLimit(): void + { + $source = "SELECT i.inv_id, i.inv_title, SUM(i.inv_total) AS total " + . "FROM Invoices AS i " + . "WHERE i.inv_status_flag = 1 " + . "GROUP BY i.inv_cst_id " + . "HAVING SUM(i.inv_total) > 500 " + . "ORDER BY total DESC " + . "LIMIT 10"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'i', + 'name' => 'inv_id', + ], + ], + 1 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'i', + 'name' => 'inv_title', + ], + ], + 2 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::FCALL->value, + 'name' => 'SUM', + 'arguments' => [ + 0 => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'i', + 'name' => 'inv_total', + ], + ], + ], + 'alias' => 'total', + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + 'alias' => 'i', + ], + ], + 'where' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'i', + 'name' => 'inv_status_flag', + ], + 'right' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '1', + ], + ], + 'orderBy' => [ + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'total', + ], + 'sort' => 328, + ], + 'groupBy' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'i', + 'name' => 'inv_cst_id', + ], + 'having' => [ + 'type' => Opcode::GREATER->value, + 'left' => [ + 'type' => Opcode::FCALL->value, + 'name' => 'SUM', + 'arguments' => [ + 0 => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'i', + 'name' => 'inv_total', + ], + ], + ], + 'right' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '500', + ], + ], + 'limit' => [ + 'number' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '10', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectCountFieldWhereGroupByOrderBy(): void + { + $source = "SELECT COUNT(*) AS cnt, inv_status_flag " + . "FROM Invoices " + . "WHERE inv_created_at IS NOT NULL " + . "GROUP BY inv_status_flag " + . "ORDER BY cnt DESC"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::FCALL->value, + 'name' => 'COUNT', + 'arguments' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + ], + 'alias' => 'cnt', + ], + 1 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_status_flag', + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'where' => [ + 'type' => Opcode::ISNOTNULL->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_created_at', + ], + ], + 'orderBy' => [ + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'cnt', + ], + 'sort' => 328, + ], + 'groupBy' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_status_flag', + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/unit/Phql/Select/DistinctTest.php b/tests/unit/Phql/Select/DistinctTest.php new file mode 100644 index 0000000..f9e643d --- /dev/null +++ b/tests/unit/Phql/Select/DistinctTest.php @@ -0,0 +1,133 @@ + + * + * For the full copyright and license information, please view the LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Phql\Tests\Unit\Phql\Select; + +use Phalcon\Phql\Parser; +use Phalcon\Phql\Scanner\Opcode; +use Phalcon\Phql\Tests\AbstractUnitTestCase; + +final class DistinctTest extends AbstractUnitTestCase +{ + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectAll(): void + { + $source = "SELECT ALL inv_status_flag " + . "FROM Invoices"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'distinct' => 0, + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_status_flag', + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectDistinct(): void + { + $source = "SELECT DISTINCT inv_status_flag " + . "FROM Invoices"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'distinct' => 1, + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_status_flag', + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectDistinctInt(): void + { + $source = "SELECT DISTINCT inv_cst_id, inv_status_flag " + . "FROM Invoices"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'distinct' => 1, + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_cst_id', + ], + ], + 1 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_status_flag', + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/unit/Phql/Select/ForUpdateTest.php b/tests/unit/Phql/Select/ForUpdateTest.php new file mode 100644 index 0000000..5415e3e --- /dev/null +++ b/tests/unit/Phql/Select/ForUpdateTest.php @@ -0,0 +1,97 @@ + + * + * For the full copyright and license information, please view the LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Phql\Tests\Unit\Phql\Select; + +use Phalcon\Phql\Parser; +use Phalcon\Phql\Scanner\Opcode; +use Phalcon\Phql\Tests\AbstractUnitTestCase; + +final class ForUpdateTest extends AbstractUnitTestCase +{ + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectForUpdate(): void + { + $source = "SELECT * " + . "FROM Invoices FOR UPDATE"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'forUpdate' => true, + + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectForUpdateWhere(): void + { + $source = "SELECT * " + . "FROM Invoices " + . "WHERE inv_id = 1 FOR UPDATE"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'where' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_id', + ], + 'right' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '1', + ], + ], + 'forUpdate' => true, + + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/unit/Phql/Select/FromTest.php b/tests/unit/Phql/Select/FromTest.php new file mode 100644 index 0000000..c8d7636 --- /dev/null +++ b/tests/unit/Phql/Select/FromTest.php @@ -0,0 +1,176 @@ + + * + * For the full copyright and license information, please view the LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Phql\Tests\Unit\Phql\Select; + +use Phalcon\Phql\Parser; +use Phalcon\Phql\Scanner\Opcode; +use Phalcon\Phql\Tests\AbstractUnitTestCase; + +final class FromTest extends AbstractUnitTestCase +{ + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectFromAliases(): void + { + $source = "SELECT i.inv_id, c.name " + . "FROM Invoices AS i, Customers AS c " + . "WHERE i.inv_cst_id = c.id"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'i', + 'name' => 'inv_id', + ], + ], + 1 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'c', + 'name' => 'name', + ], + ], + ], + 'tables' => [ + 0 => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + 'alias' => 'i', + ], + 1 => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Customers', + ], + 'alias' => 'c', + ], + ], + ], + 'where' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'i', + 'name' => 'inv_cst_id', + ], + 'right' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'c', + 'name' => 'id', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-10 + */ + public function testMvcModelQueryPhqlSelectFromMultipleTables(): void + { + $source = "SELECT * FROM Invoices, Customers"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 0 => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + 1 => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Customers', + ], + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-10 + */ + public function testMvcModelQueryPhqlSelectFromMultipleTablesWhere(): void + { + $source = "SELECT * FROM Invoices, Customers " + . "WHERE Invoices.inv_cst_id = Customers.id"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 0 => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + 1 => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Customers', + ], + ], + ], + ], + 'where' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'Invoices', + 'name' => 'inv_cst_id', + ], + 'right' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'Customers', + 'name' => 'id', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/unit/Phql/Select/GroupByTest.php b/tests/unit/Phql/Select/GroupByTest.php new file mode 100644 index 0000000..428a2be --- /dev/null +++ b/tests/unit/Phql/Select/GroupByTest.php @@ -0,0 +1,188 @@ + + * + * For the full copyright and license information, please view the LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Phql\Tests\Unit\Phql\Select; + +use Phalcon\Phql\Parser; +use Phalcon\Phql\Scanner\Opcode; +use Phalcon\Phql\Tests\AbstractUnitTestCase; + +final class GroupByTest extends AbstractUnitTestCase +{ + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectGroupBy(): void + { + $source = "SELECT inv_status_flag, COUNT(*) " + . "FROM Invoices " + . "GROUP BY inv_status_flag"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_status_flag', + ], + ], + 1 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::FCALL->value, + 'name' => 'COUNT', + 'arguments' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'groupBy' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_status_flag', + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectGroupByCountField(): void + { + $source = "SELECT inv_cst_id, inv_status_flag, COUNT(*) " + . "FROM Invoices " . + "GROUP BY inv_cst_id, inv_status_flag"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_cst_id', + ], + ], + 1 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_status_flag', + ], + ], + 2 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::FCALL->value, + 'name' => 'COUNT', + 'arguments' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'groupBy' => [ + 0 => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_cst_id', + ], + 1 => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_status_flag', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectGroupBySumField(): void + { + $source = "SELECT inv_cst_id, SUM(inv_total) " + . "FROM Invoices " + . "GROUP BY inv_cst_id"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_cst_id', + ], + ], + 1 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::FCALL->value, + 'name' => 'SUM', + 'arguments' => [ + 0 => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_total', + ], + ], + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'groupBy' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_cst_id', + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/unit/Phql/Select/HavingTest.php b/tests/unit/Phql/Select/HavingTest.php new file mode 100644 index 0000000..d8b1367 --- /dev/null +++ b/tests/unit/Phql/Select/HavingTest.php @@ -0,0 +1,225 @@ + + * + * For the full copyright and license information, please view the LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Phql\Tests\Unit\Phql\Select; + +use Phalcon\Phql\Parser; +use Phalcon\Phql\Scanner\Opcode; +use Phalcon\Phql\Tests\AbstractUnitTestCase; + +final class HavingTest extends AbstractUnitTestCase +{ + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectHavingCountAll(): void + { + $source = "SELECT inv_status_flag, COUNT(*) AS cnt " + . "FROM Invoices " + . "GROUP BY inv_status_flag " + . "HAVING COUNT(*) > 5"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_status_flag', + ], + ], + 1 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::FCALL->value, + 'name' => 'COUNT', + 'arguments' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + ], + 'alias' => 'cnt', + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'groupBy' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_status_flag', + ], + 'having' => [ + 'type' => Opcode::GREATER->value, + 'left' => [ + 'type' => Opcode::FCALL->value, + 'name' => 'COUNT', + 'arguments' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + ], + 'right' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '5', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectHavingCountField(): void + { + $source = "SELECT inv_cst_id, COUNT(*) AS cnt " + . "FROM Invoices " + . "GROUP BY inv_cst_id " + . "HAVING cnt > 10"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_cst_id', + ], + ], + 1 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::FCALL->value, + 'name' => 'COUNT', + 'arguments' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + ], + 'alias' => 'cnt', + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'groupBy' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_cst_id', + ], + 'having' => [ + 'type' => Opcode::GREATER->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'cnt', + ], + 'right' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '10', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectHavingSum(): void + { + $source = "SELECT inv_cst_id, SUM(inv_total) AS total " + . "FROM Invoices " + . "GROUP BY inv_cst_id " + . "HAVING SUM(inv_total) > 1000"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_cst_id', + ], + ], + 1 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::FCALL->value, + 'name' => 'SUM', + 'arguments' => [ + 0 => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_total', + ], + ], + ], + 'alias' => 'total', + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'groupBy' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_cst_id', + ], + 'having' => [ + 'type' => Opcode::GREATER->value, + 'left' => [ + 'type' => Opcode::FCALL->value, + 'name' => 'SUM', + 'arguments' => [ + 0 => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_total', + ], + ], + ], + 'right' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '1000', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/unit/Phql/Select/JoinTest.php b/tests/unit/Phql/Select/JoinTest.php new file mode 100644 index 0000000..247398f --- /dev/null +++ b/tests/unit/Phql/Select/JoinTest.php @@ -0,0 +1,820 @@ + + * + * For the full copyright and license information, please view the LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Phql\Tests\Unit\Phql\Select; + +use Phalcon\Phql\Parser; +use Phalcon\Phql\Scanner\Opcode; +use Phalcon\Phql\Tests\AbstractUnitTestCase; + +final class JoinTest extends AbstractUnitTestCase +{ + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectCrossJoin(): void + { + $source = "SELECT i.inv_id, c.name " + . "FROM Invoices AS i " + . "CROSS JOIN Customers AS c"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'i', + 'name' => 'inv_id', + ], + ], + 1 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'c', + 'name' => 'name', + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + 'alias' => 'i', + ], + 'joins' => [ + 'type' => Opcode::CROSSJOIN->value, + 'qualified' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Customers', + ], + 'alias' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'c', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectFullJoin(): void + { + $source = "SELECT i.inv_id, c.name " + . "FROM Invoices AS i " + . "FULL JOIN Customers AS c ON i.inv_cst_id = c.id"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'i', + 'name' => 'inv_id', + ], + ], + 1 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'c', + 'name' => 'name', + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + 'alias' => 'i', + ], + 'joins' => [ + 'type' => Opcode::FULLJOIN->value, + 'qualified' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Customers', + ], + 'alias' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'c', + ], + 'conditions' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'i', + 'name' => 'inv_cst_id', + ], + 'right' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'c', + 'name' => 'id', + ], + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectFullOuterJoin(): void + { + $source = "SELECT i.inv_id, c.name " + . "FROM Invoices AS i " + . "FULL OUTER JOIN Customers AS c ON i.inv_cst_id = c.id"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'i', + 'name' => 'inv_id', + ], + ], + 1 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'c', + 'name' => 'name', + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + 'alias' => 'i', + ], + 'joins' => [ + 'type' => Opcode::FULLJOIN->value, + 'qualified' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Customers', + ], + 'alias' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'c', + ], + 'conditions' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'i', + 'name' => 'inv_cst_id', + ], + 'right' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'c', + 'name' => 'id', + ], + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectInnerJoin(): void + { + $source = "SELECT i.inv_id, c.name " + . "FROM Invoices AS i " + . "INNER JOIN Customers AS c ON i.inv_cst_id = c.id"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'i', + 'name' => 'inv_id', + ], + ], + 1 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'c', + 'name' => 'name', + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + 'alias' => 'i', + ], + 'joins' => [ + 'type' => Opcode::INNERJOIN->value, + 'qualified' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Customers', + ], + 'alias' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'c', + ], + 'conditions' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'i', + 'name' => 'inv_cst_id', + ], + 'right' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'c', + 'name' => 'id', + ], + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-10 + */ + public function testMvcModelQueryPhqlSelectInnerJoinOnComplexCondition(): void + { + $source = "SELECT i.inv_id, c.name FROM Invoices AS i " + . "INNER JOIN Customers AS c " + . "ON (i.inv_cst_id = c.id AND i.inv_status_flag = 1)"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'i', + 'name' => 'inv_id', + ], + ], + 1 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'c', + 'name' => 'name', + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + 'alias' => 'i', + ], + 'joins' => [ + 'type' => Opcode::INNERJOIN->value, + 'qualified' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Customers', + ], + 'alias' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'c', + ], + 'conditions' => [ + 'type' => Opcode::ENCLOSED->value, + 'left' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'i', + 'name' => 'inv_cst_id', + ], + 'right' => [ + 'type' => Opcode::AND->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'c', + 'name' => 'id', + ], + 'right' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'i', + 'name' => 'inv_status_flag', + ], + ], + ], + 'right' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '1', + ], + ], + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectInnerLeftJoin(): void + { + $source = "SELECT i.inv_id, c.name, p.description " + . "FROM Invoices AS i " + . "INNER JOIN Customers AS c ON i.inv_cst_id = c.id " + . "LEFT JOIN Products AS p ON i.inv_id = p.inv_id"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'i', + 'name' => 'inv_id', + ], + ], + 1 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'c', + 'name' => 'name', + ], + ], + 2 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'p', + 'name' => 'description', + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + 'alias' => 'i', + ], + 'joins' => [ + 0 => [ + 'type' => Opcode::INNERJOIN->value, + 'qualified' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Customers', + ], + 'alias' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'c', + ], + 'conditions' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'i', + 'name' => 'inv_cst_id', + ], + 'right' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'c', + 'name' => 'id', + ], + ], + ], + 1 => [ + 'type' => Opcode::LEFTJOIN->value, + 'qualified' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Products', + ], + 'alias' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'p', + ], + 'conditions' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'i', + 'name' => 'inv_id', + ], + 'right' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'p', + 'name' => 'inv_id', + ], + ], + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectJoin(): void + { + $source = "SELECT i.inv_id, c.name " + . "FROM Invoices AS i " + . "JOIN Customers AS c ON i.inv_cst_id = c.id"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'i', + 'name' => 'inv_id', + ], + ], + 1 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'c', + 'name' => 'name', + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + 'alias' => 'i', + ], + 'joins' => [ + 'type' => Opcode::INNERJOIN->value, + 'qualified' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Customers', + ], + 'alias' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'c', + ], + 'conditions' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'i', + 'name' => 'inv_cst_id', + ], + 'right' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'c', + 'name' => 'id', + ], + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectLeftJoin(): void + { + $source = "SELECT i.inv_id, c.name " + . "FROM Invoices AS i " + . "LEFT JOIN Customers AS c ON i.inv_cst_id = c.id"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'i', + 'name' => 'inv_id', + ], + ], + 1 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'c', + 'name' => 'name', + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + 'alias' => 'i', + ], + 'joins' => [ + 'type' => Opcode::LEFTJOIN->value, + 'qualified' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Customers', + ], + 'alias' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'c', + ], + 'conditions' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'i', + 'name' => 'inv_cst_id', + ], + 'right' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'c', + 'name' => 'id', + ], + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectLeftOuterJoin(): void + { + $source = "SELECT i.inv_id, c.name " + . "FROM Invoices AS i " + . "LEFT OUTER JOIN Customers AS c ON i.inv_cst_id = c.id"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'i', + 'name' => 'inv_id', + ], + ], + 1 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'c', + 'name' => 'name', + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + 'alias' => 'i', + ], + 'joins' => [ + 'type' => Opcode::LEFTJOIN->value, + 'qualified' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Customers', + ], + 'alias' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'c', + ], + 'conditions' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'i', + 'name' => 'inv_cst_id', + ], + 'right' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'c', + 'name' => 'id', + ], + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectRightJoin(): void + { + $source = "SELECT i.inv_id, c.name " + . "FROM Invoices AS i " + . "RIGHT JOIN Customers AS c ON i.inv_cst_id = c.id"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'i', + 'name' => 'inv_id', + ], + ], + 1 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'c', + 'name' => 'name', + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + 'alias' => 'i', + ], + 'joins' => [ + 'type' => Opcode::RIGHTJOIN->value, + 'qualified' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Customers', + ], + 'alias' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'c', + ], + 'conditions' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'i', + 'name' => 'inv_cst_id', + ], + 'right' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'c', + 'name' => 'id', + ], + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectRightOuterJoin(): void + { + $source = "SELECT i.inv_id, c.name " + . "FROM Invoices AS i " + . "RIGHT OUTER JOIN Customers AS c ON i.inv_cst_id = c.id"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'i', + 'name' => 'inv_id', + ], + ], + 1 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'c', + 'name' => 'name', + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + 'alias' => 'i', + ], + 'joins' => [ + 'type' => Opcode::RIGHTJOIN->value, + 'qualified' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Customers', + ], + 'alias' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'c', + ], + 'conditions' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'i', + 'name' => 'inv_cst_id', + ], + 'right' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'c', + 'name' => 'id', + ], + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/unit/Phql/Select/KeywordCollisionsNameTest.php b/tests/unit/Phql/Select/KeywordCollisionsNameTest.php new file mode 100644 index 0000000..837f73c --- /dev/null +++ b/tests/unit/Phql/Select/KeywordCollisionsNameTest.php @@ -0,0 +1,420 @@ + + * + * For the full copyright and license information, please view the LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Phql\Tests\Unit\Phql\Select; + +use Phalcon\Phql\Parser; +use Phalcon\Phql\Scanner\Opcode; +use Phalcon\Phql\Tests\AbstractUnitTestCase; + +final class KeywordCollisionsNameTest extends AbstractUnitTestCase +{ + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectCollisionNames(): void + { + $source = "SELECT [Order], [Group] FROM Items"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Order', + ], + ], + 1 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Group', + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Items', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectCollisionNamesDeleteUpdate(): void + { + $source = "SELECT [Delete], [Update] FROM AuditLog"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Delete', + ], + ], + 1 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Update', + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'AuditLog', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectCollisionNamesOrderBy(): void + { + $source = "SELECT [Order], [Desc] FROM Items ORDER BY [Order] ASC"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Order', + ], + ], + 1 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Desc', + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Items', + ], + ], + ], + 'orderBy' => [ + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Order', + ], + 'sort' => 327, + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectCollisionNamesValuesSet(): void + { + $source = "SELECT [Values], [Set] FROM Config"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Values', + ], + ], + 1 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Set', + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Config', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectCollisionNamesWhere(): void + { + $source = "SELECT * FROM Items WHERE [Select] = 1"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Items', + ], + ], + ], + 'where' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Select', + ], + 'right' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '1', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectCollisionNamesWhereInt(): void + { + $source = "SELECT * FROM Items WHERE [In] = 1"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Items', + ], + ], + ], + 'where' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'In', + ], + 'right' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '1', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectCollisionNamesWhereIsNotNull(): void + { + $source = "SELECT * FROM Items WHERE [From] IS NOT NULL"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Items', + ], + ], + ], + 'where' => [ + 'type' => Opcode::ISNOTNULL->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'From', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectCollisionNamesWhereIsNull(): void + { + $source = "SELECT * FROM Items WHERE [Null] IS NULL"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Items', + ], + ], + ], + 'where' => [ + 'type' => Opcode::ISNULL->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Null', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectCollisionNamesWhereString(): void + { + $source = "SELECT * FROM Items WHERE [Where] = 'test'"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Items', + ], + ], + ], + 'where' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Where', + ], + 'right' => [ + 'type' => Opcode::STRING->value, + 'value' => 'test', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectCollisionNamesWhereZero(): void + { + $source = "SELECT * FROM Items WHERE [Not] = 0"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Items', + ], + ], + ], + 'where' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Not', + ], + 'right' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '0', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/unit/Phql/Select/KeywordPrefixNameTest.php b/tests/unit/Phql/Select/KeywordPrefixNameTest.php new file mode 100644 index 0000000..75d6c44 --- /dev/null +++ b/tests/unit/Phql/Select/KeywordPrefixNameTest.php @@ -0,0 +1,295 @@ + + * + * For the full copyright and license information, please view the LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Phql\Tests\Unit\Phql\Select; + +use Phalcon\Phql\Parser; +use Phalcon\Phql\Scanner\Opcode; +use Phalcon\Phql\Tests\AbstractUnitTestCase; + +final class KeywordPrefixNameTest extends AbstractUnitTestCase +{ + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectPrefixGroups(): void + { + $source = "SELECT Groups FROM Settings"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Groups', + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Settings', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectPrefixGroupsBrackets(): void + { + $source = "SELECT [Groups] FROM Settings"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Groups', + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Settings', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectPrefixNotes(): void + { + $source = "SELECT Notes FROM Contacts"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'es', + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Contacts', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectPrefixNotesBrackets(): void + { + $source = "SELECT [Notes] FROM Contacts"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Notes', + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Contacts', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectPrefixOrders(): void + { + $source = "SELECT Orders FROM Customers"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Orders', + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Customers', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectPrefixOrdersBrackets(): void + { + $source = "SELECT [Orders] FROM Customers"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Orders', + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Customers', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectPrefixWhereNotesIsNotNull(): void + { + $source = "SELECT * FROM Contacts WHERE [Notes] IS NOT NULL"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Contacts', + ], + ], + ], + 'where' => [ + 'type' => Opcode::ISNOTNULL->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Notes', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectPrefixWhereNotesLike(): void + { + $source = "SELECT * FROM Contacts WHERE [Notes] LIKE '%important%'"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Contacts', + ], + ], + ], + 'where' => [ + 'type' => Opcode::LIKE->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Notes', + ], + 'right' => [ + 'type' => Opcode::STRING->value, + 'value' => '%important%', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/unit/Phql/Select/LikeTest.php b/tests/unit/Phql/Select/LikeTest.php new file mode 100644 index 0000000..d00bf78 --- /dev/null +++ b/tests/unit/Phql/Select/LikeTest.php @@ -0,0 +1,181 @@ + + * + * For the full copyright and license information, please view the LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Phql\Tests\Unit\Phql\Select; + +use Phalcon\Phql\Parser; +use Phalcon\Phql\Scanner\Opcode; +use Phalcon\Phql\Tests\AbstractUnitTestCase; + +final class LikeTest extends AbstractUnitTestCase +{ + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectIlike(): void + { + $source = "SELECT * FROM Invoices WHERE inv_title ILIKE '%invoice%'"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'where' => [ + 'type' => Opcode::ILIKE->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_title', + ], + 'right' => [ + 'type' => Opcode::STRING->value, + 'value' => '%invoice%', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectLike(): void + { + $source = "SELECT * FROM Invoices WHERE inv_title LIKE '%test%'"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'where' => [ + 'type' => Opcode::LIKE->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_title', + ], + 'right' => [ + 'type' => Opcode::STRING->value, + 'value' => '%test%', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectNotIlike(): void + { + $source = "SELECT * FROM Invoices WHERE inv_title NOT ILIKE '%draft%'"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'where' => [ + 'type' => Opcode::NILIKE->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_title', + ], + 'right' => [ + 'type' => Opcode::STRING->value, + 'value' => '%draft%', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectNotLike(): void + { + $source = "SELECT * FROM Invoices WHERE inv_title NOT LIKE '%draft%'"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'where' => [ + 'type' => Opcode::NLIKE->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_title', + ], + 'right' => [ + 'type' => Opcode::STRING->value, + 'value' => '%draft%', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/unit/Phql/Select/LimitTest.php b/tests/unit/Phql/Select/LimitTest.php new file mode 100644 index 0000000..4ecf95c --- /dev/null +++ b/tests/unit/Phql/Select/LimitTest.php @@ -0,0 +1,377 @@ + + * + * For the full copyright and license information, please view the LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Phql\Tests\Unit\Phql\Select; + +use Phalcon\Phql\Parser; +use Phalcon\Phql\Scanner\Opcode; +use Phalcon\Phql\Tests\AbstractUnitTestCase; + +final class LimitTest extends AbstractUnitTestCase +{ + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectLimit(): void + { + $source = "SELECT * FROM Invoices LIMIT 10"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'limit' => [ + 'number' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '10', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @issue 1 + * @since 2026-04-11 + */ + public function testMvcModelQueryPhqlSelectLimitAliasedDomainAll(): void + { + $source = "SELECT r.* FROM Robots r LIMIT 10"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::DOMAINALL->value, + 'column' => 'r', + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Robots', + ], + 'alias' => 'r', + ], + ], + 'limit' => [ + 'number' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '10', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectLimitBoth(): void + { + $source = "SELECT * FROM Invoices LIMIT 20, 10"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'limit' => [ + 'number' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '10', + ], + 'offset' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '20', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-10 + */ + public function testMvcModelQueryPhqlSelectLimitBracePlaceholder(): void + { + $source = "SELECT * FROM Invoices LIMIT {limit}"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'limit' => [ + 'number' => [ + 'type' => Opcode::BPLACEHOLDER->value, + 'value' => 'limit', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-10 + */ + public function testMvcModelQueryPhqlSelectLimitBracePlaceholderOffset(): void + { + $source = "SELECT * FROM Invoices LIMIT {limit} OFFSET {offset}"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'limit' => [ + 'number' => [ + 'type' => Opcode::BPLACEHOLDER->value, + 'value' => 'limit', + ], + 'offset' => [ + 'type' => Opcode::BPLACEHOLDER->value, + 'value' => 'offset', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectLimitOffset(): void + { + $source = "SELECT * FROM Invoices LIMIT 10 OFFSET 20"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'limit' => [ + 'number' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '10', + ], + 'offset' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '20', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectLimitOffsetPlaceholders(): void + { + $source = "SELECT * FROM Invoices LIMIT :limit: OFFSET :offset:"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'limit' => [ + 'number' => [ + 'type' => Opcode::SPLACEHOLDER->value, + 'value' => 'limit', + ], + 'offset' => [ + 'type' => Opcode::SPLACEHOLDER->value, + 'value' => 'offset', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectLimitPlaceholderNum(): void + { + $source = "SELECT * FROM Invoices LIMIT ?0"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'limit' => [ + 'number' => [ + 'type' => Opcode::NPLACEHOLDER->value, + 'value' => '?0', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectWhereOrderLimit(): void + { + $source = "SELECT * " + . "FROM Invoices " + . "WHERE inv_status_flag = 1 " + . "ORDER BY inv_id DESC " + . "LIMIT 5"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'where' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_status_flag', + ], + 'right' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '1', + ], + ], + 'orderBy' => [ + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_id', + ], + 'sort' => 328, + ], + 'limit' => [ + 'number' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '5', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/unit/Phql/Select/LiteralsTest.php b/tests/unit/Phql/Select/LiteralsTest.php new file mode 100644 index 0000000..3a968fe --- /dev/null +++ b/tests/unit/Phql/Select/LiteralsTest.php @@ -0,0 +1,291 @@ + + * + * For the full copyright and license information, please view the LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Phql\Tests\Unit\Phql\Select; + +use Phalcon\Phql\Parser; +use Phalcon\Phql\Scanner\Opcode; +use Phalcon\Phql\Tests\AbstractUnitTestCase; + +final class LiteralsTest extends AbstractUnitTestCase +{ + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectLiteralsFalse(): void + { + $source = "SELECT * FROM Invoices WHERE inv_status_flag = FALSE"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'where' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_status_flag', + ], + 'right' => [ + 'type' => Opcode::FALSE->value, + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectLiteralsFloat(): void + { + $source = "SELECT * FROM Invoices WHERE inv_total = 100.5"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'where' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_total', + ], + 'right' => [ + 'type' => Opcode::DOUBLE->value, + 'value' => '100.5', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectLiteralsFloatNoLeft(): void + { + $source = "SELECT * FROM Invoices WHERE inv_total = .5"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'where' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_total', + ], + 'right' => [ + 'type' => Opcode::DOUBLE->value, + 'value' => '.5', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectLiteralsFloatNoRight(): void + { + $source = "SELECT * FROM Invoices WHERE inv_total = 100."; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'where' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_total', + ], + 'right' => [ + 'type' => Opcode::DOUBLE->value, + 'value' => '100.', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectLiteralsNull(): void + { + $source = "SELECT NULL FROM Invoices"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::NULL->value, + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectLiteralsTrue(): void + { + $source = "SELECT * FROM Invoices WHERE inv_status_flag = TRUE"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'where' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_status_flag', + ], + 'right' => [ + 'type' => Opcode::TRUE->value, + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectLiteralsWhereHex(): void + { + $source = "SELECT * FROM Invoices WHERE inv_id = 0xFF"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'where' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_id', + ], + 'right' => [ + 'type' => Opcode::HINTEGER->value, + 'value' => '0xFF', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/unit/Phql/Select/MatchAgainstTest.php b/tests/unit/Phql/Select/MatchAgainstTest.php new file mode 100644 index 0000000..360b2c4 --- /dev/null +++ b/tests/unit/Phql/Select/MatchAgainstTest.php @@ -0,0 +1,110 @@ + + * + * For the full copyright and license information, please view the LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Phql\Tests\Unit\Phql\Select; + +use Phalcon\Phql\Parser; +use Phalcon\Phql\Scanner\Opcode; +use Phalcon\Phql\Tests\AbstractUnitTestCase; + +final class MatchAgainstTest extends AbstractUnitTestCase +{ + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectAgainst(): void + { + $source = "SELECT * FROM Invoices WHERE inv_title AGAINST 'invoice'"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'where' => [ + 'type' => Opcode::AGAINST->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_title', + ], + 'right' => [ + 'type' => Opcode::STRING->value, + 'value' => 'invoice', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectMatchAgainst(): void + { + $source = "SELECT * FROM Invoices WHERE MATCH(inv_title) AGAINST ('invoice')"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'where' => [ + 'type' => Opcode::AGAINST->value, + 'left' => [ + 'type' => Opcode::FCALL->value, + 'name' => 'MATCH', + 'arguments' => [ + 0 => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_title', + ], + ], + ], + 'right' => [ + 'type' => Opcode::ENCLOSED->value, + 'left' => [ + 'type' => Opcode::STRING->value, + 'value' => 'invoice', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/unit/Phql/Select/NullTest.php b/tests/unit/Phql/Select/NullTest.php new file mode 100644 index 0000000..1130d02 --- /dev/null +++ b/tests/unit/Phql/Select/NullTest.php @@ -0,0 +1,93 @@ + + * + * For the full copyright and license information, please view the LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Phql\Tests\Unit\Phql\Select; + +use Phalcon\Phql\Parser; +use Phalcon\Phql\Scanner\Opcode; +use Phalcon\Phql\Tests\AbstractUnitTestCase; + +final class NullTest extends AbstractUnitTestCase +{ + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectWhereIsNotNull(): void + { + $source = "SELECT * FROM Invoices WHERE inv_title IS NOT NULL"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'where' => [ + 'type' => Opcode::ISNOTNULL->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_title', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectWhereIsNull(): void + { + $source = "SELECT * FROM Invoices WHERE inv_title IS NULL"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'where' => [ + 'type' => Opcode::ISNULL->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_title', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/unit/Phql/Select/OperatorsTest.php b/tests/unit/Phql/Select/OperatorsTest.php new file mode 100644 index 0000000..75727de --- /dev/null +++ b/tests/unit/Phql/Select/OperatorsTest.php @@ -0,0 +1,327 @@ + + * + * For the full copyright and license information, please view the LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Phql\Tests\Unit\Phql\Select; + +use Phalcon\Phql\Parser; +use Phalcon\Phql\Scanner\Opcode; +use Phalcon\Phql\Tests\AbstractUnitTestCase; + +final class OperatorsTest extends AbstractUnitTestCase +{ + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectAddition(): void + { + $source = "SELECT inv_total + 10 FROM Invoices"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::ADD->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_total', + ], + 'right' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '10', + ], + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectDivision(): void + { + $source = "SELECT inv_total / 2 FROM Invoices"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::DIV->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_total', + ], + 'right' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '2', + ], + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectModulo(): void + { + $source = "SELECT inv_total % 3 FROM Invoices"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::MOD->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_total', + ], + 'right' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '3', + ], + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectMultiplication(): void + { + $source = "SELECT inv_total * 1.1 FROM Invoices"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::MUL->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_total', + ], + 'right' => [ + 'type' => Opcode::DOUBLE->value, + 'value' => '1.1', + ], + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectMultiplicationAliasAs(): void + { + $source = "SELECT inv_id, inv_total * 1.1 AS total_with_tax FROM Invoices"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_id', + ], + ], + 1 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::MUL->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_total', + ], + 'right' => [ + 'type' => Opcode::DOUBLE->value, + 'value' => '1.1', + ], + ], + 'alias' => 'total_with_tax', + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectSelectAdditionMultiplicationAliasAs(): void + { + $source = "SELECT inv_id, (inv_total + 5) * 2 AS adjusted FROM Invoices"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_id', + ], + ], + 1 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::MUL->value, + 'left' => [ + 'type' => Opcode::ENCLOSED->value, + 'left' => [ + 'type' => Opcode::ADD->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_total', + ], + 'right' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '5', + ], + ], + ], + 'right' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '2', + ], + ], + 'alias' => 'adjusted', + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectSubtraction(): void + { + $source = "SELECT inv_total - 5 FROM Invoices"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::SUB->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_total', + ], + 'right' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '5', + ], + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/unit/Phql/Select/OrderByTest.php b/tests/unit/Phql/Select/OrderByTest.php new file mode 100644 index 0000000..0953261 --- /dev/null +++ b/tests/unit/Phql/Select/OrderByTest.php @@ -0,0 +1,288 @@ + + * + * For the full copyright and license information, please view the LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Phql\Tests\Unit\Phql\Select; + +use Phalcon\Phql\Parser; +use Phalcon\Phql\Scanner\Opcode; +use Phalcon\Phql\Tests\AbstractUnitTestCase; + +final class OrderByTest extends AbstractUnitTestCase +{ + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-10 + */ + public function testMvcModelQueryPhqlSelectOrderByAggregate(): void + { + $source = "SELECT inv_cst_id, COUNT(*) FROM Invoices " + . "GROUP BY inv_cst_id ORDER BY COUNT(*) DESC"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_cst_id', + ], + ], + 1 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::FCALL->value, + 'name' => 'COUNT', + 'arguments' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'orderBy' => [ + 'column' => [ + 'type' => Opcode::FCALL->value, + 'name' => 'COUNT', + 'arguments' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + ], + 'sort' => 328, + ], + 'groupBy' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_cst_id', + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectOrderByDateDescIntAsc(): void + { + $source = "SELECT * FROM Invoices ORDER BY inv_created_at DESC, inv_id ASC"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'orderBy' => [ + 0 => [ + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_created_at', + ], + 'sort' => 328, + ], + 1 => [ + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_id', + ], + 'sort' => 327, + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectOrderByFloatDescStringAscIntAsc(): void + { + $source = "SELECT * FROM Invoices " + . "ORDER BY inv_total DESC, inv_title ASC, inv_id ASC"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'orderBy' => [ + 0 => [ + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_total', + ], + 'sort' => 328, + ], + 1 => [ + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_title', + ], + 'sort' => 327, + ], + 2 => [ + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_id', + ], + 'sort' => 327, + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectOrderByInt(): void + { + $source = "SELECT * FROM Invoices ORDER BY inv_id"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'orderBy' => [ + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_id', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectOrderByIntAsc(): void + { + $source = "SELECT * FROM Invoices ORDER BY inv_id ASC"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'orderBy' => [ + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_id', + ], + 'sort' => 327, + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectOrderByIntDesc(): void + { + $source = "SELECT * FROM Invoices ORDER BY inv_id DESC"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'orderBy' => [ + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_id', + ], + 'sort' => 328, + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/unit/Phql/Select/QualifiedNamesTest.php b/tests/unit/Phql/Select/QualifiedNamesTest.php new file mode 100644 index 0000000..b0deabc --- /dev/null +++ b/tests/unit/Phql/Select/QualifiedNamesTest.php @@ -0,0 +1,125 @@ + + * + * For the full copyright and license information, please view the LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Phql\Tests\Unit\Phql\Select; + +use Phalcon\Phql\Parser; +use Phalcon\Phql\Scanner\Opcode; +use Phalcon\Phql\Tests\AbstractUnitTestCase; + +final class QualifiedNamesTest extends AbstractUnitTestCase +{ + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectModelNamespace(): void + { + $source = "SELECT * FROM App\Models\Invoices"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'App\\Models\\Invoices', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectModelNamespaceAliasAs(): void + { + $source = "SELECT i.inv_id FROM App\Models\Invoices AS i"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'i', + 'name' => 'inv_id', + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'App\\Models\\Invoices', + ], + 'alias' => 'i', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectModelNamespaceWhereInt(): void + { + $source = "SELECT * FROM App\Models\Invoices WHERE inv_id = 1"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'App\\Models\\Invoices', + ], + ], + ], + 'where' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_id', + ], + 'right' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '1', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/unit/Phql/Select/ScalarTest.php b/tests/unit/Phql/Select/ScalarTest.php new file mode 100644 index 0000000..267d2f3 --- /dev/null +++ b/tests/unit/Phql/Select/ScalarTest.php @@ -0,0 +1,521 @@ + + * + * For the full copyright and license information, please view the LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Phql\Tests\Unit\Phql\Select; + +use Phalcon\Phql\Parser; +use Phalcon\Phql\Scanner\Opcode; +use Phalcon\Phql\Tests\AbstractUnitTestCase; + +final class ScalarTest extends AbstractUnitTestCase +{ + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectAbs(): void + { + $source = "SELECT ABS(inv_total) FROM Invoices"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::FCALL->value, + 'name' => 'ABS', + 'arguments' => [ + 0 => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_total', + ], + ], + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectCoalesce(): void + { + $source = "SELECT COALESCE(inv_title, 'N/A') FROM Invoices"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::FCALL->value, + 'name' => 'COALESCE', + 'arguments' => [ + 0 => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_title', + ], + 1 => [ + 'type' => Opcode::STRING->value, + 'value' => 'N/A', + ], + ], + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectConcat(): void + { + $source = "SELECT CONCAT(inv_title, ' - paid') FROM Invoices"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::FCALL->value, + 'name' => 'CONCAT', + 'arguments' => [ + 0 => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_title', + ], + 1 => [ + 'type' => Opcode::STRING->value, + 'value' => ' - paid', + ], + ], + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectIfnull(): void + { + $source = "SELECT IFNULL(inv_title, 'N/A') FROM Invoices"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::FCALL->value, + 'name' => 'IFNULL', + 'arguments' => [ + 0 => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_title', + ], + 1 => [ + 'type' => Opcode::STRING->value, + 'value' => 'N/A', + ], + ], + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectLength(): void + { + $source = "SELECT LENGTH(inv_title) FROM Invoices"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::FCALL->value, + 'name' => 'LENGTH', + 'arguments' => [ + 0 => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_title', + ], + ], + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectLower(): void + { + $source = "SELECT LOWER(inv_title) FROM Invoices"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::FCALL->value, + 'name' => 'LOWER', + 'arguments' => [ + 0 => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_title', + ], + ], + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectMonthCount(): void + { + $source = "SELECT MONTH(inv_created_at), COUNT(*) FROM Invoices GROUP BY MONTH(inv_created_at)"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::FCALL->value, + 'name' => 'MONTH', + 'arguments' => [ + 0 => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_created_at', + ], + ], + ], + ], + 1 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::FCALL->value, + 'name' => 'COUNT', + 'arguments' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'groupBy' => [ + 'type' => Opcode::FCALL->value, + 'name' => 'MONTH', + 'arguments' => [ + 0 => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_created_at', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectNow(): void + { + $source = "SELECT NOW() FROM Invoices"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::FCALL->value, + 'name' => 'NOW', + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectRound(): void + { + $source = "SELECT ROUND(inv_total, 2) FROM Invoices"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::FCALL->value, + 'name' => 'ROUND', + 'arguments' => [ + 0 => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_total', + ], + 1 => [ + 'type' => Opcode::INTEGER->value, + 'value' => '2', + ], + ], + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectTrim(): void + { + $source = "SELECT TRIM(inv_title) FROM Invoices"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::FCALL->value, + 'name' => 'TRIM', + 'arguments' => [ + 0 => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_title', + ], + ], + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectUpper(): void + { + $source = "SELECT UPPER(inv_title) FROM Invoices"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::FCALL->value, + 'name' => 'UPPER', + 'arguments' => [ + 0 => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_title', + ], + ], + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectYear(): void + { + $source = "SELECT YEAR(inv_created_at) FROM Invoices"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::FCALL->value, + 'name' => 'YEAR', + 'arguments' => [ + 0 => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_created_at', + ], + ], + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/unit/Phql/Select/SubqueriesTest.php b/tests/unit/Phql/Select/SubqueriesTest.php new file mode 100644 index 0000000..7724f34 --- /dev/null +++ b/tests/unit/Phql/Select/SubqueriesTest.php @@ -0,0 +1,475 @@ + + * + * For the full copyright and license information, please view the LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Phql\Tests\Unit\Phql\Select; + +use Phalcon\Phql\Parser; +use Phalcon\Phql\Scanner\Opcode; +use Phalcon\Phql\Tests\AbstractUnitTestCase; + +final class SubqueriesTest extends AbstractUnitTestCase +{ + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectFieldSubquery(): void + { + $source = "SELECT i.inv_id, " + . "(SELECT COUNT(*) " + . "FROM Invoices " + . "WHERE inv_cst_id = i.inv_cst_id) AS cst_count " + . "FROM Invoices i"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'i', + 'name' => 'inv_id', + ], + ], + 1 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::SUBQUERY->value, + 'left' => [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::FCALL->value, + 'name' => 'COUNT', + 'arguments' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'where' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_cst_id', + ], + 'right' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'i', + 'name' => 'inv_cst_id', + ], + ], + ], + ], + 'alias' => 'cst_count', + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + 'alias' => 'i', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectWhereEqualsSubquery(): void + { + $source = "SELECT * " + . "FROM Invoices " + . "WHERE inv_total = (SELECT MAX(inv_total) FROM Invoices)"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'where' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_total', + ], + 'right' => [ + 'type' => Opcode::SUBQUERY->value, + 'left' => [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::FCALL->value, + 'name' => 'MAX', + 'arguments' => [ + 0 => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_total', + ], + ], + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectWhereExistsSubquery(): void + { + $source = "SELECT * " + . "FROM Invoices " + . "WHERE EXISTS " + . "(SELECT id FROM Customers WHERE id = Invoices.inv_cst_id)"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'where' => [ + 'type' => Opcode::EXISTS->value, + 'right' => [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'id', + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Customers', + ], + ], + ], + 'where' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'id', + ], + 'right' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'Invoices', + 'name' => 'inv_cst_id', + ], + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-10 + */ + public function testMvcModelQueryPhqlSelectWhereInNestedSubquery(): void + { + $source = "SELECT * FROM Invoices " + . "WHERE inv_cst_id IN " + . "(SELECT id FROM Customers " + . "WHERE id IN (SELECT cst_id FROM Orders WHERE status = 1))"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'where' => [ + 'type' => Opcode::IN->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_cst_id', + ], + 'right' => [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'id', + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Customers', + ], + ], + ], + 'where' => [ + 'type' => Opcode::IN->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'id', + ], + 'right' => [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'cst_id', + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Orders', + ], + ], + ], + 'where' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'status', + ], + 'right' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '1', + ], + ], + ], + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectWhereInSubquery(): void + { + $source = "SELECT * " + . "FROM Invoices " + . "WHERE inv_cst_id IN " + . "(SELECT id FROM Customers WHERE status = 1)"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'where' => [ + 'type' => Opcode::IN->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_cst_id', + ], + 'right' => [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'id', + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Customers', + ], + ], + ], + 'where' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'status', + ], + 'right' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '1', + ], + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectWhereNotInSubquery(): void + { + $source = "SELECT * " + . "FROM Invoices " + . "WHERE inv_cst_id NOT IN " + . "(SELECT id FROM Customers WHERE status = 0)"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'where' => [ + 'type' => Opcode::NOTIN->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_cst_id', + ], + 'right' => [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'id', + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Customers', + ], + ], + ], + 'where' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'status', + ], + 'right' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '0', + ], + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/unit/Phql/Select/UnaryMinusTest.php b/tests/unit/Phql/Select/UnaryMinusTest.php new file mode 100644 index 0000000..f08fb3b --- /dev/null +++ b/tests/unit/Phql/Select/UnaryMinusTest.php @@ -0,0 +1,100 @@ + + * + * For the full copyright and license information, please view the LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Phql\Tests\Unit\Phql\Select; + +use Phalcon\Phql\Parser; +use Phalcon\Phql\Scanner\Opcode; +use Phalcon\Phql\Tests\AbstractUnitTestCase; + +final class UnaryMinusTest extends AbstractUnitTestCase +{ + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectUnaryMinusField(): void + { + $source = "SELECT -inv_total FROM Invoices"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::MINUS->value, + 'right' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_total', + ], + ], + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectUnaryMinusWhere(): void + { + $source = "SELECT * FROM Invoices WHERE inv_total > -1"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'where' => [ + 'type' => Opcode::GREATER->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_total', + ], + 'right' => [ + 'type' => Opcode::MINUS->value, + 'right' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '1', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/unit/Phql/Select/WhereInTest.php b/tests/unit/Phql/Select/WhereInTest.php new file mode 100644 index 0000000..80cd736 --- /dev/null +++ b/tests/unit/Phql/Select/WhereInTest.php @@ -0,0 +1,117 @@ + + * + * For the full copyright and license information, please view the LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Phql\Tests\Unit\Phql\Select; + +use Phalcon\Phql\Parser; +use Phalcon\Phql\Scanner\Opcode; +use Phalcon\Phql\Tests\AbstractUnitTestCase; + +final class WhereInTest extends AbstractUnitTestCase +{ + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectWhereIn(): void + { + $source = "SELECT * FROM Invoices WHERE inv_status_flag IN (0, 1, 2)"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'where' => [ + 'type' => Opcode::IN->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_status_flag', + ], + 'right' => [ + 0 => [ + 'type' => Opcode::INTEGER->value, + 'value' => '0', + ], + 1 => [ + 'type' => Opcode::INTEGER->value, + 'value' => '1', + ], + 2 => [ + 'type' => Opcode::INTEGER->value, + 'value' => '2', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectWhereNotIn(): void + { + $source = "SELECT * FROM Invoices WHERE inv_status_flag NOT IN (0, 1)"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'where' => [ + 'type' => Opcode::NOTIN->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_status_flag', + ], + 'right' => [ + 0 => [ + 'type' => Opcode::INTEGER->value, + 'value' => '0', + ], + 1 => [ + 'type' => Opcode::INTEGER->value, + 'value' => '1', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/unit/Phql/Select/WhereLogicalTest.php b/tests/unit/Phql/Select/WhereLogicalTest.php new file mode 100644 index 0000000..34f6da5 --- /dev/null +++ b/tests/unit/Phql/Select/WhereLogicalTest.php @@ -0,0 +1,311 @@ + + * + * For the full copyright and license information, please view the LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Phql\Tests\Unit\Phql\Select; + +use Phalcon\Phql\Parser; +use Phalcon\Phql\Scanner\Opcode; +use Phalcon\Phql\Tests\AbstractUnitTestCase; + +final class WhereLogicalTest extends AbstractUnitTestCase +{ + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectWhereAnd(): void + { + $source = "SELECT * FROM Invoices WHERE inv_status_flag = 1 AND inv_total > 0"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'where' => [ + 'type' => Opcode::GREATER->value, + 'left' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_status_flag', + ], + 'right' => [ + 'type' => Opcode::AND->value, + 'left' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '1', + ], + 'right' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_total', + ], + ], + ], + 'right' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '0', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectWhereAndAnd(): void + { + $source = "SELECT * FROM Invoices WHERE inv_cst_id = 1 AND inv_status_flag = 1 AND inv_total > 0"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'where' => [ + 'type' => Opcode::GREATER->value, + 'left' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_cst_id', + ], + 'right' => [ + 'type' => Opcode::AND->value, + 'left' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '1', + ], + 'right' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_status_flag', + ], + ], + ], + 'right' => [ + 'type' => Opcode::AND->value, + 'left' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '1', + ], + 'right' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_total', + ], + ], + ], + 'right' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '0', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectWhereNot(): void + { + $source = "SELECT * FROM Invoices WHERE NOT inv_status_flag = 0"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'where' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::NOT->value, + 'right' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_status_flag', + ], + ], + 'right' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '0', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectWhereOr(): void + { + $source = "SELECT * FROM Invoices WHERE inv_status_flag = 0 OR inv_status_flag = 1"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'where' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_status_flag', + ], + 'right' => [ + 'type' => Opcode::OR->value, + 'left' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '0', + ], + 'right' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_status_flag', + ], + ], + ], + 'right' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '1', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectWhereParenthesesOrAnd(): void + { + $source = "SELECT * FROM Invoices WHERE (inv_status_flag = 1 OR inv_status_flag = 2) AND inv_total > 0"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'where' => [ + 'type' => Opcode::GREATER->value, + 'left' => [ + 'type' => Opcode::AND->value, + 'left' => [ + 'type' => Opcode::ENCLOSED->value, + 'left' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_status_flag', + ], + 'right' => [ + 'type' => Opcode::OR->value, + 'left' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '1', + ], + 'right' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_status_flag', + ], + ], + ], + 'right' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '2', + ], + ], + ], + 'right' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_total', + ], + ], + 'right' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '0', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/unit/Phql/Select/WherePlaceholdersTest.php b/tests/unit/Phql/Select/WherePlaceholdersTest.php new file mode 100644 index 0000000..577d7ff --- /dev/null +++ b/tests/unit/Phql/Select/WherePlaceholdersTest.php @@ -0,0 +1,303 @@ + + * + * For the full copyright and license information, please view the LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Phql\Tests\Unit\Phql\Select; + +use Phalcon\Phql\Parser; +use Phalcon\Phql\Scanner\Opcode; +use Phalcon\Phql\Tests\AbstractUnitTestCase; + +final class WherePlaceholdersTest extends AbstractUnitTestCase +{ + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectWherePlaceholderBrackets(): void + { + $source = "SELECT * FROM Invoices WHERE inv_id = {id}"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'where' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_id', + ], + 'right' => [ + 'type' => Opcode::BPLACEHOLDER->value, + 'value' => 'id', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectWherePlaceholderBracketsAnd(): void + { + $source = "SELECT * FROM Invoices WHERE inv_cst_id = {custId} AND inv_total > {minTotal}"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'where' => [ + 'type' => Opcode::GREATER->value, + 'left' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_cst_id', + ], + 'right' => [ + 'type' => Opcode::AND->value, + 'left' => [ + 'type' => Opcode::BPLACEHOLDER->value, + 'value' => 'custId', + ], + 'right' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_total', + ], + ], + ], + 'right' => [ + 'type' => Opcode::BPLACEHOLDER->value, + 'value' => 'minTotal', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectWherePlaceholderNum(): void + { + $source = "SELECT * FROM Invoices WHERE inv_id = ?0"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'where' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_id', + ], + 'right' => [ + 'type' => Opcode::NPLACEHOLDER->value, + 'value' => '?0', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectWherePlaceholderNumAnd(): void + { + $source = "SELECT * FROM Invoices WHERE inv_id = ?1 AND inv_status_flag = ?2"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'where' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_id', + ], + 'right' => [ + 'type' => Opcode::AND->value, + 'left' => [ + 'type' => Opcode::NPLACEHOLDER->value, + 'value' => '?1', + ], + 'right' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_status_flag', + ], + ], + ], + 'right' => [ + 'type' => Opcode::NPLACEHOLDER->value, + 'value' => '?2', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectWherePlaceholderString(): void + { + $source = "SELECT * FROM Invoices WHERE inv_title = :title:"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'where' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_title', + ], + 'right' => [ + 'type' => Opcode::SPLACEHOLDER->value, + 'value' => 'title', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectWherePlaceholderStringAnd(): void + { + $source = "SELECT * FROM Invoices WHERE inv_cst_id = :custId: AND inv_status_flag = :status:"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'where' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_cst_id', + ], + 'right' => [ + 'type' => Opcode::AND->value, + 'left' => [ + 'type' => Opcode::SPLACEHOLDER->value, + 'value' => 'custId', + ], + 'right' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_status_flag', + ], + ], + ], + 'right' => [ + 'type' => Opcode::SPLACEHOLDER->value, + 'value' => 'status', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/unit/Phql/Select/WhereTest.php b/tests/unit/Phql/Select/WhereTest.php new file mode 100644 index 0000000..5d7686c --- /dev/null +++ b/tests/unit/Phql/Select/WhereTest.php @@ -0,0 +1,433 @@ + + * + * For the full copyright and license information, please view the LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Phql\Tests\Unit\Phql\Select; + +use Phalcon\Phql\Parser; +use Phalcon\Phql\Scanner\Opcode; +use Phalcon\Phql\Tests\AbstractUnitTestCase; + +final class WhereTest extends AbstractUnitTestCase +{ + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectWhereEqInt(): void + { + $source = "SELECT * FROM Invoices WHERE inv_id = 1"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'where' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_id', + ], + 'right' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '1', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectWhereEqString(): void + { + $source = "SELECT * FROM Invoices WHERE inv_title = 'test invoice'"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'where' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_title', + ], + 'right' => [ + 'type' => Opcode::STRING->value, + 'value' => 'test invoice', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-10 + */ + public function testMvcModelQueryPhqlSelectWhereFuncLeft(): void + { + $source = "SELECT * FROM Invoices WHERE UPPER(inv_title) = 'TEST INVOICE'"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'where' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::FCALL->value, + 'name' => 'UPPER', + 'arguments' => [ + 0 => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_title', + ], + ], + ], + 'right' => [ + 'type' => Opcode::STRING->value, + 'value' => 'TEST INVOICE', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-10 + */ + public function testMvcModelQueryPhqlSelectWhereFuncLeftPlaceholder(): void + { + $source = "SELECT * FROM Invoices WHERE UPPER(inv_title) = :title:"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'where' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::FCALL->value, + 'name' => 'UPPER', + 'arguments' => [ + 0 => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_title', + ], + ], + ], + 'right' => [ + 'type' => Opcode::SPLACEHOLDER->value, + 'value' => 'title', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectWhereGtFloat(): void + { + $source = "SELECT * FROM Invoices WHERE inv_total > 100.00"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'where' => [ + 'type' => Opcode::GREATER->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_total', + ], + 'right' => [ + 'type' => Opcode::DOUBLE->value, + 'value' => '100.00', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectWhereGteFloat(): void + { + $source = "SELECT * FROM Invoices WHERE inv_total >= 100.00"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'where' => [ + 'type' => Opcode::GREATEREQUAL->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_total', + ], + 'right' => [ + 'type' => Opcode::DOUBLE->value, + 'value' => '100.00', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectWhereLtFloat(): void + { + $source = "SELECT * FROM Invoices WHERE inv_total < 100.00"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'where' => [ + 'type' => Opcode::LESS->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_total', + ], + 'right' => [ + 'type' => Opcode::DOUBLE->value, + 'value' => '100.00', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectWhereLteFloat(): void + { + $source = "SELECT * FROM Invoices WHERE inv_total <= 100.00"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'where' => [ + 'type' => Opcode::LESSEQUAL->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_total', + ], + 'right' => [ + 'type' => Opcode::DOUBLE->value, + 'value' => '100.00', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectWhereNeqInt(): void + { + $source = "SELECT * FROM Invoices WHERE inv_id != 1"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'where' => [ + 'type' => Opcode::NOTEQUALS->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_id', + ], + 'right' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '1', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectWhereNotInt(): void + { + $source = "SELECT * FROM Invoices WHERE inv_id <> 1"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + ], + 'where' => [ + 'type' => Opcode::NOTEQUALS->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_id', + ], + 'right' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '1', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/unit/Phql/Select/WithTest.php b/tests/unit/Phql/Select/WithTest.php new file mode 100644 index 0000000..c559d75 --- /dev/null +++ b/tests/unit/Phql/Select/WithTest.php @@ -0,0 +1,95 @@ + + * + * For the full copyright and license information, please view the LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Phql\Tests\Unit\Phql\Select; + +use Phalcon\Phql\Parser; +use Phalcon\Phql\Scanner\Opcode; +use Phalcon\Phql\Tests\AbstractUnitTestCase; + +final class WithTest extends AbstractUnitTestCase +{ + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectWithCache(): void + { + $source = "SELECT * FROM Invoices AS i WITH cache"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + 'alias' => 'i', + 'with' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'cache', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlSelectWithCacheShared(): void + { + $source = "SELECT * FROM Invoices AS i WITH (cache, shared)"; + $expected = [ + 'type' => Opcode::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::STARALL->value, + ], + ], + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + 'alias' => 'i', + 'with' => [ + 0 => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'cache', + ], + 1 => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'shared', + ], + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/unit/Phql/Update/CombinationTest.php b/tests/unit/Phql/Update/CombinationTest.php new file mode 100644 index 0000000..818fecd --- /dev/null +++ b/tests/unit/Phql/Update/CombinationTest.php @@ -0,0 +1,667 @@ + + * + * For the full copyright and license information, please view the LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Phql\Tests\Unit\Phql\Update; + +use Phalcon\Phql\Parser; +use Phalcon\Phql\Scanner\Opcode; +use Phalcon\Phql\Tests\AbstractUnitTestCase; + +final class CombinationTest extends AbstractUnitTestCase +{ + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlUpdate(): void + { + $source = "UPDATE Invoices " . "SET inv_status_flag = 1"; + $expected = [ + 'type' => Opcode::UPDATE->value, + 'update' => [ + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + 'values' => [ + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_status_flag', + ], + 'expr' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '1', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlUpdateAliasNumWhereNum(): void + { + $source = "UPDATE Invoices AS i " . "SET i.inv_status_flag = 1 " . "WHERE i.inv_cst_id = 1"; + $expected = [ + 'type' => Opcode::UPDATE->value, + 'update' => [ + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + 'alias' => 'i', + ], + 'values' => [ + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'i', + 'name' => 'inv_status_flag', + ], + 'expr' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '1', + ], + ], + ], + 'where' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'i', + 'name' => 'inv_cst_id', + ], + 'right' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '1', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlUpdateCalculatedWhereNumZero(): void + { + $source = "UPDATE Invoices " . "SET inv_total = inv_total * 1.1 " . "WHERE inv_status_flag = 0"; + $expected = [ + 'type' => Opcode::UPDATE->value, + 'update' => [ + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + 'values' => [ + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_total', + ], + 'expr' => [ + 'type' => Opcode::MUL->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_total', + ], + 'right' => [ + 'type' => Opcode::DOUBLE->value, + 'value' => '1.1', + ], + ], + ], + ], + 'where' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_status_flag', + ], + 'right' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '0', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlUpdateNullWhereNum(): void + { + $source = "UPDATE Invoices " . "SET inv_total = NULL " . "WHERE inv_status_flag = 0"; + $expected = [ + 'type' => Opcode::UPDATE->value, + 'update' => [ + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + 'values' => [ + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_total', + ], + 'expr' => [ + 'type' => Opcode::NULL->value, + ], + ], + ], + 'where' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_status_flag', + ], + 'right' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '0', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlUpdateNumLimit(): void + { + $source = "UPDATE Invoices " . "SET inv_status_flag = 0 " . "LIMIT 10"; + $expected = [ + 'type' => Opcode::UPDATE->value, + 'update' => [ + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + 'values' => [ + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_status_flag', + ], + 'expr' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '0', + ], + ], + ], + 'limit' => [ + 'number' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '10', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlUpdateNumPlaceholderWhereNumPlaceholder(): void + { + $source = "UPDATE Invoices " . "SET inv_status_flag = ?0, inv_total = ?1 " . "WHERE inv_id = ?2"; + $expected = [ + 'type' => Opcode::UPDATE->value, + 'update' => [ + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + 'values' => [ + 0 => [ + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_status_flag', + ], + 'expr' => [ + 'type' => Opcode::NPLACEHOLDER->value, + 'value' => '?0', + ], + ], + 1 => [ + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_total', + ], + 'expr' => [ + 'type' => Opcode::NPLACEHOLDER->value, + 'value' => '?1', + ], + ], + ], + ], + 'where' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_id', + ], + 'right' => [ + 'type' => Opcode::NPLACEHOLDER->value, + 'value' => '?2', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlUpdateNumStringWhereEqNum(): void + { + $source = "UPDATE Invoices " . "SET inv_status_flag = 1, inv_title = 'Updated' " . "WHERE inv_id = 1"; + $expected = [ + 'type' => Opcode::UPDATE->value, + 'update' => [ + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + 'values' => [ + 0 => [ + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_status_flag', + ], + 'expr' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '1', + ], + ], + 1 => [ + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_title', + ], + 'expr' => [ + 'type' => Opcode::STRING->value, + 'value' => 'Updated', + ], + ], + ], + ], + 'where' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_id', + ], + 'right' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '1', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlUpdateNumWhereEqNum(): void + { + $source = "UPDATE Invoices SET inv_status_flag = 1 WHERE inv_id = 1"; + $expected = [ + 'type' => Opcode::UPDATE->value, + 'update' => [ + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + 'values' => [ + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_status_flag', + ], + 'expr' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '1', + ], + ], + ], + 'where' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_id', + ], + 'right' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '1', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlUpdateNumWhereIn(): void + { + $source = "UPDATE Invoices " . "SET inv_status_flag = 1 " . "WHERE inv_id IN (1, 2, 3)"; + $expected = [ + 'type' => Opcode::UPDATE->value, + 'update' => [ + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + 'values' => [ + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_status_flag', + ], + 'expr' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '1', + ], + ], + ], + 'where' => [ + 'type' => Opcode::IN->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_id', + ], + 'right' => [ + 0 => [ + 'type' => Opcode::INTEGER->value, + 'value' => '1', + ], + 1 => [ + 'type' => Opcode::INTEGER->value, + 'value' => '2', + ], + 2 => [ + 'type' => Opcode::INTEGER->value, + 'value' => '3', + ], + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlUpdatePlaceholderWherePlaceholder(): void + { + $source = "UPDATE Invoices " . "SET inv_title = :title: " . "WHERE inv_id = :id:"; + $expected = [ + 'type' => Opcode::UPDATE->value, + 'update' => [ + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + 'values' => [ + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_title', + ], + 'expr' => [ + 'type' => Opcode::SPLACEHOLDER->value, + 'value' => 'title', + ], + ], + ], + 'where' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_id', + ], + 'right' => [ + 'type' => Opcode::SPLACEHOLDER->value, + 'value' => 'id', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlUpdateTrueWhereNum(): void + { + $source = "UPDATE Invoices " . "SET inv_status_flag = TRUE " . "WHERE inv_id = 1"; + $expected = [ + 'type' => Opcode::UPDATE->value, + 'update' => [ + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + 'values' => [ + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_status_flag', + ], + 'expr' => [ + 'type' => Opcode::TRUE->value, + ], + ], + ], + 'where' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_id', + ], + 'right' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '1', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-09 + */ + public function testMvcModelQueryPhqlUpdateUpperWhereNum(): void + { + $source = "UPDATE Invoices " . "SET inv_title = UPPER(inv_title) " . "WHERE inv_status_flag = 1"; + $expected = [ + 'type' => Opcode::UPDATE->value, + 'update' => [ + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + 'values' => [ + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_title', + ], + 'expr' => [ + 'type' => Opcode::FCALL->value, + 'name' => 'UPPER', + 'arguments' => [ + 0 => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_title', + ], + ], + ], + ], + ], + 'where' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_status_flag', + ], + 'right' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '1', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * @return void + * + * @author Phalcon Team + * @since 2026-04-10 + */ + public function testMvcModelQueryPhqlUpdateWhereMultipleAndConditions(): void + { + $source = "UPDATE Invoices SET inv_status_flag = 1 " + . "WHERE inv_cst_id = 1 AND inv_total > 100 AND inv_status_flag = 0"; + $expected = [ + 'type' => Opcode::UPDATE->value, + 'update' => [ + 'tables' => [ + 'qualifiedName' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'Invoices', + ], + ], + 'values' => [ + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_status_flag', + ], + 'expr' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '1', + ], + ], + ], + 'where' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::GREATER->value, + 'left' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_cst_id', + ], + 'right' => [ + 'type' => Opcode::AND->value, + 'left' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '1', + ], + 'right' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_total', + ], + ], + ], + 'right' => [ + 'type' => Opcode::AND->value, + 'left' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '100', + ], + 'right' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_status_flag', + ], + ], + ], + 'right' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '0', + ], + ], + ]; + $actual = (new Parser())->parse($source); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/unit/Scanner/OpcodeTest.php b/tests/unit/Scanner/OpcodeTest.php new file mode 100644 index 0000000..1ad3ab7 --- /dev/null +++ b/tests/unit/Scanner/OpcodeTest.php @@ -0,0 +1,96 @@ +assertSame(Opcode::SELECT, Opcode::from(309)); + $this->assertSame(Opcode::IDENTIFIER, Opcode::from(265)); + } + + public function testKeyOpcodeValues(): void + { + $this->assertSame(43, Opcode::ADD->value); + $this->assertSame(257, Opcode::IGNORE->value); + $this->assertSame(258, Opcode::INTEGER->value); + $this->assertSame(259, Opcode::DOUBLE->value); + $this->assertSame(260, Opcode::STRING->value); + $this->assertSame(265, Opcode::IDENTIFIER->value); + $this->assertSame(300, Opcode::UPDATE->value); + $this->assertSame(302, Opcode::WHERE->value); + $this->assertSame(303, Opcode::DELETE->value); + $this->assertSame(304, Opcode::FROM->value); + $this->assertSame(306, Opcode::INSERT->value); + $this->assertSame(309, Opcode::SELECT->value); + $this->assertSame(322, Opcode::NULL->value); + $this->assertSame(334, Opcode::TRUE->value); + $this->assertSame(335, Opcode::FALSE->value); + $this->assertSame(350, Opcode::FCALL->value); + $this->assertSame(352, Opcode::STARALL->value); + $this->assertSame(353, Opcode::DOMAINALL->value); + $this->assertSame(354, Opcode::EXPR->value); + $this->assertSame(355, Opcode::QUALIFIED->value); + } + + public function testLabelFallbackToName(): void + { + $this->assertSame('SELECT', Opcode::SELECT->label()); + $this->assertSame('FROM', Opcode::FROM->label()); + $this->assertSame('WHERE', Opcode::WHERE->label()); + $this->assertSame('IDENTIFIER', Opcode::IDENTIFIER->label()); + $this->assertSame('INTEGER', Opcode::INTEGER->label()); + } + + public function testLabelOperators(): void + { + $this->assertSame('+', Opcode::ADD->label()); + $this->assertSame('-', Opcode::SUB->label()); + $this->assertSame('*', Opcode::MUL->label()); + $this->assertSame('/', Opcode::DIV->label()); + $this->assertSame('%', Opcode::MOD->label()); + $this->assertSame('=', Opcode::EQUALS->label()); + $this->assertSame('<', Opcode::LESS->label()); + $this->assertSame('>', Opcode::GREATER->label()); + $this->assertSame('<=', Opcode::LESSEQUAL->label()); + $this->assertSame('>=', Opcode::GREATEREQUAL->label()); + $this->assertSame('!', Opcode::NOT->label()); + $this->assertSame('<>', Opcode::NOTEQUALS->label()); + $this->assertSame('&', Opcode::BITWISE_AND->label()); + $this->assertSame('|', Opcode::BITWISE_OR->label()); + $this->assertSame('~', Opcode::BITWISE_NOT->label()); + $this->assertSame('^', Opcode::BITWISE_XOR->label()); + $this->assertSame(':', Opcode::COLON->label()); + $this->assertSame('.', Opcode::DOT->label()); + $this->assertSame('(', Opcode::PARENTHESES_OPEN->label()); + $this->assertSame(')', Opcode::PARENTHESES_CLOSE->label()); + } + + public function testSingleCharOpcodeValues(): void + { + $this->assertSame(43, Opcode::ADD->value); // ord('+') + $this->assertSame(45, Opcode::SUB->value); // ord('-') + $this->assertSame(42, Opcode::MUL->value); // ord('*') + $this->assertSame(47, Opcode::DIV->value); // ord('/') + $this->assertSame(37, Opcode::MOD->value); // ord('%') + $this->assertSame(61, Opcode::EQUALS->value); // ord('=') + $this->assertSame(60, Opcode::LESS->value); // ord('<') + $this->assertSame(62, Opcode::GREATER->value); // ord('>') + $this->assertSame(33, Opcode::NOT->value); // ord('!') + $this->assertSame(46, Opcode::DOT->value); // ord('.') + $this->assertSame(58, Opcode::COLON->value); // ord(':') + $this->assertSame(40, Opcode::PARENTHESES_OPEN->value); // ord('(') + $this->assertSame(41, Opcode::PARENTHESES_CLOSE->value); // ord(')') + } + + public function testTryFromUnknown(): void + { + $this->assertNull(Opcode::tryFrom(9999)); + } +} diff --git a/tests/unit/Scanner/ScannerStatusTest.php b/tests/unit/Scanner/ScannerStatusTest.php new file mode 100644 index 0000000..39c20a1 --- /dev/null +++ b/tests/unit/Scanner/ScannerStatusTest.php @@ -0,0 +1,32 @@ +assertSame(-1, ScannerStatus::EOF->value); + $this->assertSame(-2, ScannerStatus::ERR->value); + $this->assertSame(-3, ScannerStatus::IMPOSSIBLE->value); + $this->assertSame(0, ScannerStatus::OK->value); + } + + public function testFromInt(): void + { + $this->assertSame(ScannerStatus::EOF, ScannerStatus::from(-1)); + $this->assertSame(ScannerStatus::ERR, ScannerStatus::from(-2)); + $this->assertSame(ScannerStatus::IMPOSSIBLE, ScannerStatus::from(-3)); + $this->assertSame(ScannerStatus::OK, ScannerStatus::from(0)); + } + + public function testTryFromUnknown(): void + { + $this->assertNull(ScannerStatus::tryFrom(99)); + } +} diff --git a/tests/unit/Scanner/ScannerTest.php b/tests/unit/Scanner/ScannerTest.php new file mode 100644 index 0000000..ea18a80 --- /dev/null +++ b/tests/unit/Scanner/ScannerTest.php @@ -0,0 +1,250 @@ +scanAll('& && | || ~ ^ !! @@ @>'); + $this->assertSame([ + Opcode::BITWISE_AND, + Opcode::TS_AND, + Opcode::BITWISE_OR, + Opcode::TS_OR, + Opcode::BITWISE_NOT, + Opcode::BITWISE_XOR, + Opcode::TS_NEGATE, + Opcode::TS_MATCHES, + Opcode::TS_CONTAINS_ANOTHER, + ], $opcodes); + } + + public function testBracketPlaceholder(): void + { + $state = new State('{id}'); + $scanner = new Scanner($state); + + $scanner->scanForToken(); + $token = $scanner->getToken(); + + $this->assertSame(Opcode::BPLACEHOLDER, $token->opcode); + $this->assertSame('id', $token->value); + } + + public function testComparisonOperators(): void + { + $opcodes = $this->scanAll('= != ! < <= > >='); + $this->assertSame([ + Opcode::EQUALS, + Opcode::NOTEQUALS, + Opcode::NOT, + Opcode::LESS, + Opcode::LESSEQUAL, + Opcode::GREATER, + Opcode::GREATEREQUAL, + ], $opcodes); + } + + public function testDoubleLiteral(): void + { + $state = new State('3.14'); + $scanner = new Scanner($state); + + $scanner->scanForToken(); + $token = $scanner->getToken(); + + $this->assertSame(Opcode::DOUBLE, $token->opcode); + $this->assertSame('3.14', $token->value); + } + + public function testDoubleQuotedString(): void + { + $state = new State('"world"'); + $scanner = new Scanner($state); + + $scanner->scanForToken(); + $token = $scanner->getToken(); + + $this->assertSame(Opcode::STRING, $token->opcode); + $this->assertSame('world', $token->value); + } + + public function testEmptyInputReturnsEof(): void + { + $state = new State(''); + $scanner = new Scanner($state); + + $this->assertSame(ScannerStatus::EOF, $scanner->scanForToken()); + } + + public function testFromKeyword(): void + { + $opcodes = $this->scanAll('FROM'); + $this->assertSame([Opcode::FROM], $opcodes); + } + + public function testHexIntegerLiteral(): void + { + $state = new State('0xFF'); + $scanner = new Scanner($state); + + $scanner->scanForToken(); + $token = $scanner->getToken(); + + $this->assertSame(Opcode::HINTEGER, $token->opcode); + } + + public function testIdentifier(): void + { + $state = new State('Invoices'); + $scanner = new Scanner($state); + + $scanner->scanForToken(); + $token = $scanner->getToken(); + + $this->assertSame(Opcode::IDENTIFIER, $token->opcode); + $this->assertSame('Invoices', $token->value); + } + + public function testIntegerLiteral(): void + { + $state = new State('42'); + $scanner = new Scanner($state); + + $scanner->scanForToken(); + $token = $scanner->getToken(); + + $this->assertSame(Opcode::INTEGER, $token->opcode); + $this->assertSame('42', $token->value); + } + + public function testNamedPlaceholder(): void + { + $state = new State(':id:'); + $scanner = new Scanner($state); + + $scanner->scanForToken(); + $token = $scanner->getToken(); + + $this->assertSame(Opcode::SPLACEHOLDER, $token->opcode); + $this->assertSame('id', $token->value); + } + + public function testNoLegacyRetcodeConstants(): void + { + $this->assertFalse(defined('Phalcon\Phql\Scanner\Scanner::PHQL_SCANNER_RETCODE_EOF')); + $this->assertFalse(defined('Phalcon\Phql\Scanner\Scanner::PHQL_SCANNER_RETCODE_ERR')); + $this->assertFalse(defined('Phalcon\Phql\Scanner\Scanner::PHQL_SCANNER_RETCODE_IMPOSSIBLE')); + } + + public function testNumericPlaceholder(): void + { + $state = new State('?0'); + $scanner = new Scanner($state); + + $scanner->scanForToken(); + $token = $scanner->getToken(); + + $this->assertSame(Opcode::NPLACEHOLDER, $token->opcode); + } + + public function testOperators(): void + { + $opcodes = $this->scanAll('+ - * / %'); + $this->assertSame([ + Opcode::ADD, + Opcode::SUB, + Opcode::MUL, + Opcode::DIV, + Opcode::MOD, + ], $opcodes); + } + + public function testReturnTypeIsScannerStatus(): void + { + $state = new State('SELECT'); + $scanner = new Scanner($state); + $result = $scanner->scanForToken(); + + $this->assertInstanceOf(ScannerStatus::class, $result); + } + + public function testSelectFromSequence(): void + { + $opcodes = $this->scanAll('SELECT * FROM Invoices'); + $this->assertSame([ + Opcode::SELECT, + Opcode::MUL, + Opcode::FROM, + Opcode::IDENTIFIER, + ], $opcodes); + } + + public function testSelectKeyword(): void + { + $opcodes = $this->scanAll('SELECT'); + $this->assertSame([Opcode::SELECT], $opcodes); + } + + public function testSingleQuotedString(): void + { + $state = new State("'hello'"); + $scanner = new Scanner($state); + + $scanner->scanForToken(); + $token = $scanner->getToken(); + + $this->assertSame(Opcode::STRING, $token->opcode); + $this->assertSame('hello', $token->value); + } + + public function testUnknownCharacterReturnsErr(): void + { + $state = new State('#'); + $scanner = new Scanner($state); + $result = $scanner->scanForToken(); + + // '#' is not a valid PHQL token — scanner returns ERR + $this->assertSame(ScannerStatus::ERR, $result); + } + + public function testWhereKeyword(): void + { + $opcodes = $this->scanAll('WHERE'); + $this->assertSame([Opcode::WHERE], $opcodes); + } + + public function testWhitespaceProducesIgnore(): void + { + $state = new State(' '); + $scanner = new Scanner($state); + + $scanner->scanForToken(); + $this->assertSame(Opcode::IGNORE, $scanner->getToken()->opcode); + } + + private function scanAll(string $input): array + { + $state = new State($input); + $scanner = new Scanner($state); + $opcodes = []; + + while (($scanner->scanForToken()) === ScannerStatus::OK) { + $token = $scanner->getToken(); + if ($token->opcode !== Opcode::IGNORE) { + $opcodes[] = $token->opcode; + } + } + + return $opcodes; + } +} diff --git a/tests/unit/Scanner/StateTest.php b/tests/unit/Scanner/StateTest.php new file mode 100644 index 0000000..47310c2 --- /dev/null +++ b/tests/unit/Scanner/StateTest.php @@ -0,0 +1,73 @@ +setActiveToken(Opcode::SELECT); + $state->setActiveToken(null); + + $this->assertNull($state->getActiveToken()); + } + + public function testConstruction(): void + { + $state = new State('SELECT'); + + $this->assertSame(6, $state->getBufferLength()); + $this->assertSame(6, $state->getStartLength()); + $this->assertSame(0, $state->getCursor()); + $this->assertSame('S', $state->getStart()); + $this->assertNull($state->getActiveToken()); + } + + public function testEmptyBuffer(): void + { + $state = new State(''); + + $this->assertSame(0, $state->getBufferLength()); + $this->assertNull($state->getStart()); + } + + public function testIncrementStart(): void + { + $state = new State('SELECT'); + $state->incrementStart(1); + + $this->assertSame(1, $state->getCursor()); + $this->assertSame('E', $state->getStart()); + } + + public function testNoSetEndMethod(): void + { + $state = new State('SELECT'); + /** @phpstan-ignore function.impossibleType */ + $this->assertFalse(method_exists($state, 'setEnd')); + } + + public function testSetActiveToken(): void + { + $state = new State('SELECT'); + $state->setActiveToken(Opcode::SELECT); + + $this->assertSame(Opcode::SELECT, $state->getActiveToken()); + } + + public function testSetCursor(): void + { + $state = new State('SELECT'); + $state->setCursor(3); + + $this->assertSame(3, $state->getCursor()); + $this->assertSame('E', $state->getStart()); + } +} diff --git a/tests/unit/Scanner/TokenTest.php b/tests/unit/Scanner/TokenTest.php new file mode 100644 index 0000000..eba7a82 --- /dev/null +++ b/tests/unit/Scanner/TokenTest.php @@ -0,0 +1,48 @@ +assertSame(Opcode::SELECT, $token->opcode); + $this->assertNull($token->value); + $this->assertSame(6, $token->length); + } + + public function testConstructionWithValue(): void + { + $token = new Token(Opcode::IDENTIFIER, 'Invoices', 8); + + $this->assertSame(Opcode::IDENTIFIER, $token->opcode); + $this->assertSame('Invoices', $token->value); + $this->assertSame(8, $token->length); + } + + public function testDefaultConstruction(): void + { + $token = new Token(); + + $this->assertNull($token->opcode); + $this->assertNull($token->value); + $this->assertSame(0, $token->length); + } + + public function testIsReadonly(): void + { + $token = new Token(Opcode::SELECT, null, 6); + + $this->expectException(\Error::class); + // @phpstan-ignore-next-line + $token->opcode = Opcode::FROM; + } +}