From 1f149ecadad9d3c83d8e81aa35956c7c2a59906d Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Wed, 8 Apr 2026 20:32:51 -0500 Subject: [PATCH 01/21] updating composer and phpcs --- .github/workflows/main.yml | 44 ++++++++ composer.json | 36 +++++-- composer.lock | 147 ++++++++++++++++++++++++++- phpcs.xml | 12 +++ src/Parser/Parser.php | 10 +- src/Parser/Status.php | 2 +- src/Scanner/Scanner.php | 85 ---------------- tests/unit/Parser/PhqlParserTest.php | 2 +- 8 files changed, 237 insertions(+), 101 deletions(-) create mode 100644 phpcs.xml diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a6ad250..4d5de3d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -17,6 +17,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 diff --git a/composer.json b/composer.json index 839847d..f8ccb11 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": { - "test": "vendor/bin/phpunit -c phpunit.xml --fail-on-all-issues", - "test-coverage": "vendor/bin/phpunit -c phpunit.xml --coverage-html tests/_output/coverage/" + "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.dist --fail-on-all-issues", + "test-coverage": "vendor/bin/phpunit -c phpunit.xml.dist --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/src/Parser/Parser.php b/src/Parser/Parser.php index 1e3add2..1a3f9b6 100644 --- a/src/Parser/Parser.php +++ b/src/Parser/Parser.php @@ -185,7 +185,7 @@ public function parse(string $phql): array } else { $parserStatus->setSyntaxError("Literals are disabled in PHQL statements"); $parserStatus->setStatus(Status::PHQL_PARSING_FAILED); - } + } break; case Opcode::PHQL_T_TRUE: if ($parserStatus->getEnableLiterals()) { @@ -193,7 +193,7 @@ public function parse(string $phql): array } else { $parserStatus->setSyntaxError("Literals are disabled in PHQL statements"); $parserStatus->setStatus(Status::PHQL_PARSING_FAILED); - } + } break; case Opcode::PHQL_T_FALSE: if ($parserStatus->getEnableLiterals()) { @@ -337,7 +337,7 @@ public function parse(string $phql): array default: $parserStatus->setStatus(Status::PHQL_PARSING_FAILED); $parserStatus->setSyntaxError("Scanner: Unknown opcode %d" . $opcode); - break; + break; } if ($parserStatus->getStatus() === Status::PHQL_PARSING_FAILED) { @@ -347,7 +347,9 @@ public function parse(string $phql): array $state->setEnd($state->getStart()); } - if ($scannerStatus === Scanner::PHQL_SCANNER_RETCODE_ERR || $scannerStatus === Scanner::PHQL_SCANNER_RETCODE_IMPOSSIBLE) { + 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); diff --git a/src/Parser/Status.php b/src/Parser/Status.php index a2f1c09..910fb28 100644 --- a/src/Parser/Status.php +++ b/src/Parser/Status.php @@ -77,4 +77,4 @@ public function setToken(Token $token): self return $this; } -} \ No newline at end of file +} diff --git a/src/Scanner/Scanner.php b/src/Scanner/Scanner.php index 6d30d85..e411eea 100644 --- a/src/Scanner/Scanner.php +++ b/src/Scanner/Scanner.php @@ -251,7 +251,6 @@ public function scanForToken(): int break 2; } case 1: - $status = self::PHQL_SCANNER_RETCODE_EOF; break; @@ -259,7 +258,6 @@ public function scanForToken(): int $yystate = 3; break 2; case 3: - $status = self::PHQL_SCANNER_RETCODE_ERR; break; @@ -278,7 +276,6 @@ public function scanForToken(): int break 2; } case 5: - $token->opcode = Opcode::PHQL_T_IGNORE; $this->state->setCursor($yycursor); return 0; @@ -299,7 +296,6 @@ public function scanForToken(): int break 2; } case 7: - $token->opcode = Opcode::PHQL_T_NOT; $this->state->setCursor($yycursor); return 0; @@ -315,7 +311,6 @@ public function scanForToken(): int $yystate = 68; break 2; case 9: - $token->opcode = Opcode::PHQL_T_MOD; $this->state->setCursor($yycursor); return 0; @@ -332,7 +327,6 @@ public function scanForToken(): int break 2; } case 11: - $token->opcode = Opcode::PHQL_T_BITWISE_AND; $this->state->setCursor($yycursor); return 0; @@ -348,37 +342,31 @@ public function scanForToken(): int $yystate = 74; break 2; case 13: - $token->opcode = Opcode::PHQL_T_PARENTHESES_OPEN; $this->state->setCursor($yycursor); return 0; case 14: - $token->opcode = Opcode::PHQL_T_PARENTHESES_CLOSE; $this->state->setCursor($yycursor); return 0; case 15: - $token->opcode = Opcode::PHQL_T_MUL; $this->state->setCursor($yycursor); return 0; case 16: - $token->opcode = Opcode::PHQL_T_ADD; $this->state->setCursor($yycursor); return 0; case 17: - $token->opcode = Opcode::PHQL_T_COMMA; $this->state->setCursor($yycursor); return 0; case 18: - $token->opcode = Opcode::PHQL_T_SUB; $this->state->setCursor($yycursor); return 0; @@ -404,13 +392,11 @@ public function scanForToken(): int break 2; } case 20: - $token->opcode = Opcode::PHQL_T_DOT; $this->state->setCursor($yycursor); return 0; case 21: - $token->opcode = Opcode::PHQL_T_DIV; $this->state->setCursor($yycursor); return 0; @@ -456,7 +442,6 @@ 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; @@ -541,7 +526,6 @@ public function scanForToken(): int break 2; } case 25: - $token->opcode = Opcode::PHQL_T_COLON; $this->state->setCursor($yycursor); return 0; @@ -562,13 +546,11 @@ public function scanForToken(): int break 2; } case 27: - $token->opcode = Opcode::PHQL_T_LESS; $this->state->setCursor($yycursor); return 0; case 28: - $token->opcode = Opcode::PHQL_T_EQUALS; $this->state->setCursor($yycursor); return 0; @@ -585,7 +567,6 @@ public function scanForToken(): int break 2; } case 30: - $token->opcode = Opcode::PHQL_T_GREATER; $this->state->setCursor($yycursor); return 0; @@ -653,7 +634,6 @@ 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")) { @@ -800,7 +780,6 @@ public function scanForToken(): int break 2; } case 41: - $token->opcode = Opcode::PHQL_T_IDENTIFIER; if (($yycursor - $q) > 1) { if ($yyinput[$q] === '\\') { @@ -1198,7 +1177,6 @@ public function scanForToken(): int break 2; } case 58: - $token->opcode = Opcode::PHQL_T_BITWISE_XOR; $this->state->setCursor($yycursor); return 0; @@ -1372,25 +1350,21 @@ public function scanForToken(): int break 2; } case 63: - $token->opcode = Opcode::PHQL_T_BITWISE_OR; $this->state->setCursor($yycursor); return 0; case 64: - $token->opcode = Opcode::PHQL_T_BITWISE_NOT; $this->state->setCursor($yycursor); return 0; case 65: - $token->opcode = Opcode::PHQL_T_TS_NEGATE; $this->state->setCursor($yycursor); return 0; case 66: - $token->opcode = Opcode::PHQL_T_NOTEQUALS; $this->state->setCursor($yycursor); return 0; @@ -1433,7 +1407,6 @@ 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; @@ -1453,7 +1426,6 @@ public function scanForToken(): int break 2; } case 72: - $token->opcode = Opcode::PHQL_T_TS_AND; $this->state->setCursor($yycursor); return 0; @@ -1511,7 +1483,6 @@ 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; @@ -1792,7 +1763,6 @@ public function scanForToken(): int break 2; } case 91: - $token->opcode = Opcode::PHQL_T_AS; $this->state->setCursor($yycursor); return 0; @@ -1884,7 +1854,6 @@ public function scanForToken(): int break 2; } case 94: - $token->opcode = Opcode::PHQL_T_BY; $this->state->setCursor($yycursor); return 0; @@ -2170,7 +2139,6 @@ public function scanForToken(): int break 2; } case 112: - $token->opcode = Opcode::PHQL_T_IN; $this->state->setCursor($yycursor); return 0; @@ -2251,7 +2219,6 @@ public function scanForToken(): int break 2; } case 114: - $token->opcode = Opcode::PHQL_T_IS; $this->state->setCursor($yycursor); return 0; @@ -2409,7 +2376,6 @@ public function scanForToken(): int break 2; } case 122: - $token->opcode = Opcode::PHQL_T_ON; $this->state->setCursor($yycursor); return 0; @@ -2493,7 +2459,6 @@ public function scanForToken(): int break 2; } case 124: - $token->opcode = Opcode::PHQL_T_OR; $this->state->setCursor($yycursor); return 0; @@ -2731,7 +2696,6 @@ public function scanForToken(): int case 138: // fall through case 139: - $token->opcode = Opcode::PHQL_T_IDENTIFIER; $token->value = substr($yyinput, $q, $yycursor - $q); $token->len = $yycursor - $q; @@ -2819,13 +2783,11 @@ public function scanForToken(): int break 2; } case 141: - $token->opcode = Opcode::PHQL_T_TS_OR; $this->state->setCursor($yycursor); return 0; case 142: - $token->opcode = Opcode::PHQL_T_SPLACEHOLDER; $token->value = substr($yyinput, $q, $yycursor - $q - 1); $token->len = $yycursor - $q - 1; @@ -2921,7 +2883,6 @@ public function scanForToken(): int break 2; } case 145: - $token->opcode = Opcode::PHQL_T_ALL; $this->state->setCursor($yycursor); return 0; @@ -3002,7 +2963,6 @@ public function scanForToken(): int break 2; } case 147: - $token->opcode = Opcode::PHQL_T_AND; $this->state->setCursor($yycursor); return 0; @@ -3083,7 +3043,6 @@ public function scanForToken(): int break 2; } case 149: - $token->opcode = Opcode::PHQL_T_ASC; $this->state->setCursor($yycursor); return 0; @@ -3265,7 +3224,6 @@ public function scanForToken(): int break 2; } case 159: - $token->opcode = Opcode::PHQL_T_END; $this->state->setCursor($yycursor); return 0; @@ -3370,7 +3328,6 @@ public function scanForToken(): int break 2; } case 163: - $token->opcode = Opcode::PHQL_T_FOR; $this->state->setCursor($yycursor); return 0; @@ -3601,7 +3558,6 @@ public function scanForToken(): int break 2; } case 177: - $token->opcode = Opcode::PHQL_T_NOT; $this->state->setCursor($yycursor); return 0; @@ -3754,7 +3710,6 @@ public function scanForToken(): int break 2; } case 185: - $token->opcode = Opcode::PHQL_T_SET; $this->state->setCursor($yycursor); return 0; @@ -3903,7 +3858,6 @@ 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; @@ -4011,7 +3965,6 @@ public function scanForToken(): int break 2; } case 198: - $token->opcode = Opcode::PHQL_T_CASE; $this->state->setCursor($yycursor); return 0; @@ -4092,7 +4045,6 @@ public function scanForToken(): int break 2; } case 200: - $token->opcode = Opcode::PHQL_T_CAST; $this->state->setCursor($yycursor); return 0; @@ -4209,7 +4161,6 @@ public function scanForToken(): int break 2; } case 205: - $token->opcode = Opcode::PHQL_T_DESC; $this->state->setCursor($yycursor); return 0; @@ -4302,7 +4253,6 @@ public function scanForToken(): int break 2; } case 208: - $token->opcode = Opcode::PHQL_T_ELSE; $this->state->setCursor($yycursor); return 0; @@ -4407,7 +4357,6 @@ public function scanForToken(): int break 2; } case 212: - $token->opcode = Opcode::PHQL_T_FROM; $this->state->setCursor($yycursor); return 0; @@ -4488,7 +4437,6 @@ public function scanForToken(): int break 2; } case 214: - $token->opcode = Opcode::PHQL_T_FULL; $this->state->setCursor($yycursor); return 0; @@ -4629,7 +4577,6 @@ public function scanForToken(): int break 2; } case 221: - $token->opcode = Opcode::PHQL_T_INTO; $this->state->setCursor($yycursor); return 0; @@ -4710,7 +4657,6 @@ public function scanForToken(): int break 2; } case 223: - $token->opcode = Opcode::PHQL_T_JOIN; $this->state->setCursor($yycursor); return 0; @@ -4791,7 +4737,6 @@ public function scanForToken(): int break 2; } case 225: - $token->opcode = Opcode::PHQL_T_LEFT; $this->state->setCursor($yycursor); return 0; @@ -4872,7 +4817,6 @@ public function scanForToken(): int break 2; } case 227: - $token->opcode = Opcode::PHQL_T_LIKE; $this->state->setCursor($yycursor); return 0; @@ -4977,7 +4921,6 @@ public function scanForToken(): int break 2; } case 231: - $token->opcode = Opcode::PHQL_T_NULL; $this->state->setCursor($yycursor); return 0; @@ -5118,7 +5061,6 @@ public function scanForToken(): int break 2; } case 238: - $token->opcode = Opcode::PHQL_T_THEN; $this->state->setCursor($yycursor); return 0; @@ -5199,7 +5141,6 @@ public function scanForToken(): int break 2; } case 240: - $token->opcode = Opcode::PHQL_T_TRUE; $this->state->setCursor($yycursor); return 0; @@ -5316,7 +5257,6 @@ public function scanForToken(): int break 2; } case 245: - $token->opcode = Opcode::PHQL_T_WHEN; $this->state->setCursor($yycursor); return 0; @@ -5409,7 +5349,6 @@ public function scanForToken(): int break 2; } case 248: - $token->opcode = Opcode::PHQL_T_WITH; $this->state->setCursor($yycursor); return 0; @@ -5526,7 +5465,6 @@ public function scanForToken(): int break 2; } case 253: - $token->opcode = Opcode::PHQL_T_CROSS; $this->state->setCursor($yycursor); return 0; @@ -5643,7 +5581,6 @@ public function scanForToken(): int break 2; } case 258: - $token->opcode = Opcode::PHQL_T_FALSE; $this->state->setCursor($yycursor); return 0; @@ -5724,7 +5661,6 @@ public function scanForToken(): int break 2; } case 260: - $token->opcode = Opcode::PHQL_T_GROUP; $this->state->setCursor($yycursor); return 0; @@ -5817,7 +5753,6 @@ public function scanForToken(): int break 2; } case 263: - $token->opcode = Opcode::PHQL_T_ILIKE; $this->state->setCursor($yycursor); return 0; @@ -5898,7 +5833,6 @@ public function scanForToken(): int break 2; } case 265: - $token->opcode = Opcode::PHQL_T_INNER; $this->state->setCursor($yycursor); return 0; @@ -5991,7 +5925,6 @@ public function scanForToken(): int break 2; } case 268: - $token->opcode = Opcode::PHQL_T_LIMIT; $this->state->setCursor($yycursor); return 0; @@ -6096,7 +6029,6 @@ public function scanForToken(): int break 2; } case 272: - $token->opcode = Opcode::PHQL_T_ORDER; $this->state->setCursor($yycursor); return 0; @@ -6177,7 +6109,6 @@ public function scanForToken(): int break 2; } case 274: - $token->opcode = Opcode::PHQL_T_OUTER; $this->state->setCursor($yycursor); return 0; @@ -6258,7 +6189,6 @@ public function scanForToken(): int break 2; } case 276: - $token->opcode = Opcode::PHQL_T_RIGHT; $this->state->setCursor($yycursor); return 0; @@ -6363,7 +6293,6 @@ public function scanForToken(): int break 2; } case 280: - $token->opcode = Opcode::PHQL_T_USING; $this->state->setCursor($yycursor); return 0; @@ -6456,7 +6385,6 @@ public function scanForToken(): int break 2; } case 283: - $token->opcode = Opcode::PHQL_T_WHERE; $this->state->setCursor($yycursor); return 0; @@ -6573,7 +6501,6 @@ public function scanForToken(): int break 2; } case 288: - $token->opcode = Opcode::PHQL_T_DELETE; $this->state->setCursor($yycursor); return 0; @@ -6666,7 +6593,6 @@ public function scanForToken(): int break 2; } case 291: - $token->opcode = Opcode::PHQL_T_EXISTS; $this->state->setCursor($yycursor); return 0; @@ -6747,7 +6673,6 @@ public function scanForToken(): int break 2; } case 293: - $token->opcode = Opcode::PHQL_T_HAVING; $this->state->setCursor($yycursor); return 0; @@ -6828,7 +6753,6 @@ public function scanForToken(): int break 2; } case 295: - $token->opcode = Opcode::PHQL_T_INSERT; $this->state->setCursor($yycursor); return 0; @@ -6921,7 +6845,6 @@ public function scanForToken(): int break 2; } case 298: - $token->opcode = Opcode::PHQL_T_OFFSET; $this->state->setCursor($yycursor); return 0; @@ -7002,7 +6925,6 @@ public function scanForToken(): int break 2; } case 300: - $token->opcode = Opcode::PHQL_T_SELECT; $this->state->setCursor($yycursor); return 0; @@ -7083,7 +7005,6 @@ public function scanForToken(): int break 2; } case 302: - $token->opcode = Opcode::PHQL_T_UPDATE; $this->state->setCursor($yycursor); return 0; @@ -7164,7 +7085,6 @@ public function scanForToken(): int break 2; } case 304: - $token->opcode = Opcode::PHQL_T_VALUES; $this->state->setCursor($yycursor); return 0; @@ -7245,7 +7165,6 @@ public function scanForToken(): int break 2; } case 306: - $token->opcode = Opcode::PHQL_T_AGAINST; $this->state->setCursor($yycursor); return 0; @@ -7326,7 +7245,6 @@ public function scanForToken(): int break 2; } case 308: - $token->opcode = Opcode::PHQL_T_BETWEEN; $this->state->setCursor($yycursor); return 0; @@ -7407,7 +7325,6 @@ public function scanForToken(): int break 2; } case 310: - $token->opcode = Opcode::PHQL_T_CONVERT; $this->state->setCursor($yycursor); return 0; @@ -7512,7 +7429,6 @@ public function scanForToken(): int break 2; } case 314: - $token->opcode = Opcode::PHQL_T_DISTINCT; $this->state->setCursor($yycursor); return 0; @@ -7554,7 +7470,6 @@ public function scanForToken(): int break 2; } case 318: - $token->opcode = Opcode::PHQL_T_BETWEEN_NOT; $this->state->setCursor($yycursor); return 0; diff --git a/tests/unit/Parser/PhqlParserTest.php b/tests/unit/Parser/PhqlParserTest.php index b7b479f..5384f00 100644 --- a/tests/unit/Parser/PhqlParserTest.php +++ b/tests/unit/Parser/PhqlParserTest.php @@ -190,4 +190,4 @@ public function testDeleteWithWhereAndOr(): void $this->assertArrayHasKey('where', $result); } -} \ No newline at end of file +} From 83a11c370920c069e12ef0b0d9abc7378626db9e Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Thu, 9 Apr 2026 13:36:45 -0500 Subject: [PATCH 02/21] corrections and aligning with cphalcon behavior --- composer.json | 4 +- resources/files/parser.php | 35 +++++++---- src/Parser.php | 86 +++++++++++++-------------- src/Parser/Status.php | 14 +++++ src/Scanner/Opcode.php | 4 +- src/Scanner/Scanner.php | 117 ++++++++++++++++++++----------------- 6 files changed, 144 insertions(+), 116 deletions(-) diff --git a/composer.json b/composer.json index f8ccb11..8c19b0a 100644 --- a/composer.json +++ b/composer.json @@ -52,8 +52,8 @@ "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.dist --fail-on-all-issues", - "test-coverage": "vendor/bin/phpunit -c phpunit.xml.dist --coverage-html tests/_output/coverage/" + "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", diff --git a/resources/files/parser.php b/resources/files/parser.php index bac010d..1da7db0 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->setRet($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,8 +3985,14 @@ 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: + case 38: $yygotominor = []; break; case 10: @@ -4669,7 +4679,8 @@ 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: @@ -5101,7 +5112,7 @@ function phql_ret_qualified_name( 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::PHQL_T_UPDATE; $ret['update'] = $update; if ($where !== null) { @@ -5130,7 +5141,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::PHQL_T_DELETE; $ret['delete'] = $delete; if ($where !== null) { @@ -5194,7 +5205,7 @@ function phql_ret_placeholder_zval(array &$ret, int $type, ?Token $value = null) function phql_ret_raw_qualified_name(array &$ret, string $tokenA, ?string $tokenB = null): void { $ret = []; - $ret['type'] = defined('PHQL_T_RAW_QUALIFIED') ? PHQL_T_RAW_QUALIFIED : 0; + $ret['type'] = Opcode::PHQL_T_RAW_QUALIFIED; if ($tokenB !== null) { /* Two-part qualified name: domain + name */ @@ -5209,7 +5220,7 @@ function phql_ret_raw_qualified_name(array &$ret, string $tokenA, ?string $token 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['type'] = Opcode::PHQL_T_FCALL; $ret['name'] = $name instanceof Token ? $name->getValue() : $name; if ($arguments !== null) { diff --git a/src/Parser.php b/src/Parser.php index 233e752..1950a53 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -4,12 +4,12 @@ namespace Phalcon\Phql; +use Phalcon\Phql\Parser\Status; use Phalcon\Phql\Scanner\Opcode; use Phalcon\Phql\Scanner\Scanner; use Phalcon\Phql\Scanner\State; use Phalcon\Phql\Scanner\Token; use RuntimeException; -use stdClass; /** * Orchestrates the PHQL lexer and parser, equivalent to @@ -36,32 +36,22 @@ public function parse(string $phql): array } $state = new State($phql); - $token = new Token(); - $scanner = new Scanner($state, $token); - - $parserObject = new \phql_Parser(); + $scanner = new Scanner($state); + $token = $scanner->getToken(); // 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; + $status = new Status($state); + $status->setToken($token); + $status->setEnableLiterals($this->enableLiterals); + + $parserObject = new \phql_Parser($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; + $state->setStartLength(mb_strlen($phql) - $state->getCursor()); + $state->setActiveToken($token->opcode); switch ($token->opcode) { case Opcode::PHQL_T_IGNORE: @@ -176,7 +166,7 @@ public function parse(string $phql): array $parserObject->phql_(\phql_Parser::PHQL_INTEGER, $this->makeParserToken($token)); } else { $errorMsg = 'Literals are disabled in PHQL statements'; - $status->status = \phql_Parser::PHQL_PARSING_FAILED; + $status->setStatus(Status::PHQL_PARSING_FAILED); } break; case Opcode::PHQL_T_DOUBLE: @@ -184,7 +174,7 @@ public function parse(string $phql): array $parserObject->phql_(\phql_Parser::PHQL_DOUBLE, $this->makeParserToken($token)); } else { $errorMsg = 'Literals are disabled in PHQL statements'; - $status->status = \phql_Parser::PHQL_PARSING_FAILED; + $status->setStatus(Status::PHQL_PARSING_FAILED); } break; case Opcode::PHQL_T_STRING: @@ -192,7 +182,7 @@ public function parse(string $phql): array $parserObject->phql_(\phql_Parser::PHQL_STRING, $this->makeParserToken($token)); } else { $errorMsg = 'Literals are disabled in PHQL statements'; - $status->status = \phql_Parser::PHQL_PARSING_FAILED; + $status->setStatus(Status::PHQL_PARSING_FAILED); } break; case Opcode::PHQL_T_TRUE: @@ -200,7 +190,7 @@ public function parse(string $phql): array $parserObject->phql_(\phql_Parser::PHQL_TRUE); } else { $errorMsg = 'Literals are disabled in PHQL statements'; - $status->status = \phql_Parser::PHQL_PARSING_FAILED; + $status->setStatus(Status::PHQL_PARSING_FAILED); } break; case Opcode::PHQL_T_FALSE: @@ -208,7 +198,7 @@ public function parse(string $phql): array $parserObject->phql_(\phql_Parser::PHQL_FALSE); } else { $errorMsg = 'Literals are disabled in PHQL statements'; - $status->status = \phql_Parser::PHQL_PARSING_FAILED; + $status->setStatus(Status::PHQL_PARSING_FAILED); } break; case Opcode::PHQL_T_HINTEGER: @@ -216,7 +206,7 @@ public function parse(string $phql): array $parserObject->phql_(\phql_Parser::PHQL_HINTEGER, $this->makeParserToken($token)); } else { $errorMsg = 'Literals are disabled in PHQL statements'; - $status->status = \phql_Parser::PHQL_PARSING_FAILED; + $status->setStatus(Status::PHQL_PARSING_FAILED); } break; @@ -343,12 +333,12 @@ public function parse(string $phql): array break; default: - $status->status = \phql_Parser::PHQL_PARSING_FAILED; + $status->setStatus(Status::PHQL_PARSING_FAILED); $errorMsg = sprintf('Scanner: Unknown opcode %d', $token->opcode); break; } - if ($status->status !== \phql_Parser::PHQL_PARSING_OK) { + if ($status->getStatus() !== Status::PHQL_PARSING_OK) { $failed = true; break; } @@ -369,12 +359,12 @@ public function parse(string $phql): array } } - $state->activeToken = 0; + $state->setActiveToken(0); - if ($status->status !== \phql_Parser::PHQL_PARSING_OK) { + if ($status->getStatus() !== Status::PHQL_PARSING_OK) { $failed = true; - if ($status->syntax_error !== null && $errorMsg === null) { - $errorMsg = $status->syntax_error; + if ($status->getSyntaxError() !== null && $errorMsg === null) { + $errorMsg = $status->getSyntaxError(); } } @@ -382,24 +372,25 @@ public function parse(string $phql): array throw new RuntimeException($errorMsg ?? 'Unknown PHQL parsing error'); } - if (!is_array($status->ret)) { + $ret = $status->getRet(); + if (!is_array($ret)) { throw new RuntimeException('PHQL parsing produced no result'); } - return $status->ret; + return $ret; } /** - * Wrap a scanner Token into the lightweight token object the parser expects. + * 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): stdClass + private function makeParserToken(Token $token): Token { - $pt = new stdClass(); - $pt->opcode = $token->opcode; - $pt->token = $token->value; - $pt->token_len = $token->len; - $pt->free_flag = 1; + $pt = new Token(); + $pt->setOpcode($token->opcode); + $pt->setValue($token->value); + $pt->setLength($token->len); return $pt; } @@ -407,20 +398,21 @@ private function makeParserToken(Token $token): stdClass /** * Mirrors phql_scanner_error_msg() in base.c. */ - private function buildScannerErrorMsg(stdClass $status, string $phql): string + private function buildScannerErrorMsg(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,7 +420,7 @@ private function buildScannerErrorMsg(stdClass $status, string $phql): string "Scanning error before '%s' when parsing: %s (%d)", $startStr, $phql, - $status->phql_length + $phqlLength ); } diff --git a/src/Parser/Status.php b/src/Parser/Status.php index 910fb28..062d5ff 100644 --- a/src/Parser/Status.php +++ b/src/Parser/Status.php @@ -12,6 +12,8 @@ class Status public const PHQL_PARSING_FAILED = 0; public const PHQL_PARSING_OK = 1; + protected mixed $ret = null; + protected ?string $syntaxError = null; protected ?Token $token = null; @@ -29,6 +31,18 @@ public function getState(): State return $this->scannerState; } + public function getRet(): mixed + { + return $this->ret; + } + + public function setRet(mixed $ret): self + { + $this->ret = $ret; + + return $this; + } + public function setEnableLiterals(bool $enable): self { $this->enableLiterals = $enable; diff --git a/src/Scanner/Opcode.php b/src/Scanner/Opcode.php index 648b666..fcdb592 100644 --- a/src/Scanner/Opcode.php +++ b/src/Scanner/Opcode.php @@ -41,7 +41,7 @@ class Opcode 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_EQUALS = 61; // ord('=') public const PHQL_T_EXISTS = 408; public const PHQL_T_EXPR = 354; public const PHQL_T_FALSE = 335; @@ -71,7 +71,7 @@ class Opcode 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_LESS = 60; // ord('<') public const PHQL_T_LESSEQUAL = 271; public const PHQL_T_LIKE = 268; public const PHQL_T_LIMIT = 312; diff --git a/src/Scanner/Scanner.php b/src/Scanner/Scanner.php index e411eea..18f758c 100644 --- a/src/Scanner/Scanner.php +++ b/src/Scanner/Scanner.php @@ -55,7 +55,7 @@ public function scanForToken(): int $yych = $yyinput[$yycursor]; $yycursor += 1; switch ($yych) { - case 0x00: + case "\x00": $yystate = 1; break 2; case "\t": @@ -256,7 +256,7 @@ public function scanForToken(): int case 2: $yystate = 3; - break 2; + break; case 3: $status = self::PHQL_SCANNER_RETCODE_ERR; break; @@ -304,12 +304,12 @@ public function scanForToken(): int $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->state->setCursor($yycursor); @@ -335,12 +335,12 @@ public function scanForToken(): int $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->state->setCursor($yycursor); @@ -1073,15 +1073,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": @@ -1374,7 +1374,7 @@ public function scanForToken(): int // fall through case 68: switch ($yych) { - case 0x00: + case "\x00": $yystate = 69; break 2; case '"': @@ -1408,8 +1408,10 @@ public function scanForToken(): int } 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 + $token->value = substr($yyinput, $yymarker, $yycursor - $yymarker - 1); + $token->len = $yycursor - $yymarker - 1; $q = $yycursor; $this->state->setCursor($yycursor); return 0; @@ -1435,7 +1437,7 @@ public function scanForToken(): int // fall through case 74: switch ($yych) { - case 0x00: + case "\x00": $yystate = 69; break 2; case '\'': @@ -2593,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": @@ -2645,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": @@ -2694,7 +2696,14 @@ public function scanForToken(): int break 2; } case 138: - // fall through + // Bracket-enclosed identifier: [name] or [First Name] + // Strip the opening [ and closing ] from the value + $token->opcode = Opcode::PHQL_T_IDENTIFIER; + $token->value = substr($yyinput, $q + 1, $yycursor - $q - 2); + $token->len = $yycursor - $q - 2; + $q = $yycursor; + $this->state->setCursor($yycursor); + return 0; case 139: $token->opcode = Opcode::PHQL_T_IDENTIFIER; $token->value = substr($yyinput, $q, $yycursor - $q); @@ -2789,8 +2798,9 @@ public function scanForToken(): int 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 + $token->value = substr($yyinput, $q + 1, $yycursor - $q - 2); + $token->len = $yycursor - $q - 2; $q = $yycursor; $this->state->setCursor($yycursor); return 0; @@ -3808,15 +3818,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": @@ -3859,8 +3869,9 @@ public function scanForToken(): int } 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 + $token->value = substr($yyinput, $q + 1, $yycursor - $q - 2); + $token->len = $yycursor - $q - 2; $q = $yycursor; $this->state->setCursor($yycursor); return 0; From 55a7824f323422b74c2d9d3e200860e4ca6a96d2 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Thu, 9 Apr 2026 14:06:50 -0500 Subject: [PATCH 03/21] fixing tests and phpcs --- src/Parser/Parser.php | 3 ++- tests/unit/Parser/PhqlParserTest.php | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Parser/Parser.php b/src/Parser/Parser.php index 1a3f9b6..8644a0c 100644 --- a/src/Parser/Parser.php +++ b/src/Parser/Parser.php @@ -347,7 +347,8 @@ public function parse(string $phql): array $state->setEnd($state->getStart()); } - if ($scannerStatus === Scanner::PHQL_SCANNER_RETCODE_ERR + if ( + $scannerStatus === Scanner::PHQL_SCANNER_RETCODE_ERR || $scannerStatus === Scanner::PHQL_SCANNER_RETCODE_IMPOSSIBLE ) { throw new Exception($parserStatus->getSyntaxError()); diff --git a/tests/unit/Parser/PhqlParserTest.php b/tests/unit/Parser/PhqlParserTest.php index 5384f00..1e1cd99 100644 --- a/tests/unit/Parser/PhqlParserTest.php +++ b/tests/unit/Parser/PhqlParserTest.php @@ -102,7 +102,7 @@ public function testUsingFQCNForSourceModel(): void $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']); + $this->assertEquals('Phalcon\\Tests\\Models\\Invoices', $result['select']['tables']['qualifiedName']['name']); } /** @@ -159,11 +159,11 @@ public function testUsingSpacesInColumnAlias(): void $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('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('Last Name', $result['select']['columns'][1]['alias']); $this->assertEquals('People', $result['select']['tables']['qualifiedName']['name']); } From be3df075341c7df4316466570666e07c5684d6b5 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Thu, 9 Apr 2026 14:10:13 -0500 Subject: [PATCH 04/21] disabling stan for now --- .github/workflows/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4d5de3d..a46cd71 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -58,8 +58,8 @@ jobs: - name: "PHPCS" run: composer cs - - name: "PHPStan" - run: composer analyze +# - name: "PHPStan" +# run: composer analyze tests: name: PHP ${{ matrix.php }} From 801ce595f95dc18131d088f1151d88da2ee1b53d Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Fri, 10 Apr 2026 18:55:57 -0500 Subject: [PATCH 05/21] additional fixes for phql statements --- resources/files/parser.php | 12 +++++++----- src/Scanner/Opcode.php | 22 +++++++++++----------- src/Scanner/Scanner.php | 31 ++++++++++++++++++------------- 3 files changed, 36 insertions(+), 29 deletions(-) diff --git a/resources/files/parser.php b/resources/files/parser.php index 1da7db0..702ca65 100644 --- a/resources/files/parser.php +++ b/resources/files/parser.php @@ -3992,9 +3992,11 @@ private function yy_reduce(int $yyruleno): void break; case 20: case 27: - case 38: $yygotominor = []; break; + case 38: + $yygotominor = null; + break; case 10: case 17: case 41: @@ -5202,18 +5204,18 @@ function phql_ret_placeholder_zval(array &$ret, int $type, ?Token $value = null) $ret['value'] = $value->getValue() ?? 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'] = Opcode::PHQL_T_RAW_QUALIFIED; 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->getValue(); + $ret['name'] = $tokenB->getValue(); } else { /* Single-part name */ - $ret['name'] = $tokenA; + $ret['name'] = $tokenA->getValue(); } } diff --git a/src/Scanner/Opcode.php b/src/Scanner/Opcode.php index fcdb592..c0047dc 100644 --- a/src/Scanner/Opcode.php +++ b/src/Scanner/Opcode.php @@ -6,7 +6,7 @@ class Opcode { - public const PHQL_T_ADD = '+'; + public const PHQL_T_ADD = 43; // ord('+') /* Literals & Identifiers */ public const PHQL_T_AGAINST = 276; @@ -18,10 +18,10 @@ class Opcode /* 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_BITWISE_AND = 38; // ord('&') + public const PHQL_T_BITWISE_NOT = 126; // ord('~') + public const PHQL_T_BITWISE_OR = 124; // ord('|') + public const PHQL_T_BITWISE_XOR = 94; // ord('^') public const PHQL_T_BPLACEHOLDER = 277; public const PHQL_T_BY = 311; public const PHQL_T_CASE = 409; @@ -34,7 +34,7 @@ class Opcode 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_DIV = 47; // ord('/') public const PHQL_T_DOMAINALL = 353; public const PHQL_T_DOT = '.'; public const PHQL_T_DOUBLE = 259; @@ -51,7 +51,7 @@ class Opcode 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_GREATER = 62; // ord('>') public const PHQL_T_GREATEREQUAL = 272; public const PHQL_T_GROUP = 313; public const PHQL_T_HAVING = 314; @@ -76,11 +76,11 @@ class Opcode 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_MOD = 37; // ord('%') + public const PHQL_T_MUL = 42; // ord('*') public const PHQL_T_NILIKE = 357; public const PHQL_T_NLIKE = 351; - public const PHQL_T_NOT = '!'; + public const PHQL_T_NOT = 33; // ord('!') public const PHQL_T_NOTEQUALS = 270; public const PHQL_T_NOTIN = 323; /** Placeholders */ @@ -102,7 +102,7 @@ class Opcode 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_SUB = 45; // ord('-') public const PHQL_T_SUBQUERY = 407; public const PHQL_T_THEN = 413; public const PHQL_T_TRUE = 334; diff --git a/src/Scanner/Scanner.php b/src/Scanner/Scanner.php index 18f758c..3ac9cca 100644 --- a/src/Scanner/Scanner.php +++ b/src/Scanner/Scanner.php @@ -34,8 +34,9 @@ public function scanForToken(): int return self::PHQL_SCANNER_RETCODE_EOF; } - $q = $yycursor; - $token = $this->token; + $q = $yycursor; + $yymarker = $yycursor; + $token = $this->token; $token->value = null; $token->opcode = null; $token->len = 0; @@ -780,18 +781,21 @@ public function scanForToken(): int break 2; } case 41: + // 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"). $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; + if (($yycursor - $yymarker) > 1) { + if ($yyinput[$yymarker] === '\\') { + $token->value = substr($yyinput, $yymarker + 1, $yycursor - $yymarker - 1); + $token->len = $yycursor - $yymarker - 1; } else { - $token->value = substr($yyinput, $q, $yycursor - $q); - $token->len = $yycursor - $q; + $token->value = substr($yyinput, $yymarker, $yycursor - $yymarker); + $token->len = $yycursor - $yymarker; } } else { - $token->value = substr($yyinput, $q, $yycursor - $q); - $token->len = $yycursor - $q; + $token->value = substr($yyinput, $yymarker, $yycursor - $yymarker); + $token->len = $yycursor - $yymarker; } $q = $yycursor; $this->state->setCursor($yycursor); @@ -2697,10 +2701,11 @@ public function scanForToken(): int } case 138: // Bracket-enclosed identifier: [name] or [First Name] - // Strip the opening [ and closing ] from the value + // Use $yymarker (set by state 56 after '[', updated by state 193 after '\]') + // so that escaped-bracket sequences correctly yield the post-escape substring. $token->opcode = Opcode::PHQL_T_IDENTIFIER; - $token->value = substr($yyinput, $q + 1, $yycursor - $q - 2); - $token->len = $yycursor - $q - 2; + $token->value = substr($yyinput, $yymarker, $yycursor - $yymarker - 1); + $token->len = $yycursor - $yymarker - 1; $q = $yycursor; $this->state->setCursor($yycursor); return 0; From e4ddb36a7a7c25e35ab07c6c139266ad7b85d411 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 11 Apr 2026 11:57:42 -0500 Subject: [PATCH 06/21] adjustments to docker --- resources/docker/develop/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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/ From 7edc61079c865b684837407e093a3818bcc7942d Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 11 Apr 2026 12:01:51 -0500 Subject: [PATCH 07/21] Changing the vars to use an enum --- resources/files/parser.php | 185 +++++++++++++++++++------------------ 1 file changed, 93 insertions(+), 92 deletions(-) diff --git a/resources/files/parser.php b/resources/files/parser.php index 702ca65..172f571 100644 --- a/resources/files/parser.php +++ b/resources/files/parser.php @@ -3913,7 +3913,7 @@ private function yy_reduce(int $yyruleno): void */ case 0: $yygotominor = $this->yystack[$this->yyidx + 0]->minor; - $this->status->setRet($this->yystack[$this->yyidx + 0]->minor); + $this->status->setAst($this->yystack[$this->yyidx + 0]->minor); break; case 1: case 2: @@ -4022,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, ); @@ -4038,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 @@ -4048,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 @@ -4057,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; @@ -4089,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: @@ -4298,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: @@ -4346,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; @@ -4354,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; @@ -4362,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; @@ -4370,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; @@ -4378,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 ); @@ -4398,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 ); @@ -4407,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 ); @@ -4416,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 ); @@ -4425,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 ); @@ -4434,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 ); @@ -4443,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 ); @@ -4452,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 ); @@ -4461,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 ); @@ -4470,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 ); @@ -4479,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 ); @@ -4488,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 ); @@ -4497,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 ); @@ -4506,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 ); @@ -4515,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 ); @@ -4524,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 ); @@ -4533,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 ); @@ -4542,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 ); @@ -4552,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 ); @@ -4561,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 ); @@ -4572,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 ); @@ -4584,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 ); @@ -4596,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 ); @@ -4604,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); @@ -4612,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 ); @@ -4623,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 ); @@ -4637,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 ); @@ -4649,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 ); @@ -4659,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 ); @@ -4667,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: @@ -4686,14 +4686,14 @@ private function yy_reduce(int $yyruleno): void $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 ); @@ -4704,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 ); @@ -4713,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 ); @@ -4735,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 ); @@ -4743,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" @@ -4763,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" @@ -4861,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); } } @@ -4871,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(), )); @@ -4888,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 { @@ -4937,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, ]; @@ -5008,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; @@ -5053,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; @@ -5096,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'] = Opcode::PHQL_T_UPDATE; + $ret['type'] = Opcode::UPDATE->value; $ret['update'] = $update; if ($where !== null) { @@ -5143,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'] = Opcode::PHQL_T_DELETE; + $ret['type'] = Opcode::DELETE->value; $ret['delete'] = $delete; if ($where !== null) { @@ -5169,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) { @@ -5201,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, Token $tokenA, ?Token $tokenB = null): void { $ret = []; - $ret['type'] = Opcode::PHQL_T_RAW_QUALIFIED; + $ret['type'] = Opcode::RAW_QUALIFIED->value; if ($tokenB !== null) { /* Two-part qualified name: domain + name */ - $ret['domain'] = $tokenA->getValue(); - $ret['name'] = $tokenB->getValue(); + $ret['domain'] = $tokenA->value; + $ret['name'] = $tokenB->value; } else { /* Single-part name */ - $ret['name'] = $tokenA->getValue(); + $ret['name'] = $tokenA->value; } } function phql_ret_func_call(array &$ret, $name, $arguments = null, $distinct = null): void { $ret = []; - $ret['type'] = Opcode::PHQL_T_FCALL; - $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; From a1ad112feb5fec786787ac0ccf8b11bf432c5f13 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 11 Apr 2026 12:02:09 -0500 Subject: [PATCH 08/21] tidying up and phpstan --- src/Parser/Parser.php | 212 +++++++++++++++++++++--------------------- src/Parser/Status.php | 65 +++++++------ 2 files changed, 139 insertions(+), 138 deletions(-) diff --git a/src/Parser/Parser.php b/src/Parser/Parser.php index 8644a0c..3385c9e 100644 --- a/src/Parser/Parser.php +++ b/src/Parser/Parser.php @@ -7,6 +7,7 @@ use Phalcon\Phql\Exception; 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 phql_Parser; @@ -21,6 +22,9 @@ public function __construct(private readonly bool $debug = false) { } + /** + * @return array + */ public function parse(string $phql): array { if (strlen($phql) === 0) { @@ -32,7 +36,7 @@ public function parse(string $phql): array $debug = fopen($this->debugFile, 'w+'); } - $codeLength = strlen($phql); + $codeLength = strlen($phql); $parserState = new State($phql); $parserStatus = new Status($parserState); $parserStatus->setEnableLiterals(true); @@ -42,152 +46,152 @@ public function parse(string $phql): array $parser->phql_Trace($debug); $state = $parserStatus->getState(); - while (0 <= $scannerStatus = $scanner->scanForToken()) { + while (ScannerStatus::OK === ($scannerStatus = $scanner->scanForToken())) { $this->token = $scanner->getToken(); $parserStatus->setToken($this->token); $state->setStartLength($codeLength - $state->getCursor()); - $opcode = $this->token->getOpcode(); - $state->setActiveToken($this->token); + $opcode = $this->token->opcode; + $state->setActiveToken($opcode); switch ($opcode) { - case Opcode::PHQL_T_IGNORE: + case Opcode::IGNORE: break; - case Opcode::PHQL_T_ADD: + case Opcode::ADD: $parser->phql_(phql_Parser::PHQL_PLUS); break; - case Opcode::PHQL_T_SUB: + case Opcode::SUB: $parser->phql_(phql_Parser::PHQL_MINUS); break; - case Opcode::PHQL_T_MUL: + case Opcode::MUL: $parser->phql_(phql_Parser::PHQL_TIMES); break; - case Opcode::PHQL_T_DIV: + case Opcode::DIV: $parser->phql_(phql_Parser::PHQL_DIVIDE); break; - case Opcode::PHQL_T_MOD: + case Opcode::MOD: $parser->phql_(phql_Parser::PHQL_MOD); break; - case Opcode::PHQL_T_AND: + case Opcode::AND: $parser->phql_(phql_Parser::PHQL_AND); break; - case Opcode::PHQL_T_OR: + case Opcode::OR: $parser->phql_(phql_Parser::PHQL_OR); break; - case Opcode::PHQL_T_EQUALS: + case Opcode::EQUALS: $parser->phql_(phql_Parser::PHQL_EQUALS); break; - case Opcode::PHQL_T_NOTEQUALS: + case Opcode::NOTEQUALS: $parser->phql_(phql_Parser::PHQL_NOTEQUALS); break; - case Opcode::PHQL_T_LESS: + case Opcode::LESS: $parser->phql_(phql_Parser::PHQL_LESS); break; - case Opcode::PHQL_T_GREATER: + case Opcode::GREATER: $parser->phql_(phql_Parser::PHQL_GREATER); break; - case Opcode::PHQL_T_GREATEREQUAL: + case Opcode::GREATEREQUAL: $parser->phql_(phql_Parser::PHQL_GREATEREQUAL); break; - case Opcode::PHQL_T_LESSEQUAL: + case Opcode::LESSEQUAL: $parser->phql_(phql_Parser::PHQL_LESSEQUAL); break; - case Opcode::PHQL_T_IDENTIFIER: - $this->phqlParseWithToken($parser, Opcode::PHQL_T_IDENTIFIER, phql_Parser::PHQL_IDENTIFIER); + case Opcode::IDENTIFIER: + $this->phqlParseWithToken($parser, Opcode::IDENTIFIER, phql_Parser::PHQL_IDENTIFIER); break; - case Opcode::PHQL_T_DOT: + case Opcode::DOT: $parser->phql_(phql_Parser::PHQL_DOT); break; - case Opcode::PHQL_T_COMMA: + case Opcode::COMMA: $parser->phql_(phql_Parser::PHQL_COMMA); break; - case Opcode::PHQL_T_PARENTHESES_OPEN: + case Opcode::PARENTHESES_OPEN: $parser->phql_(phql_Parser::PHQL_PARENTHESES_OPEN); break; - case Opcode::PHQL_T_PARENTHESES_CLOSE: + case Opcode::PARENTHESES_CLOSE: $parser->phql_(phql_Parser::PHQL_PARENTHESES_CLOSE); break; - case Opcode::PHQL_T_LIKE: + case Opcode::LIKE: $parser->phql_(phql_Parser::PHQL_LIKE); break; - case Opcode::PHQL_T_ILIKE: + case Opcode::ILIKE: $parser->phql_(phql_Parser::PHQL_ILIKE); break; - case Opcode::PHQL_T_NOT: + case Opcode::NOT: $parser->phql_(phql_Parser::PHQL_NOT); break; - case Opcode::PHQL_T_BITWISE_AND: + case Opcode::BITWISE_AND: $parser->phql_(phql_Parser::PHQL_BITWISE_AND); break; - case Opcode::PHQL_T_BITWISE_OR: + case Opcode::BITWISE_OR: $parser->phql_(phql_Parser::PHQL_BITWISE_OR); break; - case Opcode::PHQL_T_BITWISE_NOT: + case Opcode::BITWISE_NOT: $parser->phql_(phql_Parser::PHQL_BITWISE_NOT); break; - case Opcode::PHQL_T_BITWISE_XOR: + case Opcode::BITWISE_XOR: $parser->phql_(phql_Parser::PHQL_BITWISE_XOR); break; - case Opcode::PHQL_T_AGAINST: + case Opcode::AGAINST: $parser->phql_(phql_Parser::PHQL_AGAINST); break; - case Opcode::PHQL_T_CASE: + case Opcode::CASE: $parser->phql_(phql_Parser::PHQL_CASE); break; - case Opcode::PHQL_T_WHEN: + case Opcode::WHEN: $parser->phql_(phql_Parser::PHQL_WHEN); break; - case Opcode::PHQL_T_THEN: + case Opcode::THEN: $parser->phql_(phql_Parser::PHQL_THEN); break; - case Opcode::PHQL_T_END: + case Opcode::END: $parser->phql_(phql_Parser::PHQL_END); break; - case Opcode::PHQL_T_ELSE: + case Opcode::ELSE: $parser->phql_(phql_Parser::PHQL_ELSE); break; - case Opcode::PHQL_T_FOR: + case Opcode::FOR: $parser->phql_(phql_Parser::PHQL_FOR); break; - case Opcode::PHQL_T_WITH: + case Opcode::WITH: $parser->phql_(phql_Parser::PHQL_WITH); break; - case Opcode::PHQL_T_INTEGER: + case Opcode::INTEGER: if ($parserStatus->getEnableLiterals()) { - $this->phqlParseWithToken($parser, Opcode::PHQL_T_INTEGER, phql_Parser::PHQL_INTEGER); + $this->phqlParseWithToken($parser, Opcode::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: + case Opcode::DOUBLE: if ($parserStatus->getEnableLiterals()) { - $this->phqlParseWithToken($parser, Opcode::PHQL_T_DOUBLE, phql_Parser::PHQL_DOUBLE); + $this->phqlParseWithToken($parser, Opcode::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: + case Opcode::STRING: if ($parserStatus->getEnableLiterals()) { - $this->phqlParseWithToken($parser, Opcode::PHQL_T_STRING, phql_Parser::PHQL_STRING); + $this->phqlParseWithToken($parser, Opcode::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: + case Opcode::TRUE: if ($parserStatus->getEnableLiterals()) { $parser->phql_(phql_Parser::PHQL_TRUE); } else { @@ -195,7 +199,7 @@ public function parse(string $phql): array $parserStatus->setStatus(Status::PHQL_PARSING_FAILED); } break; - case Opcode::PHQL_T_FALSE: + case Opcode::FALSE: if ($parserStatus->getEnableLiterals()) { $parser->phql_(phql_Parser::PHQL_FALSE); } else { @@ -203,156 +207,155 @@ public function parse(string $phql): array $parserStatus->setStatus(Status::PHQL_PARSING_FAILED); } break; - case Opcode::PHQL_T_HINTEGER: + case Opcode::HINTEGER: if ($parserStatus->getEnableLiterals()) { - $this->phqlParseWithToken($parser, Opcode::PHQL_T_HINTEGER, phql_Parser::PHQL_HINTEGER); + $this->phqlParseWithToken($parser, Opcode::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); + case Opcode::NPLACEHOLDER: + $this->phqlParseWithToken($parser, Opcode::NPLACEHOLDER, phql_Parser::PHQL_NPLACEHOLDER); break; - case Opcode::PHQL_T_SPLACEHOLDER: - $this->phqlParseWithToken($parser, Opcode::PHQL_T_SPLACEHOLDER, phql_Parser::PHQL_SPLACEHOLDER); + case Opcode::SPLACEHOLDER: + $this->phqlParseWithToken($parser, Opcode::SPLACEHOLDER, phql_Parser::PHQL_SPLACEHOLDER); break; - case Opcode::PHQL_T_BPLACEHOLDER: - $this->phqlParseWithToken($parser, Opcode::PHQL_T_BPLACEHOLDER, phql_Parser::PHQL_BPLACEHOLDER); + case Opcode::BPLACEHOLDER: + $this->phqlParseWithToken($parser, Opcode::BPLACEHOLDER, phql_Parser::PHQL_BPLACEHOLDER); break; - case Opcode::PHQL_T_FROM: + case Opcode::FROM: $parser->phql_(phql_Parser::PHQL_FROM); break; - case Opcode::PHQL_T_UPDATE: + case Opcode::UPDATE: $parser->phql_(phql_Parser::PHQL_UPDATE); break; - case Opcode::PHQL_T_SET: + case Opcode::SET: $parser->phql_(phql_Parser::PHQL_SET); break; - case Opcode::PHQL_T_WHERE: + case Opcode::WHERE: $parser->phql_(phql_Parser::PHQL_WHERE); break; - case Opcode::PHQL_T_DELETE: + case Opcode::DELETE: $parser->phql_(phql_Parser::PHQL_DELETE); break; - case Opcode::PHQL_T_INSERT: + case Opcode::INSERT: $parser->phql_(phql_Parser::PHQL_INSERT); break; - case Opcode::PHQL_T_INTO: + case Opcode::INTO: $parser->phql_(phql_Parser::PHQL_INTO); break; - case Opcode::PHQL_T_VALUES: + case Opcode::VALUES: $parser->phql_(phql_Parser::PHQL_VALUES); break; - case Opcode::PHQL_T_SELECT: + case Opcode::SELECT: $parser->phql_(phql_Parser::PHQL_SELECT); break; - case Opcode::PHQL_T_AS: + case Opcode::AS: $parser->phql_(phql_Parser::PHQL_AS); break; - case Opcode::PHQL_T_ORDER: + case Opcode::ORDER: $parser->phql_(phql_Parser::PHQL_ORDER); break; - case Opcode::PHQL_T_BY: + case Opcode::BY: $parser->phql_(phql_Parser::PHQL_BY); break; - case Opcode::PHQL_T_LIMIT: + case Opcode::LIMIT: $parser->phql_(phql_Parser::PHQL_LIMIT); break; - case Opcode::PHQL_T_OFFSET: + case Opcode::OFFSET: $parser->phql_(phql_Parser::PHQL_OFFSET); break; - case Opcode::PHQL_T_GROUP: + case Opcode::GROUP: $parser->phql_(phql_Parser::PHQL_GROUP); break; - case Opcode::PHQL_T_HAVING: + case Opcode::HAVING: $parser->phql_(phql_Parser::PHQL_HAVING); break; - case Opcode::PHQL_T_ASC: + case Opcode::ASC: $parser->phql_(phql_Parser::PHQL_ASC); break; - case Opcode::PHQL_T_DESC: + case Opcode::DESC: $parser->phql_(phql_Parser::PHQL_DESC); break; - case Opcode::PHQL_T_IN: + case Opcode::IN: $parser->phql_(phql_Parser::PHQL_IN); break; - case Opcode::PHQL_T_ON: + case Opcode::ON: $parser->phql_(phql_Parser::PHQL_ON); break; - case Opcode::PHQL_T_INNER: + case Opcode::INNER: $parser->phql_(phql_Parser::PHQL_INNER); break; - case Opcode::PHQL_T_JOIN: + case Opcode::JOIN: $parser->phql_(phql_Parser::PHQL_JOIN); break; - case Opcode::PHQL_T_LEFT: + case Opcode::LEFT: $parser->phql_(phql_Parser::PHQL_LEFT); break; - case Opcode::PHQL_T_RIGHT: + case Opcode::RIGHT: $parser->phql_(phql_Parser::PHQL_RIGHT); break; - case Opcode::PHQL_T_CROSS: + case Opcode::CROSS: $parser->phql_(phql_Parser::PHQL_CROSS); break; - case Opcode::PHQL_T_FULL: + case Opcode::FULL: $parser->phql_(phql_Parser::PHQL_FULL); break; - case Opcode::PHQL_T_OUTER: + case Opcode::OUTER: $parser->phql_(phql_Parser::PHQL_OUTER); break; - case Opcode::PHQL_T_IS: + case Opcode::IS: $parser->phql_(phql_Parser::PHQL_IS); break; - case Opcode::PHQL_T_NULL: + case Opcode::NULL: $parser->phql_(phql_Parser::PHQL_NULL); break; - case Opcode::PHQL_T_BETWEEN: + case Opcode::BETWEEN: $parser->phql_(phql_Parser::PHQL_BETWEEN); break; - case Opcode::PHQL_T_BETWEEN_NOT: + case Opcode::BETWEEN_NOT: $parser->phql_(phql_Parser::PHQL_BETWEEN_NOT); break; - case Opcode::PHQL_T_DISTINCT: + case Opcode::DISTINCT: $parser->phql_(phql_Parser::PHQL_DISTINCT); break; - case Opcode::PHQL_T_ALL: + case Opcode::ALL: $parser->phql_(phql_Parser::PHQL_ALL); break; - case Opcode::PHQL_T_CAST: + case Opcode::CAST: $parser->phql_(phql_Parser::PHQL_CAST); break; - case Opcode::PHQL_T_CONVERT: + case Opcode::CONVERT: $parser->phql_(phql_Parser::PHQL_CONVERT); break; - case Opcode::PHQL_T_USING: + case Opcode::USING: $parser->phql_(phql_Parser::PHQL_USING); break; - case Opcode::PHQL_T_EXISTS: + case Opcode::EXISTS: $parser->phql_(phql_Parser::PHQL_EXISTS); break; default: $parserStatus->setStatus(Status::PHQL_PARSING_FAILED); - $parserStatus->setSyntaxError("Scanner: Unknown opcode %d" . $opcode); + $opcodeValue = $opcode !== null ? $opcode->value : ''; + $parserStatus->setSyntaxError("Scanner: Unknown opcode %d" . $opcodeValue); 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 + $scannerStatus === ScannerStatus::ERR + || $scannerStatus === ScannerStatus::IMPOSSIBLE ) { - throw new Exception($parserStatus->getSyntaxError()); - } elseif ($scannerStatus === Scanner::PHQL_SCANNER_RETCODE_EOF) { + throw new Exception($parserStatus->getSyntaxError() ?? ''); + } elseif ($scannerStatus === ScannerStatus::EOF) { $parser->phql_(0); } @@ -392,23 +395,22 @@ public function parse(string $phql): array */ $state->setStartLength(0); - $state->setActiveToken(0); + $state->setActiveToken(null); if ($parserStatus->getStatus() !== Status::PHQL_PARSING_OK) { - throw new Exception($parserStatus->getSyntaxError()); + throw new Exception($parserStatus->getSyntaxError() ?? ''); } + /** @var array */ return $parser->getOutput(); } private function phqlParseWithToken( phql_Parser $parser, - int $opcode, + Opcode $opcode, int $parserCode, ): void { - $newToken = new Token(); - $newToken->setOpcode($opcode); - $newToken->setValue($this->token->getValue()); + $newToken = new Token($opcode, $this->token?->value); $this->token = $newToken; diff --git a/src/Parser/Status.php b/src/Parser/Status.php index 062d5ff..3965b48 100644 --- a/src/Parser/Status.php +++ b/src/Parser/Status.php @@ -12,47 +12,32 @@ class Status public const PHQL_PARSING_FAILED = 0; public const PHQL_PARSING_OK = 1; - protected mixed $ret = null; - - 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 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 getRet(): mixed - { - return $this->ret; - } - - public function setRet(mixed $ret): self - { - $this->ret = $ret; - - return $this; - } - - 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 @@ -70,22 +55,36 @@ 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; From 523c63b51a21200c5fe5090b9bc289c2e5330231 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 11 Apr 2026 12:02:35 -0500 Subject: [PATCH 09/21] removing duplications; new enum instead of constants --- src/Enum.php | 138 ------------ src/Scanner/Opcode.php | 247 ++++++++++++---------- src/Scanner/Scanner.php | 456 ++++++++++++++++++++-------------------- src/Scanner/State.php | 55 ++--- src/Scanner/Token.php | 49 +---- src/Tokens.php | 54 ----- 6 files changed, 389 insertions(+), 610 deletions(-) delete mode 100644 src/Enum.php delete mode 100644 src/Tokens.php 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/Scanner/Opcode.php b/src/Scanner/Opcode.php index c0047dc..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 = 43; // ord('+') + 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 = 38; // ord('&') - public const PHQL_T_BITWISE_NOT = 126; // ord('~') - public const PHQL_T_BITWISE_OR = 124; // ord('|') - public const PHQL_T_BITWISE_XOR = 94; // ord('^') - 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 = 47; // ord('/') - 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 = 61; // ord('=') - 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 = 62; // ord('>') - 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 = 60; // ord('<') - 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 = 37; // ord('%') - public const PHQL_T_MUL = 42; // ord('*') - public const PHQL_T_NILIKE = 357; - public const PHQL_T_NLIKE = 351; - public const PHQL_T_NOT = 33; // ord('!') - 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 = 45; // ord('-') - 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 3ac9cca..f992d46 100644 --- a/src/Scanner/Scanner.php +++ b/src/Scanner/Scanner.php @@ -4,15 +4,13 @@ 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) @@ -25,27 +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; $yymarker = $yycursor; - $token = $this->token; - $token->value = null; - $token->opcode = null; - $token->len = 0; - $status = self::PHQL_SCANNER_RETCODE_IMPOSSIBLE; + $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; @@ -252,15 +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; case 3: - $status = self::PHQL_SCANNER_RETCODE_ERR; - break; + $status = ScannerStatus::ERR; + break 2; case 4: $yych = $yyinput[$yycursor]; @@ -277,9 +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]; @@ -297,9 +291,9 @@ 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; @@ -312,9 +306,9 @@ public function scanForToken(): int $yystate = 68; 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]; @@ -328,9 +322,9 @@ 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; @@ -343,34 +337,34 @@ public function scanForToken(): int $yystate = 74; 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]; @@ -393,14 +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]; @@ -443,12 +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; @@ -527,9 +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]; @@ -547,14 +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]; @@ -568,9 +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]; @@ -635,14 +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; @@ -650,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]; @@ -784,22 +776,24 @@ public function scanForToken(): int // 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"). - $token->opcode = Opcode::PHQL_T_IDENTIFIER; + $tokenValue = null; + $tokenLen = 0; if (($yycursor - $yymarker) > 1) { if ($yyinput[$yymarker] === '\\') { - $token->value = substr($yyinput, $yymarker + 1, $yycursor - $yymarker - 1); - $token->len = $yycursor - $yymarker - 1; + $tokenValue = substr($yyinput, $yymarker + 1, $yycursor - $yymarker - 1); + $tokenLen = $yycursor - $yymarker - 1; } else { - $token->value = substr($yyinput, $yymarker, $yycursor - $yymarker); - $token->len = $yycursor - $yymarker; + $tokenValue = substr($yyinput, $yymarker, $yycursor - $yymarker); + $tokenLen = $yycursor - $yymarker; } } else { - $token->value = substr($yyinput, $yymarker, $yycursor - $yymarker); - $token->len = $yycursor - $yymarker; + $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]; @@ -1181,9 +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]; @@ -1354,24 +1348,24 @@ 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]; @@ -1411,14 +1405,16 @@ public function scanForToken(): int break 2; } case 70: - $token->opcode = Opcode::PHQL_T_STRING; // $yymarker points to position after the opening quote (set in state 8/12) // $yycursor is past the closing quote; subtract 1 to exclude it - $token->value = substr($yyinput, $yymarker, $yycursor - $yymarker - 1); - $token->len = $yycursor - $yymarker - 1; + $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]; @@ -1432,9 +1428,9 @@ 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]; @@ -1489,12 +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]; @@ -1608,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) { @@ -1640,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) { @@ -1769,9 +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) { @@ -1860,9 +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) { @@ -2145,9 +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]; @@ -2225,9 +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]; @@ -2382,9 +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]; @@ -2465,9 +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]; @@ -2703,19 +2699,23 @@ public function scanForToken(): int // 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. - $token->opcode = Opcode::PHQL_T_IDENTIFIER; - $token->value = substr($yyinput, $yymarker, $yycursor - $yymarker - 1); - $token->len = $yycursor - $yymarker - 1; + $this->token = new Token( + Opcode::IDENTIFIER, + substr($yyinput, $yymarker, $yycursor - $yymarker - 1), + $yycursor - $yymarker - 1 + ); $q = $yycursor; $this->state->setCursor($yycursor); - return 0; + 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]; @@ -2797,18 +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; // Strip leading ':' — Query.php prepends ':' when building the placeholder - $token->value = substr($yyinput, $q + 1, $yycursor - $q - 2); - $token->len = $yycursor - $q - 2; + $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]; @@ -2898,9 +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]; @@ -2978,9 +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]; @@ -3058,9 +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]; @@ -3239,9 +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]; @@ -3343,9 +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]; @@ -3573,9 +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]; @@ -3725,9 +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]; @@ -3873,13 +3875,15 @@ public function scanForToken(): int break 2; } case 194: - $token->opcode = Opcode::PHQL_T_BPLACEHOLDER; // Strip leading ':' — Query.php handles the ':' prefix separately - $token->value = substr($yyinput, $q + 1, $yycursor - $q - 2); - $token->len = $yycursor - $q - 2; + $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]; @@ -3981,9 +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]; @@ -4061,9 +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]; @@ -4177,9 +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]; @@ -4269,9 +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]; @@ -4373,9 +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]; @@ -4453,9 +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]; @@ -4593,9 +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]; @@ -4673,9 +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]; @@ -4753,9 +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]; @@ -4833,9 +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]; @@ -4937,9 +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]; @@ -5077,9 +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]; @@ -5157,9 +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]; @@ -5273,9 +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]; @@ -5365,9 +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]; @@ -5481,9 +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]; @@ -5597,9 +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]; @@ -5677,9 +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]; @@ -5769,9 +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]; @@ -5849,9 +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]; @@ -5941,9 +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]; @@ -6045,9 +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]; @@ -6125,9 +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]; @@ -6205,9 +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]; @@ -6309,9 +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]; @@ -6401,9 +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]; @@ -6517,9 +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]; @@ -6609,9 +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]; @@ -6689,9 +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]; @@ -6769,9 +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]; @@ -6861,9 +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]; @@ -6941,9 +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]; @@ -7021,9 +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]; @@ -7101,9 +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]; @@ -7181,9 +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]; @@ -7261,9 +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]; @@ -7341,9 +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]; @@ -7445,9 +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]; @@ -7486,9 +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/State.php b/src/Scanner/State.php index ce01b41..3170d6a 100644 --- a/src/Scanner/State.php +++ b/src/Scanner/State.php @@ -6,27 +6,26 @@ class State { - public mixed $activeToken = null; public string $rawBuffer; public int $startLength; - protected int $cursor = 0; - private int $bufferLength; - protected ?string $end = null; - protected ?string $start = null; + private int $bufferLength; + private int $cursor = 0; + private ?Opcode $activeToken = null; + private ?string $start = null; public function __construct(string $buffer) { $this->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, - ]; -} From 29b72a977b2296e0b6d13bfd6eec979590c5cec7 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 11 Apr 2026 12:02:51 -0500 Subject: [PATCH 10/21] reworked parser and its literals --- src/Parser.php | 498 +++++++++++++++++-------------------------------- 1 file changed, 169 insertions(+), 329 deletions(-) diff --git a/src/Parser.php b/src/Parser.php index 1950a53..68fd71e 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -7,377 +7,203 @@ 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; /** * Orchestrates the PHQL lexer and parser, equivalent to * phql_internal_parse_phql() in base.c. */ -class Parser +final class Parser { - private bool $enableLiterals; + private bool $enableLiterals = true; - public function __construct(bool $enableLiterals = true) + public function setEnableLiterals(bool $enable): static { - $this->enableLiterals = $enableLiterals; + $this->enableLiterals = $enable; + + return $this; } /** * 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); - $scanner = new Scanner($state); - $token = $scanner->getToken(); + $state = new State($phql); + $scanner = new Scanner($state); + $token = $scanner->getToken(); + $status = new Status($state); + $parserObject = new \phql_Parser($status); - // Status object mirrors phql_parser_status in C - $status = new Status($state); $status->setToken($token); $status->setEnableLiterals($this->enableLiterals); - $parserObject = new \phql_Parser($status); - - $errorMsg = null; - $failed = false; + $errorMessage = null; + $parseFailed = false; - while (($scannerStatus = $scanner->scanForToken()) >= 0) { + while (($scannerStatus = $scanner->scanForToken()) === ScannerStatus::OK) { $state->setStartLength(mb_strlen($phql) - $state->getCursor()); - $state->setActiveToken($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->setStatus(Status::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->setStatus(Status::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->setStatus(Status::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->setStatus(Status::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->setStatus(Status::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->setStatus(Status::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->setStatus(Status::PHQL_PARSING_FAILED); - $errorMsg = sprintf('Scanner: Unknown opcode %d', $token->opcode); - break; - } + $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) { - $failed = true; + $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->setActiveToken(0); + $state->setActiveToken(null); if ($status->getStatus() !== Status::PHQL_PARSING_OK) { - $failed = true; - if ($status->getSyntaxError() !== null && $errorMsg === null) { - $errorMsg = $status->getSyntaxError(); + $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'); } - $ret = $status->getRet(); - if (!is_array($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'); } - return $ret; + return $ast; } /** @@ -387,18 +213,17 @@ public function parse(string $phql): array */ private function makeParserToken(Token $token): Token { - $pt = new Token(); - $pt->setOpcode($token->opcode); - $pt->setValue($token->value); - $pt->setLength($token->len); - - return $pt; + return new Token( + $token->opcode, + $token->value, + $token->length, + ); } /** * Mirrors phql_scanner_error_msg() in base.c. */ - private function buildScannerErrorMsg(Status $status, string $phql): string + private function buildScannerErrorMessage(Status $status, string $phql): string { $state = $status->getState(); $phqlLength = mb_strlen($phql); @@ -426,4 +251,19 @@ private function buildScannerErrorMsg(Status $status, string $phql): string return 'Scanning error near to EOF'; } + + 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) + ); + } } From 7dfadd344d8099fb8976daeaf46401420fcc4085 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 11 Apr 2026 12:03:19 -0500 Subject: [PATCH 11/21] tests adjustments --- phpunit.xml | 4 +- tests/unit/Parser/PhqlParserTest.php | 193 --------------------------- 2 files changed, 3 insertions(+), 194 deletions(-) delete mode 100644 tests/unit/Parser/PhqlParserTest.php 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/tests/unit/Parser/PhqlParserTest.php b/tests/unit/Parser/PhqlParserTest.php deleted file mode 100644 index 1e1cd99..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); - } -} From 7c72347ffc332f30cc34e57391c8d221b3e5a2ee Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 11 Apr 2026 12:04:00 -0500 Subject: [PATCH 12/21] new scannerstatus enum --- src/Scanner/ScannerStatus.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 src/Scanner/ScannerStatus.php 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 @@ + Date: Sat, 11 Apr 2026 12:04:19 -0500 Subject: [PATCH 13/21] tests and more tests --- tests/unit/Parser/StatusTest.php | 82 ++ tests/unit/ParserTest.php | 110 +++ tests/unit/Phql/Delete/CombinationTest.php | 369 ++++++++ tests/unit/Phql/Insert/CombinationTest.php | 506 +++++++++++ tests/unit/Phql/Select/AggregateTest.php | 462 ++++++++++ tests/unit/Phql/Select/BasicTest.php | 335 +++++++ tests/unit/Phql/Select/BetweenTest.php | 213 +++++ tests/unit/Phql/Select/BitwiseTest.php | 239 +++++ tests/unit/Phql/Select/BracketsNameTest.php | 105 +++ .../Select/BracketsWithEscapedNameTest.php | 61 ++ .../Phql/Select/BracketsWithSpaceNameTest.php | 231 +++++ tests/unit/Phql/Select/CaseTest.php | 228 +++++ tests/unit/Phql/Select/CastConvertTest.php | 141 +++ tests/unit/Phql/Select/ColumnAliasesTest.php | 142 +++ tests/unit/Phql/Select/ComplexTest.php | 306 +++++++ tests/unit/Phql/Select/DistinctTest.php | 130 +++ tests/unit/Phql/Select/ForUpdateTest.php | 94 ++ tests/unit/Phql/Select/FromTest.php | 174 ++++ tests/unit/Phql/Select/GroupByTest.php | 183 ++++ tests/unit/Phql/Select/HavingTest.php | 219 +++++ tests/unit/Phql/Select/JoinTest.php | 818 ++++++++++++++++++ .../Phql/Select/KeywordCollisionsNameTest.php | 420 +++++++++ .../Phql/Select/KeywordPrefixNameTest.php | 295 +++++++ tests/unit/Phql/Select/LikeTest.php | 181 ++++ tests/unit/Phql/Select/LimitTest.php | 339 ++++++++ tests/unit/Phql/Select/LiteralsTest.php | 291 +++++++ tests/unit/Phql/Select/MatchAgainstTest.php | 110 +++ tests/unit/Phql/Select/NullTest.php | 93 ++ tests/unit/Phql/Select/OperatorsTest.php | 327 +++++++ tests/unit/Phql/Select/OrderByTest.php | 288 ++++++ tests/unit/Phql/Select/QualifiedNamesTest.php | 125 +++ tests/unit/Phql/Select/ScalarTest.php | 521 +++++++++++ tests/unit/Phql/Select/SubqueriesTest.php | 475 ++++++++++ tests/unit/Phql/Select/UnaryMinusTest.php | 100 +++ tests/unit/Phql/Select/WhereInTest.php | 117 +++ tests/unit/Phql/Select/WhereLogicalTest.php | 311 +++++++ .../Phql/Select/WherePlaceholdersTest.php | 303 +++++++ tests/unit/Phql/Select/WhereTest.php | 433 +++++++++ tests/unit/Phql/Select/WithTest.php | 95 ++ tests/unit/Phql/Update/CombinationTest.php | 667 ++++++++++++++ tests/unit/Scanner/OpcodeTest.php | 96 ++ tests/unit/Scanner/ScannerStatusTest.php | 32 + tests/unit/Scanner/ScannerTest.php | 233 +++++ tests/unit/Scanner/StateTest.php | 73 ++ tests/unit/Scanner/TokenTest.php | 48 + 45 files changed, 11121 insertions(+) create mode 100644 tests/unit/Parser/StatusTest.php create mode 100644 tests/unit/ParserTest.php create mode 100644 tests/unit/Phql/Delete/CombinationTest.php create mode 100644 tests/unit/Phql/Insert/CombinationTest.php create mode 100644 tests/unit/Phql/Select/AggregateTest.php create mode 100644 tests/unit/Phql/Select/BasicTest.php create mode 100644 tests/unit/Phql/Select/BetweenTest.php create mode 100644 tests/unit/Phql/Select/BitwiseTest.php create mode 100644 tests/unit/Phql/Select/BracketsNameTest.php create mode 100644 tests/unit/Phql/Select/BracketsWithEscapedNameTest.php create mode 100644 tests/unit/Phql/Select/BracketsWithSpaceNameTest.php create mode 100644 tests/unit/Phql/Select/CaseTest.php create mode 100644 tests/unit/Phql/Select/CastConvertTest.php create mode 100644 tests/unit/Phql/Select/ColumnAliasesTest.php create mode 100644 tests/unit/Phql/Select/ComplexTest.php create mode 100644 tests/unit/Phql/Select/DistinctTest.php create mode 100644 tests/unit/Phql/Select/ForUpdateTest.php create mode 100644 tests/unit/Phql/Select/FromTest.php create mode 100644 tests/unit/Phql/Select/GroupByTest.php create mode 100644 tests/unit/Phql/Select/HavingTest.php create mode 100644 tests/unit/Phql/Select/JoinTest.php create mode 100644 tests/unit/Phql/Select/KeywordCollisionsNameTest.php create mode 100644 tests/unit/Phql/Select/KeywordPrefixNameTest.php create mode 100644 tests/unit/Phql/Select/LikeTest.php create mode 100644 tests/unit/Phql/Select/LimitTest.php create mode 100644 tests/unit/Phql/Select/LiteralsTest.php create mode 100644 tests/unit/Phql/Select/MatchAgainstTest.php create mode 100644 tests/unit/Phql/Select/NullTest.php create mode 100644 tests/unit/Phql/Select/OperatorsTest.php create mode 100644 tests/unit/Phql/Select/OrderByTest.php create mode 100644 tests/unit/Phql/Select/QualifiedNamesTest.php create mode 100644 tests/unit/Phql/Select/ScalarTest.php create mode 100644 tests/unit/Phql/Select/SubqueriesTest.php create mode 100644 tests/unit/Phql/Select/UnaryMinusTest.php create mode 100644 tests/unit/Phql/Select/WhereInTest.php create mode 100644 tests/unit/Phql/Select/WhereLogicalTest.php create mode 100644 tests/unit/Phql/Select/WherePlaceholdersTest.php create mode 100644 tests/unit/Phql/Select/WhereTest.php create mode 100644 tests/unit/Phql/Select/WithTest.php create mode 100644 tests/unit/Phql/Update/CombinationTest.php create mode 100644 tests/unit/Scanner/OpcodeTest.php create mode 100644 tests/unit/Scanner/ScannerStatusTest.php create mode 100644 tests/unit/Scanner/ScannerTest.php create mode 100644 tests/unit/Scanner/StateTest.php create mode 100644 tests/unit/Scanner/TokenTest.php diff --git a/tests/unit/Parser/StatusTest.php b/tests/unit/Parser/StatusTest.php new file mode 100644 index 0000000..97a0266 --- /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 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()); + } + + 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')); + } +} diff --git a/tests/unit/ParserTest.php b/tests/unit/ParserTest.php new file mode 100644 index 0000000..9e79c1a --- /dev/null +++ b/tests/unit/ParserTest.php @@ -0,0 +1,110 @@ +expectException(Exception::class); + $this->expectExceptionMessage('PHQL statement cannot be NULL'); + + (new Parser())->parse(''); + } + + public function testSetEnableLiteralsReturnsSelf(): void + { + $parser = new Parser(); + $result = $parser->setEnableLiterals(false); + + $this->assertSame($parser, $result); + } + + public function testSetEnableLiteralsChaining(): void + { + // Should not throw — fluent chaining works + $result = (new Parser())->setEnableLiterals(true)->parse('SELECT * FROM Invoices'); + $this->assertIsArray($result); + } + + 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 testLiteralsDisabledBlocksDouble(): void + { + $this->expectException(Exception::class); + $this->expectExceptionMessage('Literals are disabled in PHQL statements'); + + (new Parser())->setEnableLiterals(false)->parse('SELECT 1.5 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 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 testThrowsPhqlException(): void + { + try { + (new Parser())->parse(''); + } catch (\Throwable $e) { + $this->assertInstanceOf(Exception::class, $e); + $this->assertNotInstanceOf(\RuntimeException::class, $e); + } + } + + 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 testLiteralsEnabledByDefault(): void + { + // Integer literal should parse without error when literals are enabled (default) + $result = (new Parser())->parse('SELECT 1 FROM Invoices'); + $this->assertIsArray($result); + } +} 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..d67d5c7 --- /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 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-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 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 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 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-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-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); + } + + /** + * @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); + } +} diff --git a/tests/unit/Phql/Select/AggregateTest.php b/tests/unit/Phql/Select/AggregateTest.php new file mode 100644 index 0000000..329664f --- /dev/null +++ b/tests/unit/Phql/Select/AggregateTest.php @@ -0,0 +1,462 @@ + + * + * 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 + * @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 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); + } + + /** + * @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 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 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); + } +} diff --git a/tests/unit/Phql/Select/BasicTest.php b/tests/unit/Phql/Select/BasicTest.php new file mode 100644 index 0000000..64be0d5 --- /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 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 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); + } + + /** + * @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 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); + } +} diff --git a/tests/unit/Phql/Select/BetweenTest.php b/tests/unit/Phql/Select/BetweenTest.php new file mode 100644 index 0000000..f9796a3 --- /dev/null +++ b/tests/unit/Phql/Select/BetweenTest.php @@ -0,0 +1,213 @@ + + * + * 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 + * @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); + } +} diff --git a/tests/unit/Phql/Select/BitwiseTest.php b/tests/unit/Phql/Select/BitwiseTest.php new file mode 100644 index 0000000..3613b97 --- /dev/null +++ b/tests/unit/Phql/Select/BitwiseTest.php @@ -0,0 +1,239 @@ + + * + * 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..19d84d2 --- /dev/null +++ b/tests/unit/Phql/Select/BracketsWithSpaceNameTest.php @@ -0,0 +1,231 @@ + + * + * 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-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..5b00c5c --- /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 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); + } + + /** + * @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); + } +} diff --git a/tests/unit/Phql/Select/DistinctTest.php b/tests/unit/Phql/Select/DistinctTest.php new file mode 100644 index 0000000..13b01c7 --- /dev/null +++ b/tests/unit/Phql/Select/DistinctTest.php @@ -0,0 +1,130 @@ + + * + * 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..dd95b79 --- /dev/null +++ b/tests/unit/Phql/Select/ForUpdateTest.php @@ -0,0 +1,94 @@ + + * + * 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..86254b9 --- /dev/null +++ b/tests/unit/Phql/Select/FromTest.php @@ -0,0 +1,174 @@ + + * + * 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-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); + } + + /** + * @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); + } +} diff --git a/tests/unit/Phql/Select/GroupByTest.php b/tests/unit/Phql/Select/GroupByTest.php new file mode 100644 index 0000000..75e0ee4 --- /dev/null +++ b/tests/unit/Phql/Select/GroupByTest.php @@ -0,0 +1,183 @@ + + * + * 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..92f8118 --- /dev/null +++ b/tests/unit/Phql/Select/HavingTest.php @@ -0,0 +1,219 @@ + + * + * 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..3090cd8 --- /dev/null +++ b/tests/unit/Phql/Select/JoinTest.php @@ -0,0 +1,818 @@ + + * + * 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-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 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-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..c901e06 --- /dev/null +++ b/tests/unit/Phql/Select/LimitTest.php @@ -0,0 +1,339 @@ + + * + * 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-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 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 + * @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-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..819fdf8 --- /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 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); + } + + /** + * @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); + } +} diff --git a/tests/unit/Phql/Select/OperatorsTest.php b/tests/unit/Phql/Select/OperatorsTest.php new file mode 100644 index 0000000..f593df4 --- /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 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); + } + + /** + * @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 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 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); + } +} diff --git a/tests/unit/Phql/Select/OrderByTest.php b/tests/unit/Phql/Select/OrderByTest.php new file mode 100644 index 0000000..873e980 --- /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 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); + } + + /** + * @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); + } +} 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..3107deb --- /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 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 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 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 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 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 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 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 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); + } + + /** + * @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 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 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 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); + } +} diff --git a/tests/unit/Phql/Select/SubqueriesTest.php b/tests/unit/Phql/Select/SubqueriesTest.php new file mode 100644 index 0000000..f2225c0 --- /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-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); + } + + /** + * @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-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 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); + } +} 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..653c60d --- /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 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 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 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..507e7cf --- /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 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); + } + + /** + * @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); + } +} diff --git a/tests/unit/Phql/Select/WhereTest.php b/tests/unit/Phql/Select/WhereTest.php new file mode 100644 index 0000000..a218bcf --- /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 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); + } + + /** + * @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 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 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 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-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 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); + } +} 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..4f1b9ec --- /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-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); + } + + /** + * @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 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 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 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); + } +} diff --git a/tests/unit/Scanner/OpcodeTest.php b/tests/unit/Scanner/OpcodeTest.php new file mode 100644 index 0000000..af37f52 --- /dev/null +++ b/tests/unit/Scanner/OpcodeTest.php @@ -0,0 +1,96 @@ +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 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 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 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 testFromInt(): void + { + $this->assertSame(Opcode::SELECT, Opcode::from(309)); + $this->assertSame(Opcode::IDENTIFIER, Opcode::from(265)); + } + + 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..8af3fd4 --- /dev/null +++ b/tests/unit/Scanner/ScannerTest.php @@ -0,0 +1,233 @@ +scanForToken()) === ScannerStatus::OK) { + $token = $scanner->getToken(); + if ($token->opcode !== Opcode::IGNORE) { + $opcodes[] = $token->opcode; + } + } + + return $opcodes; + } + + public function testEmptyInputReturnsEof(): void + { + $state = new State(''); + $scanner = new Scanner($state); + + $this->assertSame(ScannerStatus::EOF, $scanner->scanForToken()); + } + + public function testReturnTypeIsScannerStatus(): void + { + $state = new State('SELECT'); + $scanner = new Scanner($state); + $result = $scanner->scanForToken(); + + $this->assertInstanceOf(ScannerStatus::class, $result); + } + + 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 testWhitespaceProducesIgnore(): void + { + $state = new State(' '); + $scanner = new Scanner($state); + + $scanner->scanForToken(); + $this->assertSame(Opcode::IGNORE, $scanner->getToken()->opcode); + } + + public function testSelectKeyword(): void + { + $opcodes = $this->scanAll('SELECT'); + $this->assertSame([Opcode::SELECT], $opcodes); + } + + public function testFromKeyword(): void + { + $opcodes = $this->scanAll('FROM'); + $this->assertSame([Opcode::FROM], $opcodes); + } + + public function testWhereKeyword(): void + { + $opcodes = $this->scanAll('WHERE'); + $this->assertSame([Opcode::WHERE], $opcodes); + } + + 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 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 testHexIntegerLiteral(): void + { + $state = new State('0xFF'); + $scanner = new Scanner($state); + + $scanner->scanForToken(); + $token = $scanner->getToken(); + + $this->assertSame(Opcode::HINTEGER, $token->opcode); + } + + 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 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 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 testNumericPlaceholder(): void + { + $state = new State('?0'); + $scanner = new Scanner($state); + + $scanner->scanForToken(); + $token = $scanner->getToken(); + + $this->assertSame(Opcode::NPLACEHOLDER, $token->opcode); + } + + 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 testOperators(): void + { + $opcodes = $this->scanAll('+ - * / %'); + $this->assertSame([ + Opcode::ADD, + Opcode::SUB, + Opcode::MUL, + Opcode::DIV, + Opcode::MOD, + ], $opcodes); + } + + public function testComparisonOperators(): void + { + $opcodes = $this->scanAll('= != < <= > >='); + $this->assertSame([ + Opcode::EQUALS, + Opcode::NOTEQUALS, + Opcode::LESS, + Opcode::LESSEQUAL, + Opcode::GREATER, + Opcode::GREATEREQUAL, + ], $opcodes); + } + + public function testSelectFromSequence(): void + { + $opcodes = $this->scanAll('SELECT * FROM Invoices'); + $this->assertSame([ + Opcode::SELECT, + Opcode::MUL, + Opcode::FROM, + Opcode::IDENTIFIER, + ], $opcodes); + } + + 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); + } +} diff --git a/tests/unit/Scanner/StateTest.php b/tests/unit/Scanner/StateTest.php new file mode 100644 index 0000000..be261d7 --- /dev/null +++ b/tests/unit/Scanner/StateTest.php @@ -0,0 +1,73 @@ +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 testSetActiveToken(): void + { + $state = new State('SELECT'); + $state->setActiveToken(Opcode::SELECT); + + $this->assertSame(Opcode::SELECT, $state->getActiveToken()); + } + + public function testClearActiveToken(): void + { + $state = new State('SELECT'); + $state->setActiveToken(Opcode::SELECT); + $state->setActiveToken(null); + + $this->assertNull($state->getActiveToken()); + } + + public function testIncrementStart(): void + { + $state = new State('SELECT'); + $state->incrementStart(1); + + $this->assertSame(1, $state->getCursor()); + $this->assertSame('E', $state->getStart()); + } + + public function testSetCursor(): void + { + $state = new State('SELECT'); + $state->setCursor(3); + + $this->assertSame(3, $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')); + } +} diff --git a/tests/unit/Scanner/TokenTest.php b/tests/unit/Scanner/TokenTest.php new file mode 100644 index 0000000..85d1a45 --- /dev/null +++ b/tests/unit/Scanner/TokenTest.php @@ -0,0 +1,48 @@ +assertNull($token->opcode); + $this->assertNull($token->value); + $this->assertSame(0, $token->length); + } + + public function testConstructionWithAllValues(): void + { + $token = new Token(Opcode::SELECT, null, 6); + + $this->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 testIsReadonly(): void + { + $token = new Token(Opcode::SELECT, null, 6); + + $this->expectException(\Error::class); + // @phpstan-ignore-next-line + $token->opcode = Opcode::FROM; + } +} From a58b59fe9c9615bd279fe998f42cdf47a4ecaa13 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 11 Apr 2026 12:10:48 -0500 Subject: [PATCH 14/21] workflow corrections --- .github/workflows/main.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a46cd71..6610efb 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -58,8 +58,8 @@ jobs: - name: "PHPCS" run: composer cs -# - name: "PHPStan" -# run: composer analyze + - name: "PHPStan" + run: composer analyze tests: name: PHP ${{ matrix.php }} @@ -104,6 +104,7 @@ jobs: coverage: name: Code Coverage + needs: tests runs-on: ubuntu-latest permissions: @@ -115,7 +116,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 @@ -128,7 +129,7 @@ jobs: 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 From c0a70c039fab101de3dd2fa9d02f8b9dba1768c1 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 11 Apr 2026 12:12:28 -0500 Subject: [PATCH 15/21] bumping cache version --- .github/workflows/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6610efb..5750214 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -124,12 +124,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 -c phpunit.xml --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 From 2e7602462dcf925a3f701fb3bacc26dbf043537c Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 11 Apr 2026 12:22:31 -0500 Subject: [PATCH 16/21] removing scheduled run --- .github/workflows/main.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5750214..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' From 6907846c2667562ca7b6b234966cda65f5f77008 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 11 Apr 2026 12:23:18 -0500 Subject: [PATCH 17/21] ignoring the coverage folder --- .gitignore | 1 + 1 file changed, 1 insertion(+) 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 From 66c6647b1015fa49e8c9329575dcb7d76f1bdbaf Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 11 Apr 2026 12:35:17 -0500 Subject: [PATCH 18/21] removing dead code, tightening properties --- src/Parser/Parser.php | 419 ------------------ src/Parser/Status.php | 2 +- src/Scanner/Scanner.php | 2 +- src/Scanner/State.php | 8 +- tests-old/001.phpt | 55 --- tests-old/002.phpt | 81 ---- tests-old/003.phpt | 63 --- tests-old/bug14253.phpt | 105 ----- tests-old/bug14535.phpt | 72 --- tests/unit/Phql/Select/AggregateTest.php | 42 ++ tests/unit/Phql/Select/BetweenTest.php | 121 +++++ .../Phql/Select/BracketsWithSpaceNameTest.php | 46 ++ tests/unit/Phql/Select/LimitTest.php | 38 ++ 13 files changed, 253 insertions(+), 801 deletions(-) delete mode 100644 src/Parser/Parser.php delete mode 100644 tests-old/001.phpt delete mode 100644 tests-old/002.phpt delete mode 100644 tests-old/003.phpt delete mode 100644 tests-old/bug14253.phpt delete mode 100644 tests-old/bug14535.phpt diff --git a/src/Parser/Parser.php b/src/Parser/Parser.php deleted file mode 100644 index 3385c9e..0000000 --- a/src/Parser/Parser.php +++ /dev/null @@ -1,419 +0,0 @@ - - */ - public function parse(string $phql): array - { - if (strlen($phql) === 0) { - return []; - } - - $debug = null; - if ($this->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 (ScannerStatus::OK === ($scannerStatus = $scanner->scanForToken())) { - $this->token = $scanner->getToken(); - $parserStatus->setToken($this->token); - $state->setStartLength($codeLength - $state->getCursor()); - - $opcode = $this->token->opcode; - $state->setActiveToken($opcode); - - switch ($opcode) { - case Opcode::IGNORE: - break; - - case Opcode::ADD: - $parser->phql_(phql_Parser::PHQL_PLUS); - break; - - case Opcode::SUB: - $parser->phql_(phql_Parser::PHQL_MINUS); - break; - - case Opcode::MUL: - $parser->phql_(phql_Parser::PHQL_TIMES); - break; - - case Opcode::DIV: - $parser->phql_(phql_Parser::PHQL_DIVIDE); - break; - - case Opcode::MOD: - $parser->phql_(phql_Parser::PHQL_MOD); - break; - - case Opcode::AND: - $parser->phql_(phql_Parser::PHQL_AND); - break; - - case Opcode::OR: - $parser->phql_(phql_Parser::PHQL_OR); - break; - case Opcode::EQUALS: - $parser->phql_(phql_Parser::PHQL_EQUALS); - break; - case Opcode::NOTEQUALS: - $parser->phql_(phql_Parser::PHQL_NOTEQUALS); - break; - case Opcode::LESS: - $parser->phql_(phql_Parser::PHQL_LESS); - break; - case Opcode::GREATER: - $parser->phql_(phql_Parser::PHQL_GREATER); - break; - case Opcode::GREATEREQUAL: - $parser->phql_(phql_Parser::PHQL_GREATEREQUAL); - break; - case Opcode::LESSEQUAL: - $parser->phql_(phql_Parser::PHQL_LESSEQUAL); - break; - case Opcode::IDENTIFIER: - $this->phqlParseWithToken($parser, Opcode::IDENTIFIER, phql_Parser::PHQL_IDENTIFIER); - break; - - case Opcode::DOT: - $parser->phql_(phql_Parser::PHQL_DOT); - break; - case Opcode::COMMA: - $parser->phql_(phql_Parser::PHQL_COMMA); - break; - - case Opcode::PARENTHESES_OPEN: - $parser->phql_(phql_Parser::PHQL_PARENTHESES_OPEN); - break; - case Opcode::PARENTHESES_CLOSE: - $parser->phql_(phql_Parser::PHQL_PARENTHESES_CLOSE); - break; - - case Opcode::LIKE: - $parser->phql_(phql_Parser::PHQL_LIKE); - break; - case Opcode::ILIKE: - $parser->phql_(phql_Parser::PHQL_ILIKE); - break; - case Opcode::NOT: - $parser->phql_(phql_Parser::PHQL_NOT); - break; - case Opcode::BITWISE_AND: - $parser->phql_(phql_Parser::PHQL_BITWISE_AND); - break; - case Opcode::BITWISE_OR: - $parser->phql_(phql_Parser::PHQL_BITWISE_OR); - break; - case Opcode::BITWISE_NOT: - $parser->phql_(phql_Parser::PHQL_BITWISE_NOT); - break; - case Opcode::BITWISE_XOR: - $parser->phql_(phql_Parser::PHQL_BITWISE_XOR); - break; - case Opcode::AGAINST: - $parser->phql_(phql_Parser::PHQL_AGAINST); - break; - case Opcode::CASE: - $parser->phql_(phql_Parser::PHQL_CASE); - break; - case Opcode::WHEN: - $parser->phql_(phql_Parser::PHQL_WHEN); - break; - case Opcode::THEN: - $parser->phql_(phql_Parser::PHQL_THEN); - break; - case Opcode::END: - $parser->phql_(phql_Parser::PHQL_END); - break; - case Opcode::ELSE: - $parser->phql_(phql_Parser::PHQL_ELSE); - break; - case Opcode::FOR: - $parser->phql_(phql_Parser::PHQL_FOR); - break; - case Opcode::WITH: - $parser->phql_(phql_Parser::PHQL_WITH); - break; - - case Opcode::INTEGER: - if ($parserStatus->getEnableLiterals()) { - $this->phqlParseWithToken($parser, Opcode::INTEGER, phql_Parser::PHQL_INTEGER); - } else { - $parserStatus->setSyntaxError("Literals are disabled in PHQL statements"); - $parserStatus->setStatus(Status::PHQL_PARSING_FAILED); - } - break; - case Opcode::DOUBLE: - if ($parserStatus->getEnableLiterals()) { - $this->phqlParseWithToken($parser, Opcode::DOUBLE, phql_Parser::PHQL_DOUBLE); - } else { - $parserStatus->setSyntaxError("Literals are disabled in PHQL statements"); - $parserStatus->setStatus(Status::PHQL_PARSING_FAILED); - } - break; - case Opcode::STRING: - if ($parserStatus->getEnableLiterals()) { - $this->phqlParseWithToken($parser, Opcode::STRING, phql_Parser::PHQL_STRING); - } else { - $parserStatus->setSyntaxError("Literals are disabled in PHQL statements"); - $parserStatus->setStatus(Status::PHQL_PARSING_FAILED); - } - break; - case Opcode::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::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::HINTEGER: - if ($parserStatus->getEnableLiterals()) { - $this->phqlParseWithToken($parser, Opcode::HINTEGER, phql_Parser::PHQL_HINTEGER); - } else { - $parserStatus->setSyntaxError("Integers are disabled in PHQL statements"); - $parserStatus->setStatus(Status::PHQL_PARSING_FAILED); - } - break; - - case Opcode::NPLACEHOLDER: - $this->phqlParseWithToken($parser, Opcode::NPLACEHOLDER, phql_Parser::PHQL_NPLACEHOLDER); - break; - case Opcode::SPLACEHOLDER: - $this->phqlParseWithToken($parser, Opcode::SPLACEHOLDER, phql_Parser::PHQL_SPLACEHOLDER); - break; - case Opcode::BPLACEHOLDER: - $this->phqlParseWithToken($parser, Opcode::BPLACEHOLDER, phql_Parser::PHQL_BPLACEHOLDER); - break; - - case Opcode::FROM: - $parser->phql_(phql_Parser::PHQL_FROM); - break; - case Opcode::UPDATE: - $parser->phql_(phql_Parser::PHQL_UPDATE); - break; - case Opcode::SET: - $parser->phql_(phql_Parser::PHQL_SET); - break; - case Opcode::WHERE: - $parser->phql_(phql_Parser::PHQL_WHERE); - break; - case Opcode::DELETE: - $parser->phql_(phql_Parser::PHQL_DELETE); - break; - case Opcode::INSERT: - $parser->phql_(phql_Parser::PHQL_INSERT); - break; - case Opcode::INTO: - $parser->phql_(phql_Parser::PHQL_INTO); - break; - case Opcode::VALUES: - $parser->phql_(phql_Parser::PHQL_VALUES); - break; - case Opcode::SELECT: - $parser->phql_(phql_Parser::PHQL_SELECT); - break; - case Opcode::AS: - $parser->phql_(phql_Parser::PHQL_AS); - break; - case Opcode::ORDER: - $parser->phql_(phql_Parser::PHQL_ORDER); - break; - case Opcode::BY: - $parser->phql_(phql_Parser::PHQL_BY); - break; - case Opcode::LIMIT: - $parser->phql_(phql_Parser::PHQL_LIMIT); - break; - case Opcode::OFFSET: - $parser->phql_(phql_Parser::PHQL_OFFSET); - break; - case Opcode::GROUP: - $parser->phql_(phql_Parser::PHQL_GROUP); - break; - case Opcode::HAVING: - $parser->phql_(phql_Parser::PHQL_HAVING); - break; - case Opcode::ASC: - $parser->phql_(phql_Parser::PHQL_ASC); - break; - case Opcode::DESC: - $parser->phql_(phql_Parser::PHQL_DESC); - break; - case Opcode::IN: - $parser->phql_(phql_Parser::PHQL_IN); - break; - case Opcode::ON: - $parser->phql_(phql_Parser::PHQL_ON); - break; - case Opcode::INNER: - $parser->phql_(phql_Parser::PHQL_INNER); - break; - case Opcode::JOIN: - $parser->phql_(phql_Parser::PHQL_JOIN); - break; - case Opcode::LEFT: - $parser->phql_(phql_Parser::PHQL_LEFT); - break; - case Opcode::RIGHT: - $parser->phql_(phql_Parser::PHQL_RIGHT); - break; - case Opcode::CROSS: - $parser->phql_(phql_Parser::PHQL_CROSS); - break; - case Opcode::FULL: - $parser->phql_(phql_Parser::PHQL_FULL); - break; - case Opcode::OUTER: - $parser->phql_(phql_Parser::PHQL_OUTER); - break; - case Opcode::IS: - $parser->phql_(phql_Parser::PHQL_IS); - break; - case Opcode::NULL: - $parser->phql_(phql_Parser::PHQL_NULL); - break; - case Opcode::BETWEEN: - $parser->phql_(phql_Parser::PHQL_BETWEEN); - break; - case Opcode::BETWEEN_NOT: - $parser->phql_(phql_Parser::PHQL_BETWEEN_NOT); - break; - case Opcode::DISTINCT: - $parser->phql_(phql_Parser::PHQL_DISTINCT); - break; - case Opcode::ALL: - $parser->phql_(phql_Parser::PHQL_ALL); - break; - case Opcode::CAST: - $parser->phql_(phql_Parser::PHQL_CAST); - break; - case Opcode::CONVERT: - $parser->phql_(phql_Parser::PHQL_CONVERT); - break; - case Opcode::USING: - $parser->phql_(phql_Parser::PHQL_USING); - break; - case Opcode::EXISTS: - $parser->phql_(phql_Parser::PHQL_EXISTS); - break; - - default: - $parserStatus->setStatus(Status::PHQL_PARSING_FAILED); - $opcodeValue = $opcode !== null ? $opcode->value : ''; - $parserStatus->setSyntaxError("Scanner: Unknown opcode %d" . $opcodeValue); - break; - } - - if ($parserStatus->getStatus() === Status::PHQL_PARSING_FAILED) { - break; - } - } - - if ( - $scannerStatus === ScannerStatus::ERR - || $scannerStatus === ScannerStatus::IMPOSSIBLE - ) { - throw new Exception($parserStatus->getSyntaxError() ?? ''); - } elseif ($scannerStatus === ScannerStatus::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(null); - - if ($parserStatus->getStatus() !== Status::PHQL_PARSING_OK) { - throw new Exception($parserStatus->getSyntaxError() ?? ''); - } - - /** @var array */ - return $parser->getOutput(); - } - - private function phqlParseWithToken( - phql_Parser $parser, - Opcode $opcode, - int $parserCode, - ): void { - $newToken = new Token($opcode, $this->token?->value); - - $this->token = $newToken; - - $parser->phql_($parserCode, $newToken); - } -} diff --git a/src/Parser/Status.php b/src/Parser/Status.php index 3965b48..12fae80 100644 --- a/src/Parser/Status.php +++ b/src/Parser/Status.php @@ -19,7 +19,7 @@ class Status private ?Token $token = null; public function __construct( - private State $scannerState, + private readonly State $scannerState, private int $status = self::PHQL_PARSING_OK, ) { } diff --git a/src/Scanner/Scanner.php b/src/Scanner/Scanner.php index f992d46..0584986 100644 --- a/src/Scanner/Scanner.php +++ b/src/Scanner/Scanner.php @@ -13,7 +13,7 @@ class Scanner { private Token $token; - public function __construct(private State $state) + public function __construct(private readonly State $state) { $this->token = new Token(); } diff --git a/src/Scanner/State.php b/src/Scanner/State.php index 3170d6a..909649b 100644 --- a/src/Scanner/State.php +++ b/src/Scanner/State.php @@ -6,13 +6,13 @@ class State { - public string $rawBuffer; - public int $startLength; + private ?Opcode $activeToken = null; + private readonly int $bufferLength; - private int $bufferLength; private int $cursor = 0; - private ?Opcode $activeToken = null; + public readonly string $rawBuffer; private ?string $start = null; + public int $startLength; public function __construct(string $buffer) { 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/Phql/Select/AggregateTest.php b/tests/unit/Phql/Select/AggregateTest.php index 329664f..568d940 100644 --- a/tests/unit/Phql/Select/AggregateTest.php +++ b/tests/unit/Phql/Select/AggregateTest.php @@ -58,6 +58,48 @@ public function testMvcModelQueryPhqlSelectAvgField(): void $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 * diff --git a/tests/unit/Phql/Select/BetweenTest.php b/tests/unit/Phql/Select/BetweenTest.php index f9796a3..1cf50a7 100644 --- a/tests/unit/Phql/Select/BetweenTest.php +++ b/tests/unit/Phql/Select/BetweenTest.php @@ -68,6 +68,60 @@ public function testMvcModelQueryPhqlSelectBetweenFloat(): void $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 * @@ -117,6 +171,73 @@ public function testMvcModelQueryPhqlSelectBetweenInt(): void $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); + } + /** * @return void * diff --git a/tests/unit/Phql/Select/BracketsWithSpaceNameTest.php b/tests/unit/Phql/Select/BracketsWithSpaceNameTest.php index 19d84d2..10a7913 100644 --- a/tests/unit/Phql/Select/BracketsWithSpaceNameTest.php +++ b/tests/unit/Phql/Select/BracketsWithSpaceNameTest.php @@ -19,6 +19,52 @@ 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 * diff --git a/tests/unit/Phql/Select/LimitTest.php b/tests/unit/Phql/Select/LimitTest.php index c901e06..13ca3de 100644 --- a/tests/unit/Phql/Select/LimitTest.php +++ b/tests/unit/Phql/Select/LimitTest.php @@ -128,6 +128,44 @@ public function testMvcModelQueryPhqlSelectLimit(): void $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 * From eb4b37092f7061bd3d5370643318f3c3efbd9f39 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 11 Apr 2026 14:24:16 -0500 Subject: [PATCH 19/21] reformatting and more coverage --- tests/unit/Parser/StatusTest.php | 40 +- tests/unit/ParserTest.php | 97 +++-- tests/unit/Phql/Insert/CombinationTest.php | 240 +++++------ tests/unit/Phql/Select/AggregateTest.php | 28 +- tests/unit/Phql/Select/BasicTest.php | 76 ++-- tests/unit/Phql/Select/BetweenTest.php | 92 +++-- tests/unit/Phql/Select/BitwiseTest.php | 18 +- tests/unit/Phql/Select/ComplexTest.php | 209 +++++----- tests/unit/Phql/Select/DistinctTest.php | 9 +- tests/unit/Phql/Select/ForUpdateTest.php | 7 +- tests/unit/Phql/Select/FromTest.php | 85 ++-- tests/unit/Phql/Select/GroupByTest.php | 11 +- tests/unit/Phql/Select/HavingTest.php | 18 +- tests/unit/Phql/Select/JoinTest.php | 130 +++--- tests/unit/Phql/Select/LimitTest.php | 70 ++-- tests/unit/Phql/Select/NullTest.php | 12 +- tests/unit/Phql/Select/OperatorsTest.php | 100 ++--- tests/unit/Phql/Select/OrderByTest.php | 105 ++--- tests/unit/Phql/Select/ScalarTest.php | 168 ++++---- tests/unit/Phql/Select/SubqueriesTest.php | 381 +++++++++--------- tests/unit/Phql/Select/WhereLogicalTest.php | 90 ++--- .../Phql/Select/WherePlaceholdersTest.php | 75 ++-- tests/unit/Phql/Select/WhereTest.php | 124 +++--- tests/unit/Phql/Update/CombinationTest.php | 236 +++++------ tests/unit/Scanner/OpcodeTest.php | 52 +-- tests/unit/Scanner/ScannerTest.php | 213 +++++----- tests/unit/Scanner/StateTest.php | 38 +- tests/unit/Scanner/TokenTest.php | 18 +- 28 files changed, 1411 insertions(+), 1331 deletions(-) diff --git a/tests/unit/Parser/StatusTest.php b/tests/unit/Parser/StatusTest.php index 97a0266..64663e7 100644 --- a/tests/unit/Parser/StatusTest.php +++ b/tests/unit/Parser/StatusTest.php @@ -22,61 +22,61 @@ public function testDefaultState(): void $this->assertFalse($status->getEnableLiterals()); } - public function testSetAndGetAst(): void + public function testEnableLiterals(): void { $state = new State('SELECT'); $status = new Status($state); - $ast = ['type' => 309, 'select' => []]; - $status->setAst($ast); + $status->setEnableLiterals(true); - $this->assertSame($ast, $status->getAst()); + $this->assertTrue($status->getEnableLiterals()); } - public function testSetStatus(): void + public function testGetState(): void { $state = new State('SELECT'); $status = new Status($state); - $status->setStatus(Status::PHQL_PARSING_FAILED); - - $this->assertSame(Status::PHQL_PARSING_FAILED, $status->getStatus()); + $this->assertSame($state, $status->getState()); } - public function testSetSyntaxError(): void + public function testNoGetRetMethod(): void { $state = new State('SELECT'); $status = new Status($state); - $status->setSyntaxError('Unexpected token'); - - $this->assertSame('Unexpected token', $status->getSyntaxError()); + $this->assertFalse(method_exists($status, 'getRet')); + $this->assertFalse(method_exists($status, 'setRet')); } - public function testEnableLiterals(): void + public function testSetAndGetAst(): void { $state = new State('SELECT'); $status = new Status($state); + $ast = ['type' => 309, 'select' => []]; - $status->setEnableLiterals(true); + $status->setAst($ast); - $this->assertTrue($status->getEnableLiterals()); + $this->assertSame($ast, $status->getAst()); } - public function testGetState(): void + public function testSetStatus(): void { $state = new State('SELECT'); $status = new Status($state); - $this->assertSame($state, $status->getState()); + $status->setStatus(Status::PHQL_PARSING_FAILED); + + $this->assertSame(Status::PHQL_PARSING_FAILED, $status->getStatus()); } - public function testNoGetRetMethod(): void + public function testSetSyntaxError(): void { $state = new State('SELECT'); $status = new Status($state); - $this->assertFalse(method_exists($status, 'getRet')); - $this->assertFalse(method_exists($status, 'setRet')); + $status->setSyntaxError('Unexpected token'); + + $this->assertSame('Unexpected token', $status->getSyntaxError()); } } diff --git a/tests/unit/ParserTest.php b/tests/unit/ParserTest.php index 9e79c1a..279160a 100644 --- a/tests/unit/ParserTest.php +++ b/tests/unit/ParserTest.php @@ -11,43 +11,36 @@ final class ParserTest extends AbstractUnitTestCase { - public function testParseEmptyStringThrows(): void + public function testLiteralsDisabledBlocksDouble(): void { $this->expectException(Exception::class); - $this->expectExceptionMessage('PHQL statement cannot be NULL'); + $this->expectExceptionMessage('Literals are disabled in PHQL statements'); - (new Parser())->parse(''); + (new Parser())->setEnableLiterals(false)->parse('SELECT 1.5 FROM Invoices'); } - public function testSetEnableLiteralsReturnsSelf(): void + public function testLiteralsDisabledBlocksFalse(): void { - $parser = new Parser(); - $result = $parser->setEnableLiterals(false); - - $this->assertSame($parser, $result); - } + $this->expectException(Exception::class); + $this->expectExceptionMessage('Literals are disabled in PHQL statements'); - public function testSetEnableLiteralsChaining(): void - { - // Should not throw — fluent chaining works - $result = (new Parser())->setEnableLiterals(true)->parse('SELECT * FROM Invoices'); - $this->assertIsArray($result); + (new Parser())->setEnableLiterals(false)->parse('SELECT FALSE FROM Invoices'); } - public function testLiteralsDisabledBlocksInteger(): void + public function testLiteralsDisabledBlocksHinteger(): void { $this->expectException(Exception::class); $this->expectExceptionMessage('Literals are disabled in PHQL statements'); - (new Parser())->setEnableLiterals(false)->parse('SELECT 1 FROM Invoices'); + (new Parser())->setEnableLiterals(false)->parse('SELECT 0xFF FROM Invoices'); } - public function testLiteralsDisabledBlocksDouble(): void + public function testLiteralsDisabledBlocksInteger(): void { $this->expectException(Exception::class); $this->expectExceptionMessage('Literals are disabled in PHQL statements'); - (new Parser())->setEnableLiterals(false)->parse('SELECT 1.5 FROM Invoices'); + (new Parser())->setEnableLiterals(false)->parse('SELECT 1 FROM Invoices'); } public function testLiteralsDisabledBlocksString(): void @@ -66,20 +59,60 @@ public function testLiteralsDisabledBlocksTrue(): void (new Parser())->setEnableLiterals(false)->parse('SELECT TRUE FROM Invoices'); } - public function testLiteralsDisabledBlocksFalse(): void + 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('Literals are disabled in PHQL statements'); + $this->expectExceptionMessage('PHQL statement cannot be NULL'); - (new Parser())->setEnableLiterals(false)->parse('SELECT FALSE FROM Invoices'); + (new Parser())->parse(''); } - public function testLiteralsDisabledBlocksHinteger(): void + 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->expectExceptionMessage('Literals are disabled in PHQL statements'); + $this->expectExceptionMessageMatches('/Scanning error before/'); + $this->expectExceptionMessageMatches('/\.\.\./'); - (new Parser())->setEnableLiterals(false)->parse('SELECT 0xFF FROM Invoices'); + (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 @@ -92,19 +125,11 @@ public function testThrowsPhqlException(): void } } - public function testParseSimpleSelect(): void + public function testUnknownOpcodeThrows(): void { - $result = (new Parser())->parse('SELECT * FROM Invoices'); - - $this->assertIsArray($result); - $this->assertSame(Opcode::SELECT->value, $result['type']); - $this->assertArrayHasKey('select', $result); - } + $this->expectException(Exception::class); + $this->expectExceptionMessageMatches('/Unknown opcode/'); - 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); + (new Parser())->parse('SELECT : FROM Invoices'); } } diff --git a/tests/unit/Phql/Insert/CombinationTest.php b/tests/unit/Phql/Insert/CombinationTest.php index d67d5c7..565885a 100644 --- a/tests/unit/Phql/Insert/CombinationTest.php +++ b/tests/unit/Phql/Insert/CombinationTest.php @@ -19,6 +19,71 @@ 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 * @@ -69,12 +134,11 @@ public function testMvcModelQueryPhqlInsert(): void * @return void * * @author Phalcon Team - * @since 2026-04-09 + * @since 2026-04-10 */ - public function testMvcModelQueryPhqInsertFields(): void + public function testMvcModelQueryPhqlInsertArithmeticInValues(): 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')"; + $source = "INSERT INTO Invoices (inv_total) VALUES (100 + 50)"; $expected = [ 'type' => Opcode::INSERT->value, 'qualifiedName' => [ @@ -83,46 +147,21 @@ public function testMvcModelQueryPhqInsertFields(): void ], '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', + 'type' => Opcode::ADD->value, + 'left' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '100', + ], + 'right' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '50', + ], ], ], ]; @@ -266,54 +305,6 @@ public function testMvcModelQueryPhqlInsertFieldsPlaceholders(): void $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 * @@ -383,11 +374,11 @@ public function testMvcModelQueryPhqlInsertFieldsPlaceholdersBrackets(): void * @return void * * @author Phalcon Team - * @since 2026-04-10 + * @since 2026-04-09 */ - public function testMvcModelQueryPhqlInsertArithmeticInValues(): void + public function testMvcModelQueryPhqlInsertFieldsPlaceholdersNum(): void { - $source = "INSERT INTO Invoices (inv_total) VALUES (100 + 50)"; + $source = "INSERT INTO Invoices " . "(inv_cst_id, inv_title, inv_total) " . "VALUES (?0, ?1, ?2)"; $expected = [ 'type' => Opcode::INSERT->value, 'qualifiedName' => [ @@ -396,21 +387,30 @@ public function testMvcModelQueryPhqlInsertArithmeticInValues(): void ], '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::ADD->value, - 'left' => [ - 'type' => Opcode::INTEGER->value, - 'value' => '100', - ], - 'right' => [ - 'type' => Opcode::INTEGER->value, - 'value' => '50', - ], + 'type' => Opcode::NPLACEHOLDER->value, + 'value' => '?0', + ], + 1 => [ + 'type' => Opcode::NPLACEHOLDER->value, + 'value' => '?1', + ], + 2 => [ + 'type' => Opcode::NPLACEHOLDER->value, + 'value' => '?2', ], ], ]; @@ -422,12 +422,11 @@ public function testMvcModelQueryPhqlInsertArithmeticInValues(): void * @return void * * @author Phalcon Team - * @since 2026-04-10 + * @since 2026-04-09 */ - public function testMvcModelQueryPhqlInsertFuncInValues(): void + public function testMvcModelQueryPhqlInsertFieldsTrue(): void { - $source = "INSERT INTO Invoices (inv_title, inv_total) " - . "VALUES (UPPER('test invoice'), 100.00)"; + $source = "INSERT INTO Invoices (inv_title, inv_status_flag) VALUES ('New Invoice', TRUE)"; $expected = [ 'type' => Opcode::INSERT->value, 'qualifiedName' => [ @@ -441,23 +440,16 @@ public function testMvcModelQueryPhqlInsertFuncInValues(): void ], 1 => [ 'type' => Opcode::QUALIFIED->value, - 'name' => 'inv_total', + 'name' => 'inv_status_flag', ], ], 'values' => [ 0 => [ - 'type' => Opcode::FCALL->value, - 'name' => 'UPPER', - 'arguments' => [ - 0 => [ - 'type' => Opcode::STRING->value, - 'value' => 'test invoice', - ], - ], + 'type' => Opcode::STRING->value, + 'value' => 'New Invoice', ], 1 => [ - 'type' => Opcode::DOUBLE->value, - 'value' => '100.00', + 'type' => Opcode::TRUE->value, ], ], ]; @@ -469,11 +461,12 @@ public function testMvcModelQueryPhqlInsertFuncInValues(): void * @return void * * @author Phalcon Team - * @since 2026-04-09 + * @since 2026-04-10 */ - public function testMvcModelQueryPhqlInsertFieldsTrue(): void + public function testMvcModelQueryPhqlInsertFuncInValues(): void { - $source = "INSERT INTO Invoices (inv_title, inv_status_flag) VALUES ('New Invoice', TRUE)"; + $source = "INSERT INTO Invoices (inv_title, inv_total) " + . "VALUES (UPPER('test invoice'), 100.00)"; $expected = [ 'type' => Opcode::INSERT->value, 'qualifiedName' => [ @@ -487,16 +480,23 @@ public function testMvcModelQueryPhqlInsertFieldsTrue(): void ], 1 => [ 'type' => Opcode::QUALIFIED->value, - 'name' => 'inv_status_flag', + 'name' => 'inv_total', ], ], 'values' => [ 0 => [ - 'type' => Opcode::STRING->value, - 'value' => 'New Invoice', + 'type' => Opcode::FCALL->value, + 'name' => 'UPPER', + 'arguments' => [ + 0 => [ + 'type' => Opcode::STRING->value, + 'value' => 'test invoice', + ], + ], ], 1 => [ - 'type' => Opcode::TRUE->value, + 'type' => Opcode::DOUBLE->value, + 'value' => '100.00', ], ], ]; diff --git a/tests/unit/Phql/Select/AggregateTest.php b/tests/unit/Phql/Select/AggregateTest.php index 568d940..4909c30 100644 --- a/tests/unit/Phql/Select/AggregateTest.php +++ b/tests/unit/Phql/Select/AggregateTest.php @@ -313,9 +313,9 @@ public function testMvcModelQueryPhqlSelectCountSumAvgMinMax(): void * @author Phalcon Team * @since 2026-04-09 */ - public function testMvcModelQueryPhqlSelectSumField(): void + public function testMvcModelQueryPhqlSelectMaxDate(): void { - $source = "SELECT SUM(inv_total) FROM Invoices"; + $source = "SELECT MAX(inv_created_at) FROM Invoices"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -324,11 +324,11 @@ public function testMvcModelQueryPhqlSelectSumField(): void 'type' => Opcode::EXPR->value, 'column' => [ 'type' => Opcode::FCALL->value, - 'name' => 'SUM', + 'name' => 'MAX', 'arguments' => [ 0 => [ 'type' => Opcode::QUALIFIED->value, - 'name' => 'inv_total', + 'name' => 'inv_created_at', ], ], ], @@ -352,9 +352,9 @@ public function testMvcModelQueryPhqlSelectSumField(): void * @author Phalcon Team * @since 2026-04-09 */ - public function testMvcModelQueryPhqlSelectMinField(): void + public function testMvcModelQueryPhqlSelectMaxField(): void { - $source = "SELECT MIN(inv_total) FROM Invoices"; + $source = "SELECT MAX(inv_total) FROM Invoices"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -363,7 +363,7 @@ public function testMvcModelQueryPhqlSelectMinField(): void 'type' => Opcode::EXPR->value, 'column' => [ 'type' => Opcode::FCALL->value, - 'name' => 'MIN', + 'name' => 'MAX', 'arguments' => [ 0 => [ 'type' => Opcode::QUALIFIED->value, @@ -430,9 +430,9 @@ public function testMvcModelQueryPhqlSelectMinDate(): void * @author Phalcon Team * @since 2026-04-09 */ - public function testMvcModelQueryPhqlSelectMaxDate(): void + public function testMvcModelQueryPhqlSelectMinField(): void { - $source = "SELECT MAX(inv_created_at) FROM Invoices"; + $source = "SELECT MIN(inv_total) FROM Invoices"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -441,11 +441,11 @@ public function testMvcModelQueryPhqlSelectMaxDate(): void 'type' => Opcode::EXPR->value, 'column' => [ 'type' => Opcode::FCALL->value, - 'name' => 'MAX', + 'name' => 'MIN', 'arguments' => [ 0 => [ 'type' => Opcode::QUALIFIED->value, - 'name' => 'inv_created_at', + 'name' => 'inv_total', ], ], ], @@ -469,9 +469,9 @@ public function testMvcModelQueryPhqlSelectMaxDate(): void * @author Phalcon Team * @since 2026-04-09 */ - public function testMvcModelQueryPhqlSelectMaxField(): void + public function testMvcModelQueryPhqlSelectSumField(): void { - $source = "SELECT MAX(inv_total) FROM Invoices"; + $source = "SELECT SUM(inv_total) FROM Invoices"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -480,7 +480,7 @@ public function testMvcModelQueryPhqlSelectMaxField(): void 'type' => Opcode::EXPR->value, 'column' => [ 'type' => Opcode::FCALL->value, - 'name' => 'MAX', + 'name' => 'SUM', 'arguments' => [ 0 => [ 'type' => Opcode::QUALIFIED->value, diff --git a/tests/unit/Phql/Select/BasicTest.php b/tests/unit/Phql/Select/BasicTest.php index 64be0d5..811160f 100644 --- a/tests/unit/Phql/Select/BasicTest.php +++ b/tests/unit/Phql/Select/BasicTest.php @@ -115,9 +115,9 @@ public function testMvcModelQueryPhqlSelectAllTable(): void * @author Phalcon Team * @since 2026-04-09 */ - public function testMvcModelQueryPhqlSelectInt(): void + public function testMvcModelQueryPhqlSelectIdStringFloat(): void { - $source = "SELECT inv_id FROM Invoices"; + $source = "SELECT inv_id, inv_title, inv_total FROM Invoices"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -129,6 +129,20 @@ public function testMvcModelQueryPhqlSelectInt(): void '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' => [ @@ -148,9 +162,9 @@ public function testMvcModelQueryPhqlSelectInt(): void * @author Phalcon Team * @since 2026-04-09 */ - public function testMvcModelQueryPhqlSelectIntString(): void + public function testMvcModelQueryPhqlSelectInt(): void { - $source = "SELECT inv_id, inv_title FROM Invoices"; + $source = "SELECT inv_id FROM Invoices"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -162,13 +176,6 @@ public function testMvcModelQueryPhqlSelectIntString(): void 'name' => 'inv_id', ], ], - 1 => [ - 'type' => Opcode::EXPR->value, - 'column' => [ - 'type' => Opcode::QUALIFIED->value, - 'name' => 'inv_title', - ], - ], ], 'tables' => [ 'qualifiedName' => [ @@ -188,9 +195,9 @@ public function testMvcModelQueryPhqlSelectIntString(): void * @author Phalcon Team * @since 2026-04-09 */ - public function testMvcModelQueryPhqlSelectIdStringFloat(): void + public function testMvcModelQueryPhqlSelectIntAliased(): void { - $source = "SELECT inv_id, inv_title, inv_total FROM Invoices"; + $source = "SELECT i.inv_id FROM Invoices i"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -199,21 +206,8 @@ public function testMvcModelQueryPhqlSelectIdStringFloat(): void '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', + 'domain' => 'i', + 'name' => 'inv_id', ], ], ], @@ -222,6 +216,7 @@ public function testMvcModelQueryPhqlSelectIdStringFloat(): void 'type' => Opcode::QUALIFIED->value, 'name' => 'Invoices', ], + 'alias' => 'i', ], ], ]; @@ -235,9 +230,9 @@ public function testMvcModelQueryPhqlSelectIdStringFloat(): void * @author Phalcon Team * @since 2026-04-09 */ - public function testMvcModelQueryPhqlSelectIntAliased(): void + public function testMvcModelQueryPhqlSelectIntAliasedAs(): void { - $source = "SELECT i.inv_id FROM Invoices i"; + $source = "SELECT i.inv_id FROM Invoices AS i"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -270,9 +265,9 @@ public function testMvcModelQueryPhqlSelectIntAliased(): void * @author Phalcon Team * @since 2026-04-09 */ - public function testMvcModelQueryPhqlSelectIntAliasedAs(): void + public function testMvcModelQueryPhqlSelectIntAliasedTable(): void { - $source = "SELECT i.inv_id FROM Invoices AS i"; + $source = "SELECT Invoices.inv_id FROM Invoices"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -281,7 +276,7 @@ public function testMvcModelQueryPhqlSelectIntAliasedAs(): void 'type' => Opcode::EXPR->value, 'column' => [ 'type' => Opcode::QUALIFIED->value, - 'domain' => 'i', + 'domain' => 'Invoices', 'name' => 'inv_id', ], ], @@ -291,7 +286,6 @@ public function testMvcModelQueryPhqlSelectIntAliasedAs(): void 'type' => Opcode::QUALIFIED->value, 'name' => 'Invoices', ], - 'alias' => 'i', ], ], ]; @@ -305,9 +299,9 @@ public function testMvcModelQueryPhqlSelectIntAliasedAs(): void * @author Phalcon Team * @since 2026-04-09 */ - public function testMvcModelQueryPhqlSelectIntAliasedTable(): void + public function testMvcModelQueryPhqlSelectIntString(): void { - $source = "SELECT Invoices.inv_id FROM Invoices"; + $source = "SELECT inv_id, inv_title FROM Invoices"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -316,8 +310,14 @@ public function testMvcModelQueryPhqlSelectIntAliasedTable(): void 'type' => Opcode::EXPR->value, 'column' => [ 'type' => Opcode::QUALIFIED->value, - 'domain' => 'Invoices', - 'name' => 'inv_id', + 'name' => 'inv_id', + ], + ], + 1 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_title', ], ], ], diff --git a/tests/unit/Phql/Select/BetweenTest.php b/tests/unit/Phql/Select/BetweenTest.php index 1cf50a7..3ab1275 100644 --- a/tests/unit/Phql/Select/BetweenTest.php +++ b/tests/unit/Phql/Select/BetweenTest.php @@ -175,43 +175,25 @@ public function testMvcModelQueryPhqlSelectBetweenInt(): void * @return void * * @author Phalcon Team - * @since 2026-04-11 + * @since 2026-04-09 */ - public function testMvcModelQueryPhqlSelectNotBetweenIntMultipleColumns(): void + public function testMvcModelQueryPhqlSelectNotBetweenFloat(): void { - $source = "SELECT Id, ProductName, UnitPrice " - . "FROM Product " - . "WHERE UnitPrice NOT BETWEEN 5 AND 100"; + $source = "SELECT * " + . "FROM Invoices " + . "WHERE inv_total NOT BETWEEN 10.00 AND 500.00"; $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', - ], + 'type' => Opcode::STARALL->value, ], ], 'tables' => [ 'qualifiedName' => [ 'type' => Opcode::QUALIFIED->value, - 'name' => 'Product', + 'name' => 'Invoices', ], ], ], @@ -219,17 +201,17 @@ public function testMvcModelQueryPhqlSelectNotBetweenIntMultipleColumns(): void 'type' => Opcode::BETWEEN_NOT->value, 'left' => [ 'type' => Opcode::QUALIFIED->value, - 'name' => 'UnitPrice', + 'name' => 'inv_total', ], 'right' => [ 'type' => Opcode::AND->value, 'left' => [ - 'type' => Opcode::INTEGER->value, - 'value' => '5', + 'type' => Opcode::DOUBLE->value, + 'value' => '10.00', ], 'right' => [ - 'type' => Opcode::INTEGER->value, - 'value' => '100', + 'type' => Opcode::DOUBLE->value, + 'value' => '500.00', ], ], ], @@ -244,9 +226,11 @@ public function testMvcModelQueryPhqlSelectNotBetweenIntMultipleColumns(): void * @author Phalcon Team * @since 2026-04-09 */ - public function testMvcModelQueryPhqlSelectNotBetweenFloat(): void + public function testMvcModelQueryPhqlSelectNotBetweenInt(): void { - $source = "SELECT * " . "FROM Invoices " . "WHERE inv_total NOT BETWEEN 10.00 AND 500.00"; + $source = "SELECT * " + . "FROM Invoices " + . "WHERE inv_id NOT BETWEEN 1 AND 100"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -266,17 +250,17 @@ public function testMvcModelQueryPhqlSelectNotBetweenFloat(): void 'type' => Opcode::BETWEEN_NOT->value, 'left' => [ 'type' => Opcode::QUALIFIED->value, - 'name' => 'inv_total', + 'name' => 'inv_id', ], 'right' => [ 'type' => Opcode::AND->value, 'left' => [ - 'type' => Opcode::DOUBLE->value, - 'value' => '10.00', + 'type' => Opcode::INTEGER->value, + 'value' => '1', ], 'right' => [ - 'type' => Opcode::DOUBLE->value, - 'value' => '500.00', + 'type' => Opcode::INTEGER->value, + 'value' => '100', ], ], ], @@ -289,23 +273,43 @@ public function testMvcModelQueryPhqlSelectNotBetweenFloat(): void * @return void * * @author Phalcon Team - * @since 2026-04-09 + * @since 2026-04-11 */ - public function testMvcModelQueryPhqlSelectNotBetweenInt(): void + public function testMvcModelQueryPhqlSelectNotBetweenIntMultipleColumns(): void { - $source = "SELECT * " . "FROM Invoices " . "WHERE inv_id NOT BETWEEN 1 AND 100"; + $source = "SELECT Id, ProductName, UnitPrice " + . "FROM Product " + . "WHERE UnitPrice NOT BETWEEN 5 AND 100"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ 'columns' => [ 0 => [ - 'type' => Opcode::STARALL->value, + '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' => 'Invoices', + 'name' => 'Product', ], ], ], @@ -313,13 +317,13 @@ public function testMvcModelQueryPhqlSelectNotBetweenInt(): void 'type' => Opcode::BETWEEN_NOT->value, 'left' => [ 'type' => Opcode::QUALIFIED->value, - 'name' => 'inv_id', + 'name' => 'UnitPrice', ], 'right' => [ 'type' => Opcode::AND->value, 'left' => [ 'type' => Opcode::INTEGER->value, - 'value' => '1', + 'value' => '5', ], 'right' => [ 'type' => Opcode::INTEGER->value, diff --git a/tests/unit/Phql/Select/BitwiseTest.php b/tests/unit/Phql/Select/BitwiseTest.php index 3613b97..9c3eede 100644 --- a/tests/unit/Phql/Select/BitwiseTest.php +++ b/tests/unit/Phql/Select/BitwiseTest.php @@ -27,7 +27,9 @@ final class BitwiseTest extends AbstractUnitTestCase */ public function testMvcModelQueryPhqlSelectBitwiseAnd(): void { - $source = "SELECT * " . "FROM Invoices " . "WHERE inv_status_flag & 1 = 1"; + $source = "SELECT * " + . "FROM Invoices " + . "WHERE inv_status_flag & 1 = 1"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -74,7 +76,8 @@ public function testMvcModelQueryPhqlSelectBitwiseAnd(): void */ public function testMvcModelQueryPhqlSelectBitwiseInField(): void { - $source = "SELECT inv_status_flag & 3 AS masked " . "FROM Invoices"; + $source = "SELECT inv_status_flag & 3 AS masked " + . "FROM Invoices"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -115,7 +118,8 @@ public function testMvcModelQueryPhqlSelectBitwiseInField(): void */ public function testMvcModelQueryPhqlSelectBitwiseNotField(): void { - $source = "SELECT ~inv_status_flag " . "FROM Invoices"; + $source = "SELECT ~inv_status_flag " + . "FROM Invoices"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -151,7 +155,9 @@ public function testMvcModelQueryPhqlSelectBitwiseNotField(): void */ public function testMvcModelQueryPhqlSelectBitwiseOr(): void { - $source = "SELECT * " . "FROM Invoices " . "WHERE inv_status_flag | 2 = 3"; + $source = "SELECT * " + . "FROM Invoices " + . "WHERE inv_status_flag | 2 = 3"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -198,7 +204,9 @@ public function testMvcModelQueryPhqlSelectBitwiseOr(): void */ public function testMvcModelQueryPhqlSelectBitwiseXor(): void { - $source = "SELECT * " . "FROM Invoices " . "WHERE inv_status_flag ^ 1 = 0"; + $source = "SELECT * " + . "FROM Invoices " + . "WHERE inv_status_flag ^ 1 = 0"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ diff --git a/tests/unit/Phql/Select/ComplexTest.php b/tests/unit/Phql/Select/ComplexTest.php index 5b00c5c..932143e 100644 --- a/tests/unit/Phql/Select/ComplexTest.php +++ b/tests/unit/Phql/Select/ComplexTest.php @@ -19,6 +19,111 @@ 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 * @@ -199,108 +304,4 @@ public function testMvcModelQueryPhqlSelectCountFieldWhereGroupByOrderBy(): void $this->assertSame($expected, $actual); } - /** - * @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); - } } diff --git a/tests/unit/Phql/Select/DistinctTest.php b/tests/unit/Phql/Select/DistinctTest.php index 13b01c7..f9e643d 100644 --- a/tests/unit/Phql/Select/DistinctTest.php +++ b/tests/unit/Phql/Select/DistinctTest.php @@ -27,7 +27,8 @@ final class DistinctTest extends AbstractUnitTestCase */ public function testMvcModelQueryPhqlSelectAll(): void { - $source = "SELECT ALL inv_status_flag " . "FROM Invoices"; + $source = "SELECT ALL inv_status_flag " + . "FROM Invoices"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -61,7 +62,8 @@ public function testMvcModelQueryPhqlSelectAll(): void */ public function testMvcModelQueryPhqlSelectDistinct(): void { - $source = "SELECT DISTINCT inv_status_flag " . "FROM Invoices"; + $source = "SELECT DISTINCT inv_status_flag " + . "FROM Invoices"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -95,7 +97,8 @@ public function testMvcModelQueryPhqlSelectDistinct(): void */ public function testMvcModelQueryPhqlSelectDistinctInt(): void { - $source = "SELECT DISTINCT inv_cst_id, inv_status_flag " . "FROM Invoices"; + $source = "SELECT DISTINCT inv_cst_id, inv_status_flag " + . "FROM Invoices"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ diff --git a/tests/unit/Phql/Select/ForUpdateTest.php b/tests/unit/Phql/Select/ForUpdateTest.php index dd95b79..5415e3e 100644 --- a/tests/unit/Phql/Select/ForUpdateTest.php +++ b/tests/unit/Phql/Select/ForUpdateTest.php @@ -27,7 +27,8 @@ final class ForUpdateTest extends AbstractUnitTestCase */ public function testMvcModelQueryPhqlSelectForUpdate(): void { - $source = "SELECT * " . "FROM Invoices FOR UPDATE"; + $source = "SELECT * " + . "FROM Invoices FOR UPDATE"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -58,7 +59,9 @@ public function testMvcModelQueryPhqlSelectForUpdate(): void */ public function testMvcModelQueryPhqlSelectForUpdateWhere(): void { - $source = "SELECT * " . "FROM Invoices " . "WHERE inv_id = 1 FOR UPDATE"; + $source = "SELECT * " + . "FROM Invoices " + . "WHERE inv_id = 1 FOR UPDATE"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ diff --git a/tests/unit/Phql/Select/FromTest.php b/tests/unit/Phql/Select/FromTest.php index 86254b9..65f88c5 100644 --- a/tests/unit/Phql/Select/FromTest.php +++ b/tests/unit/Phql/Select/FromTest.php @@ -23,17 +23,32 @@ final class FromTest extends AbstractUnitTestCase * @return void * * @author Phalcon Team - * @since 2026-04-10 + * @since 2026-04-09 */ - public function testMvcModelQueryPhqlSelectFromMultipleTables(): void + public function testMvcModelQueryPhqlSelectFromAliases(): void { - $source = "SELECT * FROM Invoices, Customers"; + $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::STARALL->value, + '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' => [ @@ -42,15 +57,30 @@ public function testMvcModelQueryPhqlSelectFromMultipleTables(): void '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); @@ -62,10 +92,9 @@ public function testMvcModelQueryPhqlSelectFromMultipleTables(): void * @author Phalcon Team * @since 2026-04-10 */ - public function testMvcModelQueryPhqlSelectFromMultipleTablesWhere(): void + public function testMvcModelQueryPhqlSelectFromMultipleTables(): void { - $source = "SELECT * FROM Invoices, Customers " - . "WHERE Invoices.inv_cst_id = Customers.id"; + $source = "SELECT * FROM Invoices, Customers"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -89,19 +118,6 @@ public function testMvcModelQueryPhqlSelectFromMultipleTablesWhere(): void ], ], ], - '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); @@ -111,30 +127,18 @@ public function testMvcModelQueryPhqlSelectFromMultipleTablesWhere(): void * @return void * * @author Phalcon Team - * @since 2026-04-09 + * @since 2026-04-10 */ - public function testMvcModelQueryPhqlSelectFromAliases(): void + public function testMvcModelQueryPhqlSelectFromMultipleTablesWhere(): void { - $source = "SELECT i.inv_id, c.name " . "FROM Invoices AS i, Customers AS c " . "WHERE i.inv_cst_id = c.id"; + $source = "SELECT * FROM Invoices, Customers " + . "WHERE Invoices.inv_cst_id = Customers.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', - ], + 'type' => Opcode::STARALL->value, ], ], 'tables' => [ @@ -143,14 +147,12 @@ public function testMvcModelQueryPhqlSelectFromAliases(): void 'type' => Opcode::QUALIFIED->value, 'name' => 'Invoices', ], - 'alias' => 'i', ], 1 => [ 'qualifiedName' => [ 'type' => Opcode::QUALIFIED->value, 'name' => 'Customers', ], - 'alias' => 'c', ], ], ], @@ -158,12 +160,12 @@ public function testMvcModelQueryPhqlSelectFromAliases(): void 'type' => Opcode::EQUALS->value, 'left' => [ 'type' => Opcode::QUALIFIED->value, - 'domain' => 'i', + 'domain' => 'Invoices', 'name' => 'inv_cst_id', ], 'right' => [ 'type' => Opcode::QUALIFIED->value, - 'domain' => 'c', + 'domain' => 'Customers', 'name' => 'id', ], ], @@ -171,4 +173,5 @@ public function testMvcModelQueryPhqlSelectFromAliases(): void $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 index 75e0ee4..428a2be 100644 --- a/tests/unit/Phql/Select/GroupByTest.php +++ b/tests/unit/Phql/Select/GroupByTest.php @@ -27,7 +27,9 @@ final class GroupByTest extends AbstractUnitTestCase */ public function testMvcModelQueryPhqlSelectGroupBy(): void { - $source = "SELECT inv_status_flag, COUNT(*) " . "FROM Invoices " . "GROUP BY inv_status_flag"; + $source = "SELECT inv_status_flag, COUNT(*) " + . "FROM Invoices " + . "GROUP BY inv_status_flag"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -76,7 +78,8 @@ public function testMvcModelQueryPhqlSelectGroupBy(): void */ public function testMvcModelQueryPhqlSelectGroupByCountField(): void { - $source = "SELECT inv_cst_id, inv_status_flag, COUNT(*) " . "FROM Invoices " . + $source = "SELECT inv_cst_id, inv_status_flag, COUNT(*) " + . "FROM Invoices " . "GROUP BY inv_cst_id, inv_status_flag"; $expected = [ 'type' => Opcode::SELECT->value, @@ -139,7 +142,9 @@ public function testMvcModelQueryPhqlSelectGroupByCountField(): void */ public function testMvcModelQueryPhqlSelectGroupBySumField(): void { - $source = "SELECT inv_cst_id, SUM(inv_total) " . "FROM Invoices " . "GROUP BY inv_cst_id"; + $source = "SELECT inv_cst_id, SUM(inv_total) " + . "FROM Invoices " + . "GROUP BY inv_cst_id"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ diff --git a/tests/unit/Phql/Select/HavingTest.php b/tests/unit/Phql/Select/HavingTest.php index 92f8118..d8b1367 100644 --- a/tests/unit/Phql/Select/HavingTest.php +++ b/tests/unit/Phql/Select/HavingTest.php @@ -27,8 +27,10 @@ final class HavingTest extends AbstractUnitTestCase */ public function testMvcModelQueryPhqlSelectHavingCountAll(): void { - $source = "SELECT inv_status_flag, COUNT(*) AS cnt " . "FROM Invoices " . "GROUP BY inv_status_flag " . - "HAVING COUNT(*) > 5"; + $source = "SELECT inv_status_flag, COUNT(*) AS cnt " + . "FROM Invoices " + . "GROUP BY inv_status_flag " + . "HAVING COUNT(*) > 5"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -94,8 +96,10 @@ public function testMvcModelQueryPhqlSelectHavingCountAll(): void */ public function testMvcModelQueryPhqlSelectHavingCountField(): void { - $source = "SELECT inv_cst_id, COUNT(*) AS cnt " . "FROM Invoices " . "GROUP BY inv_cst_id " . - "HAVING cnt > 10"; + $source = "SELECT inv_cst_id, COUNT(*) AS cnt " + . "FROM Invoices " + . "GROUP BY inv_cst_id " + . "HAVING cnt > 10"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -156,8 +160,10 @@ public function testMvcModelQueryPhqlSelectHavingCountField(): void */ 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"; + $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' => [ diff --git a/tests/unit/Phql/Select/JoinTest.php b/tests/unit/Phql/Select/JoinTest.php index 3090cd8..247398f 100644 --- a/tests/unit/Phql/Select/JoinTest.php +++ b/tests/unit/Phql/Select/JoinTest.php @@ -23,13 +23,13 @@ final class JoinTest extends AbstractUnitTestCase * @return void * * @author Phalcon Team - * @since 2026-04-10 + * @since 2026-04-09 */ - public function testMvcModelQueryPhqlSelectInnerJoinOnComplexCondition(): void + public function testMvcModelQueryPhqlSelectCrossJoin(): 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)"; + $source = "SELECT i.inv_id, c.name " + . "FROM Invoices AS i " + . "CROSS JOIN Customers AS c"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -59,46 +59,15 @@ public function testMvcModelQueryPhqlSelectInnerJoinOnComplexCondition(): void 'alias' => 'i', ], 'joins' => [ - 'type' => Opcode::INNERJOIN->value, - 'qualified' => [ + 'type' => Opcode::CROSSJOIN->value, + 'qualified' => [ 'type' => Opcode::QUALIFIED->value, 'name' => 'Customers', ], - 'alias' => [ + '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', - ], - ], - ], ], ], ]; @@ -112,9 +81,11 @@ public function testMvcModelQueryPhqlSelectInnerJoinOnComplexCondition(): void * @author Phalcon Team * @since 2026-04-09 */ - public function testMvcModelQueryPhqlSelectCrossJoin(): void + public function testMvcModelQueryPhqlSelectFullJoin(): void { - $source = "SELECT i.inv_id, c.name " . "FROM Invoices AS i " . "CROSS JOIN Customers AS c"; + $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' => [ @@ -144,15 +115,28 @@ public function testMvcModelQueryPhqlSelectCrossJoin(): void 'alias' => 'i', ], 'joins' => [ - 'type' => Opcode::CROSSJOIN->value, - 'qualified' => [ + 'type' => Opcode::FULLJOIN->value, + 'qualified' => [ 'type' => Opcode::QUALIFIED->value, 'name' => 'Customers', ], - 'alias' => [ + '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', + ], + ], ], ], ]; @@ -166,11 +150,11 @@ public function testMvcModelQueryPhqlSelectCrossJoin(): void * @author Phalcon Team * @since 2026-04-09 */ - public function testMvcModelQueryPhqlSelectFullJoin(): void + public function testMvcModelQueryPhqlSelectFullOuterJoin(): void { $source = "SELECT i.inv_id, c.name " . "FROM Invoices AS i " - . "FULL JOIN Customers AS c ON i.inv_cst_id = c.id"; + . "FULL OUTER JOIN Customers AS c ON i.inv_cst_id = c.id"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -235,11 +219,11 @@ public function testMvcModelQueryPhqlSelectFullJoin(): void * @author Phalcon Team * @since 2026-04-09 */ - public function testMvcModelQueryPhqlSelectFullOuterJoin(): void + public function testMvcModelQueryPhqlSelectInnerJoin(): 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"; + . "INNER JOIN Customers AS c ON i.inv_cst_id = c.id"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -269,7 +253,7 @@ public function testMvcModelQueryPhqlSelectFullOuterJoin(): void 'alias' => 'i', ], 'joins' => [ - 'type' => Opcode::FULLJOIN->value, + 'type' => Opcode::INNERJOIN->value, 'qualified' => [ 'type' => Opcode::QUALIFIED->value, 'name' => 'Customers', @@ -302,13 +286,13 @@ public function testMvcModelQueryPhqlSelectFullOuterJoin(): void * @return void * * @author Phalcon Team - * @since 2026-04-09 + * @since 2026-04-10 */ - public function testMvcModelQueryPhqlSelectInnerJoin(): void + 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"; + $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' => [ @@ -348,16 +332,34 @@ public function testMvcModelQueryPhqlSelectInnerJoin(): void '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', + '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', + ], ], ], ], diff --git a/tests/unit/Phql/Select/LimitTest.php b/tests/unit/Phql/Select/LimitTest.php index 13ca3de..4ecf95c 100644 --- a/tests/unit/Phql/Select/LimitTest.php +++ b/tests/unit/Phql/Select/LimitTest.php @@ -23,11 +23,11 @@ final class LimitTest extends AbstractUnitTestCase * @return void * * @author Phalcon Team - * @since 2026-04-10 + * @since 2026-04-09 */ - public function testMvcModelQueryPhqlSelectLimitBracePlaceholder(): void + public function testMvcModelQueryPhqlSelectLimit(): void { - $source = "SELECT * FROM Invoices LIMIT {limit}"; + $source = "SELECT * FROM Invoices LIMIT 10"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -45,8 +45,8 @@ public function testMvcModelQueryPhqlSelectLimitBracePlaceholder(): void ], 'limit' => [ 'number' => [ - 'type' => Opcode::BPLACEHOLDER->value, - 'value' => 'limit', + 'type' => Opcode::INTEGER->value, + 'value' => '10', ], ], ]; @@ -58,34 +58,33 @@ public function testMvcModelQueryPhqlSelectLimitBracePlaceholder(): void * @return void * * @author Phalcon Team - * @since 2026-04-10 + * @issue 1 + * @since 2026-04-11 */ - public function testMvcModelQueryPhqlSelectLimitBracePlaceholderOffset(): void + public function testMvcModelQueryPhqlSelectLimitAliasedDomainAll(): void { - $source = "SELECT * FROM Invoices LIMIT {limit} OFFSET {offset}"; + $source = "SELECT r.* FROM Robots r LIMIT 10"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ 'columns' => [ 0 => [ - 'type' => Opcode::STARALL->value, + 'type' => Opcode::DOMAINALL->value, + 'column' => 'r', ], ], 'tables' => [ 'qualifiedName' => [ 'type' => Opcode::QUALIFIED->value, - 'name' => 'Invoices', + 'name' => 'Robots', ], + 'alias' => 'r', ], ], 'limit' => [ 'number' => [ - 'type' => Opcode::BPLACEHOLDER->value, - 'value' => 'limit', - ], - 'offset' => [ - 'type' => Opcode::BPLACEHOLDER->value, - 'value' => 'offset', + 'type' => Opcode::INTEGER->value, + 'value' => '10', ], ], ]; @@ -99,9 +98,9 @@ public function testMvcModelQueryPhqlSelectLimitBracePlaceholderOffset(): void * @author Phalcon Team * @since 2026-04-09 */ - public function testMvcModelQueryPhqlSelectLimit(): void + public function testMvcModelQueryPhqlSelectLimitBoth(): void { - $source = "SELECT * FROM Invoices LIMIT 10"; + $source = "SELECT * FROM Invoices LIMIT 20, 10"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -122,6 +121,10 @@ public function testMvcModelQueryPhqlSelectLimit(): void 'type' => Opcode::INTEGER->value, 'value' => '10', ], + 'offset' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '20', + ], ], ]; $actual = (new Parser())->parse($source); @@ -132,33 +135,30 @@ public function testMvcModelQueryPhqlSelectLimit(): void * @return void * * @author Phalcon Team - * @issue 1 - * @since 2026-04-11 + * @since 2026-04-10 */ - public function testMvcModelQueryPhqlSelectLimitAliasedDomainAll(): void + public function testMvcModelQueryPhqlSelectLimitBracePlaceholder(): void { - $source = "SELECT r.* FROM Robots r LIMIT 10"; + $source = "SELECT * FROM Invoices LIMIT {limit}"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ 'columns' => [ 0 => [ - 'type' => Opcode::DOMAINALL->value, - 'column' => 'r', + 'type' => Opcode::STARALL->value, ], ], 'tables' => [ 'qualifiedName' => [ 'type' => Opcode::QUALIFIED->value, - 'name' => 'Robots', + 'name' => 'Invoices', ], - 'alias' => 'r', ], ], 'limit' => [ 'number' => [ - 'type' => Opcode::INTEGER->value, - 'value' => '10', + 'type' => Opcode::BPLACEHOLDER->value, + 'value' => 'limit', ], ], ]; @@ -170,11 +170,11 @@ public function testMvcModelQueryPhqlSelectLimitAliasedDomainAll(): void * @return void * * @author Phalcon Team - * @since 2026-04-09 + * @since 2026-04-10 */ - public function testMvcModelQueryPhqlSelectLimitBoth(): void + public function testMvcModelQueryPhqlSelectLimitBracePlaceholderOffset(): void { - $source = "SELECT * FROM Invoices LIMIT 20, 10"; + $source = "SELECT * FROM Invoices LIMIT {limit} OFFSET {offset}"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -192,12 +192,12 @@ public function testMvcModelQueryPhqlSelectLimitBoth(): void ], 'limit' => [ 'number' => [ - 'type' => Opcode::INTEGER->value, - 'value' => '10', + 'type' => Opcode::BPLACEHOLDER->value, + 'value' => 'limit', ], 'offset' => [ - 'type' => Opcode::INTEGER->value, - 'value' => '20', + 'type' => Opcode::BPLACEHOLDER->value, + 'value' => 'offset', ], ], ]; diff --git a/tests/unit/Phql/Select/NullTest.php b/tests/unit/Phql/Select/NullTest.php index 819fdf8..1130d02 100644 --- a/tests/unit/Phql/Select/NullTest.php +++ b/tests/unit/Phql/Select/NullTest.php @@ -25,9 +25,9 @@ final class NullTest extends AbstractUnitTestCase * @author Phalcon Team * @since 2026-04-09 */ - public function testMvcModelQueryPhqlSelectWhereIsNull(): void + public function testMvcModelQueryPhqlSelectWhereIsNotNull(): void { - $source = "SELECT * FROM Invoices WHERE inv_title IS NULL"; + $source = "SELECT * FROM Invoices WHERE inv_title IS NOT NULL"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -44,7 +44,7 @@ public function testMvcModelQueryPhqlSelectWhereIsNull(): void ], ], 'where' => [ - 'type' => Opcode::ISNULL->value, + 'type' => Opcode::ISNOTNULL->value, 'left' => [ 'type' => Opcode::QUALIFIED->value, 'name' => 'inv_title', @@ -61,9 +61,9 @@ public function testMvcModelQueryPhqlSelectWhereIsNull(): void * @author Phalcon Team * @since 2026-04-09 */ - public function testMvcModelQueryPhqlSelectWhereIsNotNull(): void + public function testMvcModelQueryPhqlSelectWhereIsNull(): void { - $source = "SELECT * FROM Invoices WHERE inv_title IS NOT NULL"; + $source = "SELECT * FROM Invoices WHERE inv_title IS NULL"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -80,7 +80,7 @@ public function testMvcModelQueryPhqlSelectWhereIsNotNull(): void ], ], 'where' => [ - 'type' => Opcode::ISNOTNULL->value, + 'type' => Opcode::ISNULL->value, 'left' => [ 'type' => Opcode::QUALIFIED->value, 'name' => 'inv_title', diff --git a/tests/unit/Phql/Select/OperatorsTest.php b/tests/unit/Phql/Select/OperatorsTest.php index f593df4..75727de 100644 --- a/tests/unit/Phql/Select/OperatorsTest.php +++ b/tests/unit/Phql/Select/OperatorsTest.php @@ -65,9 +65,9 @@ public function testMvcModelQueryPhqlSelectAddition(): void * @author Phalcon Team * @since 2026-04-09 */ - public function testMvcModelQueryPhqlSelectSubtraction(): void + public function testMvcModelQueryPhqlSelectDivision(): void { - $source = "SELECT inv_total - 5 FROM Invoices"; + $source = "SELECT inv_total / 2 FROM Invoices"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -75,14 +75,14 @@ public function testMvcModelQueryPhqlSelectSubtraction(): void 0 => [ 'type' => Opcode::EXPR->value, 'column' => [ - 'type' => Opcode::SUB->value, + 'type' => Opcode::DIV->value, 'left' => [ 'type' => Opcode::QUALIFIED->value, 'name' => 'inv_total', ], 'right' => [ 'type' => Opcode::INTEGER->value, - 'value' => '5', + 'value' => '2', ], ], ], @@ -105,9 +105,9 @@ public function testMvcModelQueryPhqlSelectSubtraction(): void * @author Phalcon Team * @since 2026-04-09 */ - public function testMvcModelQueryPhqlSelectMultiplication(): void + public function testMvcModelQueryPhqlSelectModulo(): void { - $source = "SELECT inv_total * 1.1 FROM Invoices"; + $source = "SELECT inv_total % 3 FROM Invoices"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -115,14 +115,14 @@ public function testMvcModelQueryPhqlSelectMultiplication(): void 0 => [ 'type' => Opcode::EXPR->value, 'column' => [ - 'type' => Opcode::MUL->value, + 'type' => Opcode::MOD->value, 'left' => [ 'type' => Opcode::QUALIFIED->value, 'name' => 'inv_total', ], 'right' => [ - 'type' => Opcode::DOUBLE->value, - 'value' => '1.1', + 'type' => Opcode::INTEGER->value, + 'value' => '3', ], ], ], @@ -145,9 +145,9 @@ public function testMvcModelQueryPhqlSelectMultiplication(): void * @author Phalcon Team * @since 2026-04-09 */ - public function testMvcModelQueryPhqlSelectDivision(): void + public function testMvcModelQueryPhqlSelectMultiplication(): void { - $source = "SELECT inv_total / 2 FROM Invoices"; + $source = "SELECT inv_total * 1.1 FROM Invoices"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -155,14 +155,14 @@ public function testMvcModelQueryPhqlSelectDivision(): void 0 => [ 'type' => Opcode::EXPR->value, 'column' => [ - 'type' => Opcode::DIV->value, + 'type' => Opcode::MUL->value, 'left' => [ 'type' => Opcode::QUALIFIED->value, 'name' => 'inv_total', ], 'right' => [ - 'type' => Opcode::INTEGER->value, - 'value' => '2', + 'type' => Opcode::DOUBLE->value, + 'value' => '1.1', ], ], ], @@ -185,9 +185,9 @@ public function testMvcModelQueryPhqlSelectDivision(): void * @author Phalcon Team * @since 2026-04-09 */ - public function testMvcModelQueryPhqlSelectModulo(): void + public function testMvcModelQueryPhqlSelectMultiplicationAliasAs(): void { - $source = "SELECT inv_total % 3 FROM Invoices"; + $source = "SELECT inv_id, inv_total * 1.1 AS total_with_tax FROM Invoices"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -195,16 +195,24 @@ public function testMvcModelQueryPhqlSelectModulo(): void 0 => [ 'type' => Opcode::EXPR->value, 'column' => [ - 'type' => Opcode::MOD->value, + '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::INTEGER->value, - 'value' => '3', + 'type' => Opcode::DOUBLE->value, + 'value' => '1.1', ], ], + 'alias' => 'total_with_tax', ], ], 'tables' => [ @@ -225,9 +233,9 @@ public function testMvcModelQueryPhqlSelectModulo(): void * @author Phalcon Team * @since 2026-04-09 */ - public function testMvcModelQueryPhqlSelectMultiplicationAliasAs(): void + public function testMvcModelQueryPhqlSelectSelectAdditionMultiplicationAliasAs(): void { - $source = "SELECT inv_id, inv_total * 1.1 AS total_with_tax FROM Invoices"; + $source = "SELECT inv_id, (inv_total + 5) * 2 AS adjusted FROM Invoices"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -244,15 +252,25 @@ public function testMvcModelQueryPhqlSelectMultiplicationAliasAs(): void 'column' => [ 'type' => Opcode::MUL->value, 'left' => [ - 'type' => Opcode::QUALIFIED->value, - 'name' => 'inv_total', + '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::DOUBLE->value, - 'value' => '1.1', + 'type' => Opcode::INTEGER->value, + 'value' => '2', ], ], - 'alias' => 'total_with_tax', + 'alias' => 'adjusted', ], ], 'tables' => [ @@ -273,9 +291,9 @@ public function testMvcModelQueryPhqlSelectMultiplicationAliasAs(): void * @author Phalcon Team * @since 2026-04-09 */ - public function testMvcModelQueryPhqlSelectSelectAdditionMultiplicationAliasAs(): void + public function testMvcModelQueryPhqlSelectSubtraction(): void { - $source = "SELECT inv_id, (inv_total + 5) * 2 AS adjusted FROM Invoices"; + $source = "SELECT inv_total - 5 FROM Invoices"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -283,34 +301,16 @@ public function testMvcModelQueryPhqlSelectSelectAdditionMultiplicationAliasAs() 0 => [ 'type' => Opcode::EXPR->value, 'column' => [ - 'type' => Opcode::QUALIFIED->value, - 'name' => 'inv_id', - ], - ], - 1 => [ - 'type' => Opcode::EXPR->value, - 'column' => [ - 'type' => Opcode::MUL->value, + 'type' => Opcode::SUB->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', - ], - ], + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_total', ], 'right' => [ 'type' => Opcode::INTEGER->value, - 'value' => '2', + 'value' => '5', ], ], - 'alias' => 'adjusted', ], ], 'tables' => [ diff --git a/tests/unit/Phql/Select/OrderByTest.php b/tests/unit/Phql/Select/OrderByTest.php index 873e980..70a8bb4 100644 --- a/tests/unit/Phql/Select/OrderByTest.php +++ b/tests/unit/Phql/Select/OrderByTest.php @@ -87,9 +87,9 @@ public function testMvcModelQueryPhqlSelectOrderByAggregate(): void * @author Phalcon Team * @since 2026-04-09 */ - public function testMvcModelQueryPhqlSelectOrderByInt(): void + public function testMvcModelQueryPhqlSelectOrderByDateDescIntAsc(): void { - $source = "SELECT * FROM Invoices ORDER BY inv_id"; + $source = "SELECT * FROM Invoices ORDER BY inv_created_at DESC, inv_id ASC"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -106,9 +106,19 @@ public function testMvcModelQueryPhqlSelectOrderByInt(): void ], ], 'orderBy' => [ - 'column' => [ - 'type' => Opcode::QUALIFIED->value, - 'name' => 'inv_id', + 0 => [ + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_created_at', + ], + 'sort' => 328, + ], + 1 => [ + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_id', + ], + 'sort' => 327, ], ], ]; @@ -122,9 +132,10 @@ public function testMvcModelQueryPhqlSelectOrderByInt(): void * @author Phalcon Team * @since 2026-04-09 */ - public function testMvcModelQueryPhqlSelectOrderByIntAsc(): void + public function testMvcModelQueryPhqlSelectOrderByFloatDescStringAscIntAsc(): void { - $source = "SELECT * FROM Invoices ORDER BY inv_id ASC"; + $source = "SELECT * FROM Invoices " + . "ORDER BY inv_total DESC, inv_title ASC, inv_id ASC"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -141,11 +152,27 @@ public function testMvcModelQueryPhqlSelectOrderByIntAsc(): void ], ], 'orderBy' => [ - 'column' => [ - 'type' => Opcode::QUALIFIED->value, - 'name' => 'inv_id', + 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, ], - 'sort' => 327, ], ]; $actual = (new Parser())->parse($source); @@ -158,9 +185,9 @@ public function testMvcModelQueryPhqlSelectOrderByIntAsc(): void * @author Phalcon Team * @since 2026-04-09 */ - public function testMvcModelQueryPhqlSelectOrderByIntDesc(): void + public function testMvcModelQueryPhqlSelectOrderByInt(): void { - $source = "SELECT * FROM Invoices ORDER BY inv_id DESC"; + $source = "SELECT * FROM Invoices ORDER BY inv_id"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -181,7 +208,6 @@ public function testMvcModelQueryPhqlSelectOrderByIntDesc(): void 'type' => Opcode::QUALIFIED->value, 'name' => 'inv_id', ], - 'sort' => 328, ], ]; $actual = (new Parser())->parse($source); @@ -194,9 +220,9 @@ public function testMvcModelQueryPhqlSelectOrderByIntDesc(): void * @author Phalcon Team * @since 2026-04-09 */ - public function testMvcModelQueryPhqlSelectOrderByDateDescIntAsc(): void + public function testMvcModelQueryPhqlSelectOrderByIntAsc(): void { - $source = "SELECT * FROM Invoices ORDER BY inv_created_at DESC, inv_id ASC"; + $source = "SELECT * FROM Invoices ORDER BY inv_id ASC"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -213,20 +239,11 @@ public function testMvcModelQueryPhqlSelectOrderByDateDescIntAsc(): void ], ], 'orderBy' => [ - 0 => [ - 'column' => [ - 'type' => Opcode::QUALIFIED->value, - 'name' => 'inv_created_at', - ], - 'sort' => 328, - ], - 1 => [ - 'column' => [ - 'type' => Opcode::QUALIFIED->value, - 'name' => 'inv_id', - ], - 'sort' => 327, + 'column' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_id', ], + 'sort' => 327, ], ]; $actual = (new Parser())->parse($source); @@ -239,10 +256,9 @@ public function testMvcModelQueryPhqlSelectOrderByDateDescIntAsc(): void * @author Phalcon Team * @since 2026-04-09 */ - public function testMvcModelQueryPhqlSelectOrderByFloatDescStringAscIntAsc(): void + public function testMvcModelQueryPhqlSelectOrderByIntDesc(): void { - $source = "SELECT * FROM Invoices " - . "ORDER BY inv_total DESC, inv_title ASC, inv_id ASC"; + $source = "SELECT * FROM Invoices ORDER BY inv_id DESC"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -259,30 +275,15 @@ public function testMvcModelQueryPhqlSelectOrderByFloatDescStringAscIntAsc(): vo ], ], '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, + '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/ScalarTest.php b/tests/unit/Phql/Select/ScalarTest.php index 3107deb..267d2f3 100644 --- a/tests/unit/Phql/Select/ScalarTest.php +++ b/tests/unit/Phql/Select/ScalarTest.php @@ -25,9 +25,9 @@ final class ScalarTest extends AbstractUnitTestCase * @author Phalcon Team * @since 2026-04-09 */ - public function testMvcModelQueryPhqlSelectUpper(): void + public function testMvcModelQueryPhqlSelectAbs(): void { - $source = "SELECT UPPER(inv_title) FROM Invoices"; + $source = "SELECT ABS(inv_total) FROM Invoices"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -36,11 +36,11 @@ public function testMvcModelQueryPhqlSelectUpper(): void 'type' => Opcode::EXPR->value, 'column' => [ 'type' => Opcode::FCALL->value, - 'name' => 'UPPER', + 'name' => 'ABS', 'arguments' => [ 0 => [ 'type' => Opcode::QUALIFIED->value, - 'name' => 'inv_title', + 'name' => 'inv_total', ], ], ], @@ -64,9 +64,9 @@ public function testMvcModelQueryPhqlSelectUpper(): void * @author Phalcon Team * @since 2026-04-09 */ - public function testMvcModelQueryPhqlSelectLower(): void + public function testMvcModelQueryPhqlSelectCoalesce(): void { - $source = "SELECT LOWER(inv_title) FROM Invoices"; + $source = "SELECT COALESCE(inv_title, 'N/A') FROM Invoices"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -75,12 +75,16 @@ public function testMvcModelQueryPhqlSelectLower(): void 'type' => Opcode::EXPR->value, 'column' => [ 'type' => Opcode::FCALL->value, - 'name' => 'LOWER', + 'name' => 'COALESCE', 'arguments' => [ 0 => [ 'type' => Opcode::QUALIFIED->value, 'name' => 'inv_title', ], + 1 => [ + 'type' => Opcode::STRING->value, + 'value' => 'N/A', + ], ], ], ], @@ -103,9 +107,9 @@ public function testMvcModelQueryPhqlSelectLower(): void * @author Phalcon Team * @since 2026-04-09 */ - public function testMvcModelQueryPhqlSelectTrim(): void + public function testMvcModelQueryPhqlSelectConcat(): void { - $source = "SELECT TRIM(inv_title) FROM Invoices"; + $source = "SELECT CONCAT(inv_title, ' - paid') FROM Invoices"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -114,12 +118,16 @@ public function testMvcModelQueryPhqlSelectTrim(): void 'type' => Opcode::EXPR->value, 'column' => [ 'type' => Opcode::FCALL->value, - 'name' => 'TRIM', + 'name' => 'CONCAT', 'arguments' => [ 0 => [ 'type' => Opcode::QUALIFIED->value, 'name' => 'inv_title', ], + 1 => [ + 'type' => Opcode::STRING->value, + 'value' => ' - paid', + ], ], ], ], @@ -142,9 +150,9 @@ public function testMvcModelQueryPhqlSelectTrim(): void * @author Phalcon Team * @since 2026-04-09 */ - public function testMvcModelQueryPhqlSelectLength(): void + public function testMvcModelQueryPhqlSelectIfnull(): void { - $source = "SELECT LENGTH(inv_title) FROM Invoices"; + $source = "SELECT IFNULL(inv_title, 'N/A') FROM Invoices"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -153,12 +161,16 @@ public function testMvcModelQueryPhqlSelectLength(): void 'type' => Opcode::EXPR->value, 'column' => [ 'type' => Opcode::FCALL->value, - 'name' => 'LENGTH', + 'name' => 'IFNULL', 'arguments' => [ 0 => [ 'type' => Opcode::QUALIFIED->value, 'name' => 'inv_title', ], + 1 => [ + 'type' => Opcode::STRING->value, + 'value' => 'N/A', + ], ], ], ], @@ -181,9 +193,9 @@ public function testMvcModelQueryPhqlSelectLength(): void * @author Phalcon Team * @since 2026-04-09 */ - public function testMvcModelQueryPhqlSelectConcat(): void + public function testMvcModelQueryPhqlSelectLength(): void { - $source = "SELECT CONCAT(inv_title, ' - paid') FROM Invoices"; + $source = "SELECT LENGTH(inv_title) FROM Invoices"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -192,16 +204,12 @@ public function testMvcModelQueryPhqlSelectConcat(): void 'type' => Opcode::EXPR->value, 'column' => [ 'type' => Opcode::FCALL->value, - 'name' => 'CONCAT', + 'name' => 'LENGTH', 'arguments' => [ 0 => [ 'type' => Opcode::QUALIFIED->value, 'name' => 'inv_title', ], - 1 => [ - 'type' => Opcode::STRING->value, - 'value' => ' - paid', - ], ], ], ], @@ -224,9 +232,9 @@ public function testMvcModelQueryPhqlSelectConcat(): void * @author Phalcon Team * @since 2026-04-09 */ - public function testMvcModelQueryPhqlSelectAbs(): void + public function testMvcModelQueryPhqlSelectLower(): void { - $source = "SELECT ABS(inv_total) FROM Invoices"; + $source = "SELECT LOWER(inv_title) FROM Invoices"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -235,11 +243,11 @@ public function testMvcModelQueryPhqlSelectAbs(): void 'type' => Opcode::EXPR->value, 'column' => [ 'type' => Opcode::FCALL->value, - 'name' => 'ABS', + 'name' => 'LOWER', 'arguments' => [ 0 => [ 'type' => Opcode::QUALIFIED->value, - 'name' => 'inv_total', + 'name' => 'inv_title', ], ], ], @@ -263,26 +271,34 @@ public function testMvcModelQueryPhqlSelectAbs(): void * @author Phalcon Team * @since 2026-04-09 */ - public function testMvcModelQueryPhqlSelectRound(): void + public function testMvcModelQueryPhqlSelectMonthCount(): void { - $source = "SELECT ROUND(inv_total, 2) FROM Invoices"; + $source = "SELECT MONTH(inv_created_at), COUNT(*) FROM Invoices GROUP BY MONTH(inv_created_at)"; $expected = [ 'type' => Opcode::SELECT->value, - 'select' => [ + 'select' => [ 'columns' => [ 0 => [ 'type' => Opcode::EXPR->value, 'column' => [ 'type' => Opcode::FCALL->value, - 'name' => 'ROUND', + 'name' => 'MONTH', 'arguments' => [ 0 => [ 'type' => Opcode::QUALIFIED->value, - 'name' => 'inv_total', + 'name' => 'inv_created_at', ], - 1 => [ - 'type' => Opcode::INTEGER->value, - 'value' => '2', + ], + ], + ], + 1 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ + 'type' => Opcode::FCALL->value, + 'name' => 'COUNT', + 'arguments' => [ + 0 => [ + 'type' => Opcode::STARALL->value, ], ], ], @@ -295,6 +311,16 @@ public function testMvcModelQueryPhqlSelectRound(): void ], ], ], + '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); @@ -306,9 +332,9 @@ public function testMvcModelQueryPhqlSelectRound(): void * @author Phalcon Team * @since 2026-04-09 */ - public function testMvcModelQueryPhqlSelectYear(): void + public function testMvcModelQueryPhqlSelectNow(): void { - $source = "SELECT YEAR(inv_created_at) FROM Invoices"; + $source = "SELECT NOW() FROM Invoices"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -317,13 +343,7 @@ public function testMvcModelQueryPhqlSelectYear(): void 'type' => Opcode::EXPR->value, 'column' => [ 'type' => Opcode::FCALL->value, - 'name' => 'YEAR', - 'arguments' => [ - 0 => [ - 'type' => Opcode::QUALIFIED->value, - 'name' => 'inv_created_at', - ], - ], + 'name' => 'NOW', ], ], ], @@ -345,34 +365,26 @@ public function testMvcModelQueryPhqlSelectYear(): void * @author Phalcon Team * @since 2026-04-09 */ - public function testMvcModelQueryPhqlSelectMonthCount(): void + public function testMvcModelQueryPhqlSelectRound(): void { - $source = "SELECT MONTH(inv_created_at), COUNT(*) FROM Invoices GROUP BY MONTH(inv_created_at)"; + $source = "SELECT ROUND(inv_total, 2) FROM Invoices"; $expected = [ 'type' => Opcode::SELECT->value, - 'select' => [ + 'select' => [ 'columns' => [ 0 => [ 'type' => Opcode::EXPR->value, 'column' => [ 'type' => Opcode::FCALL->value, - 'name' => 'MONTH', + 'name' => 'ROUND', 'arguments' => [ 0 => [ 'type' => Opcode::QUALIFIED->value, - 'name' => 'inv_created_at', + 'name' => 'inv_total', ], - ], - ], - ], - 1 => [ - 'type' => Opcode::EXPR->value, - 'column' => [ - 'type' => Opcode::FCALL->value, - 'name' => 'COUNT', - 'arguments' => [ - 0 => [ - 'type' => Opcode::STARALL->value, + 1 => [ + 'type' => Opcode::INTEGER->value, + 'value' => '2', ], ], ], @@ -385,16 +397,6 @@ public function testMvcModelQueryPhqlSelectMonthCount(): void ], ], ], - '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); @@ -406,9 +408,9 @@ public function testMvcModelQueryPhqlSelectMonthCount(): void * @author Phalcon Team * @since 2026-04-09 */ - public function testMvcModelQueryPhqlSelectCoalesce(): void + public function testMvcModelQueryPhqlSelectTrim(): void { - $source = "SELECT COALESCE(inv_title, 'N/A') FROM Invoices"; + $source = "SELECT TRIM(inv_title) FROM Invoices"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -417,16 +419,12 @@ public function testMvcModelQueryPhqlSelectCoalesce(): void 'type' => Opcode::EXPR->value, 'column' => [ 'type' => Opcode::FCALL->value, - 'name' => 'COALESCE', + 'name' => 'TRIM', 'arguments' => [ 0 => [ 'type' => Opcode::QUALIFIED->value, 'name' => 'inv_title', ], - 1 => [ - 'type' => Opcode::STRING->value, - 'value' => 'N/A', - ], ], ], ], @@ -449,9 +447,9 @@ public function testMvcModelQueryPhqlSelectCoalesce(): void * @author Phalcon Team * @since 2026-04-09 */ - public function testMvcModelQueryPhqlSelectIfnull(): void + public function testMvcModelQueryPhqlSelectUpper(): void { - $source = "SELECT IFNULL(inv_title, 'N/A') FROM Invoices"; + $source = "SELECT UPPER(inv_title) FROM Invoices"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -460,16 +458,12 @@ public function testMvcModelQueryPhqlSelectIfnull(): void 'type' => Opcode::EXPR->value, 'column' => [ 'type' => Opcode::FCALL->value, - 'name' => 'IFNULL', + 'name' => 'UPPER', 'arguments' => [ 0 => [ 'type' => Opcode::QUALIFIED->value, 'name' => 'inv_title', ], - 1 => [ - 'type' => Opcode::STRING->value, - 'value' => 'N/A', - ], ], ], ], @@ -492,9 +486,9 @@ public function testMvcModelQueryPhqlSelectIfnull(): void * @author Phalcon Team * @since 2026-04-09 */ - public function testMvcModelQueryPhqlSelectNow(): void + public function testMvcModelQueryPhqlSelectYear(): void { - $source = "SELECT NOW() FROM Invoices"; + $source = "SELECT YEAR(inv_created_at) FROM Invoices"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -503,7 +497,13 @@ public function testMvcModelQueryPhqlSelectNow(): void 'type' => Opcode::EXPR->value, 'column' => [ 'type' => Opcode::FCALL->value, - 'name' => 'NOW', + 'name' => 'YEAR', + 'arguments' => [ + 0 => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_created_at', + ], + ], ], ], ], diff --git a/tests/unit/Phql/Select/SubqueriesTest.php b/tests/unit/Phql/Select/SubqueriesTest.php index f2225c0..e7a1003 100644 --- a/tests/unit/Phql/Select/SubqueriesTest.php +++ b/tests/unit/Phql/Select/SubqueriesTest.php @@ -23,14 +23,96 @@ final class SubqueriesTest extends AbstractUnitTestCase * @return void * * @author Phalcon Team - * @since 2026-04-10 + * @since 2026-04-09 */ - public function testMvcModelQueryPhqlSelectWhereInNestedSubquery(): void + public function testMvcModelQueryPhqlSelectFieldSubquery(): 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))"; + $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' => [ @@ -47,64 +129,35 @@ public function testMvcModelQueryPhqlSelectWhereInNestedSubquery(): void ], ], 'where' => [ - 'type' => Opcode::IN->value, + 'type' => Opcode::EQUALS->value, 'left' => [ 'type' => Opcode::QUALIFIED->value, - 'name' => 'inv_cst_id', + 'name' => 'inv_total', ], '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', + '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' => 'Orders', - ], - ], ], - 'where' => [ - 'type' => Opcode::EQUALS->value, - 'left' => [ + 'tables' => [ + 'qualifiedName' => [ 'type' => Opcode::QUALIFIED->value, - 'name' => 'status', - ], - 'right' => [ - 'type' => Opcode::INTEGER->value, - 'value' => '1', + 'name' => 'Invoices', ], ], ], @@ -122,12 +175,12 @@ public function testMvcModelQueryPhqlSelectWhereInNestedSubquery(): void * @author Phalcon Team * @since 2026-04-09 */ - public function testMvcModelQueryPhqlSelectWhereInSubquery(): void + public function testMvcModelQueryPhqlSelectWhereExistsSubquery(): void { $source = "SELECT * " . "FROM Invoices " - . "WHERE inv_cst_id IN " - . "(SELECT id FROM Customers WHERE status = 1)"; + . "WHERE EXISTS " + . "(SELECT id FROM Customers WHERE id = Invoices.inv_cst_id)"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -144,11 +197,7 @@ public function testMvcModelQueryPhqlSelectWhereInSubquery(): void ], ], 'where' => [ - 'type' => Opcode::IN->value, - 'left' => [ - 'type' => Opcode::QUALIFIED->value, - 'name' => 'inv_cst_id', - ], + 'type' => Opcode::EXISTS->value, 'right' => [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -172,11 +221,12 @@ public function testMvcModelQueryPhqlSelectWhereInSubquery(): void 'type' => Opcode::EQUALS->value, 'left' => [ 'type' => Opcode::QUALIFIED->value, - 'name' => 'status', + 'name' => 'id', ], 'right' => [ - 'type' => Opcode::INTEGER->value, - 'value' => '1', + 'type' => Opcode::QUALIFIED->value, + 'domain' => 'Invoices', + 'name' => 'inv_cst_id', ], ], ], @@ -190,14 +240,14 @@ public function testMvcModelQueryPhqlSelectWhereInSubquery(): void * @return void * * @author Phalcon Team - * @since 2026-04-09 + * @since 2026-04-10 */ - public function testMvcModelQueryPhqlSelectWhereNotInSubquery(): void + public function testMvcModelQueryPhqlSelectWhereInNestedSubquery(): void { - $source = "SELECT * " - . "FROM Invoices " - . "WHERE inv_cst_id NOT IN " - . "(SELECT id FROM Customers WHERE status = 0)"; + $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' => [ @@ -214,7 +264,7 @@ public function testMvcModelQueryPhqlSelectWhereNotInSubquery(): void ], ], 'where' => [ - 'type' => Opcode::NOTIN->value, + 'type' => Opcode::IN->value, 'left' => [ 'type' => Opcode::QUALIFIED->value, 'name' => 'inv_cst_id', @@ -239,14 +289,41 @@ public function testMvcModelQueryPhqlSelectWhereNotInSubquery(): void ], ], 'where' => [ - 'type' => Opcode::EQUALS->value, + 'type' => Opcode::IN->value, 'left' => [ 'type' => Opcode::QUALIFIED->value, - 'name' => 'status', + 'name' => 'id', ], 'right' => [ - 'type' => Opcode::INTEGER->value, - 'value' => '0', + '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', + ], + ], ], ], ], @@ -262,12 +339,12 @@ public function testMvcModelQueryPhqlSelectWhereNotInSubquery(): void * @author Phalcon Team * @since 2026-04-09 */ - public function testMvcModelQueryPhqlSelectWhereExistsSubquery(): void + public function testMvcModelQueryPhqlSelectWhereInSubquery(): void { $source = "SELECT * " . "FROM Invoices " - . "WHERE EXISTS " - . "(SELECT id FROM Customers WHERE id = Invoices.inv_cst_id)"; + . "WHERE inv_cst_id IN " + . "(SELECT id FROM Customers WHERE status = 1)"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -284,7 +361,11 @@ public function testMvcModelQueryPhqlSelectWhereExistsSubquery(): void ], ], 'where' => [ - 'type' => Opcode::EXISTS->value, + 'type' => Opcode::IN->value, + 'left' => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_cst_id', + ], 'right' => [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -308,12 +389,11 @@ public function testMvcModelQueryPhqlSelectWhereExistsSubquery(): void 'type' => Opcode::EQUALS->value, 'left' => [ 'type' => Opcode::QUALIFIED->value, - 'name' => 'id', + 'name' => 'status', ], 'right' => [ - 'type' => Opcode::QUALIFIED->value, - 'domain' => 'Invoices', - 'name' => 'inv_cst_id', + 'type' => Opcode::INTEGER->value, + 'value' => '1', ], ], ], @@ -329,11 +409,12 @@ public function testMvcModelQueryPhqlSelectWhereExistsSubquery(): void * @author Phalcon Team * @since 2026-04-09 */ - public function testMvcModelQueryPhqlSelectWhereEqualsSubquery(): void + public function testMvcModelQueryPhqlSelectWhereNotInSubquery(): void { $source = "SELECT * " . "FROM Invoices " - . "WHERE inv_total = (SELECT MAX(inv_total) FROM Invoices)"; + . "WHERE inv_cst_id NOT IN " + . "(SELECT id FROM Customers WHERE status = 0)"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -350,126 +431,46 @@ public function testMvcModelQueryPhqlSelectWhereEqualsSubquery(): void ], ], 'where' => [ - 'type' => Opcode::EQUALS->value, + 'type' => Opcode::NOTIN->value, 'left' => [ 'type' => Opcode::QUALIFIED->value, - 'name' => 'inv_total', + 'name' => 'inv_cst_id', ], '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::SELECT->value, + 'select' => [ + 'columns' => [ + 0 => [ + 'type' => Opcode::EXPR->value, + 'column' => [ 'type' => Opcode::QUALIFIED->value, - 'name' => 'Invoices', + 'name' => 'id', ], ], ], + '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 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' => [ + 'where' => [ + 'type' => Opcode::EQUALS->value, + 'left' => [ 'type' => Opcode::QUALIFIED->value, - 'domain' => 'i', - 'name' => 'inv_id', + 'name' => 'status', ], - ], - 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', - ], - ], - ], + 'right' => [ + 'type' => Opcode::INTEGER->value, + 'value' => '0', ], - 'alias' => 'cst_count', ], ], - '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/WhereLogicalTest.php b/tests/unit/Phql/Select/WhereLogicalTest.php index 653c60d..34f6da5 100644 --- a/tests/unit/Phql/Select/WhereLogicalTest.php +++ b/tests/unit/Phql/Select/WhereLogicalTest.php @@ -79,9 +79,9 @@ public function testMvcModelQueryPhqlSelectWhereAnd(): void * @author Phalcon Team * @since 2026-04-09 */ - public function testMvcModelQueryPhqlSelectWhereOr(): void + public function testMvcModelQueryPhqlSelectWhereAndAnd(): void { - $source = "SELECT * FROM Invoices WHERE inv_status_flag = 0 OR inv_status_flag = 1"; + $source = "SELECT * FROM Invoices WHERE inv_cst_id = 1 AND inv_status_flag = 1 AND inv_total > 0"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -98,28 +98,42 @@ public function testMvcModelQueryPhqlSelectWhereOr(): void ], ], 'where' => [ - 'type' => Opcode::EQUALS->value, + 'type' => Opcode::GREATER->value, 'left' => [ 'type' => Opcode::EQUALS->value, 'left' => [ - 'type' => Opcode::QUALIFIED->value, - 'name' => 'inv_status_flag', + '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::OR->value, + 'type' => Opcode::AND->value, 'left' => [ 'type' => Opcode::INTEGER->value, - 'value' => '0', + 'value' => '1', ], 'right' => [ 'type' => Opcode::QUALIFIED->value, - 'name' => 'inv_status_flag', + 'name' => 'inv_total', ], ], ], 'right' => [ 'type' => Opcode::INTEGER->value, - 'value' => '1', + 'value' => '0', ], ], ]; @@ -133,9 +147,9 @@ public function testMvcModelQueryPhqlSelectWhereOr(): void * @author Phalcon Team * @since 2026-04-09 */ - public function testMvcModelQueryPhqlSelectWhereAndAnd(): void + public function testMvcModelQueryPhqlSelectWhereNot(): void { - $source = "SELECT * FROM Invoices WHERE inv_cst_id = 1 AND inv_status_flag = 1 AND inv_total > 0"; + $source = "SELECT * FROM Invoices WHERE NOT inv_status_flag = 0"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -152,37 +166,12 @@ public function testMvcModelQueryPhqlSelectWhereAndAnd(): void ], ], 'where' => [ - 'type' => Opcode::GREATER->value, + 'type' => Opcode::EQUALS->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', - ], - ], - ], + 'type' => Opcode::NOT->value, 'right' => [ - 'type' => Opcode::AND->value, - 'left' => [ - 'type' => Opcode::INTEGER->value, - 'value' => '1', - ], - 'right' => [ - 'type' => Opcode::QUALIFIED->value, - 'name' => 'inv_total', - ], + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_status_flag', ], ], 'right' => [ @@ -201,9 +190,9 @@ public function testMvcModelQueryPhqlSelectWhereAndAnd(): void * @author Phalcon Team * @since 2026-04-09 */ - public function testMvcModelQueryPhqlSelectWhereNot(): void + public function testMvcModelQueryPhqlSelectWhereOr(): void { - $source = "SELECT * FROM Invoices WHERE NOT inv_status_flag = 0"; + $source = "SELECT * FROM Invoices WHERE inv_status_flag = 0 OR inv_status_flag = 1"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -222,15 +211,26 @@ public function testMvcModelQueryPhqlSelectWhereNot(): void 'where' => [ 'type' => Opcode::EQUALS->value, 'left' => [ - 'type' => Opcode::NOT->value, - 'right' => [ + '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' => '0', + 'value' => '1', ], ], ]; diff --git a/tests/unit/Phql/Select/WherePlaceholdersTest.php b/tests/unit/Phql/Select/WherePlaceholdersTest.php index 507e7cf..1a803e9 100644 --- a/tests/unit/Phql/Select/WherePlaceholdersTest.php +++ b/tests/unit/Phql/Select/WherePlaceholdersTest.php @@ -25,9 +25,9 @@ final class WherePlaceholdersTest extends AbstractUnitTestCase * @author Phalcon Team * @since 2026-04-09 */ - public function testMvcModelQueryPhqlSelectWherePlaceholderNum(): void + public function testMvcModelQueryPhqlSelectWherePlaceholderBrackets(): void { - $source = "SELECT * FROM Invoices WHERE inv_id = ?0"; + $source = "SELECT * FROM Invoices WHERE inv_id = {id}"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -50,8 +50,8 @@ public function testMvcModelQueryPhqlSelectWherePlaceholderNum(): void 'name' => 'inv_id', ], 'right' => [ - 'type' => Opcode::NPLACEHOLDER->value, - 'value' => '?0', + 'type' => Opcode::BPLACEHOLDER->value, + 'value' => 'id', ], ], ]; @@ -65,9 +65,9 @@ public function testMvcModelQueryPhqlSelectWherePlaceholderNum(): void * @author Phalcon Team * @since 2026-04-09 */ - public function testMvcModelQueryPhqlSelectWherePlaceholderNumAnd(): void + public function testMvcModelQueryPhqlSelectWherePlaceholderBracketsAnd(): void { - $source = "SELECT * FROM Invoices WHERE inv_id = ?1 AND inv_status_flag = ?2"; + $source = "SELECT * FROM Invoices WHERE inv_cst_id = {custId} AND inv_total > {minTotal}"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -84,28 +84,28 @@ public function testMvcModelQueryPhqlSelectWherePlaceholderNumAnd(): void ], ], 'where' => [ - 'type' => Opcode::EQUALS->value, + 'type' => Opcode::GREATER->value, 'left' => [ 'type' => Opcode::EQUALS->value, 'left' => [ 'type' => Opcode::QUALIFIED->value, - 'name' => 'inv_id', + 'name' => 'inv_cst_id', ], 'right' => [ 'type' => Opcode::AND->value, 'left' => [ - 'type' => Opcode::NPLACEHOLDER->value, - 'value' => '?1', + 'type' => Opcode::BPLACEHOLDER->value, + 'value' => 'custId', ], 'right' => [ 'type' => Opcode::QUALIFIED->value, - 'name' => 'inv_status_flag', + 'name' => 'inv_total', ], ], ], 'right' => [ - 'type' => Opcode::NPLACEHOLDER->value, - 'value' => '?2', + 'type' => Opcode::BPLACEHOLDER->value, + 'value' => 'minTotal', ], ], ]; @@ -119,9 +119,9 @@ public function testMvcModelQueryPhqlSelectWherePlaceholderNumAnd(): void * @author Phalcon Team * @since 2026-04-09 */ - public function testMvcModelQueryPhqlSelectWherePlaceholderString(): void + public function testMvcModelQueryPhqlSelectWherePlaceholderNum(): void { - $source = "SELECT * FROM Invoices WHERE inv_title = :title:"; + $source = "SELECT * FROM Invoices WHERE inv_id = ?0"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -141,11 +141,11 @@ public function testMvcModelQueryPhqlSelectWherePlaceholderString(): void 'type' => Opcode::EQUALS->value, 'left' => [ 'type' => Opcode::QUALIFIED->value, - 'name' => 'inv_title', + 'name' => 'inv_id', ], 'right' => [ - 'type' => Opcode::SPLACEHOLDER->value, - 'value' => 'title', + 'type' => Opcode::NPLACEHOLDER->value, + 'value' => '?0', ], ], ]; @@ -159,9 +159,9 @@ public function testMvcModelQueryPhqlSelectWherePlaceholderString(): void * @author Phalcon Team * @since 2026-04-09 */ - public function testMvcModelQueryPhqlSelectWherePlaceholderStringAnd(): void + public function testMvcModelQueryPhqlSelectWherePlaceholderNumAnd(): void { - $source = "SELECT * FROM Invoices WHERE inv_cst_id = :custId: AND inv_status_flag = :status:"; + $source = "SELECT * FROM Invoices WHERE inv_id = ?1 AND inv_status_flag = ?2"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -183,13 +183,13 @@ public function testMvcModelQueryPhqlSelectWherePlaceholderStringAnd(): void 'type' => Opcode::EQUALS->value, 'left' => [ 'type' => Opcode::QUALIFIED->value, - 'name' => 'inv_cst_id', + 'name' => 'inv_id', ], 'right' => [ 'type' => Opcode::AND->value, 'left' => [ - 'type' => Opcode::SPLACEHOLDER->value, - 'value' => 'custId', + 'type' => Opcode::NPLACEHOLDER->value, + 'value' => '?1', ], 'right' => [ 'type' => Opcode::QUALIFIED->value, @@ -198,8 +198,8 @@ public function testMvcModelQueryPhqlSelectWherePlaceholderStringAnd(): void ], ], 'right' => [ - 'type' => Opcode::SPLACEHOLDER->value, - 'value' => 'status', + 'type' => Opcode::NPLACEHOLDER->value, + 'value' => '?2', ], ], ]; @@ -213,9 +213,9 @@ public function testMvcModelQueryPhqlSelectWherePlaceholderStringAnd(): void * @author Phalcon Team * @since 2026-04-09 */ - public function testMvcModelQueryPhqlSelectWherePlaceholderBrackets(): void + public function testMvcModelQueryPhqlSelectWherePlaceholderString(): void { - $source = "SELECT * FROM Invoices WHERE inv_id = {id}"; + $source = "SELECT * FROM Invoices WHERE inv_title = :title:"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -235,11 +235,11 @@ public function testMvcModelQueryPhqlSelectWherePlaceholderBrackets(): void 'type' => Opcode::EQUALS->value, 'left' => [ 'type' => Opcode::QUALIFIED->value, - 'name' => 'inv_id', + 'name' => 'inv_title', ], 'right' => [ - 'type' => Opcode::BPLACEHOLDER->value, - 'value' => 'id', + 'type' => Opcode::SPLACEHOLDER->value, + 'value' => 'title', ], ], ]; @@ -253,9 +253,9 @@ public function testMvcModelQueryPhqlSelectWherePlaceholderBrackets(): void * @author Phalcon Team * @since 2026-04-09 */ - public function testMvcModelQueryPhqlSelectWherePlaceholderBracketsAnd(): void + public function testMvcModelQueryPhqlSelectWherePlaceholderStringAnd(): void { - $source = "SELECT * FROM Invoices WHERE inv_cst_id = {custId} AND inv_total > {minTotal}"; + $source = "SELECT * FROM Invoices WHERE inv_cst_id = :custId: AND inv_status_flag = :status:"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -272,7 +272,7 @@ public function testMvcModelQueryPhqlSelectWherePlaceholderBracketsAnd(): void ], ], 'where' => [ - 'type' => Opcode::GREATER->value, + 'type' => Opcode::EQUALS->value, 'left' => [ 'type' => Opcode::EQUALS->value, 'left' => [ @@ -282,22 +282,23 @@ public function testMvcModelQueryPhqlSelectWherePlaceholderBracketsAnd(): void 'right' => [ 'type' => Opcode::AND->value, 'left' => [ - 'type' => Opcode::BPLACEHOLDER->value, + 'type' => Opcode::SPLACEHOLDER->value, 'value' => 'custId', ], 'right' => [ 'type' => Opcode::QUALIFIED->value, - 'name' => 'inv_total', + 'name' => 'inv_status_flag', ], ], ], 'right' => [ - 'type' => Opcode::BPLACEHOLDER->value, - 'value' => 'minTotal', + '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 index a218bcf..5d7686c 100644 --- a/tests/unit/Phql/Select/WhereTest.php +++ b/tests/unit/Phql/Select/WhereTest.php @@ -65,9 +65,9 @@ public function testMvcModelQueryPhqlSelectWhereEqInt(): void * @author Phalcon Team * @since 2026-04-09 */ - public function testMvcModelQueryPhqlSelectWhereNeqInt(): void + public function testMvcModelQueryPhqlSelectWhereEqString(): void { - $source = "SELECT * FROM Invoices WHERE inv_id != 1"; + $source = "SELECT * FROM Invoices WHERE inv_title = 'test invoice'"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -84,14 +84,14 @@ public function testMvcModelQueryPhqlSelectWhereNeqInt(): void ], ], 'where' => [ - 'type' => Opcode::NOTEQUALS->value, + 'type' => Opcode::EQUALS->value, 'left' => [ 'type' => Opcode::QUALIFIED->value, - 'name' => 'inv_id', + 'name' => 'inv_title', ], 'right' => [ - 'type' => Opcode::INTEGER->value, - 'value' => '1', + 'type' => Opcode::STRING->value, + 'value' => 'test invoice', ], ], ]; @@ -103,11 +103,11 @@ public function testMvcModelQueryPhqlSelectWhereNeqInt(): void * @return void * * @author Phalcon Team - * @since 2026-04-09 + * @since 2026-04-10 */ - public function testMvcModelQueryPhqlSelectWhereNotInt(): void + public function testMvcModelQueryPhqlSelectWhereFuncLeft(): void { - $source = "SELECT * FROM Invoices WHERE inv_id <> 1"; + $source = "SELECT * FROM Invoices WHERE UPPER(inv_title) = 'TEST INVOICE'"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -124,14 +124,20 @@ public function testMvcModelQueryPhqlSelectWhereNotInt(): void ], ], 'where' => [ - 'type' => Opcode::NOTEQUALS->value, + 'type' => Opcode::EQUALS->value, 'left' => [ - 'type' => Opcode::QUALIFIED->value, - 'name' => 'inv_id', + 'type' => Opcode::FCALL->value, + 'name' => 'UPPER', + 'arguments' => [ + 0 => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_title', + ], + ], ], 'right' => [ - 'type' => Opcode::INTEGER->value, - 'value' => '1', + 'type' => Opcode::STRING->value, + 'value' => 'TEST INVOICE', ], ], ]; @@ -143,11 +149,11 @@ public function testMvcModelQueryPhqlSelectWhereNotInt(): void * @return void * * @author Phalcon Team - * @since 2026-04-09 + * @since 2026-04-10 */ - public function testMvcModelQueryPhqlSelectWhereLtFloat(): void + public function testMvcModelQueryPhqlSelectWhereFuncLeftPlaceholder(): void { - $source = "SELECT * FROM Invoices WHERE inv_total < 100.00"; + $source = "SELECT * FROM Invoices WHERE UPPER(inv_title) = :title:"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -164,14 +170,20 @@ public function testMvcModelQueryPhqlSelectWhereLtFloat(): void ], ], 'where' => [ - 'type' => Opcode::LESS->value, + 'type' => Opcode::EQUALS->value, 'left' => [ - 'type' => Opcode::QUALIFIED->value, - 'name' => 'inv_total', + 'type' => Opcode::FCALL->value, + 'name' => 'UPPER', + 'arguments' => [ + 0 => [ + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_title', + ], + ], ], 'right' => [ - 'type' => Opcode::DOUBLE->value, - 'value' => '100.00', + 'type' => Opcode::SPLACEHOLDER->value, + 'value' => 'title', ], ], ]; @@ -225,9 +237,9 @@ public function testMvcModelQueryPhqlSelectWhereGtFloat(): void * @author Phalcon Team * @since 2026-04-09 */ - public function testMvcModelQueryPhqlSelectWhereLteFloat(): void + public function testMvcModelQueryPhqlSelectWhereGteFloat(): void { - $source = "SELECT * FROM Invoices WHERE inv_total <= 100.00"; + $source = "SELECT * FROM Invoices WHERE inv_total >= 100.00"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -244,7 +256,7 @@ public function testMvcModelQueryPhqlSelectWhereLteFloat(): void ], ], 'where' => [ - 'type' => Opcode::LESSEQUAL->value, + 'type' => Opcode::GREATEREQUAL->value, 'left' => [ 'type' => Opcode::QUALIFIED->value, 'name' => 'inv_total', @@ -265,9 +277,9 @@ public function testMvcModelQueryPhqlSelectWhereLteFloat(): void * @author Phalcon Team * @since 2026-04-09 */ - public function testMvcModelQueryPhqlSelectWhereGteFloat(): void + public function testMvcModelQueryPhqlSelectWhereLtFloat(): void { - $source = "SELECT * FROM Invoices WHERE inv_total >= 100.00"; + $source = "SELECT * FROM Invoices WHERE inv_total < 100.00"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -284,7 +296,7 @@ public function testMvcModelQueryPhqlSelectWhereGteFloat(): void ], ], 'where' => [ - 'type' => Opcode::GREATEREQUAL->value, + 'type' => Opcode::LESS->value, 'left' => [ 'type' => Opcode::QUALIFIED->value, 'name' => 'inv_total', @@ -303,11 +315,11 @@ public function testMvcModelQueryPhqlSelectWhereGteFloat(): void * @return void * * @author Phalcon Team - * @since 2026-04-10 + * @since 2026-04-09 */ - public function testMvcModelQueryPhqlSelectWhereFuncLeft(): void + public function testMvcModelQueryPhqlSelectWhereLteFloat(): void { - $source = "SELECT * FROM Invoices WHERE UPPER(inv_title) = 'TEST INVOICE'"; + $source = "SELECT * FROM Invoices WHERE inv_total <= 100.00"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -324,20 +336,14 @@ public function testMvcModelQueryPhqlSelectWhereFuncLeft(): void ], ], 'where' => [ - 'type' => Opcode::EQUALS->value, + 'type' => Opcode::LESSEQUAL->value, 'left' => [ - 'type' => Opcode::FCALL->value, - 'name' => 'UPPER', - 'arguments' => [ - 0 => [ - 'type' => Opcode::QUALIFIED->value, - 'name' => 'inv_title', - ], - ], + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_total', ], 'right' => [ - 'type' => Opcode::STRING->value, - 'value' => 'TEST INVOICE', + 'type' => Opcode::DOUBLE->value, + 'value' => '100.00', ], ], ]; @@ -349,11 +355,11 @@ public function testMvcModelQueryPhqlSelectWhereFuncLeft(): void * @return void * * @author Phalcon Team - * @since 2026-04-10 + * @since 2026-04-09 */ - public function testMvcModelQueryPhqlSelectWhereFuncLeftPlaceholder(): void + public function testMvcModelQueryPhqlSelectWhereNeqInt(): void { - $source = "SELECT * FROM Invoices WHERE UPPER(inv_title) = :title:"; + $source = "SELECT * FROM Invoices WHERE inv_id != 1"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -370,20 +376,14 @@ public function testMvcModelQueryPhqlSelectWhereFuncLeftPlaceholder(): void ], ], 'where' => [ - 'type' => Opcode::EQUALS->value, + 'type' => Opcode::NOTEQUALS->value, 'left' => [ - 'type' => Opcode::FCALL->value, - 'name' => 'UPPER', - 'arguments' => [ - 0 => [ - 'type' => Opcode::QUALIFIED->value, - 'name' => 'inv_title', - ], - ], + 'type' => Opcode::QUALIFIED->value, + 'name' => 'inv_id', ], 'right' => [ - 'type' => Opcode::SPLACEHOLDER->value, - 'value' => 'title', + 'type' => Opcode::INTEGER->value, + 'value' => '1', ], ], ]; @@ -397,9 +397,9 @@ public function testMvcModelQueryPhqlSelectWhereFuncLeftPlaceholder(): void * @author Phalcon Team * @since 2026-04-09 */ - public function testMvcModelQueryPhqlSelectWhereEqString(): void + public function testMvcModelQueryPhqlSelectWhereNotInt(): void { - $source = "SELECT * FROM Invoices WHERE inv_title = 'test invoice'"; + $source = "SELECT * FROM Invoices WHERE inv_id <> 1"; $expected = [ 'type' => Opcode::SELECT->value, 'select' => [ @@ -416,14 +416,14 @@ public function testMvcModelQueryPhqlSelectWhereEqString(): void ], ], 'where' => [ - 'type' => Opcode::EQUALS->value, + 'type' => Opcode::NOTEQUALS->value, 'left' => [ 'type' => Opcode::QUALIFIED->value, - 'name' => 'inv_title', + 'name' => 'inv_id', ], 'right' => [ - 'type' => Opcode::STRING->value, - 'value' => 'test invoice', + 'type' => Opcode::INTEGER->value, + 'value' => '1', ], ], ]; diff --git a/tests/unit/Phql/Update/CombinationTest.php b/tests/unit/Phql/Update/CombinationTest.php index 4f1b9ec..818fecd 100644 --- a/tests/unit/Phql/Update/CombinationTest.php +++ b/tests/unit/Phql/Update/CombinationTest.php @@ -19,80 +19,6 @@ final class CombinationTest extends AbstractUnitTestCase { - /** - * @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); - } - /** * @return void * @@ -227,50 +153,6 @@ public function testMvcModelQueryPhqlUpdateCalculatedWhereNumZero(): void $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 * @@ -614,6 +496,50 @@ public function testMvcModelQueryPhqlUpdatePlaceholderWherePlaceholder(): void $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 * @@ -664,4 +590,78 @@ public function testMvcModelQueryPhqlUpdateUpperWhereNum(): void $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 index af37f52..1ad3ab7 100644 --- a/tests/unit/Scanner/OpcodeTest.php +++ b/tests/unit/Scanner/OpcodeTest.php @@ -9,6 +9,12 @@ final class OpcodeTest extends AbstractUnitTestCase { + public function testFromInt(): void + { + $this->assertSame(Opcode::SELECT, Opcode::from(309)); + $this->assertSame(Opcode::IDENTIFIER, Opcode::from(265)); + } + public function testKeyOpcodeValues(): void { $this->assertSame(43, Opcode::ADD->value); @@ -33,21 +39,13 @@ public function testKeyOpcodeValues(): void $this->assertSame(355, Opcode::QUALIFIED->value); } - public function testSingleCharOpcodeValues(): void + public function testLabelFallbackToName(): 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(')') + $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 @@ -74,19 +72,21 @@ public function testLabelOperators(): void $this->assertSame(')', Opcode::PARENTHESES_CLOSE->label()); } - 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 testFromInt(): void + public function testSingleCharOpcodeValues(): void { - $this->assertSame(Opcode::SELECT, Opcode::from(309)); - $this->assertSame(Opcode::IDENTIFIER, Opcode::from(265)); + $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 diff --git a/tests/unit/Scanner/ScannerTest.php b/tests/unit/Scanner/ScannerTest.php index 8af3fd4..ea18a80 100644 --- a/tests/unit/Scanner/ScannerTest.php +++ b/tests/unit/Scanner/ScannerTest.php @@ -12,107 +12,84 @@ final class ScannerTest extends AbstractUnitTestCase { - private function scanAll(string $input): array - { - $state = new State($input); - $scanner = new Scanner($state); - $opcodes = []; - - while (($result = $scanner->scanForToken()) === ScannerStatus::OK) { - $token = $scanner->getToken(); - if ($token->opcode !== Opcode::IGNORE) { - $opcodes[] = $token->opcode; - } - } - - return $opcodes; - } - - public function testEmptyInputReturnsEof(): void - { - $state = new State(''); - $scanner = new Scanner($state); - - $this->assertSame(ScannerStatus::EOF, $scanner->scanForToken()); - } - - public function testReturnTypeIsScannerStatus(): void - { - $state = new State('SELECT'); - $scanner = new Scanner($state); - $result = $scanner->scanForToken(); - - $this->assertInstanceOf(ScannerStatus::class, $result); - } - - public function testNoLegacyRetcodeConstants(): void + public function testBitwiseOperators(): 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')); + $opcodes = $this->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 testWhitespaceProducesIgnore(): void + public function testBracketPlaceholder(): void { - $state = new State(' '); + $state = new State('{id}'); $scanner = new Scanner($state); $scanner->scanForToken(); - $this->assertSame(Opcode::IGNORE, $scanner->getToken()->opcode); - } - - public function testSelectKeyword(): void - { - $opcodes = $this->scanAll('SELECT'); - $this->assertSame([Opcode::SELECT], $opcodes); - } + $token = $scanner->getToken(); - public function testFromKeyword(): void - { - $opcodes = $this->scanAll('FROM'); - $this->assertSame([Opcode::FROM], $opcodes); + $this->assertSame(Opcode::BPLACEHOLDER, $token->opcode); + $this->assertSame('id', $token->value); } - public function testWhereKeyword(): void + public function testComparisonOperators(): void { - $opcodes = $this->scanAll('WHERE'); - $this->assertSame([Opcode::WHERE], $opcodes); + $opcodes = $this->scanAll('= != ! < <= > >='); + $this->assertSame([ + Opcode::EQUALS, + Opcode::NOTEQUALS, + Opcode::NOT, + Opcode::LESS, + Opcode::LESSEQUAL, + Opcode::GREATER, + Opcode::GREATEREQUAL, + ], $opcodes); } - public function testIdentifier(): void + public function testDoubleLiteral(): void { - $state = new State('Invoices'); + $state = new State('3.14'); $scanner = new Scanner($state); $scanner->scanForToken(); $token = $scanner->getToken(); - $this->assertSame(Opcode::IDENTIFIER, $token->opcode); - $this->assertSame('Invoices', $token->value); + $this->assertSame(Opcode::DOUBLE, $token->opcode); + $this->assertSame('3.14', $token->value); } - public function testIntegerLiteral(): void + public function testDoubleQuotedString(): void { - $state = new State('42'); + $state = new State('"world"'); $scanner = new Scanner($state); $scanner->scanForToken(); $token = $scanner->getToken(); - $this->assertSame(Opcode::INTEGER, $token->opcode); - $this->assertSame('42', $token->value); + $this->assertSame(Opcode::STRING, $token->opcode); + $this->assertSame('world', $token->value); } - public function testDoubleLiteral(): void + public function testEmptyInputReturnsEof(): void { - $state = new State('3.14'); + $state = new State(''); $scanner = new Scanner($state); - $scanner->scanForToken(); - $token = $scanner->getToken(); + $this->assertSame(ScannerStatus::EOF, $scanner->scanForToken()); + } - $this->assertSame(Opcode::DOUBLE, $token->opcode); - $this->assertSame('3.14', $token->value); + public function testFromKeyword(): void + { + $opcodes = $this->scanAll('FROM'); + $this->assertSame([Opcode::FROM], $opcodes); } public function testHexIntegerLiteral(): void @@ -126,28 +103,28 @@ public function testHexIntegerLiteral(): void $this->assertSame(Opcode::HINTEGER, $token->opcode); } - public function testSingleQuotedString(): void + public function testIdentifier(): void { - $state = new State("'hello'"); + $state = new State('Invoices'); $scanner = new Scanner($state); $scanner->scanForToken(); $token = $scanner->getToken(); - $this->assertSame(Opcode::STRING, $token->opcode); - $this->assertSame('hello', $token->value); + $this->assertSame(Opcode::IDENTIFIER, $token->opcode); + $this->assertSame('Invoices', $token->value); } - public function testDoubleQuotedString(): void + public function testIntegerLiteral(): void { - $state = new State('"world"'); + $state = new State('42'); $scanner = new Scanner($state); $scanner->scanForToken(); $token = $scanner->getToken(); - $this->assertSame(Opcode::STRING, $token->opcode); - $this->assertSame('world', $token->value); + $this->assertSame(Opcode::INTEGER, $token->opcode); + $this->assertSame('42', $token->value); } public function testNamedPlaceholder(): void @@ -162,27 +139,22 @@ public function testNamedPlaceholder(): void $this->assertSame('id', $token->value); } - public function testNumericPlaceholder(): void + public function testNoLegacyRetcodeConstants(): void { - $state = new State('?0'); - $scanner = new Scanner($state); - - $scanner->scanForToken(); - $token = $scanner->getToken(); - - $this->assertSame(Opcode::NPLACEHOLDER, $token->opcode); + $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 testBracketPlaceholder(): void + public function testNumericPlaceholder(): void { - $state = new State('{id}'); + $state = new State('?0'); $scanner = new Scanner($state); $scanner->scanForToken(); $token = $scanner->getToken(); - $this->assertSame(Opcode::BPLACEHOLDER, $token->opcode); - $this->assertSame('id', $token->value); + $this->assertSame(Opcode::NPLACEHOLDER, $token->opcode); } public function testOperators(): void @@ -197,17 +169,13 @@ public function testOperators(): void ], $opcodes); } - public function testComparisonOperators(): void + public function testReturnTypeIsScannerStatus(): void { - $opcodes = $this->scanAll('= != < <= > >='); - $this->assertSame([ - Opcode::EQUALS, - Opcode::NOTEQUALS, - Opcode::LESS, - Opcode::LESSEQUAL, - Opcode::GREATER, - Opcode::GREATEREQUAL, - ], $opcodes); + $state = new State('SELECT'); + $scanner = new Scanner($state); + $result = $scanner->scanForToken(); + + $this->assertInstanceOf(ScannerStatus::class, $result); } public function testSelectFromSequence(): void @@ -221,6 +189,24 @@ public function testSelectFromSequence(): void ], $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('#'); @@ -230,4 +216,35 @@ public function testUnknownCharacterReturnsErr(): void // '#' 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 index be261d7..47310c2 100644 --- a/tests/unit/Scanner/StateTest.php +++ b/tests/unit/Scanner/StateTest.php @@ -10,6 +10,15 @@ final class StateTest extends AbstractUnitTestCase { + public function testClearActiveToken(): void + { + $state = new State('SELECT'); + $state->setActiveToken(Opcode::SELECT); + $state->setActiveToken(null); + + $this->assertNull($state->getActiveToken()); + } + public function testConstruction(): void { $state = new State('SELECT'); @@ -29,30 +38,28 @@ public function testEmptyBuffer(): void $this->assertNull($state->getStart()); } - public function testSetActiveToken(): void + public function testIncrementStart(): void { $state = new State('SELECT'); - $state->setActiveToken(Opcode::SELECT); + $state->incrementStart(1); - $this->assertSame(Opcode::SELECT, $state->getActiveToken()); + $this->assertSame(1, $state->getCursor()); + $this->assertSame('E', $state->getStart()); } - public function testClearActiveToken(): void + public function testNoSetEndMethod(): void { $state = new State('SELECT'); - $state->setActiveToken(Opcode::SELECT); - $state->setActiveToken(null); - - $this->assertNull($state->getActiveToken()); + /** @phpstan-ignore function.impossibleType */ + $this->assertFalse(method_exists($state, 'setEnd')); } - public function testIncrementStart(): void + public function testSetActiveToken(): void { $state = new State('SELECT'); - $state->incrementStart(1); + $state->setActiveToken(Opcode::SELECT); - $this->assertSame(1, $state->getCursor()); - $this->assertSame('E', $state->getStart()); + $this->assertSame(Opcode::SELECT, $state->getActiveToken()); } public function testSetCursor(): void @@ -63,11 +70,4 @@ public function testSetCursor(): void $this->assertSame(3, $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')); - } } diff --git a/tests/unit/Scanner/TokenTest.php b/tests/unit/Scanner/TokenTest.php index 85d1a45..eba7a82 100644 --- a/tests/unit/Scanner/TokenTest.php +++ b/tests/unit/Scanner/TokenTest.php @@ -10,15 +10,6 @@ final class TokenTest extends AbstractUnitTestCase { - public function testDefaultConstruction(): void - { - $token = new Token(); - - $this->assertNull($token->opcode); - $this->assertNull($token->value); - $this->assertSame(0, $token->length); - } - public function testConstructionWithAllValues(): void { $token = new Token(Opcode::SELECT, null, 6); @@ -37,6 +28,15 @@ public function testConstructionWithValue(): void $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); From 25e6135e72fd5420d9ad4d822792c1cd1e45c1d0 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 11 Apr 2026 14:24:24 -0500 Subject: [PATCH 20/21] reformatting code --- src/Parser.php | 40 ++++++++++++++++++++-------------------- src/Scanner/State.php | 6 +++--- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/Parser.php b/src/Parser.php index 68fd71e..70f173c 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -19,13 +19,6 @@ final class Parser { private bool $enableLiterals = true; - public function setEnableLiterals(bool $enable): static - { - $this->enableLiterals = $enable; - - return $this; - } - /** * Parse a PHQL string and return the AST array. * @@ -200,24 +193,17 @@ public function parse(string $phql): array /** @var array|null $ast */ $ast = $status->getAst(); if (!is_array($ast)) { - throw new Exception('PHQL parsing produced no result'); + throw new Exception('PHQL parsing produced no result'); // @codeCoverageIgnore } return $ast; } - /** - * 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 + public function setEnableLiterals(bool $enable): static { - return new Token( - $token->opcode, - $token->value, - $token->length, - ); + $this->enableLiterals = $enable; + + return $this; } /** @@ -249,7 +235,7 @@ private function buildScannerErrorMessage(Status $status, string $phql): string ); } - return 'Scanning error near to EOF'; + return 'Scanning error near to EOF'; // @codeCoverageIgnore } private function handleLiteralsDisabled(Status $status): void @@ -266,4 +252,18 @@ private function handleUnknownOpcode(?Opcode $opcode, Status $status): void 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/Scanner/State.php b/src/Scanner/State.php index 909649b..7175e62 100644 --- a/src/Scanner/State.php +++ b/src/Scanner/State.php @@ -6,13 +6,13 @@ class State { + public readonly string $rawBuffer; + public int $startLength; + private ?Opcode $activeToken = null; private readonly int $bufferLength; - private int $cursor = 0; - public readonly string $rawBuffer; private ?string $start = null; - public int $startLength; public function __construct(string $buffer) { From 89924e14817d55c0feb6ff3bb94953cf64a1e87f Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 11 Apr 2026 14:25:50 -0500 Subject: [PATCH 21/21] phpcs --- tests/unit/Phql/Select/ComplexTest.php | 1 - tests/unit/Phql/Select/FromTest.php | 1 - tests/unit/Phql/Select/OrderByTest.php | 1 - tests/unit/Phql/Select/SubqueriesTest.php | 1 - tests/unit/Phql/Select/WherePlaceholdersTest.php | 1 - 5 files changed, 5 deletions(-) diff --git a/tests/unit/Phql/Select/ComplexTest.php b/tests/unit/Phql/Select/ComplexTest.php index 932143e..2fe62d5 100644 --- a/tests/unit/Phql/Select/ComplexTest.php +++ b/tests/unit/Phql/Select/ComplexTest.php @@ -303,5 +303,4 @@ public function testMvcModelQueryPhqlSelectCountFieldWhereGroupByOrderBy(): void $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 index 65f88c5..c8d7636 100644 --- a/tests/unit/Phql/Select/FromTest.php +++ b/tests/unit/Phql/Select/FromTest.php @@ -173,5 +173,4 @@ public function testMvcModelQueryPhqlSelectFromMultipleTablesWhere(): void $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 index 70a8bb4..0953261 100644 --- a/tests/unit/Phql/Select/OrderByTest.php +++ b/tests/unit/Phql/Select/OrderByTest.php @@ -285,5 +285,4 @@ public function testMvcModelQueryPhqlSelectOrderByIntDesc(): void $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 index e7a1003..7724f34 100644 --- a/tests/unit/Phql/Select/SubqueriesTest.php +++ b/tests/unit/Phql/Select/SubqueriesTest.php @@ -472,5 +472,4 @@ public function testMvcModelQueryPhqlSelectWhereNotInSubquery(): void $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 index 1a803e9..577d7ff 100644 --- a/tests/unit/Phql/Select/WherePlaceholdersTest.php +++ b/tests/unit/Phql/Select/WherePlaceholdersTest.php @@ -300,5 +300,4 @@ public function testMvcModelQueryPhqlSelectWherePlaceholderStringAnd(): void $actual = (new Parser())->parse($source); $this->assertSame($expected, $actual); } - }