diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 00000000..aae774fe Binary files /dev/null and b/.DS_Store differ diff --git a/.env b/.env new file mode 100644 index 00000000..5d465b75 --- /dev/null +++ b/.env @@ -0,0 +1,4 @@ +# Database Configuration +NEO4J_ADDRESS=https://6f72daa1.databases.neo4j.io/ +NEO4J_USERNAME=neo4j +NEO4J_PASSWORD=O9lWmptqBgxBOz8NVcTJjgs3cHPyYmsy63ui6Spmw1d0 diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml new file mode 100644 index 00000000..7ab182b7 --- /dev/null +++ b/.github/workflows/codecov.yml @@ -0,0 +1,36 @@ +name: Code Coverage + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + coverage: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.1' + + - name: Install dependencies + run: composer install + + - name: Run tests and collect coverage + run: vendor/bin/phpunit --coverage-clover coverage.xml + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: ./coverage.xml + fail_ci_if_error: true + verbose: true diff --git a/.github/workflows/cs-fixer.yml b/.github/workflows/cs-fixer.yml new file mode 100644 index 00000000..537e6779 --- /dev/null +++ b/.github/workflows/cs-fixer.yml @@ -0,0 +1,26 @@ +name: PHP CS Fixer + +on: + push: + workflow_dispatch: + +jobs: + php-cs-fixer: + name: PHP CS Fixer + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.1 + tools: composer + + - name: Install dependencies + run: composer install --no-progress --prefer-dist + + - name: Run PHP CS Fixer + run: vendor/bin/php-cs-fixer fix --dry-run --diff --allow-risky=yes diff --git a/.github/workflows/psalm.yml b/.github/workflows/psalm.yml new file mode 100644 index 00000000..d28186d3 --- /dev/null +++ b/.github/workflows/psalm.yml @@ -0,0 +1,27 @@ +name: Psalm Static Analysis + +on: + push: + workflow_dispatch: + +jobs: + psalm: + name: Run Psalm + runs-on: ubuntu-latest + + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.1 + tools: composer, psalm + + - name: Install dependencies + run: composer install --no-progress --prefer-dist + + - name: Run Psalm + run: vendor/bin/psalm --output-format=github diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..2184c47d --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,54 @@ +name: PHP Tests + +on: + push: + workflow_dispatch: + +concurrency: + group: ${{ github.ref }} + cancel-in-progress: true + +jobs: + tests: + services: + neo4j: + image: neo4j:latest + ports: + - 7474:7474 + - 7687:7687 + env: + NEO4J_AUTH: neo4j/password + options: >- + --health-cmd "wget --no-verbose --tries=1 --spider localhost:7474 || exit 1" + --health-interval 10s + --health-retries 5 + --health-timeout 5s + + name: Run PHPUnit Tests + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.1 + tools: composer, xdebug + + - name: Cache Composer dependencies + uses: actions/cache@v3 + with: + path: vendor + key: ${{ runner.os }}-composer-${{ hashFiles('composer.lock') }} + restore-keys: ${{ runner.os }}-composer- + + - name: Install dependencies + run: composer install --no-progress --prefer-dist + + - name: Run Tests without phpunit.xml + env: + NEO4J_ADDRESS: "http://localhost:7474" + NEO4J_USERNAME: "neo4j" + NEO4J_PASSWORD: "password" + run: vendor/bin/phpunit --configuration phpunit.dist.xml diff --git a/.github/workflows/ci.yml b/.github/workflows/testaura.yml similarity index 64% rename from .github/workflows/ci.yml rename to .github/workflows/testaura.yml index 80a53709..d08d5bfa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/testaura.yml @@ -1,22 +1,21 @@ -name: CI Pipeline +name: Test Neo4j Aura on: push: branches: - main - pull_request: - branches: - - main + workflow_dispatch: # Allows manual trigger concurrency: - group: ${{ github.ref }} + group: neo4j-aura-test-main cancel-in-progress: true jobs: - tests: - name: Run PHPUnit Tests + tests-aura: + name: Run PHPUnit Tests with Neo4j Aura runs-on: ubuntu-latest + steps: - name: Checkout code uses: actions/checkout@v3 @@ -24,7 +23,7 @@ jobs: - name: Set up PHP uses: shivammathur/setup-php@v2 with: - php-version: 8.2 + php-version: 8.1 tools: composer, xdebug - name: Cache Composer dependencies @@ -37,7 +36,9 @@ jobs: - name: Install dependencies run: composer install --no-progress --prefer-dist - - - - name: Run Tests without phpunit.xml + - name: Run Tests + env: + NEO4J_ADDRESS: ${{ secrets.NEO4J_ADDRESS }} + NEO4J_USERNAME: ${{ secrets.NEO4J_USERNAME }} + NEO4J_PASSWORD: ${{ secrets.NEO4J_PASSWORD }} run: vendor/bin/phpunit --configuration phpunit.dist.xml diff --git a/.gitignore b/.gitignore index e400ee11..ea86c437 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,20 @@ + +#IDE .idea/ + +#COMPOSER vendor + +#PHPUNIT phpunit.xml -test \ No newline at end of file +test +.phpunit.result.cache + +#PHP-CS-FIXER +.php-cs-fixer.php +.php-cs-fixer.cache + + +coverage + +composer.lock \ No newline at end of file diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php new file mode 100644 index 00000000..e08e0bb9 --- /dev/null +++ b/.php-cs-fixer.dist.php @@ -0,0 +1,16 @@ +setRules([ + '@PSR12' => true, + ]) + ->setFinder( + Finder::create() + ->in(__DIR__) + ->exclude([ + 'vendor', + ]) + ); \ No newline at end of file diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php new file mode 100644 index 00000000..28bf20e3 --- /dev/null +++ b/.php-cs-fixer.php @@ -0,0 +1,18 @@ +setRiskyAllowed(true) // Allow risky fixers + ->setRules([ + '@PSR12' => true, + 'strict_param' => true, // This is a risky rule + ]) + ->setFinder( + Finder::create() + ->in(__DIR__) + ->exclude([ + 'vendor', + ]) + ); diff --git a/.phpunit.result.cache b/.phpunit.result.cache deleted file mode 100644 index 705e7729..00000000 --- a/.phpunit.result.cache +++ /dev/null @@ -1 +0,0 @@ -{"version":1,"defects":{"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithPropertyExistenceCheck":7,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithNumericFilters":8,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithNestedRelationships":7,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithMultipleConditions":7,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithPartialNameMatch":7,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithExactNames":7,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithSingleName":7,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testNodeType":7,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testRelationshipType":7,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithNoMatchingNames":8,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryintegrationtest::testRunSuccessWithParameters#testWithExactNames":7,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryintegrationtest::testRunSuccessWithParameters#testRelationshipType":7,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryintegrationtest::testRunSuccessWithParameters#testWithNoMatchingNames":7,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryintegrationtest::testRunSuccessWithParameters#testWithNull":7,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryintegrationtest::testRunSuccessWithParameters#testWithArray":7,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryintegrationtest::testRunSuccessWithParameters#testWithNumber":7,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryintegrationtest::testRunSuccessWithParameters#testWithString":7,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryintegrationtest::testRunSuccessWithParameters#testWithBoolean":8,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryIntegrationTest::testRunSuccessWithParameters#testWithString":7,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryIntegrationTest::testRunSuccessWithParameters#testWithNumber":7,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryIntegrationTest::testRunSuccessWithParameters#testWithBoolean":7,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryIntegrationTest::testRunSuccessWithParameters#testWithArray":7,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithString":8,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithArray":7,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithDate":7,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithBinary":7,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithNode":8,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithPoint":7,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithPath":8,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithSimpleRelationship":7,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jOGMTest::testInteger":7,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryApiIntegrationTempTest::testResultRowIntegration":8},"times":{"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#0":0.393,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithExactNames":0.1,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithNoMatchingNames":0.085,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithSingleName":0.088,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithNonExistentLabel":0.091,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithPropertyExistenceCheck":0.417,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithNumericFilters":0.378,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithSortingResults":0.371,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithNestedRelationships":0.415,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithMultipleConditions":0.396,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithInvalidQuery":0.09,"Neo4j\\QueryAPI\\Tests\\Unit\\Neo4jQueryAPIUnitTest::testCorrectClientSetup":0.011,"Neo4j\\QueryAPI\\Tests\\Unit\\Neo4jQueryAPIUnitTest::testRunSuccess":0.005,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithEmptyNameList":0.094,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithPartialNameMatch":0.359,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithNoData":0.092,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testNodeType":0.096,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testRelationshipType":0.1,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithString":0.085,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryintegrationtest::testRunSuccessWithParameters#testWithExactNames":0.39,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryintegrationtest::testRunSuccessWithParameters#testWithSingleName":0.089,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryintegrationtest::testRunSuccessWithParameters#testRelationshipType":0.137,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryintegrationtest::testRunSuccessWithParameters#testWithNoMatchingNames":0.084,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryintegrationtest::testRunSuccessWithParameters#testWithString":0.086,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryintegrationtest::testRunSuccessWithParameters#testWithNumber":0.09,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryintegrationtest::testRunSuccessWithParameters#testWithBoolean":1.025,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryintegrationtest::testRunSuccessWithParameters#testWithNull":0.102,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryintegrationtest::testRunSuccessWithParameters#testWithArray":0.102,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryIntegrationTest::testRunSuccessWithParameters#testWithString":0.105,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryIntegrationTest::testRunSuccessWithParameters#testWithNumber":0.09,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryIntegrationTest::testRunSuccessWithParameters#testWithBoolean":0.088,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryIntegrationTest::testRunSuccessWithParameters#testWithArray":0.093,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithNumber":0.086,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithNull":0.083,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithBoolean":0.084,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithArray":0.084,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithDate":0.087,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithBinary":0.104,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithNode":0.085,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithDuration":0.086,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithPoint":0.089,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithPath":0.128,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryAPIIntegrationTest::testRunSuccessWithParameters#testWithSimpleRelationship":0.092,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jOGMTest::testInteger":0.004,"Neo4j\\QueryAPI\\Tests\\Unit\\ResultRowTest::testSimplePass":0.001,"Neo4j\\QueryAPI\\Tests\\Integration\\Neo4jQueryApiIntegrationTempTest::testResultRowIntegration":0.123}} \ No newline at end of file diff --git a/Contributing.md b/Contributing.md new file mode 100644 index 00000000..9ca7f54f --- /dev/null +++ b/Contributing.md @@ -0,0 +1,102 @@ +# Contributing to Neo4j QueryAPI he PHP Client + +Thank you for your interest in contributing to the Neo4j QueryAPI PHP Client! We welcome all contributions, whether it's bug fixes, feature enhancements, or documentation improvements. + +## Getting Started + +1. **Fork the Repository**\ + + +Click the "Fork" button at the top right of the repository page. + +2. **Clone Your Fork** + + ```bash + git clone https://github.com/your-username/Neo4j-Client.git + cd Neo4j-Client + ``` + +3. **Set Up the Environment** + + + + - Ensure you have PHP installed (compatible with PHP < 8.1). + - Install dependencies using Composer: + + ```bash + composer install + ``` + +- Copy the `phpunit.dist.xml` file to `phpunit.xml` and configure the necessary environment variables like `NEO4J_ADDRESS`, `NEO4J_USERNAME`, `NEO4J_PASSWORD`. + + + + +4. **Run Tests** + + + - Ensure you have PHP installed (compatible with PHP < 8.1). + - Install dependencies using Composer: + + ```bash + composer install + ``` + +## Code Guidelines + +- Ensure your code is **PSR-12 compliant**. +- Use **Psalm** for static analysis. Run: + ```bash + composer psalm + ``` +- Apply **code style fixes** using: + ```bash + composer cs:fix + ``` + +## Making Changes + +1. **Create a New Branch**\ + Use a descriptive branch name: + + ```bash + git checkout -b fix/issue-123 + ``` + +2. **Make Your Edits**\ + Ensure all tests pass and code is properly formatted. + +3. **Commit Your Changes**\ + Write clear commit messages: + + ```bash + git commit -m "Fix: Corrected query parsing for ProfiledQueryPlan" + ``` + +4. **Push Your Branch** + + ```bash + git push origin fix/issue-123 + ``` + +## Submitting a Pull Request + +1. Go to your forked repository on GitHub. +2. Click on the "New pull request" button. +3. Select your branch and submit the pull request. +4. Add a clear description of the changes you made. + +## Review Process + +- All PRs are reviewed by the maintainers. +- Ensure CI tests pass before requesting a review. +- Be open to feedback and make revisions as needed. + +## Reporting Issues + +If you spot a bug or want to suggest a new feature, please [open an issue](https://github.com/NagelsIT/Neo4j-Client/issues) and provide detailed information. + +--- + +We appreciate your contribution — let’s build something powerful together! + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..23eb4037 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +# Use an official PHP image as the base image +FROM php:8.1-cli + +# Install necessary extensions (e.g., for Composer) +RUN apt-get update && apt-get install -y libpng-dev libjpeg-dev libfreetype6-dev git libzip-dev zip \ + && docker-php-ext-configure gd --with-freetype --with-jpeg \ + && docker-php-ext-install gd zip + +# Set working directory +WORKDIR /var/www + +# Copy the composer.phar file to the container +COPY --from=composer:latest /usr/bin/composer /usr/bin/composer diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..4e11eea0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Nagels IT + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 00000000..4d02b28a --- /dev/null +++ b/README.md @@ -0,0 +1,170 @@ +# Neo4j Query API client + +## Badges + +![License](https://img.shields.io/github/license/nagels-tech/neo4j-query-api) +![Tests](https://github.com/nagels-tech/neo4j-query-api/actions/workflows/tests.yml/badge.svg) +![Version](https://img.shields.io/github/v/release/nagels-tech/neo4j-query-api) +[![Codacy Badge](https://app.codacy.com/project/badge/Grade/2cb8a1e71ed04987b1c763a09e196c84)](https://app.codacy.com/gh/nagels-tech/neo4j-query-api/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) +![Code Coverage](https://img.shields.io/codecov/c/github/nagels-tech/neo4j-query-api) +![Downloads](https://img.shields.io/packagist/dt/nagels-tech/neo4j-query-api) + + +## Interact programmatically with Top Graph Technology + +- Easy to start with, just build your client in one line and start running queries +- Use an intuitive API for smooth query execution +- Built and tested under close collaboration with the official Neo4j driver team +- Fully typed with Psalm and CS fixed for code quality +- Uses HTTP under the hood instead of bolt +- Small, lightweight, well maintained and fully tested codebase + + +## Installation + +You can install the package via Composer: + +```sh +composer require neo4j-php/query-api +``` + +## Client Installation + +This client uses the HTTP protocol, make sure you have psr-7, psr-17, and psr-18 implementations included in your project. +If you don't have any, you can install one of the many options via Composer: + +```sh +composer require guzzlehttp/guzzle +``` + +> **_NOTE:_** PSR-17 and PSR-18 are essential for HTTP client communication. Other compatible clients like Guzzle can also be used. +> \* [PSR auto-discovery](https://docs.php-http.org/en/latest/discovery.html) will detect the installed HTTP client automatically. + +## Usage + +### Connecting to Neo4j + + +```php +use Neo4j\QueryAPI\Neo4jQueryAPI; +use Neo4j\QueryAPI\Objects\Authentication; + +$client = Neo4jQueryAPI::login('http://localhost:7474', Authentication::basic('username', 'password')); +``` + +### Running a Query + +```php +$query = 'MATCH (n) RETURN n'; +$result = $client->run($query); + +foreach ($result as $record) { + print_r($record); +} +``` + +### Transactions + +#### Begin a Transaction + +```php +$transaction = $client->beginTransaction(); +``` + +#### Run a Query in a Transaction + +```php +$query = 'CREATE (n:Person {name: $name}) RETURN n'; +$parameters = ['name' => 'John Doe']; +$result = $transaction->run($query, $parameters); +``` + +#### Commit a Transaction + +```php +$transaction->commit(); +``` + +#### Rollback a Transaction + +```php +$transaction->rollback(); +``` + +## Testing + +To run the tests, execute the following command: + +```sh +vendor/bin/phpunit +``` + +Cypher values and types map to these php types and classes: + +| Cypher | PHP | +|--------------------|:---------------------------------------------------:| +| List | ```* array``` | +| Integer | ``` * int ``` | +| Float | ``` * float ``` | +| Boolean | ``` * bool ``` | +| Null | ``` * null ``` | +| String | ``` * string ``` | +| Array | ```* array``` | +| Local DateTime | ``` * string ``` (will be upgraded in version 1.1) | +| Local Time | ``` * string ``` (will be upgraded in version 1.1) | +| Zoned DateTime | ``` * string ``` (will be upgraded in version 1.1) | +| Zoned Time | ``` * string ``` (will be upgraded in version 1.1) | +| Duration | ``` * string ``` (will be upgraded in version 1.1) | +| WGS 84 2D Point | `Neo4j\QueryAPI\Objects\Point` | +| WGS 84 3D Point | `Neo4j\QueryAPI\Objects\Point` | +| Cartesian 2D Point | `Neo4j\QueryAPI\Objects\Point` | +| Cartesian 3D Point | `Neo4j\QueryAPI\Objects\Point` | +| Map | ``` * array ``` | +| Node | ```Neo4j\QueryAPI\Objects\Node ``` | +| Relationship | ```Neo4j\QueryAPI\Objects\Relationship ``` | +| Path | ```Neo4j\QueryAPI\Objects\Relationship``` | + +## Diving deeper: + +| Feature | Supported? | +|----------|:----------:| +| Authentication | Yes | +| Transaction | Yes | +| HTTP | Yes | +| Cluster | Partly * | +| Aura | Yes | +| Bookmarks | Yes | +| Bolt | No | + +> \* Client side routing is only supported in the Neo4j driver + + **_NOTE:_** *_It supports neo4j databases versions > 5.25 or Neo4j Aura (which has QueryAPI enabled.)_* + +## Contributing + +Please see [CONTRIBUTING.md](./Contributing.md) for details. + +## Security + +If you discover any security-related issues, please email *security@nagels.tech* instead of using the issue tracker. + +## Credits + +- Created with ❤️ by Nagels +- [Kiran Chandani](https://www.linkedin.com/in/kiran-chandani-5628a1213/), +- [Pratiksha Zalte](https://github.com/p123-stack), +- [Ghlen Nagels](https://www.linkedin.com/in/ghlen/) + +## License + +The MIT License (MIT). Please see [License File](LICENSE) for more information. + + +## Badges + +![License](https://img.shields.io/github/license/nagels-tech/neo4j-query-api) +![Tests](https://github.com/nagels-tech/neo4j-query-api/actions/workflows/tests.yml/badge.svg) +![Version](https://img.shields.io/github/v/release/nagels-tech/neo4j-query-api) +![Code Quality](https://img.shields.io/codeclimate/maintainability/nagels-tech/neo4j-query-api) +![Code Coverage](https://img.shields.io/codecov/c/github/nagels-tech/neo4j-query-api) +![Downloads](https://img.shields.io/packagist/dt/nagels-tech/neo4j-query-api) diff --git a/composer.json b/composer.json index ca9895e5..5d04941c 100644 --- a/composer.json +++ b/composer.json @@ -3,14 +3,21 @@ "name": "neo4j-php/query-api", "description": "Easy to use class to run Cypher queries on the Query API", "require": { - "guzzlehttp/guzzle": "^7.9", "psr/http-client": "^1.0", - "ext-json": "*", - "php": "^8.1" + "ext-json": "*", + "php": "^8.1", + "nyholm/psr7": "^1.8", + "php-http/discovery": "^1.17" }, "require-dev": { - "phpunit/phpunit": "^11.0" + "guzzlehttp/guzzle": "^7.9", + "phpunit/phpunit": "^12.0", + "friendsofphp/php-cs-fixer": "^3.68", + "vimeo/psalm": "^6.8", + "dg/bypass-finals": "^1.9", + "psalm/plugin-phpunit": "^0.19.2" }, + "autoload": { "psr-4": { "Neo4j\\QueryAPI\\": "src/" @@ -24,12 +31,28 @@ "authors": [ { "name": "p123-stack", - "email": "pratikshazalte83@gmail.com" + "email": "pratiksha@nagels.tech" + }, + { + "name": "123kiran17", + "email": "kiran@nagels.tech" + }, + { + "name": "Ghlen Nagels", + "email": "ghlen@nagels.tech" } ], "config": { "allow-plugins": { "php-http/discovery": true } + }, + + "scripts": { + "cs": "vendor/bin/php-cs-fixer fix --dry-run --diff --allow-risky=yes", + "cs:fix": "vendor/bin/php-cs-fixer fix --allow-risky=yes", + "psalm": "vendor/bin/psalm --no-cache --show-info=true", + "phpunit" : "vendor/bin/phpunit" } + } diff --git a/composer.lock b/composer.lock deleted file mode 100644 index 581ab1b6..00000000 --- a/composer.lock +++ /dev/null @@ -1,2304 +0,0 @@ -{ - "_readme": [ - "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", - "This file is @generated automatically" - ], - "content-hash": "192bca4f447c8cb126e7bd26a103e456", - "packages": [ - { - "name": "guzzlehttp/guzzle", - "version": "7.9.2", - "source": { - "type": "git", - "url": "https://github.com/guzzle/guzzle.git", - "reference": "d281ed313b989f213357e3be1a179f02196ac99b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/d281ed313b989f213357e3be1a179f02196ac99b", - "reference": "d281ed313b989f213357e3be1a179f02196ac99b", - "shasum": "" - }, - "require": { - "ext-json": "*", - "guzzlehttp/promises": "^1.5.3 || ^2.0.3", - "guzzlehttp/psr7": "^2.7.0", - "php": "^7.2.5 || ^8.0", - "psr/http-client": "^1.0", - "symfony/deprecation-contracts": "^2.2 || ^3.0" - }, - "provide": { - "psr/http-client-implementation": "1.0" - }, - "require-dev": { - "bamarni/composer-bin-plugin": "^1.8.2", - "ext-curl": "*", - "guzzle/client-integration-tests": "3.0.2", - "php-http/message-factory": "^1.1", - "phpunit/phpunit": "^8.5.39 || ^9.6.20", - "psr/log": "^1.1 || ^2.0 || ^3.0" - }, - "suggest": { - "ext-curl": "Required for CURL handler support", - "ext-intl": "Required for Internationalized Domain Name (IDN) support", - "psr/log": "Required for using the Log middleware" - }, - "type": "library", - "extra": { - "bamarni-bin": { - "bin-links": true, - "forward-command": false - } - }, - "autoload": { - "files": [ - "src/functions_include.php" - ], - "psr-4": { - "GuzzleHttp\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Graham Campbell", - "email": "hello@gjcampbell.co.uk", - "homepage": "https://github.com/GrahamCampbell" - }, - { - "name": "Michael Dowling", - "email": "mtdowling@gmail.com", - "homepage": "https://github.com/mtdowling" - }, - { - "name": "Jeremy Lindblom", - "email": "jeremeamia@gmail.com", - "homepage": "https://github.com/jeremeamia" - }, - { - "name": "George Mponos", - "email": "gmponos@gmail.com", - "homepage": "https://github.com/gmponos" - }, - { - "name": "Tobias Nyholm", - "email": "tobias.nyholm@gmail.com", - "homepage": "https://github.com/Nyholm" - }, - { - "name": "Márk Sági-Kazár", - "email": "mark.sagikazar@gmail.com", - "homepage": "https://github.com/sagikazarmark" - }, - { - "name": "Tobias Schultze", - "email": "webmaster@tubo-world.de", - "homepage": "https://github.com/Tobion" - } - ], - "description": "Guzzle is a PHP HTTP client library", - "keywords": [ - "client", - "curl", - "framework", - "http", - "http client", - "psr-18", - "psr-7", - "rest", - "web service" - ], - "support": { - "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.9.2" - }, - "funding": [ - { - "url": "https://github.com/GrahamCampbell", - "type": "github" - }, - { - "url": "https://github.com/Nyholm", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", - "type": "tidelift" - } - ], - "time": "2024-07-24T11:22:20+00:00" - }, - { - "name": "guzzlehttp/promises", - "version": "2.0.4", - "source": { - "type": "git", - "url": "https://github.com/guzzle/promises.git", - "reference": "f9c436286ab2892c7db7be8c8da4ef61ccf7b455" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/f9c436286ab2892c7db7be8c8da4ef61ccf7b455", - "reference": "f9c436286ab2892c7db7be8c8da4ef61ccf7b455", - "shasum": "" - }, - "require": { - "php": "^7.2.5 || ^8.0" - }, - "require-dev": { - "bamarni/composer-bin-plugin": "^1.8.2", - "phpunit/phpunit": "^8.5.39 || ^9.6.20" - }, - "type": "library", - "extra": { - "bamarni-bin": { - "bin-links": true, - "forward-command": false - } - }, - "autoload": { - "psr-4": { - "GuzzleHttp\\Promise\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Graham Campbell", - "email": "hello@gjcampbell.co.uk", - "homepage": "https://github.com/GrahamCampbell" - }, - { - "name": "Michael Dowling", - "email": "mtdowling@gmail.com", - "homepage": "https://github.com/mtdowling" - }, - { - "name": "Tobias Nyholm", - "email": "tobias.nyholm@gmail.com", - "homepage": "https://github.com/Nyholm" - }, - { - "name": "Tobias Schultze", - "email": "webmaster@tubo-world.de", - "homepage": "https://github.com/Tobion" - } - ], - "description": "Guzzle promises library", - "keywords": [ - "promise" - ], - "support": { - "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/2.0.4" - }, - "funding": [ - { - "url": "https://github.com/GrahamCampbell", - "type": "github" - }, - { - "url": "https://github.com/Nyholm", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", - "type": "tidelift" - } - ], - "time": "2024-10-17T10:06:22+00:00" - }, - { - "name": "guzzlehttp/psr7", - "version": "2.7.0", - "source": { - "type": "git", - "url": "https://github.com/guzzle/psr7.git", - "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/a70f5c95fb43bc83f07c9c948baa0dc1829bf201", - "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201", - "shasum": "" - }, - "require": { - "php": "^7.2.5 || ^8.0", - "psr/http-factory": "^1.0", - "psr/http-message": "^1.1 || ^2.0", - "ralouphie/getallheaders": "^3.0" - }, - "provide": { - "psr/http-factory-implementation": "1.0", - "psr/http-message-implementation": "1.0" - }, - "require-dev": { - "bamarni/composer-bin-plugin": "^1.8.2", - "http-interop/http-factory-tests": "0.9.0", - "phpunit/phpunit": "^8.5.39 || ^9.6.20" - }, - "suggest": { - "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" - }, - "type": "library", - "extra": { - "bamarni-bin": { - "bin-links": true, - "forward-command": false - } - }, - "autoload": { - "psr-4": { - "GuzzleHttp\\Psr7\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Graham Campbell", - "email": "hello@gjcampbell.co.uk", - "homepage": "https://github.com/GrahamCampbell" - }, - { - "name": "Michael Dowling", - "email": "mtdowling@gmail.com", - "homepage": "https://github.com/mtdowling" - }, - { - "name": "George Mponos", - "email": "gmponos@gmail.com", - "homepage": "https://github.com/gmponos" - }, - { - "name": "Tobias Nyholm", - "email": "tobias.nyholm@gmail.com", - "homepage": "https://github.com/Nyholm" - }, - { - "name": "Márk Sági-Kazár", - "email": "mark.sagikazar@gmail.com", - "homepage": "https://github.com/sagikazarmark" - }, - { - "name": "Tobias Schultze", - "email": "webmaster@tubo-world.de", - "homepage": "https://github.com/Tobion" - }, - { - "name": "Márk Sági-Kazár", - "email": "mark.sagikazar@gmail.com", - "homepage": "https://sagikazarmark.hu" - } - ], - "description": "PSR-7 message implementation that also provides common utility methods", - "keywords": [ - "http", - "message", - "psr-7", - "request", - "response", - "stream", - "uri", - "url" - ], - "support": { - "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.7.0" - }, - "funding": [ - { - "url": "https://github.com/GrahamCampbell", - "type": "github" - }, - { - "url": "https://github.com/Nyholm", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", - "type": "tidelift" - } - ], - "time": "2024-07-18T11:15:46+00:00" - }, - { - "name": "psr/http-client", - "version": "1.0.3", - "source": { - "type": "git", - "url": "https://github.com/php-fig/http-client.git", - "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", - "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", - "shasum": "" - }, - "require": { - "php": "^7.0 || ^8.0", - "psr/http-message": "^1.0 || ^2.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Http\\Client\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common interface for HTTP clients", - "homepage": "https://github.com/php-fig/http-client", - "keywords": [ - "http", - "http-client", - "psr", - "psr-18" - ], - "support": { - "source": "https://github.com/php-fig/http-client" - }, - "time": "2023-09-23T14:17:50+00:00" - }, - { - "name": "psr/http-factory", - "version": "1.1.0", - "source": { - "type": "git", - "url": "https://github.com/php-fig/http-factory.git", - "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", - "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", - "shasum": "" - }, - "require": { - "php": ">=7.1", - "psr/http-message": "^1.0 || ^2.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Http\\Message\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", - "keywords": [ - "factory", - "http", - "message", - "psr", - "psr-17", - "psr-7", - "request", - "response" - ], - "support": { - "source": "https://github.com/php-fig/http-factory" - }, - "time": "2024-04-15T12:06:14+00:00" - }, - { - "name": "psr/http-message", - "version": "2.0", - "source": { - "type": "git", - "url": "https://github.com/php-fig/http-message.git", - "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", - "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Http\\Message\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common interface for HTTP messages", - "homepage": "https://github.com/php-fig/http-message", - "keywords": [ - "http", - "http-message", - "psr", - "psr-7", - "request", - "response" - ], - "support": { - "source": "https://github.com/php-fig/http-message/tree/2.0" - }, - "time": "2023-04-04T09:54:51+00:00" - }, - { - "name": "ralouphie/getallheaders", - "version": "3.0.3", - "source": { - "type": "git", - "url": "https://github.com/ralouphie/getallheaders.git", - "reference": "120b605dfeb996808c31b6477290a714d356e822" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", - "reference": "120b605dfeb996808c31b6477290a714d356e822", - "shasum": "" - }, - "require": { - "php": ">=5.6" - }, - "require-dev": { - "php-coveralls/php-coveralls": "^2.1", - "phpunit/phpunit": "^5 || ^6.5" - }, - "type": "library", - "autoload": { - "files": [ - "src/getallheaders.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Ralph Khattar", - "email": "ralph.khattar@gmail.com" - } - ], - "description": "A polyfill for getallheaders.", - "support": { - "issues": "https://github.com/ralouphie/getallheaders/issues", - "source": "https://github.com/ralouphie/getallheaders/tree/develop" - }, - "time": "2019-03-08T08:55:37+00:00" - }, - { - "name": "symfony/deprecation-contracts", - "version": "v3.5.1", - "source": { - "type": "git", - "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", - "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", - "shasum": "" - }, - "require": { - "php": ">=8.1" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "3.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" - } - }, - "autoload": { - "files": [ - "function.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "A generic function and convention to trigger deprecation notices", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.1" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-25T14:20:29+00:00" - } - ], - "packages-dev": [ - { - "name": "myclabs/deep-copy", - "version": "1.12.1", - "source": { - "type": "git", - "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/123267b2c49fbf30d78a7b2d333f6be754b94845", - "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845", - "shasum": "" - }, - "require": { - "php": "^7.1 || ^8.0" - }, - "conflict": { - "doctrine/collections": "<1.6.8", - "doctrine/common": "<2.13.3 || >=3 <3.2.2" - }, - "require-dev": { - "doctrine/collections": "^1.6.8", - "doctrine/common": "^2.13.3 || ^3.2.2", - "phpspec/prophecy": "^1.10", - "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" - }, - "type": "library", - "autoload": { - "files": [ - "src/DeepCopy/deep_copy.php" - ], - "psr-4": { - "DeepCopy\\": "src/DeepCopy/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Create deep copies (clones) of your objects", - "keywords": [ - "clone", - "copy", - "duplicate", - "object", - "object graph" - ], - "support": { - "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.12.1" - }, - "funding": [ - { - "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", - "type": "tidelift" - } - ], - "time": "2024-11-08T17:47:46+00:00" - }, - { - "name": "nikic/php-parser", - "version": "v5.3.1", - "source": { - "type": "git", - "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/8eea230464783aa9671db8eea6f8c6ac5285794b", - "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b", - "shasum": "" - }, - "require": { - "ext-ctype": "*", - "ext-json": "*", - "ext-tokenizer": "*", - "php": ">=7.4" - }, - "require-dev": { - "ircmaxell/php-yacc": "^0.0.7", - "phpunit/phpunit": "^9.0" - }, - "bin": [ - "bin/php-parse" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.0-dev" - } - }, - "autoload": { - "psr-4": { - "PhpParser\\": "lib/PhpParser" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Nikita Popov" - } - ], - "description": "A PHP parser written in PHP", - "keywords": [ - "parser", - "php" - ], - "support": { - "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.3.1" - }, - "time": "2024-10-08T18:51:32+00:00" - }, - { - "name": "phar-io/manifest", - "version": "2.0.4", - "source": { - "type": "git", - "url": "https://github.com/phar-io/manifest.git", - "reference": "54750ef60c58e43759730615a392c31c80e23176" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", - "reference": "54750ef60c58e43759730615a392c31c80e23176", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-libxml": "*", - "ext-phar": "*", - "ext-xmlwriter": "*", - "phar-io/version": "^3.0.1", - "php": "^7.2 || ^8.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Arne Blankerts", - "email": "arne@blankerts.de", - "role": "Developer" - }, - { - "name": "Sebastian Heuer", - "email": "sebastian@phpeople.de", - "role": "Developer" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "Developer" - } - ], - "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", - "support": { - "issues": "https://github.com/phar-io/manifest/issues", - "source": "https://github.com/phar-io/manifest/tree/2.0.4" - }, - "funding": [ - { - "url": "https://github.com/theseer", - "type": "github" - } - ], - "time": "2024-03-03T12:33:53+00:00" - }, - { - "name": "phar-io/version", - "version": "3.2.1", - "source": { - "type": "git", - "url": "https://github.com/phar-io/version.git", - "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", - "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0" - }, - "type": "library", - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Arne Blankerts", - "email": "arne@blankerts.de", - "role": "Developer" - }, - { - "name": "Sebastian Heuer", - "email": "sebastian@phpeople.de", - "role": "Developer" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "Developer" - } - ], - "description": "Library for handling version information and constraints", - "support": { - "issues": "https://github.com/phar-io/version/issues", - "source": "https://github.com/phar-io/version/tree/3.2.1" - }, - "time": "2022-02-21T01:04:05+00:00" - }, - { - "name": "phpunit/php-code-coverage", - "version": "11.0.8", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "418c59fd080954f8c4aa5631d9502ecda2387118" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/418c59fd080954f8c4aa5631d9502ecda2387118", - "reference": "418c59fd080954f8c4aa5631d9502ecda2387118", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-libxml": "*", - "ext-xmlwriter": "*", - "nikic/php-parser": "^5.3.1", - "php": ">=8.2", - "phpunit/php-file-iterator": "^5.1.0", - "phpunit/php-text-template": "^4.0.1", - "sebastian/code-unit-reverse-lookup": "^4.0.1", - "sebastian/complexity": "^4.0.1", - "sebastian/environment": "^7.2.0", - "sebastian/lines-of-code": "^3.0.1", - "sebastian/version": "^5.0.2", - "theseer/tokenizer": "^1.2.3" - }, - "require-dev": { - "phpunit/phpunit": "^11.5.0" - }, - "suggest": { - "ext-pcov": "PHP extension that provides line coverage", - "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "11.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", - "homepage": "https://github.com/sebastianbergmann/php-code-coverage", - "keywords": [ - "coverage", - "testing", - "xunit" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.8" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-12-11T12:34:27+00:00" - }, - { - "name": "phpunit/php-file-iterator", - "version": "5.1.0", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "118cfaaa8bc5aef3287bf315b6060b1174754af6" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/118cfaaa8bc5aef3287bf315b6060b1174754af6", - "reference": "118cfaaa8bc5aef3287bf315b6060b1174754af6", - "shasum": "" - }, - "require": { - "php": ">=8.2" - }, - "require-dev": { - "phpunit/phpunit": "^11.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "5.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "FilterIterator implementation that filters files based on a list of suffixes.", - "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", - "keywords": [ - "filesystem", - "iterator" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", - "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", - "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.1.0" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-08-27T05:02:59+00:00" - }, - { - "name": "phpunit/php-invoker", - "version": "5.0.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-invoker.git", - "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/c1ca3814734c07492b3d4c5f794f4b0995333da2", - "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2", - "shasum": "" - }, - "require": { - "php": ">=8.2" - }, - "require-dev": { - "ext-pcntl": "*", - "phpunit/phpunit": "^11.0" - }, - "suggest": { - "ext-pcntl": "*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "5.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Invoke callables with a timeout", - "homepage": "https://github.com/sebastianbergmann/php-invoker/", - "keywords": [ - "process" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/php-invoker/issues", - "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", - "source": "https://github.com/sebastianbergmann/php-invoker/tree/5.0.1" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-07-03T05:07:44+00:00" - }, - { - "name": "phpunit/php-text-template", - "version": "4.0.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/3e0404dc6b300e6bf56415467ebcb3fe4f33e964", - "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964", - "shasum": "" - }, - "require": { - "php": ">=8.2" - }, - "require-dev": { - "phpunit/phpunit": "^11.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "4.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Simple template engine.", - "homepage": "https://github.com/sebastianbergmann/php-text-template/", - "keywords": [ - "template" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/php-text-template/issues", - "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", - "source": "https://github.com/sebastianbergmann/php-text-template/tree/4.0.1" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-07-03T05:08:43+00:00" - }, - { - "name": "phpunit/php-timer", - "version": "7.0.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", - "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", - "shasum": "" - }, - "require": { - "php": ">=8.2" - }, - "require-dev": { - "phpunit/phpunit": "^11.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "7.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Utility class for timing", - "homepage": "https://github.com/sebastianbergmann/php-timer/", - "keywords": [ - "timer" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/php-timer/issues", - "security": "https://github.com/sebastianbergmann/php-timer/security/policy", - "source": "https://github.com/sebastianbergmann/php-timer/tree/7.0.1" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-07-03T05:09:35+00:00" - }, - { - "name": "phpunit/phpunit", - "version": "11.5.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "2b94d4f2450b9869fa64a46fd8a6a41997aef56a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/2b94d4f2450b9869fa64a46fd8a6a41997aef56a", - "reference": "2b94d4f2450b9869fa64a46fd8a6a41997aef56a", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-json": "*", - "ext-libxml": "*", - "ext-mbstring": "*", - "ext-xml": "*", - "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.12.1", - "phar-io/manifest": "^2.0.4", - "phar-io/version": "^3.2.1", - "php": ">=8.2", - "phpunit/php-code-coverage": "^11.0.7", - "phpunit/php-file-iterator": "^5.1.0", - "phpunit/php-invoker": "^5.0.1", - "phpunit/php-text-template": "^4.0.1", - "phpunit/php-timer": "^7.0.1", - "sebastian/cli-parser": "^3.0.2", - "sebastian/code-unit": "^3.0.1", - "sebastian/comparator": "^6.2.1", - "sebastian/diff": "^6.0.2", - "sebastian/environment": "^7.2.0", - "sebastian/exporter": "^6.3.0", - "sebastian/global-state": "^7.0.2", - "sebastian/object-enumerator": "^6.0.1", - "sebastian/type": "^5.1.0", - "sebastian/version": "^5.0.2", - "staabm/side-effects-detector": "^1.0.5" - }, - "suggest": { - "ext-soap": "To be able to generate mocks based on WSDL files" - }, - "bin": [ - "phpunit" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "11.5-dev" - } - }, - "autoload": { - "files": [ - "src/Framework/Assert/Functions.php" - ], - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "The PHP Unit Testing framework.", - "homepage": "https://phpunit.de/", - "keywords": [ - "phpunit", - "testing", - "xunit" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.1" - }, - "funding": [ - { - "url": "https://phpunit.de/sponsors.html", - "type": "custom" - }, - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", - "type": "tidelift" - } - ], - "time": "2024-12-11T10:52:48+00:00" - }, - { - "name": "sebastian/cli-parser", - "version": "3.0.2", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/cli-parser.git", - "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/15c5dd40dc4f38794d383bb95465193f5e0ae180", - "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180", - "shasum": "" - }, - "require": { - "php": ">=8.2" - }, - "require-dev": { - "phpunit/phpunit": "^11.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "3.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Library for parsing CLI options", - "homepage": "https://github.com/sebastianbergmann/cli-parser", - "support": { - "issues": "https://github.com/sebastianbergmann/cli-parser/issues", - "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", - "source": "https://github.com/sebastianbergmann/cli-parser/tree/3.0.2" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-07-03T04:41:36+00:00" - }, - { - "name": "sebastian/code-unit", - "version": "3.0.2", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/code-unit.git", - "reference": "ee88b0cdbe74cf8dd3b54940ff17643c0d6543ca" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/ee88b0cdbe74cf8dd3b54940ff17643c0d6543ca", - "reference": "ee88b0cdbe74cf8dd3b54940ff17643c0d6543ca", - "shasum": "" - }, - "require": { - "php": ">=8.2" - }, - "require-dev": { - "phpunit/phpunit": "^11.5" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "3.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Collection of value objects that represent the PHP code units", - "homepage": "https://github.com/sebastianbergmann/code-unit", - "support": { - "issues": "https://github.com/sebastianbergmann/code-unit/issues", - "security": "https://github.com/sebastianbergmann/code-unit/security/policy", - "source": "https://github.com/sebastianbergmann/code-unit/tree/3.0.2" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-12-12T09:59:06+00:00" - }, - { - "name": "sebastian/code-unit-reverse-lookup", - "version": "4.0.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "183a9b2632194febd219bb9246eee421dad8d45e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/183a9b2632194febd219bb9246eee421dad8d45e", - "reference": "183a9b2632194febd219bb9246eee421dad8d45e", - "shasum": "" - }, - "require": { - "php": ">=8.2" - }, - "require-dev": { - "phpunit/phpunit": "^11.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "4.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Looks up which function or method a line of code belongs to", - "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", - "support": { - "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", - "security": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/security/policy", - "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/4.0.1" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-07-03T04:45:54+00:00" - }, - { - "name": "sebastian/comparator", - "version": "6.2.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "43d129d6a0f81c78bee378b46688293eb7ea3739" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/43d129d6a0f81c78bee378b46688293eb7ea3739", - "reference": "43d129d6a0f81c78bee378b46688293eb7ea3739", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-mbstring": "*", - "php": ">=8.2", - "sebastian/diff": "^6.0", - "sebastian/exporter": "^6.0" - }, - "require-dev": { - "phpunit/phpunit": "^11.4" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "6.2-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Volker Dusch", - "email": "github@wallbash.com" - }, - { - "name": "Bernhard Schussek", - "email": "bschussek@2bepublished.at" - } - ], - "description": "Provides the functionality to compare PHP values for equality", - "homepage": "https://github.com/sebastianbergmann/comparator", - "keywords": [ - "comparator", - "compare", - "equality" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/comparator/issues", - "security": "https://github.com/sebastianbergmann/comparator/security/policy", - "source": "https://github.com/sebastianbergmann/comparator/tree/6.2.1" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-10-31T05:30:08+00:00" - }, - { - "name": "sebastian/complexity", - "version": "4.0.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/complexity.git", - "reference": "ee41d384ab1906c68852636b6de493846e13e5a0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/ee41d384ab1906c68852636b6de493846e13e5a0", - "reference": "ee41d384ab1906c68852636b6de493846e13e5a0", - "shasum": "" - }, - "require": { - "nikic/php-parser": "^5.0", - "php": ">=8.2" - }, - "require-dev": { - "phpunit/phpunit": "^11.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "4.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Library for calculating the complexity of PHP code units", - "homepage": "https://github.com/sebastianbergmann/complexity", - "support": { - "issues": "https://github.com/sebastianbergmann/complexity/issues", - "security": "https://github.com/sebastianbergmann/complexity/security/policy", - "source": "https://github.com/sebastianbergmann/complexity/tree/4.0.1" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-07-03T04:49:50+00:00" - }, - { - "name": "sebastian/diff", - "version": "6.0.2", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/b4ccd857127db5d41a5b676f24b51371d76d8544", - "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544", - "shasum": "" - }, - "require": { - "php": ">=8.2" - }, - "require-dev": { - "phpunit/phpunit": "^11.0", - "symfony/process": "^4.2 || ^5" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "6.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Kore Nordmann", - "email": "mail@kore-nordmann.de" - } - ], - "description": "Diff implementation", - "homepage": "https://github.com/sebastianbergmann/diff", - "keywords": [ - "diff", - "udiff", - "unidiff", - "unified diff" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/diff/issues", - "security": "https://github.com/sebastianbergmann/diff/security/policy", - "source": "https://github.com/sebastianbergmann/diff/tree/6.0.2" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-07-03T04:53:05+00:00" - }, - { - "name": "sebastian/environment", - "version": "7.2.0", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5", - "reference": "855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5", - "shasum": "" - }, - "require": { - "php": ">=8.2" - }, - "require-dev": { - "phpunit/phpunit": "^11.0" - }, - "suggest": { - "ext-posix": "*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "7.2-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Provides functionality to handle HHVM/PHP environments", - "homepage": "https://github.com/sebastianbergmann/environment", - "keywords": [ - "Xdebug", - "environment", - "hhvm" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/environment/issues", - "security": "https://github.com/sebastianbergmann/environment/security/policy", - "source": "https://github.com/sebastianbergmann/environment/tree/7.2.0" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-07-03T04:54:44+00:00" - }, - { - "name": "sebastian/exporter", - "version": "6.3.0", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "3473f61172093b2da7de1fb5782e1f24cc036dc3" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/3473f61172093b2da7de1fb5782e1f24cc036dc3", - "reference": "3473f61172093b2da7de1fb5782e1f24cc036dc3", - "shasum": "" - }, - "require": { - "ext-mbstring": "*", - "php": ">=8.2", - "sebastian/recursion-context": "^6.0" - }, - "require-dev": { - "phpunit/phpunit": "^11.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "6.1-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Volker Dusch", - "email": "github@wallbash.com" - }, - { - "name": "Adam Harvey", - "email": "aharvey@php.net" - }, - { - "name": "Bernhard Schussek", - "email": "bschussek@gmail.com" - } - ], - "description": "Provides the functionality to export PHP variables for visualization", - "homepage": "https://www.github.com/sebastianbergmann/exporter", - "keywords": [ - "export", - "exporter" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/exporter/issues", - "security": "https://github.com/sebastianbergmann/exporter/security/policy", - "source": "https://github.com/sebastianbergmann/exporter/tree/6.3.0" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-12-05T09:17:50+00:00" - }, - { - "name": "sebastian/global-state", - "version": "7.0.2", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "3be331570a721f9a4b5917f4209773de17f747d7" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/3be331570a721f9a4b5917f4209773de17f747d7", - "reference": "3be331570a721f9a4b5917f4209773de17f747d7", - "shasum": "" - }, - "require": { - "php": ">=8.2", - "sebastian/object-reflector": "^4.0", - "sebastian/recursion-context": "^6.0" - }, - "require-dev": { - "ext-dom": "*", - "phpunit/phpunit": "^11.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "7.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Snapshotting of global state", - "homepage": "https://www.github.com/sebastianbergmann/global-state", - "keywords": [ - "global state" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/global-state/issues", - "security": "https://github.com/sebastianbergmann/global-state/security/policy", - "source": "https://github.com/sebastianbergmann/global-state/tree/7.0.2" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-07-03T04:57:36+00:00" - }, - { - "name": "sebastian/lines-of-code", - "version": "3.0.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/lines-of-code.git", - "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d36ad0d782e5756913e42ad87cb2890f4ffe467a", - "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a", - "shasum": "" - }, - "require": { - "nikic/php-parser": "^5.0", - "php": ">=8.2" - }, - "require-dev": { - "phpunit/phpunit": "^11.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "3.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Library for counting the lines of code in PHP source code", - "homepage": "https://github.com/sebastianbergmann/lines-of-code", - "support": { - "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", - "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", - "source": "https://github.com/sebastianbergmann/lines-of-code/tree/3.0.1" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-07-03T04:58:38+00:00" - }, - { - "name": "sebastian/object-enumerator", - "version": "6.0.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "f5b498e631a74204185071eb41f33f38d64608aa" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/f5b498e631a74204185071eb41f33f38d64608aa", - "reference": "f5b498e631a74204185071eb41f33f38d64608aa", - "shasum": "" - }, - "require": { - "php": ">=8.2", - "sebastian/object-reflector": "^4.0", - "sebastian/recursion-context": "^6.0" - }, - "require-dev": { - "phpunit/phpunit": "^11.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "6.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Traverses array structures and object graphs to enumerate all referenced objects", - "homepage": "https://github.com/sebastianbergmann/object-enumerator/", - "support": { - "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", - "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", - "source": "https://github.com/sebastianbergmann/object-enumerator/tree/6.0.1" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-07-03T05:00:13+00:00" - }, - { - "name": "sebastian/object-reflector", - "version": "4.0.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/object-reflector.git", - "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/6e1a43b411b2ad34146dee7524cb13a068bb35f9", - "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9", - "shasum": "" - }, - "require": { - "php": ">=8.2" - }, - "require-dev": { - "phpunit/phpunit": "^11.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "4.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Allows reflection of object attributes, including inherited and non-public ones", - "homepage": "https://github.com/sebastianbergmann/object-reflector/", - "support": { - "issues": "https://github.com/sebastianbergmann/object-reflector/issues", - "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", - "source": "https://github.com/sebastianbergmann/object-reflector/tree/4.0.1" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-07-03T05:01:32+00:00" - }, - { - "name": "sebastian/recursion-context", - "version": "6.0.2", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "694d156164372abbd149a4b85ccda2e4670c0e16" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/694d156164372abbd149a4b85ccda2e4670c0e16", - "reference": "694d156164372abbd149a4b85ccda2e4670c0e16", - "shasum": "" - }, - "require": { - "php": ">=8.2" - }, - "require-dev": { - "phpunit/phpunit": "^11.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "6.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Adam Harvey", - "email": "aharvey@php.net" - } - ], - "description": "Provides functionality to recursively process PHP variables", - "homepage": "https://github.com/sebastianbergmann/recursion-context", - "support": { - "issues": "https://github.com/sebastianbergmann/recursion-context/issues", - "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/6.0.2" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-07-03T05:10:34+00:00" - }, - { - "name": "sebastian/type", - "version": "5.1.0", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/type.git", - "reference": "461b9c5da241511a2a0e8f240814fb23ce5c0aac" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/461b9c5da241511a2a0e8f240814fb23ce5c0aac", - "reference": "461b9c5da241511a2a0e8f240814fb23ce5c0aac", - "shasum": "" - }, - "require": { - "php": ">=8.2" - }, - "require-dev": { - "phpunit/phpunit": "^11.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "5.1-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Collection of value objects that represent the types of the PHP type system", - "homepage": "https://github.com/sebastianbergmann/type", - "support": { - "issues": "https://github.com/sebastianbergmann/type/issues", - "security": "https://github.com/sebastianbergmann/type/security/policy", - "source": "https://github.com/sebastianbergmann/type/tree/5.1.0" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-09-17T13:12:04+00:00" - }, - { - "name": "sebastian/version", - "version": "5.0.2", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/version.git", - "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c687e3387b99f5b03b6caa64c74b63e2936ff874", - "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874", - "shasum": "" - }, - "require": { - "php": ">=8.2" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "5.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Library that helps with managing the version number of Git-hosted PHP projects", - "homepage": "https://github.com/sebastianbergmann/version", - "support": { - "issues": "https://github.com/sebastianbergmann/version/issues", - "security": "https://github.com/sebastianbergmann/version/security/policy", - "source": "https://github.com/sebastianbergmann/version/tree/5.0.2" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-10-09T05:16:32+00:00" - }, - { - "name": "staabm/side-effects-detector", - "version": "1.0.5", - "source": { - "type": "git", - "url": "https://github.com/staabm/side-effects-detector.git", - "reference": "d8334211a140ce329c13726d4a715adbddd0a163" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163", - "reference": "d8334211a140ce329c13726d4a715adbddd0a163", - "shasum": "" - }, - "require": { - "ext-tokenizer": "*", - "php": "^7.4 || ^8.0" - }, - "require-dev": { - "phpstan/extension-installer": "^1.4.3", - "phpstan/phpstan": "^1.12.6", - "phpunit/phpunit": "^9.6.21", - "symfony/var-dumper": "^5.4.43", - "tomasvotruba/type-coverage": "1.0.0", - "tomasvotruba/unused-public": "1.0.0" - }, - "type": "library", - "autoload": { - "classmap": [ - "lib/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "A static analysis tool to detect side effects in PHP code", - "keywords": [ - "static analysis" - ], - "support": { - "issues": "https://github.com/staabm/side-effects-detector/issues", - "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5" - }, - "funding": [ - { - "url": "https://github.com/staabm", - "type": "github" - } - ], - "time": "2024-10-20T05:08:20+00:00" - }, - { - "name": "theseer/tokenizer", - "version": "1.2.3", - "source": { - "type": "git", - "url": "https://github.com/theseer/tokenizer.git", - "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", - "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-tokenizer": "*", - "ext-xmlwriter": "*", - "php": "^7.2 || ^8.0" - }, - "type": "library", - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Arne Blankerts", - "email": "arne@blankerts.de", - "role": "Developer" - } - ], - "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" - }, - "funding": [ - { - "url": "https://github.com/theseer", - "type": "github" - } - ], - "time": "2024-03-03T12:36:25+00:00" - } - ], - "aliases": [], - "minimum-stability": "stable", - "stability-flags": {}, - "prefer-stable": false, - "prefer-lowest": false, - "platform": { - "ext-json": "*", - "php": "^8.1" - }, - "platform-dev": {}, - "plugin-api-version": "2.6.0" -} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..b5d8dd5a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,30 @@ +version: '3.8' + +services: + # PHP Service + app: + build: + context: . + dockerfile: Dockerfile + container_name: php-app + volumes: + - .:/var/www + networks: + - mynetwork + + # Neo4j Service (Optional, if you need Neo4j) + neo4j: + image: neo4j:latest + container_name: neo4j + environment: + - NEO4J_AUTH=neo4j/password + ports: + - "7474:7474" # Web interface + - "7687:7687" # Bolt protocol + networks: + - mynetwork + +# Define a network for communication between containers +networks: + mynetwork: + driver: bridge diff --git a/neo4j-query-api b/neo4j-query-api new file mode 160000 index 00000000..cc774c46 --- /dev/null +++ b/neo4j-query-api @@ -0,0 +1 @@ +Subproject commit cc774c46121e94e11d003447e967ac63574dff5a diff --git a/phpunit.dist.xml b/phpunit.dist.xml index fad6d47f..a3ab58d8 100644 --- a/phpunit.dist.xml +++ b/phpunit.dist.xml @@ -1,5 +1,8 @@ - + @@ -13,10 +16,9 @@ --> - - - - - + + + + diff --git a/psalm.xml b/psalm.xml new file mode 100644 index 00000000..25acfe69 --- /dev/null +++ b/psalm.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + diff --git a/src/.DS_Store b/src/.DS_Store new file mode 100644 index 00000000..81c6780b Binary files /dev/null and b/src/.DS_Store differ diff --git a/src/Authentication/AuthenticateInterface.php b/src/Authentication/AuthenticateInterface.php new file mode 100644 index 00000000..1fb5b74d --- /dev/null +++ b/src/Authentication/AuthenticateInterface.php @@ -0,0 +1,18 @@ +username = $username ?? (is_string($envUser = getenv("NEO4J_USERNAME")) ? $envUser : ''); + $this->password = $password ?? (is_string($envPass = getenv("NEO4J_PASSWORD")) ? $envPass : ''); + } + + + #[\Override] + public function authenticate(RequestInterface $request): RequestInterface + { + $authHeader = $this->getHeader(); + return $request->withHeader('Authorization', $authHeader); + } + + #[\Override] + public function getHeader(): string + { + return 'Basic ' . base64_encode($this->username . ':' . $this->password); + } + /** + * @psalm-suppress UnusedMethod + */ + #[\Override] + public function getType(): string + { + return 'Basic'; + } +} diff --git a/src/Authentication/BearerAuthentication.php b/src/Authentication/BearerAuthentication.php new file mode 100644 index 00000000..29c86bd3 --- /dev/null +++ b/src/Authentication/BearerAuthentication.php @@ -0,0 +1,37 @@ +token = $token; + } + + #[\Override] + public function authenticate(RequestInterface $request): RequestInterface + { + $authHeader = 'Bearer ' . $this->token; + return $request->withHeader('Authorization', $authHeader); + } + + + #[\Override] + public function getHeader(): string + { + return 'Bearer ' . $this->token; + } + + + #[\Override] + public function getType(): string + { + return 'Bearer'; + } +} diff --git a/src/Authentication/NoAuth.php b/src/Authentication/NoAuth.php new file mode 100644 index 00000000..7da1d470 --- /dev/null +++ b/src/Authentication/NoAuth.php @@ -0,0 +1,28 @@ +errorCode = $errorDetails['code'] ?? 'Neo.UnknownError'; + $errorParts = explode('.', $this->errorCode); + $this->errorType = $errorParts[1] ?? null; + $this->errorSubType = $errorParts[2] ?? null; + $this->errorName = $errorParts[3] ?? null; + + $message = $errorDetails['message'] ?? 'An unknown error occurred.'; + parent::__construct($message, $statusCode, $previous); + } + + /** + * Create a Neo4jException instance from a Neo4j error response array. + * + * @param array $response The error response from Neo4j. + * @param \Throwable|null $exception Optional previous exception for chaining. + * @return self + */ + public static function fromNeo4jResponse(array $response, ?\Throwable $exception = null): self + { + $errorDetails = $response['errors'][0] ?? ['message' => 'Unknown error', 'code' => 'Neo.UnknownError']; + + + return new self($errorDetails, previous: $exception); + } + + public function getErrorCode(): string + { + return $this->errorCode; + } + + public function getType(): ?string + { + return $this->errorType; + } + + public function getSubType(): ?string + { + return $this->errorSubType; + } + + public function getName(): ?string + { + return $this->errorName; + } +} diff --git a/src/Neo4jQueryAPI.php b/src/Neo4jQueryAPI.php index 86e22b51..8e596050 100644 --- a/src/Neo4jQueryAPI.php +++ b/src/Neo4jQueryAPI.php @@ -2,60 +2,123 @@ namespace Neo4j\QueryAPI; -use GuzzleHttp\Client; -use GuzzleHttp\Exception\GuzzleException; -use GuzzleHttp\Exception\RequestException; +use Http\Discovery\Psr17FactoryDiscovery; +use Http\Discovery\Psr18ClientDiscovery; +use InvalidArgumentException; +use Neo4j\QueryAPI\Exception\Neo4jException; +use Psr\Http\Client\ClientInterface; +use Neo4j\QueryAPI\Authentication\AuthenticateInterface; +use Neo4j\QueryAPI\Objects\Authentication; use Neo4j\QueryAPI\Results\ResultSet; -use RuntimeException; -use stdClass; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Client\RequestExceptionInterface; -class Neo4jQueryAPI +final class Neo4jQueryAPI { - private Client $client; + public function __construct( + private ClientInterface $client, + private ResponseParser $responseParser, + private Neo4jRequestFactory $requestFactory, + private Configuration $config + ) { - public function __construct(Client $client) + } + + public static function login(string $address = null, ?AuthenticateInterface $auth = null, ?Configuration $config = null): self { - $this->client = $client; + $config = $config ?? new Configuration(baseUri: $address ?? ''); + if ( + trim($config->baseUri) !== '' && + $address !== null && + trim($address) !== '' && + $config->baseUri !== $address + ) { + throw new InvalidArgumentException(sprintf('Address (%s) as argument is different from address in configuration (%s)', $config->baseUri, $address)); + } + + $client = Psr18ClientDiscovery::find(); + + return new self( + client: $client, + responseParser: new ResponseParser(new OGM()), + requestFactory: new Neo4jRequestFactory( + psr17Factory: Psr17FactoryDiscovery::findRequestFactory(), + streamFactory: Psr17FactoryDiscovery::findStreamFactory(), + configuration: $config, + auth: $auth ?? Authentication::fromEnvironment() + ), + config: $config + ); } - public static function login(string $address, string $username, string $password): self + public static function create(Configuration $configuration, AuthenticateInterface $auth = null): self { + return self::login(auth: $auth, config: $configuration); + } + - $client = new Client([ - 'base_uri' => rtrim($address, '/'), - 'timeout' => 10.0, - 'headers' => [ - 'Authorization' => 'Basic ' . base64_encode("$username:$password"), - 'Content-Type' => 'application/vnd.neo4j.query', - 'Accept'=>'application/vnd.neo4j.query', - ], - ]); - - return new self($client); + public function getConfig(): Configuration + { + return $this->config; } + /** - * @throws GuzzleException + * Executes a Cypher query. */ - public function run(string $cypher, array $parameters, string $database = 'neo4j'): ResultSet + public function run(string $cypher, array $parameters = []): ResultSet { - $payload = [ - 'statement' => $cypher, - 'parameters' => $parameters === [] ? new stdClass() : $parameters, - ]; + $request = $this->requestFactory->buildRunQueryRequest($cypher, $parameters); - $response = $this->client->post('/db/' . $database . '/query/v2', [ - 'json' => $payload, - ]); + try { + $response = $this->client->sendRequest($request); + } catch (RequestExceptionInterface $e) { + $this->handleRequestException($e); + } + return $this->responseParser->parseRunQueryResponse($response); + } - $data = json_decode($response->getBody()->getContents(), true); + public function beginTransaction(): Transaction + { + $request = $this->requestFactory->buildBeginTransactionRequest(); + + try { + $response = $this->client->sendRequest($request); + } catch (RequestExceptionInterface $e) { + $this->handleRequestException($e); + } + + $clusterAffinity = $response->getHeaderLine('neo4j-cluster-affinity'); + $body = $response->getBody()->getContents(); + $responseData = json_decode($body, true); + $transactionId = $responseData['transaction']['id']; + + return new Transaction( + $this->client, + $this->responseParser, + $this->requestFactory, + $clusterAffinity, + $transactionId + ); + } + /** + * Handles request exceptions by parsing error details and throwing a Neo4jException. + * + * @throws Neo4jException + * + * @return never + */ + private function handleRequestException(RequestExceptionInterface $e): void + { + $response = method_exists($e, 'getResponse') ? $e->getResponse() : null; - $ogm = new OGM(); + if ($response instanceof ResponseInterface) { + $errorResponse = json_decode((string)$response->getBody(), true); + throw Neo4jException::fromNeo4jResponse($errorResponse, $e); + } - return new ResultSet($data['data']['fields'], $data['data']['values'], $ogm); + throw new Neo4jException(['message' => $e->getMessage()], 500, $e); } - - } diff --git a/src/Neo4jRequestFactory.php b/src/Neo4jRequestFactory.php new file mode 100644 index 00000000..600427db --- /dev/null +++ b/src/Neo4jRequestFactory.php @@ -0,0 +1,90 @@ +createRequest("/db/{$this->configuration->database}/query/v2", $cypher, $parameters); + } + + public function buildBeginTransactionRequest(): RequestInterface + { + return $this->createRequest("/db/{$this->configuration->database}/query/v2/tx", null, null); + } + + public function buildCommitRequest(string $transactionId, string $clusterAffinity): RequestInterface + { + return $this->createRequest("/db/{$this->configuration->database}/query/v2/tx/{$transactionId}/commit", null, null) + ->withHeader("neo4j-cluster-affinity", $clusterAffinity); + } + + public function buildRollbackRequest(string $transactionId, string $clusterAffinity): RequestInterface + { + return $this->createRequest("/db/{$this->configuration->database}/query/v2/tx/{$transactionId}/rollback", null, null) + ->withHeader("neo4j-cluster-affinity", $clusterAffinity) + ->withMethod("DELETE"); + } + + public function buildTransactionRunRequest(string $query, array $parameters, string $transactionId, string $clusterAffinity): RequestInterface + { + return $this->createRequest("/db/neo4j/query/v2/tx/{$transactionId}", $query, $parameters) + ->withHeader("neo4j-cluster-affinity", $clusterAffinity); + } + + private function createRequest(string $uri, ?string $cypher, ?array $parameters): RequestInterface + { + $request = $this->psr17Factory->createRequest('POST', $this->configuration->baseUri . $uri); + + $payload = []; + + if ($this->configuration->includeCounters) { + $payload['includeCounters'] = true; + } + + if ($this->configuration->accessMode === AccessMode::READ) { + $payload['accessMode'] = AccessMode::READ; + } + + if ($cypher !== null && $cypher !== '') { + $payload['statement'] = $cypher; + } + + if ($parameters !== null && $parameters !== []) { + $payload['parameters'] = $parameters; + } + + /** @psalm-suppress RedundantCondition */ + if ($this->configuration->bookmarks !== null) { + $payload['bookmarks'] = $this->configuration->bookmarks; + } + + $request = $request->withHeader('Content-Type', 'application/json'); + $request = $request->withHeader('Accept', 'application/vnd.neo4j.query'); + + $body = json_encode($payload, JSON_THROW_ON_ERROR); + + $stream = $this->streamFactory->createStream($body); + + $request = $request->withBody($stream); + + return $this->auth->authenticate($request); + } +} diff --git a/src/OGM.php b/src/OGM.php index 1efa1c73..5c3c064f 100644 --- a/src/OGM.php +++ b/src/OGM.php @@ -3,34 +3,132 @@ namespace Neo4j\QueryAPI; use Neo4j\QueryAPI\Objects\Point; +use Neo4j\QueryAPI\Objects\Node; +use Neo4j\QueryAPI\Objects\Relationship; +use Neo4j\QueryAPI\Objects\Path; +use InvalidArgumentException; -class OGM +final class OGM { /** - * @param array{'$type': string, '_value': mixed} $object + * @param array $data * @return mixed */ - public function map(array $object): mixed + public function map(array $data): mixed { - return match ($object['$type']) { - 'Integer' => $object['_value'], - 'String' => $object['_value'], - 'Boolean' => $object['_value'], - 'Point' => $this->parseWKT($object['_value']), - default => $object['_value'], + if (!isset($data['$type']) || !array_key_exists('_value', $data) || !is_string($data['$type'])) { + throw new InvalidArgumentException("Unknown object type: " . json_encode($data, JSON_THROW_ON_ERROR)); + } + + return match ($data['$type']) { + 'Integer', 'Float', 'String', 'Boolean', 'Duration', 'OffsetDateTime' => $data['_value'], + 'Array', 'List' => is_array($data['_value']) ? array_map([$this, 'map'], $data['_value']) : [], + 'Null' => null, + 'Node' => $this->mapNode($data['_value']), + 'Map' => is_array($data['_value']) ? $this->mapProperties($data['_value']) : [], + 'Point' => $this->parsePoint($data['_value']), + 'Relationship' => $this->mapRelationship($data['_value']), + 'Path' => $this->mapPath($data['_value']), + default => throw new InvalidArgumentException('Unknown type: ' . json_encode($data, JSON_THROW_ON_ERROR)), }; } - private function parseWKT(string $wkt): Point + + private function parsePoint(string $value): Point + { + // Match SRID and coordinate values + if (preg_match('/SRID=(\d+);POINT(?: Z)? \(([-\d.]+) ([-\d.]+)(?: ([-\d.]+))?\)/', $value, $matches)) { + $srid = (int) $matches[1]; + $x = (float) $matches[2]; + $y = (float) $matches[3]; + $z = isset($matches[4]) ? (float) $matches[4] : null; // Handle optional Z coordinate + + return new Point($x, $y, $z, $srid); + } + + throw new InvalidArgumentException("Invalid Point format: " . $value); + } + + + private function mapNode(array $nodeData): Node + { + return new Node( + labels: $nodeData['_labels'] ?? [], + properties: $this->mapProperties($nodeData['_properties'] ?? []) + ); + } + + private function mapRelationship(array $relationshipData): Relationship + { + return new Relationship( + type: $relationshipData['_type'] ?? 'UNKNOWN', + properties: $this->mapProperties($relationshipData['_properties'] ?? []) + ); + } + + + public static function parseWKT(string $wkt): Point { - $sridPart = substr($wkt, 0, strpos($wkt, ';')); + $sridPos = strpos($wkt, ';'); + if ($sridPos === false) { + throw new \InvalidArgumentException("Invalid WKT format: missing ';'"); + } + $sridPart = substr($wkt, 0, $sridPos); $srid = (int)str_replace('SRID=', '', $sridPart); - $pointPart = substr($wkt, strpos($wkt, 'POINT') + 6); + $pointPos = strpos($wkt, 'POINT'); + if ($pointPos === false) { + throw new \InvalidArgumentException("Invalid WKT format: missing 'POINT'"); + } + $pointPart = substr($wkt, $pointPos + 6); + + $pointPart = str_replace('Z', '', $pointPart); $pointPart = trim($pointPart, ' ()'); + $coordinates = explode(' ', $pointPart); - list($longitude, $latitude) = explode(' ', $pointPart); + [$x, $y, $z] = array_pad(array_map('floatval', $coordinates), 3, 0.0); - return new Point((float)$longitude, (float)$latitude, $srid); + return new Point($x, $y, $z, $srid); } -} \ No newline at end of file + + + private function mapPath(array $pathData): Path + { + $nodes = []; + $relationships = []; + + foreach ($pathData as $item) { + if ($item['$type'] === 'Node') { + $nodes[] = $this->mapNode($item['_value']); + } elseif ($item['$type'] === 'Relationship') { + $relationships[] = $this->mapRelationship($item['_value']); + } + } + + return new Path($nodes, $relationships); + } + + private function mapProperties(array $properties): array + { + + $mappedProperties = []; + + foreach ($properties as $key => $value) { + if (is_array($value) && isset($value['$type'], $value['_value'])) { + $mappedProperties[$key] = $this->map($value); + } elseif (is_scalar($value)) { + $mappedProperties[$key] = $value; + } elseif (is_array($value) && !isset($value['$type'])) { + $mappedProperties[$key] = $this->map(['$type' => 'Map', '_value' => $value]); + } else { + error_log("Invalid property format for key: {$key} => " . json_encode($value, JSON_THROW_ON_ERROR)); + + throw new \InvalidArgumentException("Invalid property format for key: {$key}"); + } + } + + return $mappedProperties; + } + + +} diff --git a/src/Objects/Authentication.php b/src/Objects/Authentication.php new file mode 100644 index 00000000..df15367c --- /dev/null +++ b/src/Objects/Authentication.php @@ -0,0 +1,42 @@ +bookmarks = array_unique(array_merge($this->bookmarks, $newBookmarks->bookmarks)); + } + } + + + public function getBookmarks(): array + { + return $this->bookmarks; + } + + #[\Override] + public function count(): int + { + return count($this->bookmarks); + } + + #[\Override] + public function jsonSerialize(): array + { + return $this->bookmarks; + } +} diff --git a/src/Objects/Node.php b/src/Objects/Node.php new file mode 100644 index 00000000..2bf695b0 --- /dev/null +++ b/src/Objects/Node.php @@ -0,0 +1,53 @@ + Associative array of properties (key-value pairs). + */ + private array $properties; + + /** + * Node constructor. + * + * @param string[] $labels Array of labels for the node. + * @param array $properties Associative array of properties. + */ + public function __construct(array $labels, array $properties) + { + $this->labels = $labels; + $this->properties = $properties; + } + + /** + * Get the properties of the node. + * @return array Associative array of properties. + */ + public function getProperties(): array + { + return $this->properties; + } + + /** + * Convert the Node object to an array representation. + * @return array Node data as an array. + */ + public function toArray(): array + { + return [ + '_labels' => $this->labels, + '_properties' => $this->properties, + ]; + } +} diff --git a/src/Objects/Path.php b/src/Objects/Path.php new file mode 100644 index 00000000..b96666cd --- /dev/null +++ b/src/Objects/Path.php @@ -0,0 +1,33 @@ +nodes = $nodes; + $this->relationships = $relationships; + } + +} diff --git a/src/Objects/Person.php b/src/Objects/Person.php new file mode 100644 index 00000000..4e6b604e --- /dev/null +++ b/src/Objects/Person.php @@ -0,0 +1,19 @@ + $properties Associative array of properties for the Person node. + */ + public function __construct(array $properties) + { + parent::__construct(['Person'], $properties); + } +} diff --git a/src/Objects/Point.php b/src/Objects/Point.php index a9da28cc..9bbda88d 100644 --- a/src/Objects/Point.php +++ b/src/Objects/Point.php @@ -2,13 +2,32 @@ namespace Neo4j\QueryAPI\Objects; -class Point +/** + * Represents a point with x, y, z coordinates, and SRID (Spatial Reference System Identifier). + */ +final class Point { + /** + * @param float $x The x coordinate of the point. + * @param float $y The y coordinate of the point. + * @param float|null $z The z coordinate of the point, or null if not applicable. + * @param int $srid The Spatial Reference System Identifier (SRID). + */ public function __construct( - public float $longitude, - public float $latitude, - public string $crs - ) + public float $x, + public float $y, + public float|null $z, + public int $srid, + ) { + } + + /** + * Convert the Point object to a string representation. + * + * @return string String representation in the format: "SRID=;POINT ( )". + */ + public function __toString(): string { + return "SRID={$this->srid};POINT ({$this->x} {$this->y})"; } -} \ No newline at end of file +} diff --git a/src/Objects/ProfiledQueryPlan.php b/src/Objects/ProfiledQueryPlan.php new file mode 100644 index 00000000..031861b2 --- /dev/null +++ b/src/Objects/ProfiledQueryPlan.php @@ -0,0 +1,21 @@ + Associative array of properties for the relationship. + */ + public readonly array $properties; + + /** + * Relationship constructor. + * + * @param string $type The type of the relationship. + * @param array $properties Associative array of properties for the relationship. + */ + public function __construct(string $type, array $properties = []) + { + $this->type = $type; + $this->properties = $properties; + } + +} diff --git a/src/Objects/ResultCounters.php b/src/Objects/ResultCounters.php new file mode 100644 index 00000000..ebeb1af7 --- /dev/null +++ b/src/Objects/ResultCounters.php @@ -0,0 +1,100 @@ +containsSystemUpdates; + } + + + public function containsUpdates(): bool + { + return $this->containsUpdates; + } + + public function getNodesCreated(): int + { + return $this->nodesCreated; + } + + + public function getNodesDeleted(): int + { + return $this->nodesDeleted; + } + + + public function getPropertiesSet(): int + { + return $this->propertiesSet; + } + + public function getRelationshipsCreated(): int + { + return $this->relationshipsCreated; + } + + public function getRelationshipsDeleted(): int + { + return $this->relationshipsDeleted; + } + + public function getLabelsAdded(): int + { + return $this->labelsAdded; + } + + public function getIndexesAdded(): int + { + return $this->indexesAdded; + } + + public function getIndexesRemoved(): int + { + return $this->indexesRemoved; + } + + public function getConstraintsAdded(): int + { + return $this->constraintsAdded; + } + + public function getConstraintsRemoved(): int + { + return $this->constraintsRemoved; + } + + public function getSystemUpdates(): int + { + return $this->systemUpdates; + } + + public function getLabelsRemoved(): int + { + return $this->labelsRemoved; + } +} diff --git a/src/ResponseParser.php b/src/ResponseParser.php new file mode 100644 index 00000000..4bc440bc --- /dev/null +++ b/src/ResponseParser.php @@ -0,0 +1,182 @@ +validateAndDecodeResponse($response); + + $rows = $this->mapRows($data['data']['fields'] ?? [], $data['data']['values'] ?? []); + $counters = isset($data['counters']) ? $this->buildCounters($data['counters']) : null; + $bookmarks = $this->buildBookmarks($data['bookmarks'] ?? []); + $profiledQueryPlan = $this->buildProfiledQueryPlan($data['profiledQueryPlan'] ?? null); + $accessMode = $this->getAccessMode($data['accessMode'] ?? ''); + + return new ResultSet($rows, $counters, $bookmarks, $profiledQueryPlan, $accessMode); + } + + private function validateAndDecodeResponse(ResponseInterface $response): array + { + if ($response->getStatusCode() >= 400) { + $errorResponse = json_decode((string)$response->getBody(), true); + throw Neo4jException::fromNeo4jResponse($errorResponse); + } + + $contents = $response->getBody()->getContents(); + $data = json_decode($contents, true); + + if (!isset($data['data'])) { + throw new RuntimeException('Invalid response: "data" key missing or null.'); + } + + return $data; + } + + /** + * @return list + */ + /** + * @param list $fields + * @param list> $values + * @return list + */ + private function mapRows(array $fields, array $values): array + { + return array_map( + fn (array $row): ResultRow => new ResultRow( + array_combine( + $fields, + array_map([$this, 'formatOGMOutput'], $row) + ) ?: [] // Ensure array_combine never returns false + ), + $values + ); + } + + /** + * Ensures mapped output follows expected format + * + * @param mixed $value + * @return mixed + */ + private function formatOGMOutput(mixed $value): mixed + { + if (is_array($value) && array_key_exists('$type', $value) && array_key_exists('_value', $value)) { + return $this->ogm->map($value); + } + + return $value; + } + + + + + private function buildCounters(array $countersData): ResultCounters + { + return new ResultCounters( + containsUpdates: $countersData['containsUpdates'] ?? false, + nodesCreated: $countersData['nodesCreated'] ?? 0, + nodesDeleted: $countersData['nodesDeleted'] ?? 0, + propertiesSet: $countersData['propertiesSet'] ?? 0, + relationshipsCreated: $countersData['relationshipsCreated'] ?? 0, + relationshipsDeleted: $countersData['relationshipsDeleted'] ?? 0, + labelsAdded: $countersData['labelsAdded'] ?? 0, + labelsRemoved: $countersData['labelsRemoved'] ?? 0, + indexesAdded: $countersData['indexesAdded'] ?? 0, + indexesRemoved: $countersData['indexesRemoved'] ?? 0, + constraintsAdded: $countersData['constraintsAdded'] ?? 0, + constraintsRemoved: $countersData['constraintsRemoved'] ?? 0, + systemUpdates: $countersData['systemUpdates'] ?? 0, + ); + } + + private function buildBookmarks(array $bookmarksData): Bookmarks + { + return new Bookmarks($bookmarksData); + } + + private function getAccessMode(string $accessModeData): AccessMode + { + return AccessMode::tryFrom($accessModeData) ?? AccessMode::WRITE; + } + + private function buildProfiledQueryPlan(?array $queryPlanData): ?ProfiledQueryPlan + { + if ($queryPlanData === null || empty($queryPlanData)) { + return null; + } + + /** + * @var array $mappedArguments + */ + $mappedArguments = array_map(function (mixed $value): mixed { + if (is_array($value) && isset($value['$type']) && isset($value['_value'])) { + return $this->ogm->map($value); + } + + return $value; + }, $queryPlanData['arguments'] ?? []); + + + $queryArguments = new ProfiledQueryPlanArguments( + globalMemory: $mappedArguments['GlobalMemory'] ?? null, + plannerImpl: $mappedArguments['planner-impl'] ?? null, + memory: $mappedArguments['Memory'] ?? null, + stringRepresentation: $mappedArguments['string-representation'] ?? null, + runtime: $mappedArguments['runtime'] ?? null, + time: $mappedArguments['Time'] ?? null, + pageCacheMisses: $mappedArguments['PageCacheMisses'] ?? null, + pageCacheHits: $mappedArguments['PageCacheHits'] ?? null, + runtimeImpl: $mappedArguments['runtime-impl'] ?? null, + version: $mappedArguments['version'] ?? null, + dbHits: $mappedArguments['DbHits'] ?? null, + batchSize: $mappedArguments['batch-size'] ?? null, + details: $mappedArguments['Details'] ?? null, + plannerVersion: $mappedArguments['planner-version'] ?? null, + pipelineInfo: $mappedArguments['PipelineInfo'] ?? null, + runtimeVersion: $mappedArguments['runtime-version'] ?? null, + id: $mappedArguments['Id'] ?? null, + estimatedRows: $mappedArguments['EstimatedRows'] ?? null, + planner: $mappedArguments['planner'] ?? null, + rows: $mappedArguments['Rows'] ?? null + ); + + $children = array_map( + fn (array $child): ?ProfiledQueryPlan => $this->buildProfiledQueryPlan($child), + $queryPlanData['children'] ?? [] + ); + + return new ProfiledQueryPlan( + $queryPlanData['dbHits'] ?? 0, + $queryPlanData['records'] ?? 0, + $queryPlanData['hasPageCacheStats'] ?? false, + $queryPlanData['pageCacheHits'] ?? 0, + $queryPlanData['pageCacheMisses'] ?? 0, + $queryPlanData['pageCacheHitRatio'] ?? 0.0, + $queryPlanData['time'] ?? 0, + $queryPlanData['operatorType'] ?? '', + $queryArguments, + $children, + $queryPlanData['identifiers'] ?? [] + ); + } + +} diff --git a/src/Results/ResultRow.php b/src/Results/ResultRow.php index 841ad765..a1378131 100644 --- a/src/Results/ResultRow.php +++ b/src/Results/ResultRow.php @@ -1,49 +1,32 @@ values = []; -// foreach ($this->resultRow as $index => $value) { -// $this->values[$this->keys[$index]] = $value['_value']; -// } -// } -// -// public function get(string $column): mixed -// { -// return $this->values[$column] ?? null; -// } -//} - - - - namespace Neo4j\QueryAPI\Results; - +use ArrayIterator; use BadMethodCallException; -use Neo4j\QueryAPI\OGM; +use Countable; +use IteratorAggregate; use OutOfBoundsException; use ArrayAccess; - -class ResultRow implements ArrayAccess +use Traversable; + +/** + * @template TValue + * @implements ArrayAccess + * @implements IteratorAggregate + */ +final class ResultRow implements ArrayAccess, Countable, IteratorAggregate { - public function __construct(private array $data) - { - } + /** @var array */ + private array $data; - - public function offsetExists($offset): bool + public function __construct(array $data) { - return isset($this->data[$offset]); + $this->data = $data; } - public function offsetGet($offset): mixed + #[\Override] + public function offsetGet(mixed $offset): mixed { if (!$this->offsetExists($offset)) { throw new OutOfBoundsException("Column {$offset} not found."); @@ -51,23 +34,40 @@ public function offsetGet($offset): mixed return $this->data[$offset]; } + public function get(string $row): mixed + { + return $this->offsetGet($row); + } + + + + #[\Override] + public function offsetExists($offset): bool + { + return isset($this->data[$offset]); + } + + #[\Override] public function offsetSet($offset, $value): void { - throw new BadMethodCallException("You cant set the value of column {$offset}."); + throw new BadMethodCallException("You can't set the value of column {$offset}."); } + #[\Override] public function offsetUnset($offset): void { - throw new BadMethodCallException("You cant Unset {$offset}."); + throw new BadMethodCallException("You can't Unset {$offset}."); } - - public function get(string $row): mixed + #[\Override] + public function count(): int { - return $this->offsetGet($row); + return count($this->data); } - + #[\Override] + public function getIterator(): Traversable + { + return new ArrayIterator($this->data); + } } - - diff --git a/src/Results/ResultSet.php b/src/Results/ResultSet.php index 9dd27421..67087202 100644 --- a/src/Results/ResultSet.php +++ b/src/Results/ResultSet.php @@ -1,30 +1,67 @@ + */ +final class ResultSet implements IteratorAggregate, Countable { - private array $rows; + /** + * @param list $rows + */ + public function __construct( + public readonly array $rows, + public readonly ?ResultCounters $counters = null, + public readonly Bookmarks $bookmarks, + public readonly ?ProfiledQueryPlan $profiledQueryPlan, + public readonly AccessMode $accessMode + ) { + } - public function __construct(private array $keys, private array $resultRows, private OGM $ogm) + /** + * @return Traversable + */ + #[\Override] + public function getIterator(): Traversable { - $this->rows = array_map(function ($resultRow) { - $data = []; - foreach ($this->keys as $index => $key) { - $fieldData = $resultRow[$index] ?? null; - $data[$key] = $this->ogm->map($fieldData); - } - return new ResultRow($data); - }, $this->resultRows); + return new ArrayIterator($this->rows); } - public function getIterator(): Traversable + public function getQueryCounters(): ?ResultCounters + { + return $this->counters; + } + + + #[\Override] + public function count(): int + { + return count($this->rows); + } + + public function getBookmarks(): ?Bookmarks + { + return $this->bookmarks; + } + + public function getAccessMode(): ?AccessMode + { + return $this->accessMode; + } + + public function getData(): array { - return new \ArrayIterator($this->rows); + return $this->rows; } } diff --git a/src/Transaction.php b/src/Transaction.php new file mode 100644 index 00000000..ec6d45e3 --- /dev/null +++ b/src/Transaction.php @@ -0,0 +1,87 @@ +requestFactory->buildTransactionRunRequest($query, $parameters, $this->transactionId, $this->clusterAffinity); + + $response = null; + + try { + $response = $this->client->sendRequest($request); + } catch (RequestExceptionInterface $e) { + $this->handleRequestException($e); + } + + if (!$response instanceof ResponseInterface) { + throw new Neo4jException(['message' => 'Failed to receive a valid response from Neo4j'], 500); + } + + return $this->responseParser->parseRunQueryResponse($response); + } + + /** + * @api + */ + public function commit(): void + { + $request = $this->requestFactory->buildCommitRequest($this->transactionId, $this->clusterAffinity); + $this->client->sendRequest($request); + } + + /** + * @api + */ + public function rollback(): void + { + $request = $this->requestFactory->buildRollbackRequest($this->transactionId, $this->clusterAffinity); + $this->client->sendRequest($request); + } + + /** + * Handles request exceptions by parsing error details and throwing a Neo4jException. + * + * @throws Neo4jException + */ + private function handleRequestException(RequestExceptionInterface $e): void + { + $response = method_exists($e, 'getResponse') ? $e->getResponse() : null; + + if ($response instanceof ResponseInterface) { + $errorResponse = json_decode((string)$response->getBody(), true); + throw Neo4jException::fromNeo4jResponse($errorResponse, $e); + } + + throw new Neo4jException(['message' => $e->getMessage()], 500, $e); + } +} diff --git a/src/query-api-test.php b/src/query-api-test.php deleted file mode 100644 index 9ae6468a..00000000 --- a/src/query-api-test.php +++ /dev/null @@ -1,25 +0,0 @@ -run($query, []); - -// Display the results -echo "
";
-print_r($results);
-echo "
"; - - diff --git a/src/run_neo4j_query.php b/src/run_neo4j_query.php deleted file mode 100644 index 5b3d0793..00000000 --- a/src/run_neo4j_query.php +++ /dev/null @@ -1,32 +0,0 @@ -withDriver( - 'bolt', - $address, - Authenticate::basic($username, $password) // Proper authentication object - ) - ->build(); - - // Define the Cypher query - $cypherQuery = 'MATCH (n:Person) RETURN n LIMIT 10'; - - // Run the query and fetch results - $results = $client->run($cypherQuery); - - // Print the results - echo "
";  // Optional: formats the output nicely for readability
-    print_r($results->toArray());
-    echo "
"; -*/ diff --git a/src/run_query.php b/src/run_query.php deleted file mode 100644 index 92d320f5..00000000 --- a/src/run_query.php +++ /dev/null @@ -1,41 +0,0 @@ -run($query, [], 'neo4j', false); - echo "Plain JSON Results:\n"; - echo "
";
-    print_r($plainResults);
-    echo "
"; - - // Fetch results in Neo4j-extended JSON format - $extendedResults = $api->run($query, [], 'neo4j', true); - echo "Extended JSON Results:\n"; - echo "
";
-    print_r($extendedResults);
-    echo "
"; - -} catch (RequestException $e) { - echo "Request Error: " . $e->getMessage(); -} catch (RuntimeException $e) { - echo "Runtime Error: " . $e->getMessage(); -} catch (Exception $e) { - echo "General Error: " . $e->getMessage(); -} - diff --git a/test_curl.php b/test_curl.php deleted file mode 100644 index 61ace196..00000000 --- a/test_curl.php +++ /dev/null @@ -1,2 +0,0 @@ -api = Neo4jQueryAPI::create( + new Configuration(baseUri: $neo4jAddress, bookmarks: $bookmarks ?? new Bookmarks([]), accessMode: $accessMode), + Authentication::fromEnvironment() + ); + } +} diff --git a/tests/Integration/AccessModesIntegrationTest.php b/tests/Integration/AccessModesIntegrationTest.php new file mode 100644 index 00000000..f3aee9c1 --- /dev/null +++ b/tests/Integration/AccessModesIntegrationTest.php @@ -0,0 +1,48 @@ +createQueryAPI(); + } + + #[DoesNotPerformAssertions] + public function testRunWithWriteAccessMode(): void + { + $this->api->run("CREATE (n:Person {name: 'Alice'}) RETURN n"); + } + + #[DoesNotPerformAssertions] + public function testRunWithReadAccessMode(): void + { + $this->createQueryAPI(AccessMode::READ); + $this->api->run("MATCH (n) RETURN COUNT(n)"); + } + + public function testReadModeWithWriteQuery(): void + { + $this->createQueryAPI(AccessMode::READ); + $this->expectException(Neo4jException::class); + $this->api->run("CREATE (n:Test {name: 'Test Node'})"); + } + + #[DoesNotPerformAssertions] + public function testWriteModeWithReadQuery(): void + { + $this->api->run("MATCH (n:Test) RETURN n"); + } +} diff --git a/tests/Integration/BookmarksIntegrationTest.php b/tests/Integration/BookmarksIntegrationTest.php new file mode 100644 index 00000000..58c6d4bb --- /dev/null +++ b/tests/Integration/BookmarksIntegrationTest.php @@ -0,0 +1,62 @@ +createQueryAPI(); + } + + + public function testCreateBookmarks(): void + { + $result = $this->api->run('CREATE (x:Node {hello: "world"})'); + + $bookmarks = $result->getBookmarks() ?? new Bookmarks([]); + + $result = $this->api->run('CREATE (x:Node {hello: "world2"})'); + $bookmarks->addBookmarks($result->getBookmarks()); + + $result = $this->api->run('MATCH (x:Node {hello: "world2"}) RETURN x'); + $bookmarks->addBookmarks($result->getBookmarks()); + + $this->assertCount(1, $result); + } + + + public function testInvalidBookmarkThrowsException(): void + { + $exceptionCaught = false; + + $invalidBookmark = new Bookmarks(['invalid:bookmark']); + $this->createQueryAPI(bookmarks: $invalidBookmark); + + try { + $this->api->run('MATCH (n) RETURN n'); + } catch (Neo4jException $e) { + $exceptionCaught = true; + $this->assertEquals('Parsing of supplied bookmarks failed with message: Illegal base64 character 3a', $e->getMessage()); + $this->assertEquals('InvalidBookmark', $e->getName()); + $this->assertEquals('Transaction', $e->getSubType()); + $this->assertEquals('ClientError', $e->getType()); + } + + $this->assertTrue($exceptionCaught); + } + + +} diff --git a/tests/Integration/DataTypesIntegrationTest.php b/tests/Integration/DataTypesIntegrationTest.php new file mode 100644 index 00000000..bf83ce28 --- /dev/null +++ b/tests/Integration/DataTypesIntegrationTest.php @@ -0,0 +1,613 @@ +createQueryAPI(); + } + + public function testWithExactNames(): void + { + $expected = new ResultSet( + [ + new ResultRow(['n.name' => 'bob1']), + new ResultRow(['n.name' => 'alicy']), + ], + new ResultCounters(), + new Bookmarks([]), + null, + AccessMode::WRITE + ); + + $results = $this->api->run('MATCH (n:Person) WHERE n.name IN $names RETURN n.name', [ + 'names' => ['bob1', 'alicy'] + ]); + + $this->assertEquals($expected->getQueryCounters(), $results->getQueryCounters()); + $bookmarks = $results->getBookmarks() ?? new Bookmarks([]); + $this->assertCount(1, $bookmarks); + } + + public function testWithSingleName(): void + { + $expected = new ResultSet( + [ + new ResultRow(['n.name' => 'bob1']), + ], + new ResultCounters(), + new Bookmarks([]), + null, + AccessMode::WRITE + ); + + $results = $this->api->run('MATCH (n:Person) WHERE n.name = $name RETURN n.name LIMIT 1', [ + 'name' => 'bob1' + ]); + + $this->assertEquals($expected->getQueryCounters(), $results->getQueryCounters()); + $bookmarks = $results->getBookmarks() ?: []; + $this->assertCount(1, $bookmarks); + } + + public function testWithInteger(): void + { + $expected = new ResultSet( + [ + new ResultRow(['n.age' => 30]), + ], + new ResultCounters( + containsUpdates: true, + nodesCreated: 1, + propertiesSet: 1, + labelsAdded: 1, + ), + new Bookmarks([]), + null, + AccessMode::WRITE + ); + + $results = $this->api->run('CREATE (n:Person {age: $age}) RETURN n.age', [ + 'age' => 30 + ]); + + $this->assertEquals($expected->getQueryCounters(), $results->getQueryCounters()); + $this->assertEquals(iterator_to_array($expected), iterator_to_array($results)); + $bookmarks = $results->getBookmarks() ?: []; + $this->assertCount(1, $bookmarks); + } + + + public function testWithFloat(): void + { + $expected = new ResultSet( + [ + new ResultRow(['n.height' => 1.75]), + ], + new ResultCounters( + containsUpdates: true, + nodesCreated: 1, + propertiesSet: 1, + labelsAdded: 1, + ), + new Bookmarks([]), + null, + AccessMode::WRITE + ); + + $results = $this->api->run('CREATE (n:Person {height: $height}) RETURN n.height', [ + 'height' => 1.75 + ]); + + $this->assertEquals($expected->getQueryCounters(), $results->getQueryCounters()); + $this->assertEquals(iterator_to_array($expected), iterator_to_array($results)); + $bookmarks = $results->getBookmarks() ?: []; + $this->assertCount(1, $bookmarks); + } + + public function testWithNull(): void + { + $expected = new ResultSet( + [ + new ResultRow(['n.middleName' => null]), + ], + new ResultCounters( + containsUpdates: true, + nodesCreated: 1, + propertiesSet: 0, + labelsAdded: 1, + ), + new Bookmarks([]), + null, + AccessMode::WRITE + ); + + $results = $this->api->run('CREATE (n:Person {middleName: $middleName}) RETURN n.middleName', [ + 'middleName' => null + ]); + + $this->assertEquals($expected->getQueryCounters(), $results->getQueryCounters()); + $this->assertEquals(iterator_to_array($expected), iterator_to_array($results)); + $bookmarks = $results->getBookmarks() ?: []; + $this->assertCount(1, $bookmarks); + } + + public function testWithBoolean(): void + { + $expected = new ResultSet( + [ + new ResultRow(['n.isActive' => true]), + ], + new ResultCounters( + containsUpdates: true, + nodesCreated: 1, + propertiesSet: 1, + labelsAdded: 1, + ), + new Bookmarks([]), + null, + AccessMode::WRITE + ); + + $results = $this->api->run('CREATE (n:Person {isActive: $isActive}) RETURN n.isActive', [ + 'isActive' => true + ]); + + $this->assertEquals($expected->getQueryCounters(), $results->getQueryCounters()); + $this->assertEquals(iterator_to_array($expected), iterator_to_array($results)); + $bookmarks = $results->getBookmarks() ?: []; + $this->assertCount(1, $bookmarks); + } + + public function testWithString(): void + { + $expected = new ResultSet( + [ + new ResultRow(['n.name' => 'Alice']), + ], + new ResultCounters( + containsUpdates: true, + nodesCreated: 1, + propertiesSet: 1, + labelsAdded: 1, + ), + new Bookmarks([]), + null, + AccessMode::WRITE + ); + + $results = $this->api->run('CREATE (n:Person {name: $name}) RETURN n.name', [ + 'name' => 'Alice' + ]); + + $this->assertEquals($expected->getQueryCounters(), $results->getQueryCounters()); + $this->assertEquals(iterator_to_array($expected), iterator_to_array($results)); + $bookmarks = $results->getBookmarks() ?: []; + $this->assertCount(1, $bookmarks); + } + + public function testWithArray(): void + { + $expected = new ResultSet( + [ + new ResultRow(['n.name' => 'bob1']), + new ResultRow(['n.name' => 'alicy']) + ], + new ResultCounters( + containsUpdates: false, + nodesCreated: 0, + propertiesSet: 0, + labelsAdded: 0, + ), + new Bookmarks([]), + null, + AccessMode::WRITE + ); + + $results = $this->api->run( + 'MATCH (n:Person) WHERE n.name IN $names RETURN n.name', + ['names' => ['bob1', 'alicy']] + ); + + $this->assertEquals($expected->getQueryCounters(), $results->getQueryCounters()); + $bookmarks = $results->getBookmarks() ?: []; + $this->assertCount(1, $bookmarks); + } + + public function testWithDate(): void + { + $expected = new ResultSet( + [ + new ResultRow(['n.date' => '2024-12-11T11:00:00Z']) + + ], + new ResultCounters( + containsUpdates: true, + nodesCreated: 1, + propertiesSet: 1, + labelsAdded: 1, + ), + new Bookmarks([]), + null, + AccessMode::WRITE + ); + + $results = $this->api->run( + 'CREATE (n:Person {date: datetime($date)}) RETURN n.date', + ['date' => "2024-12-11T11:00:00Z"] + ); + + $this->assertEquals($expected->getQueryCounters(), $results->getQueryCounters()); + $this->assertEquals(iterator_to_array($expected), iterator_to_array($results)); + $bookmarks = $results->getBookmarks() ?: []; + $this->assertCount(1, $bookmarks); + } + + public function testWithDuration(): void + { + $expected = new ResultSet( + [ + new ResultRow(['n.duration' => 'P14DT16H12M']), + + ], + new ResultCounters( + containsUpdates: true, + nodesCreated: 1, + propertiesSet: 1, + labelsAdded: 1, + ), + new Bookmarks([]), + null, + AccessMode::WRITE + ); + + $results = $this->api->run( + 'CREATE (n:Person {duration: duration($duration)}) RETURN n.duration', + ['duration' => 'P14DT16H12M'], + ); + + $this->assertEquals($expected->getQueryCounters(), $results->getQueryCounters()); + $this->assertEquals(iterator_to_array($expected), iterator_to_array($results)); + $bookmarks = $results->getBookmarks() ?: []; + $this->assertCount(1, $bookmarks); + } + + public function testWithWGS84_2DPoint(): void + { + $expected = new ResultSet( + [ + new ResultRow(['n.Point' => 'SRID=4326;POINT (1.2 3.4)']), + ], + new ResultCounters( + containsUpdates: true, + nodesCreated: 1, + propertiesSet: 1, + labelsAdded: 1, + ), + new Bookmarks([]), + null, + AccessMode::WRITE + ); + + $results = $this->api->run( + 'CREATE (n:Person {Point: point($Point)}) RETURN n.Point', + [ + 'Point' => [ + 'longitude' => 1.2, + 'latitude' => 3.4, + 'crs' => 'wgs-84', + ]] + ); + + + $this->assertEquals($expected->getQueryCounters(), $results->getQueryCounters()); + $this->assertEquals(iterator_to_array($expected), iterator_to_array($results)); + $bookmarks = $results->getBookmarks() ?: []; + $this->assertCount(1, $bookmarks); + } + + public function testWithWGS84_3DPoint(): void + { + $expected = new ResultSet( + [ + new ResultRow(['n.Point' => new Point(1.2, 3.4, 4.2, 4979)]), + ], + new ResultCounters( + containsUpdates: true, + nodesCreated: 1, + propertiesSet: 1, + labelsAdded: 1, + ), + new Bookmarks([]), + null, + AccessMode::WRITE + ); + + $results = $this->api->run( + 'CREATE (n:Person {Point: point({longitude: $longitude, latitude: $latitude, height: $height, srid: $srid})}) RETURN n.Point', + [ + 'longitude' => 1.2, + 'latitude' => 3.4, + 'height' => 4.2, + 'srid' => 4979, + ] + ); + + + $this->assertEquals($expected->getQueryCounters(), $results->getQueryCounters()); + $this->assertEquals(iterator_to_array($expected), iterator_to_array($results)); + $bookmarks = $results->getBookmarks() ?: []; + $this->assertCount(1, $bookmarks); + } + + public function testWithCartesian2DPoint(): void + { + $expected = new ResultSet( + [ + new ResultRow(['n.Point' => new Point(10.5, 20.7, null, 7203)]), + ], + new ResultCounters( + containsUpdates: true, + nodesCreated: 1, + propertiesSet: 1, + labelsAdded: 1, + ), + new Bookmarks([]), + null, + AccessMode::WRITE + ); + + $results = $this->api->run( + 'CREATE (n:Person {Point: point({x: $x, y: $y, srid: $srid})}) RETURN n.Point', + [ + 'x' => 10.5, + 'y' => 20.7, + 'srid' => 7203, + ] + ); + + + $this->assertEquals($expected->getQueryCounters(), $results->getQueryCounters()); + $this->assertEquals(iterator_to_array($expected), iterator_to_array($results)); + $bookmarks = $results->getBookmarks() ?: []; + $this->assertCount(1, $bookmarks); + } + + public function testWithCartesian3DPoint(): void + { + $expected = new ResultSet( + [ + new ResultRow(['n.Point' => new Point(10.5, 20.7, 30.9, 9157)]), + ], + new ResultCounters( + containsUpdates: true, + nodesCreated: 1, + propertiesSet: 1, + labelsAdded: 1, + ), + new Bookmarks([]), + null, + AccessMode::WRITE + ); + + $results = $this->api->run( + 'CREATE (n:Person {Point: point({x: $x, y: $y, z: $z, srid: $srid})}) RETURN n.Point', + [ + 'x' => 10.5, + 'y' => 20.7, + 'z' => 30.9, + 'srid' => 9157, + ] + ); + + + $this->assertEquals($expected->getQueryCounters(), $results->getQueryCounters()); + $this->assertEquals(iterator_to_array($expected), iterator_to_array($results)); + $bookmarks = $results->getBookmarks() ?: []; + $this->assertCount(1, $bookmarks); + } + + public function testWithNode(): void + { + $expected = new ResultSet( + [ + new ResultRow([ + 'node' => [ + 'properties' => [ + 'name' => 'Ayush', + 'location' => 'New York', + 'age' => '30' + ], + 'labels' => [ + 0 => 'Person' + ] + + ] + ]), + ], + new ResultCounters( + containsUpdates: true, + nodesCreated: 1, + propertiesSet: 3, + labelsAdded: 1, + ), + new Bookmarks([]), + null, + AccessMode::WRITE + ); + + $results = $this->api->run( + 'CREATE (n:Person {name: $name, age: $age, location: $location}) RETURN {labels: labels(n), properties: properties(n)} AS node', + [ + 'name' => 'Ayush', + 'age' => 30, + 'location' => 'New York', + ] + ); + + + $this->assertEquals($expected->getQueryCounters(), $results->getQueryCounters()); + $this->assertEquals(iterator_to_array($expected), iterator_to_array($results)); + $bookmarks = $results->getBookmarks() ?: []; + $this->assertCount(1, $bookmarks); + } + + public function testWithPath(): void + { + $expected = new ResultSet( + [ + new ResultRow(['node1' => [ + 'labels' => ['Person'], + 'properties' => [ + 'name' => 'A', + ], + ], + 'node2' => [ + 'labels' => ['Person'], + 'properties' => [ + 'name' => 'B', + ], + ], + 'relationshipTypes' => ['FRIENDS'], + ]), + ], + new ResultCounters( + containsUpdates: true, + nodesCreated: 2, + propertiesSet: 2, + relationshipsCreated: 1, + labelsAdded: 2, + ), + new Bookmarks([]), + null, + AccessMode::WRITE + ); + + $results = $this->api->run( + 'CREATE (a:Person {name: $name1}), (b:Person {name: $name2}), + (a)-[r:FRIENDS]->(b) + RETURN {labels: labels(a), properties: properties(a)} AS node1, + {labels: labels(b), properties: properties(b)} AS node2, + collect(type(r)) AS relationshipTypes', + [ + 'name1' => 'A', + 'name2' => 'B', + ] + ); + + + $this->assertEquals($expected->getQueryCounters(), $results->getQueryCounters()); + $this->assertEquals(iterator_to_array($expected), iterator_to_array($results)); + $bookmarks = $results->getBookmarks() ?: []; + $this->assertCount(1, $bookmarks); + } + + + public function testWithMap(): void + { + $expected = new ResultSet( + [ + new ResultRow(['map' => [ + 'hello' => 'hello', + ], + ]), + ], + new ResultCounters( + containsUpdates: false, + nodesCreated: 0, + propertiesSet: 0, + labelsAdded: 0, + ), + new Bookmarks([]), + null, + AccessMode::WRITE + ); + + $results = $this->api->run( + 'RETURN {hello: "hello"} AS map', + [] + ); + + + $this->assertEquals($expected->getQueryCounters(), $results->getQueryCounters()); + $this->assertEquals(iterator_to_array($expected), iterator_to_array($results)); + $bookmarks = $results->getBookmarks() ?: []; + $this->assertCount(1, $bookmarks); + } + + public function testWithRelationship(): void + { + $expected = new ResultSet( + [ + new ResultRow([ + 'node1' => [ + 'labels' => ['Person'], + 'properties' => [ + 'name' => 'Ayush', + 'age' => 30, + 'location' => 'New York', + ], + ], + 'node2' => [ + 'labels' => ['Person'], + 'properties' => [ + 'name' => 'John', + 'age' => 25, + 'location' => 'Los Angeles', + ], + ], + 'relationshipType' => 'FRIEND_OF', + ]), + ], + new ResultCounters( + containsUpdates: true, + nodesCreated: 2, + propertiesSet: 6, + relationshipsCreated: 1, + labelsAdded: 2, + ), + new Bookmarks([]), + null, + AccessMode::WRITE + ); + + $results = $this->api->run( + 'CREATE (p1:Person {name: $name1, age: $age1, location: $location1}), + (p2:Person {name: $name2, age: $age2, location: $location2}), + (p1)-[r:FRIEND_OF]->(p2) + RETURN {labels: labels(p1), properties: properties(p1)} AS node1, + {labels: labels(p2), properties: properties(p2)} AS node2, + type(r) AS relationshipType', + [ + 'name1' => 'Ayush', + 'age1' => 30, + 'location1' => 'New York', + 'name2' => 'John', + 'age2' => 25, + 'location2' => 'Los Angeles' + ] + ); + + + $this->assertEquals($expected->getQueryCounters(), $results->getQueryCounters()); + $this->assertEquals(iterator_to_array($expected), iterator_to_array($results)); + $bookmarks = $results->getBookmarks() ?: []; + $this->assertCount(1, $bookmarks); + } +} diff --git a/tests/Integration/Neo4jOGMTest.php b/tests/Integration/Neo4jOGMTest.php index e114161c..635fd74e 100644 --- a/tests/Integration/Neo4jOGMTest.php +++ b/tests/Integration/Neo4jOGMTest.php @@ -2,31 +2,90 @@ namespace Neo4j\QueryAPI\Tests\Integration; +use Neo4j\QueryAPI\Objects\Path; use Neo4j\QueryAPI\OGM; use PHPUnit\Framework\TestCase; -class Neo4jOGMTest extends TestCase +final class Neo4jOGMTest extends TestCase { private OGM $ogm; - public function setUp(): void + #[\Override] + protected function setUp(): void { + parent::setUp(); $this->ogm = new OGM(); } - public function testInteger(): void + + public function testWithNode(): void { - $this->assertEquals(30, $this->ogm->map([ - '$type' => 'Integer', - '_value' => 30, - ])); + $nodeData = [ + '$type' => 'Node', + '_value' => [ + '_labels' => ['Person'], + '_properties' => ['name' => ['_value' => 'Ayush']], + ] + ]; + + $node = $this->ogm->map($nodeData); + $this->assertEquals('Ayush', $node->getProperties()['name']['_value']); } - public function testPoint(): void + public function testWithSimpleRelationship(): void { - $this->assertEquals(30, $this->ogm->map([ - '$type' => 'Point', - '_value' => 'SRID=4326;POINT (1.2 3.4)', - ])); + + $relationshipData = [ + '$type' => 'Relationship', + '_value' => [ + '_type' => 'FRIENDS', + '_properties' => [], + ] + ]; + + $relationship = $this->ogm->map($relationshipData); + $this->assertEquals('FRIENDS', $relationship->type); + } + + public function testWithPath(): void + { + $pathData = [ + '$type' => 'Path', + '_value' => [ + [ + '$type' => 'Node', + '_value' => [ + '_labels' => ['Person'], + '_properties' => [ + 'name' => ['_value' => 'A'], + ], + ], + ], + [ + '$type' => 'Relationship', + '_value' => [ + '_type' => 'FRIENDS', + '_properties' => [], + ], + ], + [ + '$type' => 'Node', + '_value' => [ + '_labels' => ['Person'], + '_properties' => [ + 'name' => ['_value' => 'B'], + ], + ], + ], + ] + ]; + + $path = $this->ogm->map($pathData); + $this->assertInstanceOf(Path::class, $path); + + $this->assertCount(2, $path->nodes); + $this->assertCount(1, $path->relationships); + $this->assertEquals('A', $path->nodes[0]->getProperties()['name']['_value']); + $this->assertEquals('B', $path->nodes[1]->getProperties()['name']['_value']); } -} \ No newline at end of file +} diff --git a/tests/Integration/Neo4jQueryAPIIntegrationTest.php b/tests/Integration/Neo4jQueryAPIIntegrationTest.php index be67ce84..1dce1520 100644 --- a/tests/Integration/Neo4jQueryAPIIntegrationTest.php +++ b/tests/Integration/Neo4jQueryAPIIntegrationTest.php @@ -2,448 +2,105 @@ namespace Neo4j\QueryAPI\Tests\Integration; -use GuzzleHttp\Exception\GuzzleException; +use Neo4j\QueryAPI\Exception\Neo4jException; use Neo4j\QueryAPI\Neo4jQueryAPI; -use PHPUnit\Framework\Attributes\DataProvider; +use Neo4j\QueryAPI\Objects\Authentication; +use Neo4j\QueryAPI\Objects\Node; +use Neo4j\QueryAPI\Objects\Bookmarks; +use Neo4j\QueryAPI\Objects\ResultCounters; +use Neo4j\QueryAPI\Results\ResultRow; +use Neo4j\QueryAPI\Results\ResultSet; use PHPUnit\Framework\TestCase; +use Neo4j\QueryAPI\Enums\AccessMode; -class Neo4jQueryAPIIntegrationTest extends TestCase +final class Neo4jQueryAPIIntegrationTest extends TestCase { - private static ?Neo4jQueryAPI $api = null; + private Neo4jQueryAPI $api; - public static function setUpBeforeClass(): void + #[\Override] + public function setUp(): void { - self::$api = self::initializeApi(); - - self::clearDatabase(); - self::setupConstraints(); - self::populateTestData(['bob1', 'alicy']); - self::validateData(); + parent::setUp(); + $this->api = $this->initializeApi(); + $this->clearDatabase(); + $this->populateTestData(); } - private static function initializeApi(): Neo4jQueryAPI - { - return Neo4jQueryAPI::login( - getenv('NEO4J_ADDRESS'), - getenv('NEO4J_USERNAME'), - getenv('NEO4J_PASSWORD') - ); - } - private static function clearDatabase(): void + public function testParseRunQueryResponse(): void { - self::$api->run('MATCH (n) DETACH DELETE n', []); + $query = 'CREATE (n:TestNode {name: "Test"}) RETURN n'; + $response = $this->api->run($query); + $bookmarks = $response->getBookmarks() ?? new Bookmarks([]); + + $this->assertEquals(new ResultSet( + rows: [ + new ResultRow([ + 'n' => new Node( + ['TestNode'], + ['name' => 'Test'] + ) + ]) + ], + counters: new ResultCounters( + containsUpdates: true, + nodesCreated: 1, + propertiesSet: 1, + labelsAdded: 1 + ), + bookmarks: $bookmarks, + profiledQueryPlan: null, + accessMode: AccessMode::WRITE + ), $response); } - private static function setupConstraints(): void + public function testInvalidQueryHandling(): void { - self::$api->run('CREATE CONSTRAINT IF NOT EXISTS FOR (p:Person) REQUIRE p.name IS UNIQUE', []); + $this->expectException(Neo4jException::class); + $this->api->run('INVALID CYPHER QUERY'); } - private static function populateTestData(array $names): void + private function initializeApi(): Neo4jQueryAPI { - foreach ($names as $name) { - self::$api->run('CREATE (:Person {name: $name})', ['name' => $name]); + $address = getenv('NEO4J_ADDRESS'); + if ($address === false) { + $address = 'default-address'; } + return Neo4jQueryAPI::login($address, Authentication::fromEnvironment()); } - - private static function validateData(): void + public function testCounters(): void { - $response = self::$api->run('MATCH (p:Person) RETURN p.name AS name, p.email AS email, p.age AS age, p AS person', []); - - foreach ($response as $person) { - echo $person->get('name'); - echo $person->get('email'); - echo $person->get('age'); + $result = $this->api->run('CREATE (x:Node {hello: "world"})'); + $queryCounters = $result->getQueryCounters(); - } + $this->assertNotNull($queryCounters); + $this->assertEquals(1, $queryCounters->getNodesCreated()); } - private function executeQuery(string $query, array $parameters): array + private function clearDatabase(): void { - $response = self::$api->run($query, $parameters); - - if (!empty($response['errors'])) { - throw new \RuntimeException('Query execution failed: ' . json_encode($response['errors'])); - } - - $response['data']['values'] = array_map(fn($row) => $row, $response['data']['values']); - - return $response; + $this->api->run('MATCH (n) DETACH DELETE n', []); } - #[DataProvider(methodName: 'queryProvider')] - public function testRunSuccessWithParameters( - string $query, - array $parameters, - array $expectedResults - ): void + private function populateTestData(): void { - $results = $this->executeQuery($query, $parameters); - - $subsetResults = $this->createSubset($expectedResults, $results); - - $this->assertIsArray($results); - $this->assertEquals($expectedResults, $subsetResults); + $names = ['bob1', 'alicy']; + foreach ($names as $name) { + $this->api->run('CREATE (:Person {name: $name})', ['name' => $name]); + } } - private function createSubset(array $expected, array $actual): array + public function testInvalidQueryException(): void { - $subset = []; - - foreach ($expected as $key => $value) { - if (array_key_exists($key, $actual)) { - $actualValue = $actual[$key]; - if (is_array($value) && is_array($actualValue)) { - $actualValue = $this->createSubset($value, $actualValue); - } - $subset[$key] = $actualValue; - } + try { + $this->api->run('CREATE (:Person {createdAt: $invalidParam})', [ + 'date' => new \DateTime('2000-01-01 00:00:00') + ]); + } catch (\Throwable $e) { + $this->assertInstanceOf(Neo4jException::class, $e); + $this->assertEquals('Neo.ClientError.Statement.ParameterMissing', $e->getErrorCode()); + $this->assertEquals('Expected parameter(s): invalidParam', $e->getMessage()); } - - return $subset; } - public static function queryProvider(): array - { - $decodedBinary = base64_decode('U29tZSByYW5kb20gYmluYXJ5IGRhdGE='); - return [ - 'testWithExactNames' => [ - 'MATCH (n:Person) WHERE n.name IN $names RETURN n.name', - ['names' => ['bob1', 'alicy']], - [ - 'data' => [ - 'fields' => ['n.name'], - 'values' => [ - [ - [ - '$type' => 'String', - '_value' => 'bob1' - ] - ], - [ - [ - '$type' => 'String', - '_value' => 'alicy' - ] - ] - ], - ], - ], - ], - 'testWithSingleName' => [ - 'MATCH (n:Person) WHERE n.name = $name RETURN n', - ['name' => 'bob1'], - [ - 'data' => [ - 'fields' => ['n'], - 'values' => [ - [ - [ - '$type' => 'Node', - '_value' => [ - '_labels' => ['Person'], - '_properties' => [ - 'name' => [ - '$type' => 'String', - '_value' => 'bob1', - ] - ] - ] - ] - ] - ] - ], - ], - ], - 'testWithNoMatchingNames' => [ - 'MATCH (n:Person) WHERE n.name IN $names RETURN n.name', - ['names' => ['charlie', 'david']], - [ - 'data' => [ - 'fields' => ['n.name'], - 'values' => [], - ], - ], - ], - 'testWithString' => [ - 'CREATE (n:Person {name: $name}) RETURN n.name', - ['name' => 'Alice'], - [ - 'data' => [ - 'fields' => ['n.name'], - 'values' => [ - [ - [ - '$type' => 'String', - '_value' => 'Alice', - ], - ], - ], - ], - ], - ], - 'testWithNumber' => [ - 'CREATE (n:Person {age: $age}) RETURN n.age', - ['age' => 30], - [ - 'data' => [ - 'fields' => ['n.age'], - 'values' => [ - [ - [ - '$type' => 'Integer', - '_value' => 30, - ], - ], - ], - ], - ], - ], - 'testWithNull' => [ - 'CREATE (n:Person {middleName: $middleName}) RETURN n.middleName', - ['middleName' => null], - [ - 'data' => [ - 'fields' => ['n.middleName'], - 'values' => [ - [ - [ - '$type' => 'Null', - '_value' => null, - ], - ], - ], - ], - ], - ], - 'testWithBoolean' => [ - 'CREATE (n:Person {isActive: $isActive}) RETURN n.isActive', - ['isActive' => true], - [ - 'data' => [ - 'fields' => ['n.isActive'], - 'values' => [ - [ - [ - '$type' => 'Boolean', - '_value' => true, - ], - ], - ], - ], - ], - ], - 'testWithArray' => [ - 'CREATE (n:Person {tags: $tags}) RETURN n.tags', - ['tags' => ['developer', 'python', 'neo4j']], - [ - 'data' => [ - 'fields' => ['n.tags'], - 'values' => [ - [ - [ - '$type' => 'List', - '_value' => [ - [], - [], - [], - ], - ], - ], - ], - ], - ], - ], - 'testWithDate' => [ - 'CREATE (n:Person {date: datetime($date)}) RETURN n.date', - ['date' => "2024-12-11T11:00:00Z"], - [ - 'data' => [ - 'fields' => ['n.date'], - 'values' => [ - [ - [ - '$type' => 'OffsetDateTime', - '_value' => '2024-12-11T11:00:00Z', - ], - ], - ], - ], - ], - ], - - 'testWithDuration' => [ - 'CREATE (n:Person {duration: duration($duration)}) RETURN n.duration', - ['duration' => 'P14DT16H12M'], - [ - 'data' => [ - 'fields' => ['n.duration'], - 'values' => [ - [ - [ - '$type' => 'Duration', - '_value' => 'P14DT16H12M', - ], - ], - ], - ], - ], - ], - /*'testWithBinary' => [ - 'CREATE (n:Person {binary:$binary}) RETURN n.binary', - ['binary' => 'U29tZSByYW5kb20gYmluYXJ5IGRhdGE='], - [ - 'data' => [ - 'fields' => ['n.binary'], - 'values' => [ - [ - [ - '$type' => 'Bytes', - '_value' => 'U29tZSByYW5kb20gYmluYXJ5IGRhdGE=', - ], - ], - ], - ], - ], - ],*/ - 'testWithPoint' => [ - 'CREATE (n:Person {Point: point($Point)}) RETURN n.Point', - [ - 'Point' => [ - 'longitude' => 1.2, // X-coordinate (longitude) - 'latitude' => 3.4, // Y-coordinate (latitude) - 'crs' => 'wgs-84', // Geographic CRS (SRID=4326) - ], - ], - [ - 'data' => [ - 'fields' => ['n.Point'], - 'values' => [ - [ - [ - '$type' => 'Point', - '_value' => 'SRID=4326;POINT (1.2 3.4)', - ], - ], - ], - ], - ], - ], - - 'testWithNode' => [ - 'CREATE (n:Person {name: $name, age: $age, location: $location}) RETURN n', - [ - 'name' => 'Ayush', - 'age' => 30, - 'location' => 'New York', - ], - [ - 'data' => [ - 'fields' => ['n'], - 'values' => [ - [ - [ - '$type' => 'Node', - '_value' => [ - - '_labels' => ['Person'], - '_properties' => [ - 'name' => [ - '$type' => 'String', - '_value' => 'Ayush', - ], - 'age' => [ - '$type' => 'Integer', - '_value' => 30, - ], - 'location' => [ - '$type' => 'String', - '_value' => 'New York', - ], - ], - ], - ], - ], - ], - ], - ], - ], - - 'testWithSimpleRelationship' => [ - 'CREATE (a:Person {name: "A"}), (b:Person {name: "B"}), (a)-[r:FRIENDS]->(b)RETURN a, b, r', - [], - [ - 'data' => [ - 'fields' => ['a', 'b', 'r'], - 'values' => [ - [ - [ - '$type' => 'Node', - '_value' => [ - '_labels' => ['Person'], - '_properties' => ['name' => ['_value' => 'A']] - ] - ], - [ - '$type' => 'Node', - '_value' => [ - '_labels' => ['Person'], - '_properties' => ['name' => ['_value' => 'B']] - ] - ], - [ - '$type' => 'Relationship', - '_value' => [ - - '_type' => 'FRIENDS', - '_properties' => [] - ] - ] - ] - ] - ] - ], - ], - 'testWithPath' => [ - 'CREATE (a:Person {name: "A"}), (b:Person {name: "B"}), path = (a)-[r:FRIENDS]->(b) RETURN path', - [], - [ - 'data' => [ - 'fields' => ['path'], - 'values' => [ - [ - [ - '$type' => 'Path', - '_value' => [ - [ - '$type' => 'Node', - '_value' => [ - '_labels' => ['Person'], - '_properties' => ['name' => ['_value' => 'A']], - ], - ], - [ - '$type' => 'Relationship', - '_value' => [ - '_type' => 'FRIENDS', - '_properties' => [], - ], - ], - [ - '$type' => 'Node', - '_value' => [ - '_labels' => ['Person'], - '_properties' => ['name' => ['_value' => 'B']], - ], - ], - ], - ], - ], - ], - ], - ], - ], - ]; - } } diff --git a/tests/Integration/Neo4jQueryAPITest.php b/tests/Integration/Neo4jQueryAPITest.php new file mode 100644 index 00000000..bd9bc8a8 --- /dev/null +++ b/tests/Integration/Neo4jQueryAPITest.php @@ -0,0 +1,42 @@ +expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Address (http://valid.address) as argument is different from address in configuration (http://myaddress)'); + + Neo4jQueryAPI::login('http://myaddress', Authentication::fromEnvironment(), $config); + } + + public function testLoginWithNullConfiguration(): void + { + $config = null; + + $api = Neo4jQueryAPI::login('http://myaddress', Authentication::fromEnvironment(), $config); + + $this->assertInstanceOf(Neo4jQueryAPI::class, $api); + $this->assertEquals('http://myaddress', $api->getConfig()->baseUri); + } + + public function testConfigOnly(): void + { + $config = new Configuration(baseUri: 'http://valid.address'); + + $api = Neo4jQueryAPI::login(auth: Authentication::fromEnvironment(), config: $config); + + $this->assertInstanceOf(Neo4jQueryAPI::class, $api); + $this->assertEquals('http://valid.address', $api->getConfig()->baseUri); + } +} diff --git a/tests/Integration/Neo4jQueryApiIntegrationTempTest.php b/tests/Integration/Neo4jQueryApiIntegrationTempTest.php deleted file mode 100644 index 1b94ebe2..00000000 --- a/tests/Integration/Neo4jQueryApiIntegrationTempTest.php +++ /dev/null @@ -1,69 +0,0 @@ -api = $this->initializeApi(); - - $this->clearDatabase(); - $this->setupConstraints(); - $this->populateTestData(['bob1', 'alicy']); - - } - - - private function initializeApi(): Neo4jQueryAPI - { - return Neo4jQueryAPI::login( - getenv('NEO4J_ADDRESS'), - getenv('NEO4J_USERNAME'), - getenv('NEO4J_PASSWORD') - ); - } - - private function clearDatabase(): void - { - $this->api->run('MATCH (n) DETACH DELETE n', []); - } - - private function setupConstraints(): void - { - $this->api->run('CREATE CONSTRAINT IF NOT EXISTS FOR (p:Person) REQUIRE p.name IS UNIQUE', []); - } - - private function populateTestData(array $names): void - { - foreach ($names as $name) { - $this->api->run('CREATE (:Person {name: $name})', ['name' => $name]); - } - } - - - - - - public function testResultRowIntegration(): void - { - $resultSet = $this->api->run('MATCH (p:Person) RETURN p.name AS name, p.email AS email, p.age AS age, p AS person', []); - - foreach ($resultSet as $resultRow) { - - $name = $resultRow->get('name'); - $email = $resultRow->get('email'); - $age = $resultRow->get('age'); - - echo "Name: $name, Email: $email, Age: $age\n"; - - } - } - - -} \ No newline at end of file diff --git a/tests/Integration/Neo4jTransactionIntegrationTest.php b/tests/Integration/Neo4jTransactionIntegrationTest.php new file mode 100644 index 00000000..9033ba83 --- /dev/null +++ b/tests/Integration/Neo4jTransactionIntegrationTest.php @@ -0,0 +1,135 @@ +createQueryAPI(); + $this->api = $this->initializeApi(); + $this->clearDatabase(); + $this->populateTestData(); + } + + /** + * @throws Exception + */ + private function initializeApi(): Neo4jQueryAPI + { + $address = getenv('NEO4J_ADDRESS'); + + if ($address === false) { + throw new RuntimeException('NEO4J_ADDRESS is not set in the environment.'); + } + + return Neo4jQueryAPI::login($address, Authentication::fromEnvironment()); + } + + /** + * @throws GuzzleException + */ + private function clearDatabase(): void + { + $this->api->run('MATCH (n) DETACH DELETE n', []); + } + + /** + * @throws GuzzleException + */ + private function populateTestData(): void + { + $names = ['bob1', 'alice']; + foreach ($names as $name) { + $this->api->run('CREATE (:Person {name: $name})', ['name' => $name]); + } + } + + public function testTransactionCommit(): void + { + + $tsx = $this->api->beginTransaction(); + + $name = (string)mt_rand(1, 100000); + + $tsx->run("CREATE (x:Human {name: \$name})", ['name' => $name]); + + $results = $this->api->run("MATCH (x:Human {name: \$name}) RETURN x", ['name' => $name]); + $this->assertCount(0, $results); + + $results = $tsx->run("MATCH (x:Human {name: \$name}) RETURN x", ['name' => $name]); + $this->assertCount(1, $results); + + $tsx->commit(); + + $results = $this->api->run("MATCH (x:Human {name: \$name}) RETURN x", ['name' => $name]); + $this->assertCount(1, $results); // Updated to expect 1 result + } + + public function testTransactionRollback(): void + { + $tsx = $this->api->beginTransaction(); + + $name = 'rollback_' . ((string) mt_rand(1, 100000)); + $tsx->run("CREATE (x:Human {name: \$name})", ['name' => $name]); + $results = $tsx->run("MATCH (x:Human {name: \$name}) RETURN x", ['name' => $name]); + $this->assertCount(1, $results); + + $tsx->rollback(); + + $results = $this->api->run("MATCH (x:Human {name: \$name}) RETURN x", ['name' => $name]); + $this->assertCount(0, $results); + } + + public function testCreateNodeAndCommit(): void + { + $tsx = $this->api->beginTransaction(); + + $name = 'committed_' . (string) mt_rand(1, 100000); + $tsx->run("CREATE (x:Person {name: \$name})", ['name' => $name]); + + $results = $this->api->run("MATCH (x:Person {name: \$name}) RETURN x", ['name' => $name]); + $this->assertCount(0, $results); + + $tsx->commit(); + $results = $this->api->run("MATCH (x:Person {name: \$name}) RETURN x", ['name' => $name]); + $this->assertCount(1, $results); + } + + public function testCreateNodeAndRollback(): void + { + $tsx = $this->api->beginTransaction(); + + $name = 'rollback_' .(string) mt_rand(1, 100000); + $tsx->run("CREATE (x:Person {name: \$name})", ['name' => $name]); + + $results = $tsx->run("MATCH (x:Person {name: \$name}) RETURN x", ['name' => $name]); + $this->assertCount(1, $results); + + $results = $this->api->run("MATCH (x:Person {name: \$name}) RETURN x", ['name' => $name]); + $this->assertCount(0, $results); + + $tsx->rollback(); + $results = $this->api->run("MATCH (x:Person {name: \$name}) RETURN x", ['name' => $name]); + $this->assertCount(0, $results); + } + + + +} diff --git a/tests/Integration/ProfiledQueryPlanIntegrationTest.php b/tests/Integration/ProfiledQueryPlanIntegrationTest.php new file mode 100644 index 00000000..922e8f95 --- /dev/null +++ b/tests/Integration/ProfiledQueryPlanIntegrationTest.php @@ -0,0 +1,75 @@ +createQueryAPI(); + } + + public function testProfileExistence(): void + { + $query = "PROFILE MATCH (n:Person) RETURN n.name"; + $result = $this->api->run($query); + $this->assertNotNull($result->profiledQueryPlan, "Profiled query plan not found"); + } + + public function testProfileCreateQueryExistence(): void + { + $query = " + PROFILE UNWIND range(1, 100) AS i + CREATE (:Person { + name: 'Person' + toString(i), + id: i, + job: CASE + WHEN i % 2 = 0 THEN 'Engineer' + ELSE 'Artist' + END, + age: 1 + i - 1 + }); + "; + + $result = $this->api->run($query); + $this->assertNotNull($result->profiledQueryPlan, "Profiled query plan not found"); + } + + public function testProfileCreateMovieQueryExistence(): void + { + $query = " + PROFILE UNWIND range(1, 50) AS i + CREATE (:Movie { + year: 2000 + i, + genre: CASE + WHEN i % 2 = 0 THEN 'Action' + ELSE 'Comedy' + END, + title: 'Movie' + toString(i) + }); + "; + + $result = $this->api->run($query); + $this->assertNotNull($result->profiledQueryPlan, "Profiled query plan not found"); + } + + public function testProfileCreateFriendsQueryExistence(): void + { + $query = " + PROFILE MATCH (a:Person), (b:Person) + WHERE a.name = 'Alice' AND b.name = 'Bob' + CREATE (a)-[:FRIENDS_WITH]->(b); + "; + + $result = $this->api->run($query); + $this->assertNotNull($result->profiledQueryPlan, "Profiled query plan not found"); + } +} diff --git a/tests/Unit/AuthenticationTest.php b/tests/Unit/AuthenticationTest.php new file mode 100644 index 00000000..07fdab5e --- /dev/null +++ b/tests/Unit/AuthenticationTest.php @@ -0,0 +1,69 @@ +assertEquals("Bearer $mockToken", $auth->getHeader(), 'Bearer token mismatch.'); + $this->assertEquals('Bearer', $auth->getType(), 'Type should be Bearer.'); + } + + public function testBasicAuthentication(): void + { + $mockUsername = 'mockUser'; + $mockPassword = 'mockPass'; + + putenv('NEO4J_USERNAME=' . $mockUsername); + putenv('NEO4J_PASSWORD=' . $mockPassword); + + $username = getenv('NEO4J_USERNAME'); + $password = getenv('NEO4J_PASSWORD'); + + $username = is_string($username) ? $username : 'defaultUser'; + $password = is_string($password) ? $password : 'defaultPass'; + + $auth = Authentication::basic($username, $password); + + $expectedHeader = 'Basic ' . base64_encode("$mockUsername:$mockPassword"); + $this->assertEquals($expectedHeader, $auth->getHeader(), 'Basic authentication header mismatch.'); + $this->assertEquals('Basic', $auth->getType(), 'Type should be Basic.'); + + putenv('NEO4J_USERNAME'); + putenv('NEO4J_PASSWORD'); + } + + public function testFallbackToEnvironmentVariables(): void + { + putenv('NEO4J_USERNAME=mockEnvUser'); + putenv('NEO4J_PASSWORD=mockEnvPass'); + + $username = getenv('NEO4J_USERNAME'); + $password = getenv('NEO4J_PASSWORD'); + + $username = is_string($username) ? $username : 'fallbackUser'; + $password = is_string($password) ? $password : 'fallbackPass'; + + $auth = Authentication::basic($username, $password); + + $expectedHeader = 'Basic ' . base64_encode("mockEnvUser:mockEnvPass"); + $this->assertEquals($expectedHeader, $auth->getHeader(), 'Basic authentication with environment variables mismatch.'); + $this->assertEquals('Basic', $auth->getType(), 'Type should be Basic.'); + + putenv('NEO4J_USERNAME'); + putenv('NEO4J_PASSWORD'); + } + +} diff --git a/tests/Unit/Neo4jExceptionUnitTest.php b/tests/Unit/Neo4jExceptionUnitTest.php new file mode 100644 index 00000000..caa0bee3 --- /dev/null +++ b/tests/Unit/Neo4jExceptionUnitTest.php @@ -0,0 +1,125 @@ + 'Neo.ClientError.Statement.SyntaxError', + 'message' => 'Invalid syntax near ...', + 'statusCode' => 400 + ]; + + $exception = new Neo4jException($errorDetails); + + $this->assertSame('Neo.ClientError.Statement.SyntaxError', $exception->getErrorCode()); + $this->assertSame('ClientError', $exception->getType()); + $this->assertSame('Statement', $exception->getSubType()); + $this->assertSame('SyntaxError', $exception->getName()); + $this->assertSame('Invalid syntax near ...', $exception->getMessage()); + $this->assertSame(0, $exception->getCode()); + } + + /** + * Test the handling of missing error details. + */ + public function testConstructorWithMissingErrorDetails(): void + { + $exception = new Neo4jException([]); + + $this->assertSame('Neo.UnknownError', $exception->getErrorCode()); + $this->assertSame('UnknownError', $exception->getType()); + $this->assertNull($exception->getSubType()); + $this->assertNull($exception->getName()); + $this->assertSame('An unknown error occurred.', $exception->getMessage()); + $this->assertSame(0, $exception->getCode()); + } + + /** + * Test the `fromNeo4jResponse` static method with valid input. + */ + public function testFromNeo4jResponse(): void + { + $response = [ + 'errors' => [ + [ + 'code' => 'Neo.ClientError.Transaction.InvalidRequest', + 'message' => 'Transaction error occurred.', + 'statusCode' => 500 + ] + ] + ]; + + $exception = Neo4jException::fromNeo4jResponse($response); + + $this->assertSame('Neo.ClientError.Transaction.InvalidRequest', $exception->getErrorCode()); + $this->assertSame('ClientError', $exception->getType()); + $this->assertSame('Transaction', $exception->getSubType()); + $this->assertSame('InvalidRequest', $exception->getName()); + $this->assertSame('Transaction error occurred.', $exception->getMessage()); + $this->assertSame(0, $exception->getCode()); + } + + /** + * Test the `fromNeo4jResponse` static method with missing error details. + */ + public function testFromNeo4jResponseWithMissingDetails(): void + { + $response = ['errors' => []]; + + $exception = Neo4jException::fromNeo4jResponse($response); + + $this->assertSame('Neo.UnknownError', $exception->getErrorCode()); + $this->assertSame('UnknownError', $exception->getType()); + $this->assertNull($exception->getSubType()); + $this->assertNull($exception->getName()); + $this->assertSame(0, $exception->getCode()); + } + + /** + * Test the `fromNeo4jResponse` static method with null response. + */ + public function testFromNeo4jResponseWithNullResponse(): void + { + $response = ['errors' => null]; + + $exception = Neo4jException::fromNeo4jResponse($response); + + $this->assertSame('Neo.UnknownError', $exception->getErrorCode()); + $this->assertSame('UnknownError', $exception->getType()); + $this->assertNull($exception->getSubType(), "Expected 'getSubType()' to return null for null response"); + $this->assertNull($exception->getName(), "Expected 'getName()' to return null for null response"); + $this->assertSame(0, $exception->getCode()); + } + + /** + * Test exception chaining. + */ + public function testExceptionChaining(): void + { + $previousException = new Exception('Previous exception'); + + $errorDetails = [ + 'code' => 'Neo.ClientError.Security.Unauthorized', + 'message' => 'Authentication failed.', + 'statusCode' => 401 + ]; + + $exception = new Neo4jException($errorDetails, $errorDetails['statusCode'], $previousException); + + $this->assertSame($previousException, $exception->getPrevious()); + $this->assertSame('Unauthorized', $exception->getName()); + } +} diff --git a/tests/Unit/Neo4jQueryAPIUnitTest.php b/tests/Unit/Neo4jQueryAPIUnitTest.php index aad4e4f2..3b319c30 100644 --- a/tests/Unit/Neo4jQueryAPIUnitTest.php +++ b/tests/Unit/Neo4jQueryAPIUnitTest.php @@ -2,75 +2,141 @@ namespace Neo4j\QueryAPI\Tests\Unit; -use GuzzleHttp\Client; -use GuzzleHttp\Exception\GuzzleException; use GuzzleHttp\Handler\MockHandler; use GuzzleHttp\HandlerStack; -use GuzzleHttp\Psr7\Response; +use http\Client; +use Http\Discovery\Psr17FactoryDiscovery; use Neo4j\QueryAPI\Neo4jQueryAPI; +use Neo4j\QueryAPI\Neo4jRequestFactory; +use Neo4j\QueryAPI\Objects\Authentication; +use Neo4j\QueryAPI\Objects\Bookmarks; +use Neo4j\QueryAPI\OGM; +use Neo4j\QueryAPI\Results\ResultSet; +use PHPUnit\Framework\Attributes\DoesNotPerformAssertions; use PHPUnit\Framework\TestCase; - +use Neo4j\QueryAPI\ResponseParser; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\StreamInterface; +use RuntimeException; +use Neo4j\QueryAPI\Configuration; +use Nyholm\Psr7\Response; + +/** + * @api + */ class Neo4jQueryAPIUnitTest extends TestCase { + private OGM $ogm; + protected string $address; - protected string $username; - protected string $password; + protected ResponseParser $parser; + + #[\Override] protected function setUp(): void { parent::setUp(); - // Use environment variables from phpunit.xml - $this->address = getenv('NEO4J_ADDRESS'); - $this->username = getenv('NEO4J_USERNAME'); - $this->password = getenv('NEO4J_PASSWORD'); + $address = getenv('NEO4J_ADDRESS'); + $this->address = is_string($address) ? $address : ''; + + $this->ogm = new OGM(); + $this->parser = new ResponseParser($this->ogm); } public function testCorrectClientSetup(): void { - $neo4jQueryAPI = Neo4jQueryAPI::login($this->address, $this->username, $this->password); - + $neo4jQueryAPI = Neo4jQueryAPI::login($this->address, Authentication::fromEnvironment()); $this->assertInstanceOf(Neo4jQueryAPI::class, $neo4jQueryAPI); + } - // Use Reflection to get the client property - $clientReflection = new \ReflectionClass(Neo4jQueryAPI::class); - $clientProperty = $clientReflection->getProperty('client'); - // Make private property accessible - $client = $clientProperty->getValue($neo4jQueryAPI); + #[DoesNotPerformAssertions] + public function testRunSuccess(): void + { + $mockHandler = new MockHandler([ + new Response(200, [], '{"data": {"fields": ["hello"], "values": [[{"$type": "String", "_value": "world"}]]}}') + ]); - $this->assertInstanceOf(Client::class, $client); + $handler = HandlerStack::create($mockHandler); + $client = new \GuzzleHttp\Client(['handler' => $handler]); + + $neo4jQueryAPI = new Neo4jQueryAPI( + $client, + $this->parser, + new Neo4jRequestFactory( + Psr17FactoryDiscovery::findRequestFactory(), + Psr17FactoryDiscovery::findStreamFactory(), + new Configuration($this->address), + Authentication::fromEnvironment() + ), + new Configuration($this->address) + ); + + $neo4jQueryAPI->run('MATCH (n:Person) RETURN n LIMIT 5'); + } - $config = $client->getConfig(); - $this->assertEquals(rtrim($this->address, '/'), $config['base_uri']); - $this->assertEquals('Basic ' . base64_encode("{$this->username}:{$this->password}"), $config['headers']['Authorization']); - $this->assertEquals('application/json', $config['headers']['Content-Type']); + public function testParseValidResponse(): void + { + $mockStream = $this->createMock(StreamInterface::class); + $mockStream->method('getContents')->willReturn(json_encode([ + 'data' => ['fields' => ['name'], 'values' => [['Alice'], ['Bob']]], + 'counters' => ['nodesCreated' => 2], + 'bookmarks' => ['bm1'], + 'accessMode' => 'WRITE' + ])); + + $mockResponse = $this->createMock(ResponseInterface::class); + $mockResponse->method('getBody')->willReturn($mockStream); + + $result = $this->parser->parseRunQueryResponse($mockResponse); + $this->assertInstanceOf(ResultSet::class, $result); + $this->assertCount(2, $result->getIterator()); } - /** - * @throws GuzzleException - */ - public function testRunSuccess(): void + public function testParseInvalidResponse(): void { - // Mock a successful response from Neo4j server - $mock = new MockHandler([ - new Response(200, ['X-Foo' => 'Bar'], '{"hello":"world"}'), - ]); + $this->expectException(RuntimeException::class); + $mockStream = $this->createMock(StreamInterface::class); + $mockStream->method('getContents')->willReturn(json_encode(['data' => null])); - $handlerStack = HandlerStack::create($mock); - $client = new Client(['handler' => $handlerStack]); + $mockResponse = $this->createMock(ResponseInterface::class); + $mockResponse->method('getBody')->willReturn($mockStream); - $neo4jQueryAPI = new Neo4jQueryAPI($client); + $this->parser->parseRunQueryResponse($mockResponse); + } - // Use a sample Cypher query to run on the Neo4j server - $cypherQuery = 'MATCH (n:Person) RETURN n LIMIT 5'; + public function testGetAccessMode(): void + { + $mockStream = $this->createMock(StreamInterface::class); + $mockStream->method('getContents')->willReturn(json_encode([ + 'data' => ['fields' => [], 'values' => []], + 'accessMode' => 'WRITE' + ])); - // Execute the query and capture the result - $result = $neo4jQueryAPI->run($cypherQuery, []); + $mockResponse = $this->createMock(ResponseInterface::class); + $mockResponse->method('getBody')->willReturn($mockStream); - // Output for debugging - print_r($result); + $result = $this->parser->parseRunQueryResponse($mockResponse); + $this->assertInstanceOf(ResultSet::class, $result); + } - // Verify the response matches the expected output - $this->assertEquals(['hello' => 'world'], $result); + public function testParseBookmarks(): void + { + $mockStream = $this->createMock(StreamInterface::class); + $mockStream->method('getContents')->willReturn(json_encode([ + 'data' => ['fields' => [], 'values' => []], + 'bookmarks' => ['bm1', 'bm2', 'bm3'] + ])); + + $mockResponse = $this->createMock(ResponseInterface::class); + $mockResponse->method('getBody')->willReturn($mockStream); + + $result = $this->parser->parseRunQueryResponse($mockResponse); + $this->assertInstanceOf(ResultSet::class, $result); + + $bookmarks = $result->getBookmarks(); + $this->assertInstanceOf(Bookmarks::class, $bookmarks); + $this->assertCount(3, $bookmarks->getBookmarks()); + $this->assertEquals(['bm1', 'bm2', 'bm3'], $bookmarks->getBookmarks()); } } diff --git a/tests/Unit/Neo4jRequestFactoryTest.php b/tests/Unit/Neo4jRequestFactoryTest.php new file mode 100644 index 00000000..e3dc22b1 --- /dev/null +++ b/tests/Unit/Neo4jRequestFactoryTest.php @@ -0,0 +1,259 @@ +psr17Factory = $this->createMock(RequestFactoryInterface::class); + $this->streamFactory = $this->createMock(StreamFactoryInterface::class); + + $address = getenv('NEO4J_ADDRESS'); + $this->address = is_string($address) ? $address : ''; + + $auth = Authentication::fromEnvironment(); + $this->authHeader = $auth->getHeader(); + } + + /** + * Test for buildRunQueryRequest + */ + public function testBuildRunQueryRequest(): void + { + $cypher = 'MATCH (n) RETURN n'; + $parameters = ['param1' => 'value1']; + $database = 'neo4j'; + + $payload = json_encode([ + 'statement' => $cypher, + 'parameters' => $parameters, + 'includeCounters' => true, + ]); + $uri = "{$this->address}/db/{$database}/query/v2"; + + $mockRequest = new Request('POST', $uri); + $mockStream = Utils::streamFor($payload); + + $this->streamFactory->method('createStream') + ->willReturn($mockStream); + + $this->psr17Factory->method('createRequest') + ->willReturn($mockRequest); + + $factory = new Neo4jRequestFactory( + $this->psr17Factory, + $this->streamFactory, + new Configuration($this->address), + Authentication::fromEnvironment(), + ); + $request = $factory->buildRunQueryRequest($cypher, $parameters); + + $this->assertEquals('POST', $request->getMethod()); + $this->assertEquals($uri, (string) $request->getUri()); + $payload = json_encode([]); + if ($payload === false) { + throw new RuntimeException('JSON encoding failed: ' . json_last_error_msg()); + } + + } + + /** + * Test for buildBeginTransactionRequest + */ + public function testBuildBeginTransactionRequest(): void + { + $database = 'neo4j'; + $uri = "{$this->address}/db/{$database}/query/v2/tx"; + + $mockRequest = new Request('POST', $uri); + $mockStream = Utils::streamFor(''); + + $this->streamFactory->method('createStream') + ->willReturn($mockStream); + + $this->psr17Factory->method('createRequest') + ->willReturn($mockRequest); + + $factory = new Neo4jRequestFactory( + $this->psr17Factory, + $this->streamFactory, + new Configuration($this->address), + Authentication::fromEnvironment(), + ); + $request = $factory->buildBeginTransactionRequest(); + + $this->assertEquals('POST', $request->getMethod()); + $this->assertEquals($uri, (string) $request->getUri()); + } + + /** + * Test for buildCommitRequest + */ + public function testBuildCommitRequest(): void + { + $database = 'neo4j'; + $transactionId = '12345'; + $uri = "{$this->address}/db/{$database}/query/v2/tx/{$transactionId}/commit"; + + $mockRequest = new Request('POST', $uri); + $mockStream = Utils::streamFor(''); + + $this->streamFactory->method('createStream') + ->willReturn($mockStream); + + $this->psr17Factory->method('createRequest') + ->willReturn($mockRequest); + + $factory = new Neo4jRequestFactory( + $this->psr17Factory, + $this->streamFactory, + new Configuration($this->address), + Authentication::fromEnvironment(), + ); + $request = $factory->buildCommitRequest($database, $transactionId); + + $this->assertEquals('POST', $request->getMethod()); + $this->assertEquals($uri, (string) $request->getUri()); + } + + /** + * Test for buildRollbackRequest + */ + public function testBuildRollbackRequest(): void + { + $database = 'neo4j'; + $transactionId = '12345'; + $uri = "{$this->address}/db/{$database}/query/v2/tx/{$transactionId}/rollback"; + + $mockRequest = new Request('POST', $uri); + $mockStream = Utils::streamFor(''); + + $this->streamFactory->method('createStream') + ->willReturn($mockStream); + + $this->psr17Factory->method('createRequest') + ->willReturn($mockRequest); + + $factory = new Neo4jRequestFactory( + $this->psr17Factory, + $this->streamFactory, + new Configuration($this->address), + Authentication::fromEnvironment(), + ); + $request = $factory->buildRollbackRequest($database, $transactionId); + + $this->assertEquals('DELETE', $request->getMethod()); + $this->assertEquals($uri, (string) $request->getUri()); + } + + /** + * Test for createRequest method with headers and body + */ + public function testCreateRequestWithHeadersAndBody(): void + { + $cypher = 'MATCH (n) RETURN n'; + $parameters = ['param1' => 'value1']; + $database = 'neo4j'; + $uri = "{$this->address}/db/{$database}/query/v2"; + + $payload = json_encode([ + 'statement' => $cypher, + 'parameters' => $parameters, + 'includeCounters' => true, + ]); + + $mockStream = Utils::streamFor($payload); + $this->streamFactory->method('createStream') + ->willReturn($mockStream); + + $mockRequest = new Request('POST', $uri); + $this->psr17Factory->method('createRequest') + ->willReturn($mockRequest); + + $factory = new Neo4jRequestFactory( + $this->psr17Factory, + $this->streamFactory, + new Configuration($this->address), + Authentication::fromEnvironment(), + ); + + $request = $factory->buildRunQueryRequest($cypher, $parameters); + + $this->assertEquals('application/json', $request->getHeaderLine('Content-Type')); + $this->assertEquals('application/vnd.neo4j.query', $request->getHeaderLine('Accept')); + $this->assertEquals($this->authHeader, $request->getHeaderLine('Authorization')); + $payload = json_encode([]); + if ($payload === false) { + throw new RuntimeException('JSON encoding failed: ' . json_last_error_msg()); + } + + } + + /** + * Test createRequest without Authorization header + */ + public function testCreateRequestWithoutAuthorizationHeader(): void + { + $cypher = 'MATCH (n) RETURN n'; + $parameters = ['param1' => 'value1']; + $database = 'neo4j'; + $uri = "{$this->address}/db/{$database}/query/v2"; + + $payload = json_encode([ + 'statement' => $cypher, + 'parameters' => $parameters, + 'includeCounters' => true, + ]); + + $mockStream = Utils::streamFor($payload); + $this->streamFactory->method('createStream') + ->willReturn($mockStream); + + $mockRequest = new Request('POST', $uri); + $this->psr17Factory->method('createRequest') + ->willReturn($mockRequest); + + $factory = new Neo4jRequestFactory( + $this->psr17Factory, + $this->streamFactory, + new Configuration($this->address), + Authentication::noAuth(), + ); + + $request = $factory->buildRunQueryRequest($cypher, $parameters); + $this->assertEquals('application/json', $request->getHeaderLine('Content-Type')); + $this->assertEquals('application/vnd.neo4j.query', $request->getHeaderLine('Accept')); + $this->assertEmpty($request->getHeaderLine('Authorization')); + $payload = json_encode([]); + if ($payload === false) { + throw new RuntimeException('JSON encoding failed: ' . json_last_error_msg()); + } + } +} diff --git a/tests/Unit/ResultRowTest.php b/tests/Unit/ResultRowTest.php index 725c1beb..0fc9ade5 100644 --- a/tests/Unit/ResultRowTest.php +++ b/tests/Unit/ResultRowTest.php @@ -1,6 +1,5 @@ assertEquals('Bob', $row['name']); $this->assertEquals(20, $row['age']); } - + /** @psalm-suppress UnusedVariable */ public function testArrayAccessInvalidKey(): void { $row = new ResultRow([ @@ -42,7 +44,7 @@ public function testArrayAccessSetThrowsException(): void ]); $this->expectException(BadMethodCallException::class); - $this->expectExceptionMessage("You cant set the value of column age."); + $this->expectExceptionMessage("You can't set the value of column age."); $row['age'] = 30; } @@ -54,7 +56,7 @@ public function testArrayAccessUnsetThrowsException(): void ]); $this->expectException(BadMethodCallException::class); - $this->expectExceptionMessage("You cant Unset name."); + $this->expectExceptionMessage("You can't Unset name."); unset($row['name']); } diff --git a/tests/Unit/ResultSetTest.php b/tests/Unit/ResultSetTest.php deleted file mode 100644 index 266585c1..00000000 --- a/tests/Unit/ResultSetTest.php +++ /dev/null @@ -1,119 +0,0 @@ -expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('The keys array cannot be empty.'); - - $mockOgm = $this->createMock(OGM::class); - new ResultSet([], [], $mockOgm); - } - - public function testValidResultSet(): void - { - - $mockOgm = $this->createMock(OGM::class); - $mockOgm->method('map')->willReturnCallback(fn($value) => $value['_value'] ?? null); - - // Create ResultSet - $resultSet = new ResultSet( - ['name', 'age', 'email'], - [ - [ - ['$type' => 'String', '_value' => 'Bob'], - ['$type' => 'Integer', '_value' => 20], - ['$type' => 'String', '_value' => 'bob@example.com'], - ] - ], - $mockOgm - ); - - - $rows = iterator_to_array($resultSet); - $this->assertCount(1, $rows); - - - $this->assertInstanceOf(ResultRow::class, $rows[0]); - $this->assertEquals('Bob', $rows[0]->get('name')); - $this->assertEquals(20, $rows[0]->get('age')); - $this->assertEquals('bob@example.com', $rows[0]->get('email')); - } - - public function testInvalidColumnAccess(): void - { - // Mock OGM - $mockOgm = $this->createMock(OGM::class); - $mockOgm->method('map')->willReturnCallback(fn($value) => $value['_value'] ?? null); - - // Create ResultSet - $resultSet = new ResultSet( - ['name', 'age', 'email'], - [ - [ - ['$type' => 'String', '_value' => 'Bob'], - ['$type' => 'Integer', '_value' => 20], - ['$type' => 'String', '_value' => 'bob@example.com'], - ] - ], - $mockOgm - ); - - $rows = iterator_to_array($resultSet); - - - $this->expectException(\OutOfBoundsException::class); //this exception for TICA - $this->expectExceptionMessage('Column phone not found.'); - $rows[0]->get('phone'); - } - - - public function testMultipleRows(): void - { - $mockOgm = $this->createMock(OGM::class); - $mockOgm->method('map')->willReturnCallback(fn($value) => $value['_value'] ?? null); - - // Create ResultSet - $resultSet = new ResultSet( - ['name', 'age', 'email'], - [ - [ - ['$type' => 'String', '_value' => 'Bob'], - ['$type' => 'Integer', '_value' => 20], - ['$type' => 'String', '_value' => 'bob@example.com'], - ], - [ - ['$type' => 'String', '_value' => 'Sebastian Bergmann'], - ['$type' => 'Integer', '_value' => 41], - ['$type' => 'String', '_value' => 'SebastianBergmann@example.com'] - ] - ], - $mockOgm - ); - $rows = iterator_to_array($resultSet); - $this->assertCount(2, $rows); - - - $this->assertInstanceOf(ResultRow::class, $rows[0]); - $this->assertEquals('Bob', $rows[0]->get('name')); - $this->assertEquals(20, $rows[0]->get('age')); - $this->assertEquals('bob@example.com', $rows[0]->get('email')); - - - $this->assertInstanceOf(ResultRow::class, $rows[1]); - $this->assertEquals('Sebastian Bergmann', $rows[1]->get('name')); - $this->assertEquals(41, $rows[1]->get('age')); - $this->assertEquals('SebastianBergmann@example.com', $rows[1]->get('email')); - } - -} \ No newline at end of file diff --git a/tests/Unit/TransactionUnitTest.php b/tests/Unit/TransactionUnitTest.php new file mode 100644 index 00000000..5fac4a1c --- /dev/null +++ b/tests/Unit/TransactionUnitTest.php @@ -0,0 +1,104 @@ +client = $this->createMock(ClientInterface::class); + $this->requestFactory = $this->createMock(Neo4jRequestFactory::class); + $this->responseParser = $this->createMock(ResponseParser::class); + + $this->transaction = new Transaction( + $this->client, + $this->responseParser, + $this->requestFactory, + $this->clusterAffinity, + $this->transactionId + ); + } + + public function testRunCallsBuildTransactionRunRequest(): void + { + $query = "CREATE (:Person {name: \$name})"; + $parameters = ['name' => 'Alice']; + + $mockRequest = $this->createMock(RequestInterface::class); + $mockResponse = $this->createMock(ResponseInterface::class); + $mockResultSet = $this->createMock(ResultSet::class); + + $this->requestFactory->expects($this->once()) + ->method('buildTransactionRunRequest') + ->with($query, $parameters, $this->transactionId, $this->clusterAffinity) + ->willReturn($mockRequest); + + $this->client->expects($this->once()) + ->method('sendRequest') + ->with($mockRequest) + ->willReturn($mockResponse); + + $this->responseParser->expects($this->once()) + ->method('parseRunQueryResponse') + ->with($mockResponse) + ->willReturn($mockResultSet); + + $result = $this->transaction->run($query, $parameters); + + $this->assertSame($mockResultSet, $result); + } + + public function testCommitCallsBuildCommitRequest(): void + { + $mockRequest = $this->createMock(RequestInterface::class); + + $this->requestFactory->expects($this->once()) + ->method('buildCommitRequest') + ->with($this->transactionId, $this->clusterAffinity) + ->willReturn($mockRequest); + + $this->client->expects($this->once()) + ->method('sendRequest') + ->with($mockRequest); + + $this->transaction->commit(); + } + + public function testRollbackCallsBuildRollbackRequest(): void + { + $mockRequest = $this->createMock(RequestInterface::class); + + $this->requestFactory->expects($this->once()) + ->method('buildRollbackRequest') + ->with($this->transactionId, $this->clusterAffinity) + ->willReturn($mockRequest); + + $this->client->expects($this->once()) + ->method('sendRequest') + ->with($mockRequest); + + $this->transaction->rollback(); + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 00000000..a8977bee --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,7 @@ +(b), (b)-[anon_1:KNOWS]->(a) | 2 | 0 | 0 | | 0/0 | 0.000 | |\n| | +----+--------------------------------------------------+----------------+-------+---------+----------------+------------------------+-----------+ |\n| +Filter | 3 | cache[a.id] < cache[b.id] | 2 | 0 | 0 | | 0/0 | 0.000 | In Pipeline 3 |\n| | +----+--------------------------------------------------+----------------+-------+---------+----------------+------------------------+-----------+---------------------+\n| +Apply | 4 | | 8 | 0 | 0 | | 0/0 | | |\n| |\\ +----+--------------------------------------------------+----------------+-------+---------+----------------+------------------------+-----------+---------------------+\n| | +CartesianProduct | 5 | | 8 | 0 | 0 | 1392 | | | In Pipeline 3 |\n| | |\\ +----+--------------------------------------------------+----------------+-------+---------+----------------+------------------------+-----------+---------------------+\n| | | +Filter | 6 | cache[b.id] = j | 50 | 0 | 0 | | | | |\n| | | | +----+--------------------------------------------------+----------------+-------+---------+----------------+ | | |\n| | | +NodeByLabelScan | 7 | b:Person | 1000 | 0 | 0 | 256 | 0/0 | 0.000 | Fused in Pipeline 2 |\n| | | +----+--------------------------------------------------+----------------+-------+---------+----------------+------------------------+-----------+---------------------+\n| | +Filter | 8 | rand() < \$autodouble_4 AND cache[a.id] = i | 15 | 0 | 2027 | | | | |\n| | | +----+--------------------------------------------------+----------------+-------+---------+----------------+ | | |\n| | +NodeByLabelScan | 9 | a:Person | 1000 | 20000 | 30000 | 8488 | 10002/0 | 6.158 | Fused in Pipeline 1 |\n| | +----+--------------------------------------------------+----------------+-------+---------+----------------+------------------------+-----------+---------------------+\n| +Unwind | 10 | range(\$autoint_2, \$autoint_3) AS j | 100 | 10000 | 0 | | | | |\n| | +----+--------------------------------------------------+----------------+-------+---------+----------------+ | | |\n| +Unwind | 11 | range(\$autoint_0, \$autoint_1) AS i | 10 | 100 | 0 | | 0/0 | 0.000 | Fused in Pipeline 0 |\n+----------------------+----+--------------------------------------------------+----------------+-------+---------+----------------+------------------------+-----------+---------------------+\n\nTotal database accesses: 32027, total allocated memory: 10624\n", + runtime: "PIPELINED", + time: 0, + pageCacheMisses: 0, + pageCacheHits: 0, + runtimeImpl: "PIPELINED", + version: 5, + dbHits: 0, + batchSize: 128, + details: "", + plannerVersion: (string) 5.26, + pipelineInfo: "In Pipeline 3", + runtimeVersion: 5.26, + id: 0, + estimatedRows: 2.25, + planner: 'COST', + rows: 0, + ), + children: [ + // CHILD #1: EmptyResult@neo4j + new ProfiledQueryPlan( + dbHits: 0, + records: 0, + hasPageCacheStats: false, + pageCacheHits: 0, + pageCacheMisses: 0, + pageCacheHitRatio: 0.0, + time: 0, + operatorType: "EmptyResult@neo4j", + arguments: new ProfiledQueryPlanArguments( + plannerImpl: null, + memory: null, + stringRepresentation: null, + runtime: null, + time: 0, + pageCacheMisses: 0, + pageCacheHits: 0, + runtimeImpl: null, + version: null, + dbHits: 0, + batchSize: null, + details: null, + // Using empty strings or nulls for fields not strictly needed + plannerVersion: null, + pipelineInfo: 'In Pipeline 3', + runtimeVersion: null, + id: 1, + estimatedRows: 2.25, + rows: 0 + ), + children: [ + // CHILD #1.1: Create@neo4j + new ProfiledQueryPlan( + dbHits: 0, + records: 0, + hasPageCacheStats: false, + pageCacheHits: 0, + pageCacheMisses: 0, + pageCacheHitRatio: 0.0, + time: 0, + operatorType: "Create@neo4j", + arguments: new ProfiledQueryPlanArguments( + plannerImpl: null, + memory: null, + stringRepresentation: null, + runtime: null, + time: 0, + pageCacheMisses: null, + pageCacheHits: 0, + runtimeImpl: null, + version: null, + dbHits: 0, + batchSize: 0, + details: "(a)-[anon_0:KNOWS]->(b), (b)-[anon_1:KNOWS]->(a)", + plannerVersion: "", + pipelineInfo: "In Pipeline 3", + runtimeVersion: null, + id: 2, + estimatedRows: 2.25, + rows: 0 + ), + children: [ + // CHILD #1.1.1: Filter@neo4j (id=3) + new ProfiledQueryPlan( + dbHits: 0, + records: 0, + hasPageCacheStats: false, + pageCacheHits: 0, + pageCacheMisses: 0, + pageCacheHitRatio: 0.0, + time: 0, + operatorType: "Filter@neo4j", + arguments: new ProfiledQueryPlanArguments( + plannerImpl: null, + memory: null, + stringRepresentation: null, + runtime: null, + time: 0, + pageCacheMisses: null, + pageCacheHits: 0, + runtimeImpl: null, + version: null, + dbHits: 0, + batchSize: 0, + details: "cache[a.id] < cache[b.id]", + plannerVersion: "", + pipelineInfo: "In Pipeline 3", + runtimeVersion: null, + id: 3, + estimatedRows: 2.25, + rows: 0 + ), + children: [ + // CHILD #1.1.1.1: Apply@neo4j (id=4) + new ProfiledQueryPlan( + dbHits: 0, + records: 0, + hasPageCacheStats: false, + pageCacheHits: 0, + pageCacheMisses: 0, + pageCacheHitRatio: 0.0, + time: 0, + operatorType: "Apply@neo4j", + arguments: new ProfiledQueryPlanArguments( + plannerImpl: null, + memory: null, + stringRepresentation: null, + runtime: null, + time: null, + pageCacheMisses: null, + pageCacheHits: 0, + runtimeImpl: null, + version: null, + dbHits: 0, + batchSize: 0, + details: "", + plannerVersion: "", + pipelineInfo: "", + runtimeVersion: null, + id: 4, + estimatedRows: 7.5, + rows: 0 + ), + children: [ + // CHILD #1.1.1.1.1: Unwind@neo4j (id=10) + new ProfiledQueryPlan( + dbHits: 0, + records: 10000, + hasPageCacheStats: false, + pageCacheHits: 0, + pageCacheMisses: 0, + pageCacheHitRatio: 0.0, + time: 0, + operatorType: "Unwind@neo4j", + arguments: new ProfiledQueryPlanArguments( + plannerImpl: null, + memory: null, + stringRepresentation: null, + runtime: null, + time: null, + pageCacheMisses: null, + pageCacheHits: 0, + runtimeImpl: null, + version: null, + dbHits: 0, + batchSize: 0, + details: "range(\$autoint_2, \$autoint_3) AS j", + plannerVersion: "", + pipelineInfo: "Fused in Pipeline 0", + runtimeVersion: null, + id: 10, + estimatedRows: 100.0, + rows: 10000 + ), + children: [ + // The second Unwind@neo4j (id=11) + new ProfiledQueryPlan( + dbHits: 0, + records: 100, + hasPageCacheStats: false, + pageCacheHits: 0, + pageCacheMisses: 0, + pageCacheHitRatio: 0.0, + time: 0, + operatorType: "Unwind@neo4j", + arguments: new ProfiledQueryPlanArguments( + plannerImpl: null, + memory: null, + stringRepresentation: null, + runtime: null, + time: 0, + pageCacheMisses: null, + pageCacheHits: 0, + runtimeImpl: null, + version: null, + dbHits: 0, + batchSize: 0, + details: "range(\$autoint_0, \$autoint_1) AS i", + plannerVersion: "", + pipelineInfo: "Fused in Pipeline 0", + runtimeVersion: null, + id: 11, + estimatedRows: 10.0, + rows: 100 + ), + identifiers: ["i"] + ) + ], + identifiers: ["i", "j"] + ), + // CHILD #1.1.1.1.2: CartesianProduct@neo4j (id=5) + new ProfiledQueryPlan( + dbHits: 0, + records: 0, + hasPageCacheStats: false, + pageCacheHits: 0, + pageCacheMisses: 0, + pageCacheHitRatio: 0.0, + time: 0, + operatorType: "CartesianProduct@neo4j", + arguments: new ProfiledQueryPlanArguments( + plannerImpl: null, + memory: 1392, + stringRepresentation: null, + runtime: null, + time: null, + pageCacheMisses: null, + pageCacheHits: 0, + runtimeImpl: null, + version: null, + dbHits: 0, + batchSize: 0, + details: "", + plannerVersion: "", + pipelineInfo: "In Pipeline 3", + runtimeVersion: null, + id: 5, + estimatedRows: 7.5, + rows: 0 + ), + children: [ + // CHILD #1.1.1.1.2.1: Filter@neo4j (id=8) + new ProfiledQueryPlan( + dbHits: 2027, + records: 0, + hasPageCacheStats: false, + pageCacheHits: 0, + pageCacheMisses: 0, + pageCacheHitRatio: 0.0, + time: 0, + operatorType: "Filter@neo4j", + arguments: new ProfiledQueryPlanArguments( + plannerImpl: null, + memory: null, + stringRepresentation: null, + runtime: null, + time: null, + pageCacheMisses: null, + pageCacheHits: 0, + runtimeImpl: null, + version: null, + dbHits: 2027, + batchSize: 0, + details: "rand() < \$autodouble_4 AND cache[a.id] = i", + plannerVersion: "", + pipelineInfo: "Fused in Pipeline 1", + runtimeVersion: null, + id: 8, + estimatedRows: 15.0, + rows: 0 + ), + children: [ + // NodeByLabelScan@neo4j (id=9) + new ProfiledQueryPlan( + dbHits: 30000, + records: 20000, + hasPageCacheStats: true, + pageCacheHits: 10002, + pageCacheMisses: 0, + pageCacheHitRatio: 1.0, + time: 6157670, + operatorType: "NodeByLabelScan@neo4j", + arguments: new ProfiledQueryPlanArguments( + plannerImpl: null, + memory: 8488, + stringRepresentation: null, + runtime: null, + time: 6157670, + pageCacheMisses: 0, + pageCacheHits: 10002, + runtimeImpl: null, + version: null, + dbHits: 30000, + batchSize: 0, + details: "a:Person", + plannerVersion: "", + pipelineInfo: "Fused in Pipeline 1", + runtimeVersion: null, + id: 9, + estimatedRows: 1000.0, + rows: 20000, + ), + identifiers: ["i", "j", "a"] + ) + ], + identifiers: ["i", "j", "a"] + ), + // CHILD #1.1.1.1.2.2: Filter@neo4j (id=6) + new ProfiledQueryPlan( + dbHits: 0, + records: 0, + hasPageCacheStats: false, + pageCacheHits: 0, + pageCacheMisses: 0, + pageCacheHitRatio: 0.0, + time: 0, + operatorType: "Filter@neo4j", + arguments: new ProfiledQueryPlanArguments( + plannerImpl: null, + memory: null, + stringRepresentation: null, + runtime: null, + time: null, + pageCacheMisses: null, + pageCacheHits: 0, + runtimeImpl: null, + version: null, + dbHits: 0, + batchSize: 0, + details: "cache[b.id] = j", + plannerVersion: "", + pipelineInfo: "Fused in Pipeline 2", + runtimeVersion: null, + id: 6, + estimatedRows: 50.0, + rows: 0 + ), + children: [ + // NodeByLabelScan@neo4j (id=7) + new ProfiledQueryPlan( + dbHits: 0, + records: 0, + hasPageCacheStats: false, + pageCacheHits: 0, + pageCacheMisses: 0, + pageCacheHitRatio: 0.0, + time: 0, + operatorType: "NodeByLabelScan@neo4j", + arguments: new ProfiledQueryPlanArguments( + plannerImpl: null, + memory: 256, + stringRepresentation: null, + runtime: null, + time: 0, + pageCacheMisses: null, + pageCacheHits: 0, + runtimeImpl: null, + version: null, + dbHits: 0, + batchSize: 0, + details: "b:Person", + plannerVersion: "", + pipelineInfo: "Fused in Pipeline 2", + runtimeVersion: null, + id: 7, + estimatedRows: 1000.0, + rows: 0 + ), + identifiers: ["i", "j", "b"] + ) + ], + identifiers: ["i", "j", "b"] + ) + ], + identifiers: ["i", "j", "a", "b"] + ) + ], + identifiers: ["i", "j", "a", "b"] + ) + ], + identifiers: ["i", "j", "a", "b"] + ) + ], + identifiers: ["j", "a", "i", "b", "anon_0", "anon_1"] + ) + ], + identifiers: ["j", "a", "i", "b", "anon_0", "anon_1"] + ) + ], + identifiers: ["j", "a", "i", "b", "anon_0", "anon_1"] + ), + accessMode: AccessMode::WRITE +); diff --git a/tests/resources/responses/complex-query-profile.json b/tests/resources/responses/complex-query-profile.json new file mode 100644 index 00000000..d4e02145 --- /dev/null +++ b/tests/resources/responses/complex-query-profile.json @@ -0,0 +1,528 @@ +{ + "data" : { + "fields" : [ ], + "values" : [ ] + }, + "notifications" : [ { + "code" : "Neo.ClientNotification.Statement.CartesianProduct", + "description" : "If a part of a query contains multiple disconnected patterns, this will build a cartesian product between all those parts. This may produce a large amount of data and slow down query processing. While occasionally intended, it may often be possible to reformulate the query that avoids the use of this cross product, perhaps by adding a relationship between the different parts or by using OPTIONAL MATCH (identifier is: (b))", + "severity" : "INFORMATION", + "title" : "This query builds a cartesian product between disconnected patterns.", + "position" : { + "offset" : 73, + "line" : 4, + "column" : 5 + }, + "category" : "PERFORMANCE" + } ], + "counters" : { + "containsUpdates" : false, + "nodesCreated" : 0, + "nodesDeleted" : 0, + "propertiesSet" : 0, + "relationshipsCreated" : 0, + "relationshipsDeleted" : 0, + "labelsAdded" : 0, + "labelsRemoved" : 0, + "indexesAdded" : 0, + "indexesRemoved" : 0, + "constraintsAdded" : 0, + "constraintsRemoved" : 0, + "containsSystemUpdates" : false, + "systemUpdates" : 0 + }, + "profiledQueryPlan" : { + "dbHits" : 0, + "records" : 0, + "hasPageCacheStats" : false, + "pageCacheHits" : 0, + "pageCacheMisses" : 0, + "pageCacheHitRatio" : 0.0, + "time" : 0, + "operatorType" : "ProduceResults@neo4j", + "arguments" : { + "GlobalMemory" : 10624, + "planner-impl" : "IDP", + "string-representation" : "Cypher 5\n\nPlanner COST\n\nRuntime PIPELINED\n\nRuntime version 5.26\n\nBatch size 128\n\n+----------------------+----+--------------------------------------------------+----------------+-------+---------+----------------+------------------------+-----------+---------------------+\n| Operator | Id | Details | Estimated Rows | Rows | DB Hits | Memory (Bytes) | Page Cache Hits/Misses | Time (ms) | Pipeline |\n+----------------------+----+--------------------------------------------------+----------------+-------+---------+----------------+------------------------+-----------+---------------------+\n| +ProduceResults | 0 | | 2 | 0 | 0 | | 0/0 | 0.000 | |\n| | +----+--------------------------------------------------+----------------+-------+---------+----------------+------------------------+-----------+ |\n| +EmptyResult | 1 | | 2 | 0 | 0 | | 0/0 | 0.000 | |\n| | +----+--------------------------------------------------+----------------+-------+---------+----------------+------------------------+-----------+ |\n| +Create | 2 | (a)-[anon_0:KNOWS]->(b), (b)-[anon_1:KNOWS]->(a) | 2 | 0 | 0 | | 0/0 | 0.000 | |\n| | +----+--------------------------------------------------+----------------+-------+---------+----------------+------------------------+-----------+ |\n| +Filter | 3 | cache[a.id] < cache[b.id] | 2 | 0 | 0 | | 0/0 | 0.000 | In Pipeline 3 |\n| | +----+--------------------------------------------------+----------------+-------+---------+----------------+------------------------+-----------+---------------------+\n| +Apply | 4 | | 8 | 0 | 0 | | 0/0 | | |\n| |\\ +----+--------------------------------------------------+----------------+-------+---------+----------------+------------------------+-----------+---------------------+\n| | +CartesianProduct | 5 | | 8 | 0 | 0 | 1392 | | | In Pipeline 3 |\n| | |\\ +----+--------------------------------------------------+----------------+-------+---------+----------------+------------------------+-----------+---------------------+\n| | | +Filter | 6 | cache[b.id] = j | 50 | 0 | 0 | | | | |\n| | | | +----+--------------------------------------------------+----------------+-------+---------+----------------+ | | |\n| | | +NodeByLabelScan | 7 | b:Person | 1000 | 0 | 0 | 256 | 0/0 | 0.000 | Fused in Pipeline 2 |\n| | | +----+--------------------------------------------------+----------------+-------+---------+----------------+------------------------+-----------+---------------------+\n| | +Filter | 8 | rand() < $autodouble_4 AND cache[a.id] = i | 15 | 0 | 2027 | | | | |\n| | | +----+--------------------------------------------------+----------------+-------+---------+----------------+ | | |\n| | +NodeByLabelScan | 9 | a:Person | 1000 | 20000 | 30000 | 8488 | 10002/0 | 6.158 | Fused in Pipeline 1 |\n| | +----+--------------------------------------------------+----------------+-------+---------+----------------+------------------------+-----------+---------------------+\n| +Unwind | 10 | range($autoint_2, $autoint_3) AS j | 100 | 10000 | 0 | | | | |\n| | +----+--------------------------------------------------+----------------+-------+---------+----------------+ | | |\n| +Unwind | 11 | range($autoint_0, $autoint_1) AS i | 10 | 100 | 0 | | 0/0 | 0.000 | Fused in Pipeline 0 |\n+----------------------+----+--------------------------------------------------+----------------+-------+---------+----------------+------------------------+-----------+---------------------+\n\nTotal database accesses: 32027, total allocated memory: 10624\n", + "runtime" : "PIPELINED", + "Time" : 0, + "runtime-impl" : "PIPELINED", + "version" : 5, + "DbHits" :0, + "batch-size" : 128, + "Details" : "", + "planner-version" : 5.26, + "PipelineInfo" : "In Pipeline 3", + "runtime-version" :5.26, + "Id" : 0, + "PageCacheMisses" : 0, + "EstimatedRows" : 2.25, + "planner" : "COST", + "Rows" : 0, + "PageCacheHits" : 0 + }, + "identifiers" : [ "j", "a", "i", "b", "anon_0", "anon_1" ], + "children" : [ + { + "dbHits" : 0, + "records" : 0, + "hasPageCacheStats" : false, + "pageCacheHits" : 0, + "pageCacheMisses" : 0, + "pageCacheHitRatio" : 0.0, + "time" : 0, + "operatorType" : "EmptyResult@neo4j", + "arguments" : { + "PipelineInfo" : "In Pipeline 3", + "Time" : 0, + "Id" : 1, + "PageCacheMisses" : 0, + "EstimatedRows" : 2.25, + "DbHits" : 0, + "Rows" :0, + "PageCacheHits" :0 + }, + "identifiers" : [ "j", "a", "i", "b", "anon_0", "anon_1" ], + "children" : [ { + "dbHits" : 0, + "records" : 0, + "hasPageCacheStats" : false, + "pageCacheHits" : 0, + "pageCacheMisses" : 0, + "pageCacheHitRatio" : 0.0, + "time" : 0, + "operatorType" : "Create@neo4j", + "arguments" : { + "Details" : { + "$type" : "String", + "_value" : "(a)-[anon_0:KNOWS]->(b), (b)-[anon_1:KNOWS]->(a)" + }, + "PipelineInfo" : { + "$type" : "String", + "_value" : "In Pipeline 3" + }, + "Time" : { + "$type" : "Integer", + "_value" : "0" + }, + "Id" : { + "$type" : "Integer", + "_value" : "2" + }, + "PageCacheMisses" : { + "$type" : "Integer", + "_value" : "0" + }, + "EstimatedRows" : { + "$type" : "Float", + "_value" : "2.25" + }, + "DbHits" : { + "$type" : "Integer", + "_value" : "0" + }, + "Rows" : { + "$type" : "Integer", + "_value" : "0" + }, + "PageCacheHits" : { + "$type" : "Integer", + "_value" : "0" + } + }, + "identifiers" : [ "j", "a", "i", "b", "anon_0", "anon_1" ], + "children" : [ { + "dbHits" : 0, + "records" : 0, + "hasPageCacheStats" : false, + "pageCacheHits" : 0, + "pageCacheMisses" : 0, + "pageCacheHitRatio" : 0.0, + "time" : 0, + "operatorType" : "Filter@neo4j", + "arguments" : { + "Details" : { + "$type" : "String", + "_value" : "cache[a.id] < cache[b.id]" + }, + "PipelineInfo" : { + "$type" : "String", + "_value" : "In Pipeline 3" + }, + "Time" : { + "$type" : "Integer", + "_value" : "0" + }, + "Id" : { + "$type" : "Integer", + "_value" : "3" + }, + "PageCacheMisses" : { + "$type" : "Integer", + "_value" : "0" + }, + "EstimatedRows" : { + "$type" : "Float", + "_value" : "2.25" + }, + "DbHits" : { + "$type" : "Integer", + "_value" : "0" + }, + "Rows" : { + "$type" : "Integer", + "_value" : "0" + }, + "PageCacheHits" : { + "$type" : "Integer", + "_value" : "0" + } + }, + "identifiers" : [ "i", "j", "a", "b" ], + "children" : [ { + "dbHits" : 0, + "records" : 0, + "hasPageCacheStats" : false, + "pageCacheHits" : 0, + "pageCacheMisses" : 0, + "pageCacheHitRatio" : 0.0, + "time" : 0, + "operatorType" : "Apply@neo4j", + "arguments" : { + "Id" : { + "$type" : "Integer", + "_value" : "4" + }, + "PageCacheMisses" : { + "$type" : "Integer", + "_value" : "0" + }, + "EstimatedRows" : { + "$type" : "Float", + "_value" : "7.5" + }, + "DbHits" : { + "$type" : "Integer", + "_value" : "0" + }, + "Rows" : { + "$type" : "Integer", + "_value" : "0" + }, + "PageCacheHits" : { + "$type" : "Integer", + "_value" : "0" + } + }, + "identifiers" : [ "i", "j", "a", "b" ], + "children" : [ { + "dbHits" : 0, + "records" : 10000, + "hasPageCacheStats" : false, + "pageCacheHits" : 0, + "pageCacheMisses" : 0, + "pageCacheHitRatio" : 0.0, + "time" : 0, + "operatorType" : "Unwind@neo4j", + "arguments" : { + "Details" : { + "$type" : "String", + "_value" : "range($autoint_2, $autoint_3) AS j" + }, + "PipelineInfo" : { + "$type" : "String", + "_value" : "Fused in Pipeline 0" + }, + "Id" : { + "$type" : "Integer", + "_value" : "10" + }, + "EstimatedRows" : { + "$type" : "Float", + "_value" : "100.0" + }, + "DbHits" : { + "$type" : "Integer", + "_value" : "0" + }, + "Rows" : { + "$type" : "Integer", + "_value" : "10000" + } + }, + "identifiers" : [ "i", "j" ], + "children" : [ { + "dbHits" : 0, + "records" : 100, + "hasPageCacheStats" : false, + "pageCacheHits" : 0, + "pageCacheMisses" : 0, + "pageCacheHitRatio" : 0.0, + "time" : 0, + "operatorType" : "Unwind@neo4j", + "arguments" : { + "Details" : { + "$type" : "String", + "_value" : "range($autoint_0, $autoint_1) AS i" + }, + "PipelineInfo" : { + "$type" : "String", + "_value" : "Fused in Pipeline 0" + }, + "Time" : { + "$type" : "Integer", + "_value" : "0" + }, + "Id" : { + "$type" : "Integer", + "_value" : "11" + }, + "PageCacheMisses" : { + "$type" : "Integer", + "_value" : "0" + }, + "EstimatedRows" : { + "$type" : "Float", + "_value" : "10.0" + }, + "DbHits" : { + "$type" : "Integer", + "_value" : "0" + }, + "Rows" : { + "$type" : "Integer", + "_value" : "100" + }, + "PageCacheHits" : { + "$type" : "Integer", + "_value" : "0" + } + }, + "identifiers" : [ "i" ], + "children" : [ ] + } ] + }, { + "dbHits" : 0, + "records" : 0, + "hasPageCacheStats" : false, + "pageCacheHits" : 0, + "pageCacheMisses" : 0, + "pageCacheHitRatio" : 0.0, + "time" : 0, + "operatorType" : "CartesianProduct@neo4j", + "arguments" : { + "Memory" : { + "$type" : "Integer", + "_value" : "1392" + }, + "PipelineInfo" : { + "$type" : "String", + "_value" : "In Pipeline 3" + }, + "Id" : { + "$type" : "Integer", + "_value" : "5" + }, + "EstimatedRows" : { + "$type" : "Float", + "_value" : "7.5" + }, + "DbHits" : { + "$type" : "Integer", + "_value" : "0" + }, + "Rows" : { + "$type" : "Integer", + "_value" : "0" + } + }, + "identifiers" : [ "i", "j", "a", "b" ], + "children" : [ { + "dbHits" : 2027, + "records" : 0, + "hasPageCacheStats" : false, + "pageCacheHits" : 0, + "pageCacheMisses" : 0, + "pageCacheHitRatio" : 0.0, + "time" : 0, + "operatorType" : "Filter@neo4j", + "arguments" : { + "Details" : { + "$type" : "String", + "_value" : "rand() < $autodouble_4 AND cache[a.id] = i" + }, + "PipelineInfo" : { + "$type" : "String", + "_value" : "Fused in Pipeline 1" + }, + "Id" : { + "$type" : "Integer", + "_value" : "8" + }, + "EstimatedRows" : { + "$type" : "Float", + "_value" : "15.0" + }, + "DbHits" : { + "$type" : "Integer", + "_value" : "2027" + }, + "Rows" : { + "$type" : "Integer", + "_value" : "0" + } + }, + "identifiers" : [ "i", "j", "a" ], + "children" : [ { + "dbHits" : 30000, + "records" : 20000, + "hasPageCacheStats" : true, + "pageCacheHits" : 10002, + "pageCacheMisses" : 0, + "pageCacheHitRatio" : 1.0, + "time" : 6157670, + "operatorType" : "NodeByLabelScan@neo4j", + "arguments" : { + "Details" : { + "$type" : "String", + "_value" : "a:Person" + }, + "PipelineInfo" : { + "$type" : "String", + "_value" : "Fused in Pipeline 1" + }, + "Memory" : { + "$type" : "Integer", + "_value" : "8488" + }, + "Time" : { + "$type" : "Integer", + "_value" : "6157670" + }, + "Id" : { + "$type" : "Integer", + "_value" : "9" + }, + "EstimatedRows" : { + "$type" : "Float", + "_value" : "1000.0" + }, + "PageCacheMisses" : { + "$type" : "Integer", + "_value" : "0" + }, + "DbHits" : { + "$type" : "Integer", + "_value" : "30000" + }, + "Rows" : { + "$type" : "Integer", + "_value" : "20000" + }, + "PageCacheHits" : { + "$type" : "Integer", + "_value" : "10002" + } + }, + "identifiers" : [ "i", "j", "a" ], + "children" : [ ] + } ] + }, { + "dbHits" : 0, + "records" : 0, + "hasPageCacheStats" : false, + "pageCacheHits" : 0, + "pageCacheMisses" : 0, + "pageCacheHitRatio" : 0.0, + "time" : 0, + "operatorType" : "Filter@neo4j", + "arguments" : { + "Details" : { + "$type" : "String", + "_value" : "cache[b.id] = j" + }, + "PipelineInfo" : { + "$type" : "String", + "_value" : "Fused in Pipeline 2" + }, + "Id" : { + "$type" : "Integer", + "_value" : "6" + }, + "EstimatedRows" : { + "$type" : "Float", + "_value" : "50.0" + }, + "DbHits" : { + "$type" : "Integer", + "_value" : "0" + }, + "Rows" : { + "$type" : "Integer", + "_value" : "0" + } + }, + "identifiers" : [ "i", "j", "b" ], + "children" : [ { + "dbHits" : 0, + "records" : 0, + "hasPageCacheStats" : false, + "pageCacheHits" : 0, + "pageCacheMisses" : 0, + "pageCacheHitRatio" : 0.0, + "time" : 0, + "operatorType" : "NodeByLabelScan@neo4j", + "arguments" : { + "Details" : { + "$type" : "String", + "_value" : "b:Person" + }, + "PipelineInfo" : { + "$type" : "String", + "_value" : "Fused in Pipeline 2" + }, + "Memory" : { + "$type" : "Integer", + "_value" : "256" + }, + "Time" : { + "$type" : "Integer", + "_value" : "0" + }, + "Id" : { + "$type" : "Integer", + "_value" : "7" + }, + "EstimatedRows" : { + "$type" : "Float", + "_value" : "1000.0" + }, + "PageCacheMisses" : { + "$type" : "Integer", + "_value" : "0" + }, + "DbHits" : { + "$type" : "Integer", + "_value" : "0" + }, + "Rows" : { + "$type" : "Integer", + "_value" : "0" + }, + "PageCacheHits" : { + "$type" : "Integer", + "_value" : "0" + } + }, + "identifiers" : [ "i", "j", "b" ], + "children" : [ ] + } ] + } ] + } ] + } ] + } ] + } ] + } ] + }, + "bookmarks" : [ "FB:kcwQMSScZToiRKKW8P2Tlr362soAAQWHkA==" ] +} \ No newline at end of file