From 029a82784b4215b804f3c1ee1b6904ef58a70a90 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Wed, 8 Apr 2026 14:53:48 -0500 Subject: [PATCH 1/7] adding docker to the project --- docker-compose.yml | 65 +++++++++++++++++++++++++ resources/docker/develop/.bashrc | 73 +++++++++++++++++++++++++++++ resources/docker/develop/Dockerfile | 60 ++++++++++++++++++++++++ resources/docker/develop/extra.ini | 9 ++++ resources/files/parser.php | 2 +- 5 files changed, 208 insertions(+), 1 deletion(-) create mode 100644 docker-compose.yml create mode 100644 resources/docker/develop/.bashrc create mode 100644 resources/docker/develop/Dockerfile create mode 100644 resources/docker/develop/extra.ini diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..81be8dc --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,65 @@ +services: + phql-dev-8.1: + container_name: phql-dev-8.1 + hostname: phql-dev-81 + build: + dockerfile: resources/docker/develop/Dockerfile + args: + PHP_VERSION: 8.1 + working_dir: /app + volumes: + - .:/app + extra_hosts: + - "host.docker.internal:host-gateway" + + phql-dev-8.2: + container_name: phql-dev-8.2 + hostname: phql-dev-82 + build: + dockerfile: resources/docker/develop/Dockerfile + args: + PHP_VERSION: 8.2 + working_dir: /app + volumes: + - .:/app + extra_hosts: + - "host.docker.internal:host-gateway" + + phql-dev-8.3: + container_name: phql-dev-8.3 + hostname: phql-dev-83 + build: + dockerfile: resources/docker/develop/Dockerfile + args: + PHP_VERSION: 8.3 + working_dir: /app + volumes: + - .:/app + extra_hosts: + - "host.docker.internal:host-gateway" + + phql-dev-8.4: + container_name: phql-dev-8.4 + hostname: phql-dev-84 + build: + dockerfile: resources/docker/develop/Dockerfile + args: + PHP_VERSION: 8.4 + working_dir: /app + volumes: + - .:/app + extra_hosts: + - "host.docker.internal:host-gateway" + + phql-dev-8.5: + container_name: phql-dev-8.5 + hostname: phql-dev-85 + build: + dockerfile: resources/docker/develop/Dockerfile + args: + PHP_VERSION: 8.5 + working_dir: /app + volumes: + - .:/app + extra_hosts: + - "host.docker.internal:host-gateway" diff --git a/resources/docker/develop/.bashrc b/resources/docker/develop/.bashrc new file mode 100644 index 0000000..db0df49 --- /dev/null +++ b/resources/docker/develop/.bashrc @@ -0,0 +1,73 @@ +#!/bin/bash + +# Easier navigation: .., ..., ...., ....., ~ and - +alias ..="cd .." +alias ...="cd ../.." +alias ....="cd ../../.." +alias .....="cd ../../../.." +alias ~="cd ~" # `cd` is probably faster to type though +alias -- -="cd -" + +# Shortcuts +alias g="git" +alias h="history" + +# Detect which `ls` flavor is in use +if ls --color > /dev/null 2>&1; then # GNU `ls` + colorflag="--color" +else # OS X `ls` + colorflag="-G" +fi + +# List all files colorized in long format +# shellcheck disable=SC2139 +alias l="ls -lF ${colorflag}" + +# List all files colorized in long format, including dot files +# shellcheck disable=SC2139 +alias la="ls -laF ${colorflag}" + +# List only directories +# shellcheck disable=SC2139 +alias lsd="ls -lF ${colorflag} | grep --color=never '^d'" + +# See: https://superuser.com/a/656746/280737 +alias ll='LC_ALL="C.UTF-8" ls -alF' + +# Always use color output for `ls` +# shellcheck disable=SC2139 +alias ls="command ls ${colorflag}" +export LS_COLORS='no=00:fi=00:di=01;34:ln=01;36:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arj=01;31:*.taz=01;31:*.lzh=01;31:*.zip=01;31:*.z=01;31:*.Z=01;31:*.gz=01;31:*.bz2=01;31:*.deb=01;31:*.rpm=01;31:*.jar=01;31:*.jpg=01;35:*.jpeg=01;35:*.gif=01;35:*.bmp=01;35:*.pbm=01;35:*.pgm=01;35:*.ppm=01;35:*.tga=01;35:*.xbm=01;35:*.xpm=01;35:*.tif=01;35:*.tiff=01;35:*.png=01;35:*.mov=01;35:*.mpg=01;35:*.mpeg=01;35:*.avi=01;35:*.fli=01;35:*.gl=01;35:*.dl=01;35:*.xcf=01;35:*.xwd=01;35:*.ogg=01;35:*.mp3=01;35:*.wav=01;35:' + +# Always enable colored `grep` output +alias grep='grep --color=auto ' + +# Enable aliases to be sudo’ed +alias sudo='sudo ' + +# Get week number +alias week='date +%V' + +# Stopwatch +alias timer='echo "Timer started. Stop with Ctrl-D." && date && time cat && date' + +# Canonical hex dump; some systems have this symlinked +command -v hd > /dev/null || alias hd="hexdump -C" + +# vhosts +alias hosts='sudo nano /etc/hosts' + +# copy working directory +alias cwd='pwd | tr -d "\r\n" | xclip -selection clipboard' + +# copy file interactive +alias cp='cp -i' + +# move file interactive +alias mv='mv -i' + +# untar +alias untar='tar xvf' + +PATH=$PATH:./vendor/bin + diff --git a/resources/docker/develop/Dockerfile b/resources/docker/develop/Dockerfile new file mode 100644 index 0000000..022831d --- /dev/null +++ b/resources/docker/develop/Dockerfile @@ -0,0 +1,60 @@ +ARG PHP_VERSION=8.5 + +FROM php:${PHP_VERSION}-fpm AS base + +ARG PHP_VERSION=8.5 +ARG UID=1000 +ARG GID=1000 +ARG USER=phalcon +ARG GROUP=phalcon + +# hadolint ignore=DL3022 +COPY --from=composer/composer:2 --chown=${USER}:${GROUP} --chmod=0770 /usr/bin/composer /usr/bin/composer +# hadolint ignore=DL3022 +COPY --from=ghcr.io/php/pie:bin /pie /usr/bin/pie + +# Add user and group +RUN groupadd -g "${GID}" "${GROUP}" \ + && RUN useradd -l -m -u "${UID}" -g "${GID}" "${USER}" \ + && RUN usermod -s /bin/bash "${USER}" \ + && RUN mkdir /app \ + && RUN chown "${USER}":"${GROUP}" /app \ + && RUN chmod 0770 /app \ +# Install needed packages \ + && RUN apt update -y \ + && RUN apt install -yq --no-install-recommends apt-utils git locales nano sudo unzip wget zip \ +# Configure locales \ + && RUN sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen \ + && RUN dpkg-reconfigure --frontend=noninteractive locales \ + && RUN update-locale LANG=en_US.UTF-8 \ +# Install xdebug for code coverage \ + && RUN apt install -yq --no-install-recommends $PHPIZE_DEPS \ + && RUN pie install xdebug/xdebug \ + && RUN apt-get purge -y --auto-remove $PHPIZE_DEPS \ +# Copy ini file \ + && RUN mv /usr/local/etc/php/php.ini-production /usr/local/etc/php/php.ini \ +# Set correct pid file location and permissions \ + && RUN mkdir -p /run/php \ + && RUN chown "${USER}":"${GROUP}" /run/php \ + && RUN chmod 0770 /run/php \ +# Cleanup \ + && RUN apt-get autoremove --purge -y \ + && RUN apt-get autoclean -y \ + && RUN apt-get clean -y \ + && RUN rm -rf /tmp/* /var/tmp/* \ + && RUN find /var/cache/apt/archives /var/lib/apt/lists -not -name lock -type f -delete \ + && RUN find /var/cache -type f -delete \ + && RUN 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/ + +ENV LANG=en_US.UTF-8 \ + LANGUAGE=en_US.UTF-8 \ + LC_ALL=en_US.UTF-8 + +SHELL [ "/bin/bash", "--rcfile", "/home/${USER}/.bashrc", "-o", "pipefail", "-c" ] + +WORKDIR /app + +#USER ${USER} diff --git a/resources/docker/develop/extra.ini b/resources/docker/develop/extra.ini new file mode 100644 index 0000000..786272c --- /dev/null +++ b/resources/docker/develop/extra.ini @@ -0,0 +1,9 @@ +error_reporting=E_ALL +display_errors="On" +display_startup_errors="On" +log_errors="On" +error_log=/tmp/php_errors.log +memory_limit=512M +apc.enable_cli="On" +session.save_path="/tmp" +xdebug.mode="coverage" diff --git a/resources/files/parser.php b/resources/files/parser.php index c96008a..bac010d 100644 --- a/resources/files/parser.php +++ b/resources/files/parser.php @@ -1029,7 +1029,7 @@ public function getOutput(): array 'argument_item', ]; public mixed $yyTraceFILE = null; - public string $yyTracePrompt = ''; + public ?string $yyTracePrompt = ''; static $yy_action = [ /* 0 */ 50, From 6ccc8f73589fe5baf73e784724b6d9e82fff84a3 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Wed, 8 Apr 2026 14:54:01 -0500 Subject: [PATCH 2/7] updating composer --- composer.json | 11 ++-- composer.lock | 137 ++++++++++++++++++++++++++++++++------------------ 2 files changed, 95 insertions(+), 53 deletions(-) diff --git a/composer.json b/composer.json index 2819dc2..839847d 100644 --- a/composer.json +++ b/composer.json @@ -12,7 +12,11 @@ "php": ">=8.1", "ext-mbstring": "*" }, + "suggest": { + "phalcon/phalcon": "The Phalcon framework this module integrates with" + }, "require-dev": { + "phpstan/phpstan": "^2.0", "phpunit/phpunit": "^10.5" }, "autoload": { @@ -25,11 +29,12 @@ }, "autoload-dev": { "psr-4": { - "Phalcon\\Phql\\Tests\\": "tests/" + "Phalcon\\Phql\\Tests\\": "tests/", + "Phalcon\\Phql\\Tests\\Unit\\": "tests/unit/" } }, "scripts": { - "test": "phpunit", - "test:coverage": "phpunit --coverage-html 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/" } } diff --git a/composer.lock b/composer.lock index dde99a3..c3cba04 100644 --- a/composer.lock +++ b/composer.lock @@ -9,16 +9,16 @@ "packages-dev": [ { "name": "myclabs/deep-copy", - "version": "1.13.1", + "version": "1.13.4", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c" + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/1720ddd719e16cf0db4eb1c6eca108031636d46c", - "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", "shasum": "" }, "require": { @@ -57,7 +57,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.13.1" + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" }, "funding": [ { @@ -65,20 +65,20 @@ "type": "tidelift" } ], - "time": "2025-04-29T12:36:36+00:00" + "time": "2025-08-01T08:46:24+00:00" }, { "name": "nikic/php-parser", - "version": "v5.5.0", + "version": "v5.7.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "ae59794362fe85e051a58ad36b289443f57be7a9" + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/ae59794362fe85e051a58ad36b289443f57be7a9", - "reference": "ae59794362fe85e051a58ad36b289443f57be7a9", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", "shasum": "" }, "require": { @@ -97,7 +97,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "5.x-dev" } }, "autoload": { @@ -121,9 +121,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.5.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" }, - "time": "2025-05-31T08:24:38+00:00" + "time": "2025-12-06T11:56:16+00:00" }, { "name": "phar-io/manifest", @@ -566,16 +566,16 @@ }, { "name": "phpunit/phpunit", - "version": "10.5.47", + "version": "10.5.63", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "3637b3e50d32ab3a0d1a33b3b6177169ec3d95a3" + "reference": "33198268dad71e926626b618f3ec3966661e4d90" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3637b3e50d32ab3a0d1a33b3b6177169ec3d95a3", - "reference": "3637b3e50d32ab3a0d1a33b3b6177169ec3d95a3", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/33198268dad71e926626b618f3ec3966661e4d90", + "reference": "33198268dad71e926626b618f3ec3966661e4d90", "shasum": "" }, "require": { @@ -585,7 +585,7 @@ "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.13.1", + "myclabs/deep-copy": "^1.13.4", "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=8.1", @@ -596,13 +596,13 @@ "phpunit/php-timer": "^6.0.0", "sebastian/cli-parser": "^2.0.1", "sebastian/code-unit": "^2.0.0", - "sebastian/comparator": "^5.0.3", + "sebastian/comparator": "^5.0.5", "sebastian/diff": "^5.1.1", "sebastian/environment": "^6.1.0", - "sebastian/exporter": "^5.1.2", + "sebastian/exporter": "^5.1.4", "sebastian/global-state": "^6.0.2", "sebastian/object-enumerator": "^5.0.0", - "sebastian/recursion-context": "^5.0.0", + "sebastian/recursion-context": "^5.0.1", "sebastian/type": "^4.0.0", "sebastian/version": "^4.0.1" }, @@ -647,7 +647,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.47" + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.63" }, "funding": [ { @@ -671,7 +671,7 @@ "type": "tidelift" } ], - "time": "2025-06-20T11:29:11+00:00" + "time": "2026-01-27T05:48:37+00:00" }, { "name": "sebastian/cli-parser", @@ -843,16 +843,16 @@ }, { "name": "sebastian/comparator", - "version": "5.0.3", + "version": "5.0.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e" + "reference": "55dfef806eb7dfeb6e7a6935601fef866f8ca48d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e", - "reference": "a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/55dfef806eb7dfeb6e7a6935601fef866f8ca48d", + "reference": "55dfef806eb7dfeb6e7a6935601fef866f8ca48d", "shasum": "" }, "require": { @@ -908,15 +908,27 @@ "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", "security": "https://github.com/sebastianbergmann/comparator/security/policy", - "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.3" + "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.5" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" } ], - "time": "2024-10-18T14:56:07+00:00" + "time": "2026-01-24T09:25:16+00:00" }, { "name": "sebastian/complexity", @@ -1109,16 +1121,16 @@ }, { "name": "sebastian/exporter", - "version": "5.1.2", + "version": "5.1.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "955288482d97c19a372d3f31006ab3f37da47adf" + "reference": "0735b90f4da94969541dac1da743446e276defa6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/955288482d97c19a372d3f31006ab3f37da47adf", - "reference": "955288482d97c19a372d3f31006ab3f37da47adf", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/0735b90f4da94969541dac1da743446e276defa6", + "reference": "0735b90f4da94969541dac1da743446e276defa6", "shasum": "" }, "require": { @@ -1127,7 +1139,7 @@ "sebastian/recursion-context": "^5.0" }, "require-dev": { - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^10.5" }, "type": "library", "extra": { @@ -1175,15 +1187,27 @@ "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", "security": "https://github.com/sebastianbergmann/exporter/security/policy", - "source": "https://github.com/sebastianbergmann/exporter/tree/5.1.2" + "source": "https://github.com/sebastianbergmann/exporter/tree/5.1.4" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" } ], - "time": "2024-03-02T07:17:12+00:00" + "time": "2025-09-24T06:09:11+00:00" }, { "name": "sebastian/global-state", @@ -1419,23 +1443,23 @@ }, { "name": "sebastian/recursion-context", - "version": "5.0.0", + "version": "5.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "05909fb5bc7df4c52992396d0116aed689f93712" + "reference": "47e34210757a2f37a97dcd207d032e1b01e64c7a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/05909fb5bc7df4c52992396d0116aed689f93712", - "reference": "05909fb5bc7df4c52992396d0116aed689f93712", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/47e34210757a2f37a97dcd207d032e1b01e64c7a", + "reference": "47e34210757a2f37a97dcd207d032e1b01e64c7a", "shasum": "" }, "require": { "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^10.5" }, "type": "library", "extra": { @@ -1470,15 +1494,28 @@ "homepage": "https://github.com/sebastianbergmann/recursion-context", "support": { "issues": "https://github.com/sebastianbergmann/recursion-context/issues", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/5.0.0" + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/5.0.1" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" } ], - "time": "2023-02-03T07:05:40+00:00" + "time": "2025-08-10T07:50:56+00:00" }, { "name": "sebastian/type", @@ -1591,16 +1628,16 @@ }, { "name": "theseer/tokenizer", - "version": "1.2.3", + "version": "1.3.1", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", - "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", "shasum": "" }, "require": { @@ -1629,7 +1666,7 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.2.3" + "source": "https://github.com/theseer/tokenizer/tree/1.3.1" }, "funding": [ { @@ -1637,7 +1674,7 @@ "type": "github" } ], - "time": "2024-03-03T12:36:25+00:00" + "time": "2025-11-17T20:03:58+00:00" } ], "aliases": [], @@ -1650,5 +1687,5 @@ "ext-mbstring": "*" }, "platform-dev": {}, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" } From 4bd3410864a7ea194a78fca718b3e19de59ed26c Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Wed, 8 Apr 2026 14:54:46 -0500 Subject: [PATCH 3/7] logic corrections and test passing --- src/Scanner/Scanner.php | 103 +++++++++++---------- src/Scanner/State.php | 11 ++- src/Scanner/Token.php | 5 +- tests/PhqlParserTest.php | 193 --------------------------------------- 4 files changed, 66 insertions(+), 246 deletions(-) delete mode 100644 tests/PhqlParserTest.php diff --git a/src/Scanner/Scanner.php b/src/Scanner/Scanner.php index 1d56148..6d30d85 100644 --- a/src/Scanner/Scanner.php +++ b/src/Scanner/Scanner.php @@ -29,12 +29,23 @@ public function scanForToken(): int { $yyinput = $this->state->getRawBuffer(); $yycursor = $this->state->getCursor(); - $q = $yycursor; - $token = $this->token; + + if ($yycursor >= $this->state->getBufferLength()) { + return self::PHQL_SCANNER_RETCODE_EOF; + } + + $q = $yycursor; + $token = $this->token; + $token->value = null; + $token->opcode = null; + $token->len = 0; $status = self::PHQL_SCANNER_RETCODE_IMPOSSIBLE; while (self::PHQL_SCANNER_RETCODE_IMPOSSIBLE == $status) { + if ($yycursor >= $this->state->getBufferLength()) { + return self::PHQL_SCANNER_RETCODE_EOF; + } $yych = 0; $yyaccept = 0; $yystate = 0; @@ -47,9 +58,9 @@ public function scanForToken(): int case 0x00: $yystate = 1; break 2; - case '\t': - case '\n': - case '\r': + case "\t": + case "\n": + case "\r": case ' ': $yystate = 4; break 2; @@ -255,9 +266,9 @@ public function scanForToken(): int case 4: $yych = $yyinput[$yycursor]; switch ($yych) { - case '\t': - case '\n': - case '\r': + case "\t": + case "\n": + case "\r": case ' ': $yycursor += 1; $yystate = 4; @@ -854,9 +865,8 @@ public function scanForToken(): int break 2; } case 45: - $yych = $yyinput[$yycursor]; - $yystate = 46; - break 2; + $yych = $yyinput[$yycursor]; + // fall through case 46: switch ($yych) { case '0': @@ -1093,11 +1103,11 @@ public function scanForToken(): int case 0x06: case 0x07: case 0x08: - case '\t': - case '\n': - case '\v': - case '\f': - case '\r': + case "\t": + case "\n": + case "\v": + case "\f": + case "\r": case 0x0E: case 0x0F: case 0x10: @@ -1194,9 +1204,8 @@ public function scanForToken(): int return 0; case 59: - $yych = $yyinput[$yycursor]; - $yystate = 60; - break 2; + $yych = $yyinput[$yycursor]; + // fall through case 60: switch ($yych) { case '0': @@ -1387,9 +1396,8 @@ public function scanForToken(): int return 0; case 67: - $yych = $yyinput[$yycursor]; - $yystate = 68; - break 2; + $yych = $yyinput[$yycursor]; + // fall through case 68: switch ($yych) { case 0x00: @@ -1436,7 +1444,7 @@ public function scanForToken(): int case 71: $yych = $yyinput[$yycursor]; switch ($yych) { - case '\n': + case "\n": $yystate = 69; break 2; default: @@ -1451,9 +1459,8 @@ public function scanForToken(): int return 0; case 73: - $yych = $yyinput[$yycursor]; - $yystate = 74; - break 2; + $yych = $yyinput[$yycursor]; + // fall through case 74: switch ($yych) { case 0x00: @@ -1475,7 +1482,7 @@ public function scanForToken(): int case 75: $yych = $yyinput[$yycursor]; switch ($yych) { - case '\n': + case "\n": $yystate = 69; break 2; default: @@ -2617,9 +2624,8 @@ public function scanForToken(): int break 2; } case 135: - $yych = $yyinput[$yycursor]; - $yystate = 136; - break 2; + $yych = $yyinput[$yycursor]; + // fall through case 136: switch ($yych) { case 0x00: @@ -2631,11 +2637,11 @@ public function scanForToken(): int case 0x06: case 0x07: case 0x08: - case '\t': - case '\n': - case '\v': - case '\f': - case '\r': + case "\t": + case "\n": + case "\v": + case "\f": + case "\r": case 0x0E: case 0x0F: case 0x10: @@ -2683,11 +2689,11 @@ public function scanForToken(): int case 0x06: case 0x07: case 0x08: - case '\t': - case '\n': - case '\v': - case '\f': - case '\r': + case "\t": + case "\n": + case "\v": + case "\f": + case "\r": case 0x0E: case 0x0F: case 0x10: @@ -2723,13 +2729,12 @@ public function scanForToken(): int break 2; } case 138: - $yystate = 139; - break 2; + // fall through case 139: $token->opcode = Opcode::PHQL_T_IDENTIFIER; - $token->value = substr($yyinput, $q, $yycursor - $q - 1); - $token->len = $yycursor - $q - 1; + $token->value = substr($yyinput, $q, $yycursor - $q); + $token->len = $yycursor - $q; $q = $yycursor; $this->state->setCursor($yycursor); return 0; @@ -3857,11 +3862,11 @@ public function scanForToken(): int case 0x06: case 0x07: case 0x08: - case '\t': - case '\n': - case '\v': - case '\f': - case '\r': + case "\t": + case "\n": + case "\v": + case "\f": + case "\r": case 0x0E: case 0x0F: case 0x10: diff --git a/src/Scanner/State.php b/src/Scanner/State.php index 4315f37..ce01b41 100644 --- a/src/Scanner/State.php +++ b/src/Scanner/State.php @@ -10,14 +10,16 @@ class State public string $rawBuffer; public int $startLength; protected int $cursor = 0; + private int $bufferLength; protected ?string $end = null; protected ?string $start = null; public function __construct(string $buffer) { - $this->rawBuffer = $buffer; - $this->startLength = mb_strlen($buffer); + $this->bufferLength = strlen($buffer); + $this->rawBuffer = $buffer . "\0"; // null terminator for look-ahead safety in scanner + $this->startLength = mb_strlen($buffer); if ($this->startLength > 0) { $this->setStart($buffer[0]); $this->setEnd($buffer[0]); @@ -29,6 +31,11 @@ public function getActiveToken(): mixed return $this->activeToken; } + public function getBufferLength(): int + { + return $this->bufferLength; + } + public function getRawBuffer(): string { return $this->rawBuffer; diff --git a/src/Scanner/Token.php b/src/Scanner/Token.php index 3646aff..ce8d1dd 100644 --- a/src/Scanner/Token.php +++ b/src/Scanner/Token.php @@ -7,8 +7,9 @@ class Token { protected int $length = 0; - protected mixed $opcode = null; - protected mixed $value = null; + public int $len = 0; + public mixed $opcode = null; + public mixed $value = null; public function getLength(): int { diff --git a/tests/PhqlParserTest.php b/tests/PhqlParserTest.php deleted file mode 100644 index b03ea25..0000000 --- a/tests/PhqlParserTest.php +++ /dev/null @@ -1,193 +0,0 @@ -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); - - // Verify structure with assertions - $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); - - // Verify structure - $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']); - - // The parser includes brackets in FQCN - $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); - - // Verify structure - $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); - - // Verify structure - $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); - - // Verify structure - $this->assertIsArray($result); - $this->assertArrayHasKey('type', $result); - $this->assertArrayHasKey('delete', $result); - - // Verify delete structure - $this->assertArrayHasKey('tables', $result['delete']); - $this->assertEquals('co_invoices', $result['delete']['tables']['qualifiedName']['name']); - - // Verify WHERE clause exists - $this->assertArrayHasKey('where', $result); - } -} - From 4bbb102821c06ea2bd4f6769e80563f45dfa4125 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Wed, 8 Apr 2026 14:55:00 -0500 Subject: [PATCH 4/7] tests for the parser --- phpunit.xml | 10 +- tests/AbstractUnitTestCase.php | 85 ++++++++ .../.phpunit.result.cache/test-results | 1 + tests/unit/Parser/PhqlParserTest.php | 193 ++++++++++++++++++ 4 files changed, 284 insertions(+), 5 deletions(-) create mode 100644 tests/AbstractUnitTestCase.php create mode 100644 tests/_output/.phpunit.result.cache/test-results create mode 100644 tests/unit/Parser/PhqlParserTest.php diff --git a/phpunit.xml b/phpunit.xml index 9b873f6..24a70b1 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -3,19 +3,19 @@ xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd" bootstrap="vendor/autoload.php" colors="true" - cacheDirectory=".phpunit.cache" + cacheDirectory="tests/_output/.phpunit.result.cache" beStrictAboutOutputDuringTests="false" failOnRisky="false" failOnWarning="true"> - - tests + + tests/unit - src - resources + src + resources diff --git a/tests/AbstractUnitTestCase.php b/tests/AbstractUnitTestCase.php new file mode 100644 index 0000000..447846d --- /dev/null +++ b/tests/AbstractUnitTestCase.php @@ -0,0 +1,85 @@ + + * + * 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; + +use PHPUnit\Framework\TestCase; +use ReflectionClass; +use ReflectionException; + +use function array_slice; +use function array_unshift; +use function call_user_func_array; +use function func_get_args; +use function is_object; + +abstract class AbstractUnitTestCase extends TestCase +{ + /** + * Calls a private or protected method. + * + * @throws ReflectionException + */ + public function callProtectedMethod( + string|object $obj, + string $method + ): mixed { + $reflectionClass = new ReflectionClass($obj); + $reflectionMethod = $reflectionClass->getMethod($method); + $reflectionMethod->setAccessible(true); + + if (!is_object($obj)) { + $obj = $reflectionClass->newInstanceWithoutConstructor(); + } + + $args = array_slice(func_get_args(), 2); + array_unshift($args, $obj); + + return call_user_func_array( + [$reflectionMethod, 'invoke'], + $args + ); + } + + /** + * Returns the value of a protected property. + * + * @throws ReflectionException + */ + public function getProtectedProperty( + object|string $obj, + string $property + ): mixed { + $reflection = new ReflectionClass($obj); + $property = $reflection->getProperty($property); + $property->setAccessible(true); + + return $property->getValue($obj); + } + + /** + * Sets a protected property. + * + * @throws ReflectionException + */ + public function setProtectedProperty( + object|string $obj, + string $property, + mixed $value + ): void { + $reflection = new ReflectionClass($obj); + $property = $reflection->getProperty($property); + $property->setAccessible(true); + $property->setValue($obj, $value); + } +} diff --git a/tests/_output/.phpunit.result.cache/test-results b/tests/_output/.phpunit.result.cache/test-results new file mode 100644 index 0000000..f51ef3c --- /dev/null +++ b/tests/_output/.phpunit.result.cache/test-results @@ -0,0 +1 @@ +{"version":2,"defects":{"Phalcon\\Phql\\Tests\\Unit\\Parser\\PhqlParserTest::testSelectWithLimit":7,"Phalcon\\Phql\\Tests\\Unit\\Parser\\PhqlParserTest::testSelectWithBetween":8,"Phalcon\\Phql\\Tests\\Unit\\Parser\\PhqlParserTest::testUsingFQCNForSourceModel":7,"Phalcon\\Phql\\Tests\\Unit\\Parser\\PhqlParserTest::testSelectWithNotBetween":8,"Phalcon\\Phql\\Tests\\Unit\\Parser\\PhqlParserTest::testUsingSpacesInColumnAlias":7,"Phalcon\\Phql\\Tests\\Unit\\Parser\\PhqlParserTest::testDeleteWithWhereAndOr":8},"times":{"Phalcon\\Phql\\Tests\\Unit\\Parser\\PhqlParserTest::testSelectWithLimit":0.01,"Phalcon\\Phql\\Tests\\Unit\\Parser\\PhqlParserTest::testSelectWithBetween":0.003,"Phalcon\\Phql\\Tests\\Unit\\Parser\\PhqlParserTest::testUsingFQCNForSourceModel":0.002,"Phalcon\\Phql\\Tests\\Unit\\Parser\\PhqlParserTest::testSelectWithNotBetween":0.004,"Phalcon\\Phql\\Tests\\Unit\\Parser\\PhqlParserTest::testUsingSpacesInColumnAlias":0.002,"Phalcon\\Phql\\Tests\\Unit\\Parser\\PhqlParserTest::testDeleteWithWhereAndOr":0.004}} \ No newline at end of file diff --git a/tests/unit/Parser/PhqlParserTest.php b/tests/unit/Parser/PhqlParserTest.php new file mode 100644 index 0000000..d933d02 --- /dev/null +++ b/tests/unit/Parser/PhqlParserTest.php @@ -0,0 +1,193 @@ + + * + * 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 6dff46584b2d6dff5c51c06ce8ad88bf9e45fbfd Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Wed, 8 Apr 2026 14:55:30 -0500 Subject: [PATCH 5/7] Revert "tests for the parser" This reverts commit 4bbb102821c06ea2bd4f6769e80563f45dfa4125. --- phpunit.xml | 10 +- tests/AbstractUnitTestCase.php | 85 -------- .../.phpunit.result.cache/test-results | 1 - tests/unit/Parser/PhqlParserTest.php | 193 ------------------ 4 files changed, 5 insertions(+), 284 deletions(-) delete mode 100644 tests/AbstractUnitTestCase.php delete mode 100644 tests/_output/.phpunit.result.cache/test-results delete mode 100644 tests/unit/Parser/PhqlParserTest.php diff --git a/phpunit.xml b/phpunit.xml index 24a70b1..9b873f6 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -3,19 +3,19 @@ xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd" bootstrap="vendor/autoload.php" colors="true" - cacheDirectory="tests/_output/.phpunit.result.cache" + cacheDirectory=".phpunit.cache" beStrictAboutOutputDuringTests="false" failOnRisky="false" failOnWarning="true"> - - tests/unit + + tests - src - resources + src + resources diff --git a/tests/AbstractUnitTestCase.php b/tests/AbstractUnitTestCase.php deleted file mode 100644 index 447846d..0000000 --- a/tests/AbstractUnitTestCase.php +++ /dev/null @@ -1,85 +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; - -use PHPUnit\Framework\TestCase; -use ReflectionClass; -use ReflectionException; - -use function array_slice; -use function array_unshift; -use function call_user_func_array; -use function func_get_args; -use function is_object; - -abstract class AbstractUnitTestCase extends TestCase -{ - /** - * Calls a private or protected method. - * - * @throws ReflectionException - */ - public function callProtectedMethod( - string|object $obj, - string $method - ): mixed { - $reflectionClass = new ReflectionClass($obj); - $reflectionMethod = $reflectionClass->getMethod($method); - $reflectionMethod->setAccessible(true); - - if (!is_object($obj)) { - $obj = $reflectionClass->newInstanceWithoutConstructor(); - } - - $args = array_slice(func_get_args(), 2); - array_unshift($args, $obj); - - return call_user_func_array( - [$reflectionMethod, 'invoke'], - $args - ); - } - - /** - * Returns the value of a protected property. - * - * @throws ReflectionException - */ - public function getProtectedProperty( - object|string $obj, - string $property - ): mixed { - $reflection = new ReflectionClass($obj); - $property = $reflection->getProperty($property); - $property->setAccessible(true); - - return $property->getValue($obj); - } - - /** - * Sets a protected property. - * - * @throws ReflectionException - */ - public function setProtectedProperty( - object|string $obj, - string $property, - mixed $value - ): void { - $reflection = new ReflectionClass($obj); - $property = $reflection->getProperty($property); - $property->setAccessible(true); - $property->setValue($obj, $value); - } -} diff --git a/tests/_output/.phpunit.result.cache/test-results b/tests/_output/.phpunit.result.cache/test-results deleted file mode 100644 index f51ef3c..0000000 --- a/tests/_output/.phpunit.result.cache/test-results +++ /dev/null @@ -1 +0,0 @@ -{"version":2,"defects":{"Phalcon\\Phql\\Tests\\Unit\\Parser\\PhqlParserTest::testSelectWithLimit":7,"Phalcon\\Phql\\Tests\\Unit\\Parser\\PhqlParserTest::testSelectWithBetween":8,"Phalcon\\Phql\\Tests\\Unit\\Parser\\PhqlParserTest::testUsingFQCNForSourceModel":7,"Phalcon\\Phql\\Tests\\Unit\\Parser\\PhqlParserTest::testSelectWithNotBetween":8,"Phalcon\\Phql\\Tests\\Unit\\Parser\\PhqlParserTest::testUsingSpacesInColumnAlias":7,"Phalcon\\Phql\\Tests\\Unit\\Parser\\PhqlParserTest::testDeleteWithWhereAndOr":8},"times":{"Phalcon\\Phql\\Tests\\Unit\\Parser\\PhqlParserTest::testSelectWithLimit":0.01,"Phalcon\\Phql\\Tests\\Unit\\Parser\\PhqlParserTest::testSelectWithBetween":0.003,"Phalcon\\Phql\\Tests\\Unit\\Parser\\PhqlParserTest::testUsingFQCNForSourceModel":0.002,"Phalcon\\Phql\\Tests\\Unit\\Parser\\PhqlParserTest::testSelectWithNotBetween":0.004,"Phalcon\\Phql\\Tests\\Unit\\Parser\\PhqlParserTest::testUsingSpacesInColumnAlias":0.002,"Phalcon\\Phql\\Tests\\Unit\\Parser\\PhqlParserTest::testDeleteWithWhereAndOr":0.004}} \ No newline at end of file diff --git a/tests/unit/Parser/PhqlParserTest.php b/tests/unit/Parser/PhqlParserTest.php deleted file mode 100644 index d933d02..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 21587e539c9f4573f6ad1210c876da7a78e07e6a Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Wed, 8 Apr 2026 14:58:06 -0500 Subject: [PATCH 6/7] adding tests and structure --- .gitignore | 2 +- tests/_output/.gitkeep | 0 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 tests/_output/.gitkeep diff --git a/.gitignore b/.gitignore index 235a189..3256564 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,6 @@ composer.phar phql.log # PHPUnit -.phpunit.cache/ +.phpunit.result.cache/ coverage/ coverage.xml diff --git a/tests/_output/.gitkeep b/tests/_output/.gitkeep new file mode 100644 index 0000000..e69de29 From 7d6939a4e3db11e45d6df7f96d385ba267b1d75a Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Wed, 8 Apr 2026 15:53:05 -0500 Subject: [PATCH 7/7] new workflow --- .github/workflows/main.yml | 93 +++++++++++++++++++++++++++++++++++++ .github/workflows/tests.yml | 84 --------------------------------- 2 files changed, 93 insertions(+), 84 deletions(-) create mode 100644 .github/workflows/main.yml delete mode 100644 .github/workflows/tests.yml diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..a6ad250 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,93 @@ +name: "PHQL CI" + +on: + schedule: + - cron: '0 2 * * *' # Daily at 02:00 runs only on default branch + push: + paths-ignore: + - '**.md' + - '**.txt' + pull_request: + workflow_dispatch: + +permissions: {} + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/master' }} + +jobs: + tests: + name: PHP ${{ matrix.php }} + runs-on: ubuntu-latest + + permissions: + contents: read + + strategy: + fail-fast: false + 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: mbstring + coverage: none + tools: composer:v2 + env: + COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Validate composer.json + run: composer validate --no-check-all --no-check-publish + + - name: Install dependencies + uses: ramsey/composer-install@v3 + with: + composer-options: "--prefer-dist" + + - name: Run tests + run: composer test + + coverage: + name: Code Coverage + runs-on: ubuntu-latest + + permissions: + contents: read + + steps: + - uses: actions/checkout@v6 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + extensions: mbstring + coverage: xdebug + tools: composer:v2 + env: + COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Install dependencies + uses: ramsey/composer-install@v3 + with: + composer-options: "--prefer-dist" + + - name: Run tests with coverage + run: vendor/bin/phpunit --coverage-clover coverage.xml + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v5 + with: + file: ./coverage.xml + fail_ci_if_error: false \ No newline at end of file diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml deleted file mode 100644 index 9728265..0000000 --- a/.github/workflows/tests.yml +++ /dev/null @@ -1,84 +0,0 @@ -name: Tests - -on: - push: - -jobs: - test: - name: PHP ${{ matrix.php-version }} - runs-on: ubuntu-latest - - strategy: - fail-fast: false - matrix: - php-version: ['8.1', '8.2', '8.3', '8.4', '8.5'] - - steps: - - uses: actions/checkout@v5 - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php-version }} - extensions: mbstring - coverage: none - tools: composer:v2 - - - name: Validate composer.json - run: composer validate --strict - - - name: Get composer cache directory - id: composer-cache - run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - - - name: Cache dependencies - uses: actions/cache@v4 - with: - path: ${{ steps.composer-cache.outputs.dir }} - key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} - restore-keys: ${{ runner.os }}-composer- - - - name: Install dependencies - run: composer install --prefer-dist --no-progress --no-suggest - - - name: Run tests - run: composer test - - coverage: - name: Code Coverage - runs-on: ubuntu-latest - if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master') - - steps: - - uses: actions/checkout@v5 - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: '8.3' - extensions: mbstring - coverage: xdebug - tools: composer:v2 - - - name: Get composer cache directory - id: composer-cache - run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - - - name: Cache dependencies - uses: actions/cache@v4 - with: - path: ${{ steps.composer-cache.outputs.dir }} - key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} - restore-keys: ${{ runner.os }}-composer- - - - name: Install dependencies - run: composer install --prefer-dist --no-progress --no-suggest - - - name: Run tests with coverage - run: vendor/bin/phpunit --coverage-clover coverage.xml - - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v4 - with: - file: ./coverage.xml - fail_ci_if_error: false