diff --git a/Contributing.md b/Contributing.md new file mode 100644 index 0000000..d7c41ee --- /dev/null +++ b/Contributing.md @@ -0,0 +1,94 @@ +# 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 < 7.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**\ + Our tests use PHPUnit. To run tests: + + ```bash + composer/phpunit + ``` + +## 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/README.md b/README.md index 9935454..88ad470 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,138 @@ +Neo4jQueryAPI client + +The Neo4j QueryAPI client is for developers and data engineers who want to interact programmatically with Neo4j databases — running queries, handling results, and managing database configurations. It offers: + +- Easy configuration to pick and choose drivers +- An intuitive API for smooth query execution +- Extensibility for custom use cases +- Built and tested under close collaboration with the official Neo4j driver team +- Easier to start with, just need a client to any neo4j instance +- Fully typed with Psalm and CS fixed for code quality +- It does not supports Bolt, Rather compatible with HTTP, and auto-routed drivers + + + # Query API -Usage example: +A PHP client for Neo4j, a graph database. + +## Installation + +You can install the package via Composer: + +```sh +composer require this-repo/neo4j-client +``` + +## Usage + +### Connecting to Neo4j ```php use Neo4j\QueryAPI\Neo4jQueryAPI; -use Neo4j\QueryAPI\Objects\Authentication; +use Neo4j\QueryAPI\Authentication\AuthenticateInterface; + +$client = Neo4jQueryAPI::login('http://localhost:7474', new AuthenticateInterface('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 | +|--------------------|:-----------------:| +| Single name | | +| Integer | ``` * int ``` | +| Float | ``` * float ``` | +| Boolean | ``` * bool ``` | +| Null | ``` * null ``` | +| String | ``` * string ``` | +| Array | | +| Date | | +| Duration | | +| 2D Point | | +| 3D Point | | +| Cartesian 2D Point | | +| Cartesian 3D Point | | +| Node | | +| Path | | +| Map | | +| Exact name | | +| Bookmarks | Yes | + +## Diving deeper: + +| Feature | Supported? | +|----------|:-------------:| +| Authentication | Yes | +| Transaction | Yes | +| HTTP | Yes | +| Cluster | Yes | +| Aura | Partly (recent versions) | +| Bookmarks | Yes | + +> **_NOTE:_** It supports neo4j databases versions > 5.25 (which has QueryAPI enabled.) + + + +## Contributing + +Please see CONTRIBUTING for details. + +## Security + +If you discover any security-related issues, please email *security@nagels.tech* instead of using the issue tracker. + +## Credits + +- [Your Name](https://github.com/your-github-username) +- [All Contributors](https://github.com/your-repo/neo4j-client/graphs/contributors) + +## License -$client = Neo4jQueryAPI::login('https://myaddress.com', Authentication::bearer('mytokken')) -``` \ No newline at end of file +The MIT License (MIT). Please see License File for more information. \ No newline at end of file diff --git a/composer.json b/composer.json index 107ee37..c6d022e 100644 --- a/composer.json +++ b/composer.json @@ -51,7 +51,8 @@ "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" + "psalm": "vendor/bin/psalm --no-cache --show-info=true", + "phpunit" : "vendor/bin/phpunit" } } diff --git a/phpunit.dist.xml b/phpunit.dist.xml index 0067675..158b08f 100644 --- a/phpunit.dist.xml +++ b/phpunit.dist.xml @@ -13,5 +13,9 @@ --> + + + + diff --git a/src/Neo4jQueryAPI.php b/src/Neo4jQueryAPI.php index 332ac78..8e59605 100644 --- a/src/Neo4jQueryAPI.php +++ b/src/Neo4jQueryAPI.php @@ -4,7 +4,6 @@ use Http\Discovery\Psr17FactoryDiscovery; use Http\Discovery\Psr18ClientDiscovery; -use http\Exception\RuntimeException; use InvalidArgumentException; use Neo4j\QueryAPI\Exception\Neo4jException; use Psr\Http\Client\ClientInterface; @@ -16,20 +15,15 @@ final class Neo4jQueryAPI { - private Configuration $config; - public function __construct( private ClientInterface $client, private ResponseParser $responseParser, private Neo4jRequestFactory $requestFactory, - ?Configuration $config = null + private Configuration $config ) { - $this->config = $config ?? new Configuration(baseUri: 'http://myaddress'); // Default configuration if not provided + } - /** - * @api - */ public static function login(string $address = null, ?AuthenticateInterface $auth = null, ?Configuration $config = null): self { $config = $config ?? new Configuration(baseUri: $address ?? ''); @@ -57,19 +51,18 @@ public static function login(string $address = null, ?AuthenticateInterface $aut ); } - /** - * @api - */ - public function create(Configuration $configuration, AuthenticateInterface $auth = null): self + public static function create(Configuration $configuration, AuthenticateInterface $auth = null): self { return self::login(auth: $auth, config: $configuration); } + public function getConfig(): Configuration { return $this->config; } + /** * Executes a Cypher query. */ @@ -122,7 +115,7 @@ 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); + $errorResponse = json_decode((string)$response->getBody(), true); throw Neo4jException::fromNeo4jResponse($errorResponse, $e); } diff --git a/src/Neo4jRequestFactory.php b/src/Neo4jRequestFactory.php index 3003af2..bd78fbf 100644 --- a/src/Neo4jRequestFactory.php +++ b/src/Neo4jRequestFactory.php @@ -8,9 +8,6 @@ use Psr\Http\Message\RequestInterface; use Psr\Http\Message\StreamFactoryInterface; -/** - * @api - */ class Neo4jRequestFactory { public function __construct( diff --git a/src/OGM.php b/src/OGM.php index 65517fd..72b6c40 100644 --- a/src/OGM.php +++ b/src/OGM.php @@ -8,9 +8,6 @@ use Neo4j\QueryAPI\Objects\Path; use InvalidArgumentException; -/** - * @api - */ class OGM { /** @@ -57,14 +54,14 @@ private function mapNode(array $nodeData): Node { return new Node( labels: $nodeData['_labels'] ?? [], - properties: $this->mapProperties($nodeData['_properties'] ?? []) // ✅ Fix: Ensure properties exist + properties: $this->mapProperties($nodeData['_properties'] ?? []) ); } private function mapRelationship(array $relationshipData): Relationship { return new Relationship( - type: $relationshipData['_type'] ?? 'UNKNOWN', // ✅ Fix: Default to 'UNKNOWN' + type: $relationshipData['_type'] ?? 'UNKNOWN', properties: $this->mapProperties($relationshipData['_properties'] ?? []) ); } diff --git a/src/Objects/Authentication.php b/src/Objects/Authentication.php index 166c746..9dd0f1d 100644 --- a/src/Objects/Authentication.php +++ b/src/Objects/Authentication.php @@ -7,9 +7,6 @@ use Neo4j\QueryAPI\Authentication\BearerAuthentication; use Neo4j\QueryAPI\Authentication\NoAuth; -/** - * @api - */ class Authentication { public static function basic(string $username, string $password): AuthenticateInterface @@ -32,9 +29,6 @@ public static function fromEnvironment(): AuthenticateInterface ); } - - - public static function noAuth(): AuthenticateInterface { return new NoAuth(); diff --git a/src/Objects/Bookmarks.php b/src/Objects/Bookmarks.php index a79215a..d158552 100644 --- a/src/Objects/Bookmarks.php +++ b/src/Objects/Bookmarks.php @@ -4,10 +4,7 @@ use JsonSerializable; -/** - * @api - */ -class Bookmarks implements \Countable, JsonSerializable +final class Bookmarks implements \Countable, JsonSerializable { public function __construct(private array $bookmarks) { diff --git a/src/Objects/Node.php b/src/Objects/Node.php index 4fe6e84..2bf695b 100644 --- a/src/Objects/Node.php +++ b/src/Objects/Node.php @@ -6,9 +6,6 @@ * Represents a Neo4j Node with labels and properties. */ -/** - * @api - */ class Node { /** @@ -33,19 +30,8 @@ public function __construct(array $labels, array $properties) $this->properties = $properties; } - /** - * Get the labels of the node. - * @api - * @return string[] Array of labels. - */ - public function getLabels(): array - { - return $this->labels; - } - /** * Get the properties of the node. - * @api * @return array Associative array of properties. */ public function getProperties(): array @@ -55,7 +41,6 @@ public function getProperties(): array /** * Convert the Node object to an array representation. - * @api * @return array Node data as an array. */ public function toArray(): array diff --git a/src/Objects/Path.php b/src/Objects/Path.php index 2d15c2a..bdfb28c 100644 --- a/src/Objects/Path.php +++ b/src/Objects/Path.php @@ -6,20 +6,17 @@ * Represents a path in a Neo4j graph, consisting of nodes and relationships. */ -/** - * @api - */ class Path { /** * @var Node[] Array of nodes in the path. */ - private array $nodes; + public readonly array $nodes; /** * @var Relationship[] Array of relationships in the path. */ - private array $relationships; + public readonly array $relationships; /** * Path constructor. @@ -33,23 +30,4 @@ public function __construct(array $nodes, array $relationships) $this->relationships = $relationships; } - /** - * Get the nodes in the path. - * @api - * @return Node[] Array of nodes. - */ - public function getNodes(): array - { - return $this->nodes; - } - - /** - * Get the relationships in the path. - * @api - * @return Relationship[] Array of relationships. - */ - public function getRelationships(): array - { - return $this->relationships; - } } diff --git a/src/Objects/Person.php b/src/Objects/Person.php index b1f0431..cc937c8 100644 --- a/src/Objects/Person.php +++ b/src/Objects/Person.php @@ -6,9 +6,6 @@ * @psalm-suppress UnusedClass * Represents a Person node in the Neo4j graph. */ -/** - * @api - */ class Person extends Node { /** diff --git a/src/Objects/Point.php b/src/Objects/Point.php index 721abe4..4a79ec5 100644 --- a/src/Objects/Point.php +++ b/src/Objects/Point.php @@ -5,10 +5,6 @@ /** * Represents a point with x, y, z coordinates, and SRID (Spatial Reference System Identifier). */ -/** - * @api - */ - class Point { /** @@ -25,46 +21,6 @@ public function __construct( ) { } - /** - * Get the x coordinate of the point. - * @api - * @return float x coordinate value. - */ - public function getX(): float - { - return $this->x; - } - - /** - * Get the y coordinate of the point. - * @api - * @return float y coordinate value. - */ - public function getY(): float - { - return $this->y; - } - - /** - * Get the z coordinate of the point. - * @api - * @return float|null z coordinate value, or null if not applicable. - */ - public function getZ(): float|null - { - return $this->z; - } - - /** - * Get the SRID (Spatial Reference System Identifier) of the point. - * @api - * @return int SRID value. - */ - public function getSrid(): int - { - return $this->srid; - } - /** * Convert the Point object to a string representation. * diff --git a/src/Objects/ProfiledQueryPlanArguments.php b/src/Objects/ProfiledQueryPlanArguments.php index 889c49a..74e4c48 100644 --- a/src/Objects/ProfiledQueryPlanArguments.php +++ b/src/Objects/ProfiledQueryPlanArguments.php @@ -2,9 +2,6 @@ namespace Neo4j\QueryAPI\Objects; -/** - * @api - */ class ProfiledQueryPlanArguments { public function __construct( diff --git a/src/Objects/Relationship.php b/src/Objects/Relationship.php index 156e311..9d441f3 100644 --- a/src/Objects/Relationship.php +++ b/src/Objects/Relationship.php @@ -6,20 +6,17 @@ * Represents a relationship in a Neo4j graph, with a type and associated properties. */ -/** - * @api - */ class Relationship { /** * @var string The type of the relationship (e.g., "FRIENDS_WITH", "WORKS_FOR"). */ - private string $type; + public readonly string $type; /** * @var array Associative array of properties for the relationship. */ - private array $properties; + public readonly array $properties; /** * Relationship constructor. @@ -33,23 +30,4 @@ public function __construct(string $type, array $properties = []) $this->properties = $properties; } - /** - * Get the type of the relationship. - * @api - * @return string The type of the relationship. - */ - public function getType(): string - { - return $this->type; - } - - /** - * Get the properties of the relationship. - * @api - * @return array Associative array of properties. - */ - public function getProperties(): array - { - return $this->properties; - } } diff --git a/src/Objects/ResultCounters.php b/src/Objects/ResultCounters.php index d041e1c..08669b9 100644 --- a/src/Objects/ResultCounters.php +++ b/src/Objects/ResultCounters.php @@ -2,9 +2,6 @@ namespace Neo4j\QueryAPI\Objects; -/** - * @api - */ class ResultCounters { public function __construct( diff --git a/src/Results/ResultSet.php b/src/Results/ResultSet.php index 63be9b1..5660aec 100644 --- a/src/Results/ResultSet.php +++ b/src/Results/ResultSet.php @@ -12,7 +12,6 @@ use Traversable; /** - * @api * @template TValue * @implements IteratorAggregate */ @@ -22,11 +21,11 @@ class ResultSet implements IteratorAggregate, Countable * @param list $rows */ public function __construct( - private readonly array $rows, - private readonly ?ResultCounters $counters = null, - private readonly Bookmarks $bookmarks, - private readonly ?ProfiledQueryPlan $profiledQueryPlan, - private readonly AccessMode $accessMode + public readonly array $rows, + public readonly ?ResultCounters $counters = null, + public readonly Bookmarks $bookmarks, + public readonly ?ProfiledQueryPlan $profiledQueryPlan, + public readonly AccessMode $accessMode ) { } @@ -44,14 +43,7 @@ public function getQueryCounters(): ?ResultCounters return $this->counters; } - public function getProfiledQueryPlan(): ?ProfiledQueryPlan - { - return $this->profiledQueryPlan; - } - /** - * @api - */ #[\Override] public function count(): int { diff --git a/src/Transaction.php b/src/Transaction.php index 0e85c5f..ec6d45e 100644 --- a/src/Transaction.php +++ b/src/Transaction.php @@ -35,7 +35,7 @@ public function run(string $query, array $parameters): ResultSet { $request = $this->requestFactory->buildTransactionRunRequest($query, $parameters, $this->transactionId, $this->clusterAffinity); - $response = null; // ✅ Ensures response is always defined + $response = null; try { $response = $this->client->sendRequest($request); @@ -75,7 +75,6 @@ public function rollback(): void */ private function handleRequestException(RequestExceptionInterface $e): void { - // ✅ Corrected: Check if exception has a response $response = method_exists($e, 'getResponse') ? $e->getResponse() : null; if ($response instanceof ResponseInterface) { diff --git a/tests/CreatesQueryAPI.php b/tests/CreatesQueryAPI.php new file mode 100644 index 0000000..c4b4465 --- /dev/null +++ b/tests/CreatesQueryAPI.php @@ -0,0 +1,26 @@ +api = Neo4jQueryAPI::create( + new Configuration(baseUri: $neo4jAddress, accessMode: $accessMode), + Authentication::fromEnvironment() + ); + } +} diff --git a/tests/Integration/AccessModesIntegrationTest.php b/tests/Integration/AccessModesIntegrationTest.php new file mode 100644 index 0000000..f3aee9c --- /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 0000000..d63b444 --- /dev/null +++ b/tests/Integration/BookmarksIntegrationTest.php @@ -0,0 +1,37 @@ +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); + } + +} diff --git a/tests/Integration/DataTypesIntegrationTest.php b/tests/Integration/DataTypesIntegrationTest.php new file mode 100644 index 0000000..bf83ce2 --- /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 de6dc0b..60d50cc 100644 --- a/tests/Integration/Neo4jOGMTest.php +++ b/tests/Integration/Neo4jOGMTest.php @@ -23,7 +23,6 @@ protected function setUp(): void public function testWithNode(): void { - // Ensure the property $ogm is referenced $nodeData = [ '$type' => 'Node', '_value' => [ @@ -36,10 +35,9 @@ public function testWithNode(): void $this->assertEquals('Ayush', $node->getProperties()['name']['_value']); } - // Example of using $ogm in another test public function testWithSimpleRelationship(): void { - // Mapping the Relationship + $relationshipData = [ '$type' => 'Relationship', '_value' => [ @@ -49,10 +47,9 @@ public function testWithSimpleRelationship(): void ]; $relationship = $this->ogm->map($relationshipData); - $this->assertEquals('FRIENDS', $relationship->getType()); + $this->assertEquals('FRIENDS', $relationship->type); } - // More tests... public function testWithPath(): void { $pathData = [ @@ -63,7 +60,7 @@ public function testWithPath(): void '_value' => [ '_labels' => ['Person'], '_properties' => [ - 'name' => ['_value' => 'A'], // ✅ Now correctly wrapped + 'name' => ['_value' => 'A'], ], ], ], @@ -79,7 +76,7 @@ public function testWithPath(): void '_value' => [ '_labels' => ['Person'], '_properties' => [ - 'name' => ['_value' => 'B'], // ✅ Now correctly wrapped + 'name' => ['_value' => 'B'], ], ], ], @@ -88,11 +85,10 @@ public function testWithPath(): void $path = $this->ogm->map($pathData); - // Assertions - $this->assertCount(2, $path->getNodes()); - $this->assertCount(1, $path->getRelationships()); - $this->assertEquals('A', $path->getNodes()[0]->getProperties()['name']['_value']); - $this->assertEquals('B', $path->getNodes()[1]->getProperties()['name']['_value']); + $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']); } } diff --git a/tests/Integration/Neo4jQueryAPIIntegrationTest.php b/tests/Integration/Neo4jQueryAPIIntegrationTest.php index ba7a74d..1dce152 100644 --- a/tests/Integration/Neo4jQueryAPIIntegrationTest.php +++ b/tests/Integration/Neo4jQueryAPIIntegrationTest.php @@ -2,28 +2,16 @@ namespace Neo4j\QueryAPI\Tests\Integration; -use GuzzleHttp\Client; -use GuzzleHttp\Handler\MockHandler; -use GuzzleHttp\HandlerStack; use Neo4j\QueryAPI\Exception\Neo4jException; use Neo4j\QueryAPI\Neo4jQueryAPI; -use Neo4j\QueryAPI\Neo4jRequestFactory; use Neo4j\QueryAPI\Objects\Authentication; use Neo4j\QueryAPI\Objects\Node; -use Neo4j\QueryAPI\Objects\Point; -use Neo4j\QueryAPI\Objects\ProfiledQueryPlan; use Neo4j\QueryAPI\Objects\Bookmarks; use Neo4j\QueryAPI\Objects\ResultCounters; -use Neo4j\QueryAPI\OGM; use Neo4j\QueryAPI\Results\ResultRow; use Neo4j\QueryAPI\Results\ResultSet; -use Nyholm\Psr7\Factory\Psr17Factory; use PHPUnit\Framework\TestCase; use Neo4j\QueryAPI\Enums\AccessMode; -use Neo4j\QueryAPI\ResponseParser; -use Neo4j\QueryAPI\Configuration; -use GuzzleHttp\Psr7\Response; -use RuntimeException; final class Neo4jQueryAPIIntegrationTest extends TestCase { @@ -89,293 +77,6 @@ public function testCounters(): void $this->assertEquals(1, $queryCounters->getNodesCreated()); } - 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 testProfileExistence(): void - { - $query = "PROFILE MATCH (n:Person) RETURN n.name"; - $result = $this->api->run($query); - $this->assertNotNull($result->getProfiledQueryPlan(), "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->getProfiledQueryPlan(), "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->getProfiledQueryPlan(), "profiled query plan not found"); - } - - public function testProfileCreateFriendsQueryExistence(): void - { - $query = " - PROFILE UNWIND range(1, 100) AS i - UNWIND range(1, 100) AS j - MATCH (a:Person {id: i}), (b:Person {id: j}) - WHERE a.id <> b.id AND rand() < 0.1 - CREATE (a)-[:FRIENDS]->(b); - "; - - $result = $this->api->run($query); - - - $this->assertNotNull($result->getProfiledQueryPlan(), "profiled query plan not found"); - } - - public function testProfileCreateWatchedRelationshipExistence(): void - { - - $query = " - PROFILE UNWIND range(1, 50) AS i - MATCH (p:Person), (m:Movie {year: 2000 + i}) - CREATE (p)-[:WATCHED]->(m); - "; - - $result = $this->api->run($query); - - $this->assertNotNull($result->getProfiledQueryPlan(), "profiled query plan not found"); - } - - public function testProfileCreateWatchedWithFilters(): void - { - $query = " - PROFILE UNWIND range(1, 50) AS i - MATCH (p:Person), (m:Movie {year: 2000 + i}) - WHERE p.age > 25 AND m.genre = 'Action' - CREATE (p)-[:WATCHED]->(m); - "; - - $result = $this->api->run($query); - $this->assertNotNull($result->getProfiledQueryPlan(), "profiled query plan not found"); - } - - public function testProfileCreateKnowsBidirectionalRelationships(): void - { - $query = " - PROFILE UNWIND range(1, 100) AS i - UNWIND range(1, 100) AS j - MATCH (a:Person {id: i}), (b:Person {id: j}) - WHERE a.id < b.id AND rand() < 0.1 - CREATE (a)-[:KNOWS]->(b), (b)-[:KNOWS]->(a); - "; - - $result = $this->api->run($query); - $this->assertNotNull($result->getProfiledQueryPlan(), "profiled query plan not found"); - - $body = file_get_contents(__DIR__ . '/../resources/responses/complex-query-profile.json'); - - if ($body === false) { - throw new RuntimeException('Failed to read the file: ' . __DIR__ . '/../resources/responses/complex-query-profile.json'); - } - - $mockSack = new MockHandler([ - new Response(200, [], $body), - ]); - - $handler = HandlerStack::create($mockSack); - $client = new Client(['handler' => $handler]); - - $neo4jAddress = getenv('NEO4J_ADDRESS'); - if (!is_string($neo4jAddress) || trim($neo4jAddress) === '') { - throw new RuntimeException('NEO4J_ADDRESS is not set.'); - } - - - $auth = Authentication::fromEnvironment(); - - $api = new Neo4jQueryAPI( - $client, - new ResponseParser(new OGM()), - new Neo4jRequestFactory( - new Psr17Factory(), - new Psr17Factory(), - new Configuration($neo4jAddress), - $auth - ) - ); - - - $result = $api->run($query); - - $plan = $result->getProfiledQueryPlan(); - $this->assertNotNull($plan, "The result of the query should not be null."); - - $expected = require __DIR__ . '/../resources/expected/complex-query-profile.php'; - - $this->assertEquals($expected->getProfiledQueryPlan(), $plan, "Profiled query plan does not match the expected value."); - } - - public function testProfileCreateActedInRelationships(): void - { - $query = " - PROFILE UNWIND range(1, 50) AS i - MATCH (p:Person {id: i}), (m:Movie {year: 2000 + i}) - WHERE p.job = 'Artist' - CREATE (p)-[:ACTED_IN]->(m); - "; - - $result = $this->api->run($query); - $this->assertNotNull($result->getProfiledQueryPlan(), "profiled query plan not found"); - } - - public function testChildQueryPlanExistence(): void - { - $result = $this->api->run("PROFILE MATCH (n:Person {name: 'Alice'}) RETURN n.name"); - - $profiledQueryPlan = $result->getProfiledQueryPlan(); - $this->assertNotNull($profiledQueryPlan); - $this->assertNotEmpty($profiledQueryPlan->children); - - foreach ($profiledQueryPlan->children as $child) { - $this->assertInstanceOf(ProfiledQueryPlan::class, $child); - } - } - // - // public function testImpersonatedUserSuccess(): void - // { - // $this->markTestSkipped("stuck"); - // - // $result = $this->api->run( - // "PROFILE MATCH (n:Person {name: 'Alice'}) RETURN n.name", - // [], - // $this->config->database, - // new Bookmarks([]), - // 'HAPPYBDAY' - // ); - // - // $impersonatedUser = $result->getImpersonatedUser(); - // $this->assertNotNull($impersonatedUser, "Impersonated user should not be null."); - // } - - // // - // // - // public function testImpersonatedUserFailure(): void - // { - // $this->markTestSkipped("stuck"); - // $this->expectException(Neo4jException::class); - // - // - // $this->api->run( - // "PROFILE MATCH (n:Person {name: 'Alice'}) RETURN n.name", - // [], - // 'neo4j', - // null, - // 'invalidUser' - // ); - // } - - // // - // #[DoesNotPerformAssertions] - // public function testRunWithWriteAccessMode(): void - // { - // $result = $this->api->run( - // "CREATE (n:Person {name: 'Alice'}) RETURN n", - // [], - // 'neo4j', - // null, - // null, - // AccessMode::WRITE - // ); - // - // } - // - // #[DoesNotPerformAssertions] - // public function testRunWithReadAccessMode(): void - // { - // $result = $this->api->run( - // "MATCH (n) RETURN COUNT(n)", - // [], - // 'neo4j', - // null, - // null, - // AccessMode::READ - // ); - // } - - // - // public function testReadModeWithWriteQuery(): void - // { - // $this->expectException(Neo4jException::class); - // $this->expectExceptionMessage("Writing in read access mode not allowed. Attempted write to neo4j"); - // - // try { - // $this->api->run( - // "CREATE (n:Test {name: 'Test Node'})", - // [], - // $this->config->database, - // new Bookmarks([]), - // null, - // AccessMode::WRITE - // ); - // } catch (Neo4jException $e) { - // error_log('Caught expected Neo4jException: ' . $e->getMessage()); - // throw $e; - // } - // } - // - // - // #[DoesNotPerformAssertions] - // public function testWriteModeWithReadQuery(): void - // { - // $this->api->run( - // "MATCH (n:Test) RETURN n", - // [], - // 'neo4j', - // null, - // null, - // AccessMode::WRITE - // //cos write encapsulates read - // ); - // } - - private function clearDatabase(): void { $this->api->run('MATCH (n) DETACH DELETE n', []); @@ -402,608 +103,4 @@ public function testInvalidQueryException(): void } } - 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()); - - // Ensure results are not empty - $this->assertNotEmpty(iterator_to_array($results), 'No results returned from query.'); - - $filteredResults = array_values(array_filter( - iterator_to_array($results), - fn (ResultRow $row) => in_array($row['n.name'] ?? '', ['bob1', 'alicy'], true) - )); - - $this->assertEquals(iterator_to_array($expected), $filteredResults); - - $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()); - - $filteredResults = array_slice(iterator_to_array($results), 0, 1); - $this->assertEquals(iterator_to_array($expected), $filteredResults); - - $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()); - $this->assertEquals(iterator_to_array($expected), iterator_to_array($results)); - $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/Neo4jTransactionIntegrationTest.php b/tests/Integration/Neo4jTransactionIntegrationTest.php index a4e7087..9033ba8 100644 --- a/tests/Integration/Neo4jTransactionIntegrationTest.php +++ b/tests/Integration/Neo4jTransactionIntegrationTest.php @@ -6,6 +6,7 @@ use Neo4j\QueryAPI\Objects\Authentication; use GuzzleHttp\Exception\GuzzleException; use Neo4j\QueryAPI\Neo4jQueryAPI; +use Neo4j\QueryAPI\Tests\CreatesQueryAPI; use PHPUnit\Framework\TestCase; use RuntimeException; @@ -14,23 +15,14 @@ */ class Neo4jTransactionIntegrationTest extends TestCase { - /** @psalm-suppress PropertyNotSetInConstructor */ - private Neo4jQueryAPI $api; + use CreatesQueryAPI; - /** - * @throws GuzzleException - */ #[\Override] - public function setUp(): void + protected function setUp(): void { parent::setUp(); - $address = is_string(getenv('NEO4J_ADDRESS')) ? getenv('NEO4J_ADDRESS') : ''; - - if ($address === '') { - throw new RuntimeException('NEO4J_ADDRESS is not set.'); - } - + $this->createQueryAPI(); $this->api = $this->initializeApi(); $this->clearDatabase(); $this->populateTestData(); diff --git a/tests/Integration/ProfiledQueryPlanIntegrationTest.php b/tests/Integration/ProfiledQueryPlanIntegrationTest.php new file mode 100644 index 0000000..922e8f9 --- /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/Neo4jQueryAPIUnitTest.php b/tests/Unit/Neo4jQueryAPIUnitTest.php index 5dae33d..16ca56a 100644 --- a/tests/Unit/Neo4jQueryAPIUnitTest.php +++ b/tests/Unit/Neo4jQueryAPIUnitTest.php @@ -71,7 +71,8 @@ public function testRunSuccess(): void Psr17FactoryDiscovery::findStreamFactory(), new Configuration($this->address), Authentication::fromEnvironment() - ) + ), + new Configuration($this->address) ); $neo4jQueryAPI->run('MATCH (n:Person) RETURN n LIMIT 5');