diff --git a/src/Neo4jQueryAPI.php b/src/Neo4jQueryAPI.php index e328f6ad..6676cae1 100644 --- a/src/Neo4jQueryAPI.php +++ b/src/Neo4jQueryAPI.php @@ -6,7 +6,10 @@ use GuzzleHttp\Client; use GuzzleHttp\Exception\GuzzleException; use GuzzleHttp\Exception\RequestException; +use Neo4j\QueryAPI\Objects\ChildQueryPlan; +use Neo4j\QueryAPI\Objects\QueryArguments; use Neo4j\QueryAPI\Objects\ResultCounters; +use Neo4j\QueryAPI\Objects\ProfiledQueryPlan; use Neo4j\QueryAPI\Results\ResultRow; use Neo4j\QueryAPI\Results\ResultSet; use Neo4j\QueryAPI\Exception\Neo4jException; @@ -27,18 +30,13 @@ public function __construct(Client $client) public static function login(string $address, string $username, string $password): self { - $username = 'neo4j'; - $password = '9lWmptqBgxBOz8NVcTJjgs3cHPyYmsy63ui6Spmw1d0'; - $connectionUrl = 'https://6f72daa1.databases.neo4j.io/db/neo4j/query/v2'; - - $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', + 'Accept' => 'application/vnd.neo4j.query', ], ]); @@ -80,6 +78,10 @@ public function run(string $cypher, array $parameters = [], string $database = ' return new ResultRow($data); }, $values); + if (isset($data['profiledQueryPlan'])) { + $profile = $this->createProfileData($data['profiledQueryPlan']); + } + $resultCounters = new ResultCounters( containsUpdates: $data['counters']['containsUpdates'] ?? false, nodesCreated: $data['counters']['nodesCreated'] ?? 0, @@ -97,30 +99,25 @@ public function run(string $cypher, array $parameters = [], string $database = ' systemUpdates: $data['counters']['systemUpdates'] ?? 0 ); - $resultSet = new ResultSet($rows, $resultCounters, new Bookmarks($data['bookmarks'] ?? [])); - - - return $resultSet; - - } catch (RequestException $e) { - { - $response = $e->getResponse(); - if ($response !== null) { - $contents = $response->getBody()->getContents(); - $errorResponse = json_decode($contents, true); + return new ResultSet( + $rows, + $resultCounters, + new Bookmarks($data['bookmarks'] ?? []), + $profile + ); + } catch (RequestExceptionInterface $e) { + $response = $e->getResponse(); + if ($response !== null) { + $contents = $response->getBody()->getContents(); + $errorResponse = json_decode($contents, true); throw Neo4jException::fromNeo4jResponse($errorResponse, $e); - } - - throw $e; } - throw new RuntimeException('Error executing query: ' . $e->getMessage(), 0, $e); + + throw $e; } } - - - public function beginTransaction(string $database = 'neo4j'): Transaction { $response = $this->client->post("/db/neo4j/query/v2/tx"); @@ -129,8 +126,52 @@ public function beginTransaction(string $database = 'neo4j'): Transaction $responseData = json_decode($response->getBody(), true); $transactionId = $responseData['transaction']['id']; + return new Transaction($this->client, $clusterAffinity, $transactionId); + } + private function createProfileData(array $data): ProfiledQueryPlan + { + $arguments = $data['arguments']; + + $queryArguments = new QueryArguments( + $arguments['globalMemory'] ?? 0, + $arguments['plannerImpl'] ?? '', + $arguments['memory'] ?? 0, + $arguments['stringRepresentation'] ?? '', + is_string($arguments['runtime'] ?? '') ? $arguments['runtime'] : json_encode($arguments['runtime']), + $arguments['runtimeImpl'] ?? '', + $arguments['dbHits'] ?? 0, + $arguments['batchSize'] ?? 0, + $arguments['details'] ?? '', + $arguments['plannerVersion'] ?? '', + $arguments['pipelineInfo'] ?? '', + $arguments['runtimeVersion'] ?? '', + $arguments['id'] ?? 0, + $arguments['estimatedRows'] ?? 0.0, + is_string($arguments['planner'] ?? '') ? $arguments['planner'] : json_encode($arguments['planner']), + $arguments['rows'] ?? 0 + ); + + $profiledQueryPlan = new ProfiledQueryPlan( + $data['dbHits'], + $data['records'], + $data['hasPageCacheStats'], + $data['pageCacheHits'], + $data['pageCacheMisses'], + $data['pageCacheHitRatio'], + $data['time'], + $data['operatorType'], + $queryArguments + ); + + foreach($data['children'] as $child) { + $childQueryPlan = $this->createProfileData($child); + + $profiledQueryPlan->addChild($childQueryPlan); + } - return new Transaction($this->client, $clusterAffinity, $transactionId); + return $profiledQueryPlan; } + + } diff --git a/src/Objects/ProfiledQueryPlan.php b/src/Objects/ProfiledQueryPlan.php new file mode 100644 index 00000000..9e20f4e6 --- /dev/null +++ b/src/Objects/ProfiledQueryPlan.php @@ -0,0 +1,101 @@ + + */ + private array $children; + + public function __construct( + ?int $dbHits = 0, // Default to 0 if null + ?int $records = 0, + ?bool $hasPageCacheStats = false, + ?int $pageCacheHits = 0, + ?int $pageCacheMisses = 0, + ?float $pageCacheHitRatio = 0.0, + ?int $time = 0, + ?string $operatorType = '', + QueryArguments $arguments + ) { + $this->dbHits = $dbHits ?? 0; + $this->records = $records ?? 0; + $this->hasPageCacheStats = $hasPageCacheStats ?? false; + $this->pageCacheHits = $pageCacheHits ?? 0; + $this->pageCacheMisses = $pageCacheMisses ?? 0; + $this->pageCacheHitRatio = $pageCacheHitRatio ?? 0.0; + $this->time = $time ?? 0; + $this->operatorType = $operatorType ?? ''; + $this->arguments = $arguments; + } + + public function getDbHits(): int + { + return $this->dbHits; + } + + public function getRecords(): int + { + return $this->records; + } + + public function hasPageCacheStats(): bool + { + return $this->hasPageCacheStats; + } + + public function getPageCacheHits(): int + { + return $this->pageCacheHits; + } + + public function getPageCacheMisses(): int + { + return $this->pageCacheMisses; + } + + public function getPageCacheHitRatio(): float + { + return $this->pageCacheHitRatio; + } + + public function getTime(): int + { + return $this->time; + } + + public function getOperatorType(): string + { + return $this->operatorType; + } + + public function getArguments(): QueryArguments + { + return $this->arguments; + } + + /** + * @return list + */ + public function getChildren(): array + { + return $this->children; + } + + public function addChild(ProfiledQueryPlan $child): void + { + $this->children[] = $child; + } +} diff --git a/src/Objects/QueryArguments.php b/src/Objects/QueryArguments.php new file mode 100644 index 00000000..dbee8c92 --- /dev/null +++ b/src/Objects/QueryArguments.php @@ -0,0 +1,139 @@ +globalMemory = $globalMemory ?? 0; + $this->plannerImpl = $plannerImpl ?? ''; + $this->memory = $memory ?? 0; + $this->stringRepresentation = $stringRepresentation ?? ''; + $this->runtime = is_string($runtime) ? $runtime : json_encode($runtime); + $this->runtimeImpl = $runtimeImpl ?? ''; + $this->dbHits = $dbHits ?? 0; + $this->batchSize = $batchSize ?? 0; + $this->details = $details ?? ''; + $this->plannerVersion = $plannerVersion ?? ''; + $this->pipelineInfo = $pipelineInfo ?? ''; + $this->runtimeVersion = $runtimeVersion ?? ''; + $this->id = $id ?? 0; + $this->estimatedRows = $estimatedRows ?? 0.0; + $this->planner = $planner ?? ''; + $this->rows = $rows ?? 0; + } + + public function getGlobalMemory(): int + { + return $this->globalMemory; + } + + public function getPlannerImpl(): string + { + return $this->plannerImpl; + } + + public function getMemory(): int + { + return $this->memory; + } + + public function getStringRepresentation(): string + { + return $this->stringRepresentation; + } + + public function getRuntime(): string + { + return $this->runtime; + } + + public function getRuntimeImpl(): string + { + return $this->runtimeImpl; + } + + public function getDbHits(): int + { + return $this->dbHits; + } + + public function getBatchSize(): int + { + return $this->batchSize; + } + + public function getDetails(): string + { + return $this->details; + } + + public function getPlannerVersion(): string + { + return $this->plannerVersion; + } + + public function getPipelineInfo(): string + { + return $this->pipelineInfo; + } + + public function getRuntimeVersion(): string + { + return $this->runtimeVersion; + } + + public function getId(): int + { + return $this->id; + } + + public function getEstimatedRows(): float + { + return $this->estimatedRows; + } + + public function getPlanner(): string + { + return $this->planner; + } + + public function getRows(): int + { + return $this->rows; + } +} diff --git a/src/Profile.php b/src/Profile.php new file mode 100644 index 00000000..c6a7b0ff --- /dev/null +++ b/src/Profile.php @@ -0,0 +1,62 @@ +neo4jUrl = $url; + $this->username = $username; + $this->password = $password; + $this->client = new Client(); + } + + public function executeQuery($query ,$parameters=[]) + { + $response = $this->client->post($this->neo4jUrl, [ + 'auth' => [$this->username, $this->password], + 'json' => [ + 'statement' => $query, + 'parameters'=>$parameters + ] + ]); + + return json_decode($response->getBody(), true); + } + + public function formatResponse($data): array + { + $output = [ + "data" => [ + "fields" => [], + "values" => [] + ], + "profiledQueryPlan" => [], + "bookmarks" => $data['bookmarks'] ?? [] + ]; + + if (isset($data['result']['columns']) && isset($data['result']['rows'])) { + $output["data"]["fields"] = $data['result']['columns']; + foreach ($data['result']['rows'] as $row) { + $output["data"]["values"][] = $row; + } + } + + if (isset($data['profiledQueryPlan'])) { + $output["profiledQueryPlan"] = $data['profiledQueryPlan']; + } + + return $output; + } +} + diff --git a/src/Results/ResultSet.php b/src/Results/ResultSet.php index 0c062f2c..3f25cfc4 100644 --- a/src/Results/ResultSet.php +++ b/src/Results/ResultSet.php @@ -5,6 +5,9 @@ use ArrayIterator; use Countable; use IteratorAggregate; +use Neo4j\QueryAPI\Objects\ChildQueryPlan; +use Neo4j\QueryAPI\Objects\ProfiledQueryPlan; +use Neo4j\QueryAPI\Objects\QueryArguments; use Neo4j\QueryAPI\Objects\ResultCounters; use Neo4j\QueryAPI\Objects\Bookmarks; // Make sure to include the Bookmarks class use Traversable; @@ -17,7 +20,8 @@ class ResultSet implements IteratorAggregate, Countable public function __construct( private readonly array $rows, private ResultCounters $counters, - private Bookmarks $bookmarks + private Bookmarks $bookmarks, + private ?ProfiledQueryPlan $profiledQueryPlan = null ) { } @@ -26,12 +30,21 @@ public function getIterator(): Traversable { return new ArrayIterator($this->rows); } - public function getQueryCounters(): ?ResultCounters { return $this->counters; } + public function getProfiledQueryPlan(): ?ProfiledQueryPlan + { + return $this->profiledQueryPlan; + } + + public function getChildQueryPlan(): ?ChildQueryPlan + { + return $this->childQueryPlan; + } + public function count(): int { return count($this->rows); diff --git a/src/Transaction.php b/src/Transaction.php index b824a772..b4bdd777 100644 --- a/src/Transaction.php +++ b/src/Transaction.php @@ -3,6 +3,7 @@ namespace Neo4j\QueryAPI; use Neo4j\QueryAPI\Exception\Neo4jException; +use Neo4j\QueryAPI\Objects\Bookmarks; use Neo4j\QueryAPI\Objects\ResultCounters; use Neo4j\QueryAPI\Results\ResultRow; use Neo4j\QueryAPI\Results\ResultSet; @@ -53,7 +54,41 @@ public function run(string $query, array $parameters): ResultSet $values = $data['data']['values']; if (empty($values)) { - return new ResultSet([], new ResultCounters( + return new ResultSet( + rows: [], + counters: new ResultCounters( + containsUpdates: $data['counters']['containsUpdates'], + nodesCreated: $data['counters']['nodesCreated'], + nodesDeleted: $data['counters']['nodesDeleted'], + propertiesSet: $data['counters']['propertiesSet'], + relationshipsCreated: $data['counters']['relationshipsCreated'], + relationshipsDeleted: $data['counters']['relationshipsDeleted'], + labelsAdded: $data['counters']['labelsAdded'], + labelsRemoved: $data['counters']['labelsRemoved'], + indexesAdded: $data['counters']['indexesAdded'], + indexesRemoved: $data['counters']['indexesRemoved'], + constraintsAdded: $data['counters']['constraintsAdded'], + constraintsRemoved: $data['counters']['constraintsRemoved'], + containsSystemUpdates: $data['counters']['containsSystemUpdates'], + systemUpdates: $data['counters']['systemUpdates'] + ), + bookmarks: new Bookmarks($data['bookmarks'] ?? []) + ); + } + + $ogm = new OGM(); + $rows = array_map(function ($resultRow) use ($ogm, $keys) { + $data = []; + foreach ($keys as $index => $key) { + $fieldData = $resultRow[$index] ?? null; + $data[$key] = $ogm->map($fieldData); + } + return new ResultRow($data); + }, $values); + + return new ResultSet( + rows: $rows, + counters: new ResultCounters( containsUpdates: $data['counters']['containsUpdates'], nodesCreated: $data['counters']['nodesCreated'], nodesDeleted: $data['counters']['nodesDeleted'], @@ -68,35 +103,9 @@ public function run(string $query, array $parameters): ResultSet constraintsRemoved: $data['counters']['constraintsRemoved'], containsSystemUpdates: $data['counters']['containsSystemUpdates'], systemUpdates: $data['counters']['systemUpdates'] - )); - } - - $ogm = new OGM(); - $rows = array_map(function ($resultRow) use ($ogm, $keys) { - $data = []; - foreach ($keys as $index => $key) { - $fieldData = $resultRow[$index] ?? null; - $data[$key] = $ogm->map($fieldData); - } - return new ResultRow($data); - }, $values); - - return new ResultSet($rows, new ResultCounters( - containsUpdates: $data['counters']['containsUpdates'], - nodesCreated: $data['counters']['nodesCreated'], - nodesDeleted: $data['counters']['nodesDeleted'], - propertiesSet: $data['counters']['propertiesSet'], - relationshipsCreated: $data['counters']['relationshipsCreated'], - relationshipsDeleted: $data['counters']['relationshipsDeleted'], - labelsAdded: $data['counters']['labelsAdded'], - labelsRemoved: $data['counters']['labelsRemoved'], - indexesAdded: $data['counters']['indexesAdded'], - indexesRemoved: $data['counters']['indexesRemoved'], - constraintsAdded: $data['counters']['constraintsAdded'], - constraintsRemoved: $data['counters']['constraintsRemoved'], - containsSystemUpdates: $data['counters']['containsSystemUpdates'], - systemUpdates: $data['counters']['systemUpdates'] - )); + ), + bookmarks: new Bookmarks($data['bookmarks'] ?? []) + ); } public function commit(): void diff --git a/src/neo4jQuery.php b/src/neo4jQuery.php deleted file mode 100644 index cad9a5f5..00000000 --- a/src/neo4jQuery.php +++ /dev/null @@ -1,56 +0,0 @@ -post($connectionUrl, [ - 'json' => [ - 'statement' => 'CREATE (n:Person {name: $name}) RETURN n.name AS name', - 'parameters' => [ - 'name' => 'Alice' - ], - 'includeCounters' => true, - 'bookmarks' => $resultCounters->getBookmarks(), - ], - 'headers' => [ - 'Authorization' => 'Basic ' . $auth, - 'Content-Type' => 'application/json', - 'Accept' => 'application/json', - ], -]); - -$data = json_decode($response->getBody()->getContents(), true); - -foreach ($data['counters'] as $key => $value) { - $resultCounters->setCounter($key, $value); -} - -if (isset($data['bookmark'])) { - $resultCounters->addBookmark($data['bookmark']); -} diff --git a/tests/Integration/Neo4jQueryAPIIntegrationTest.php b/tests/Integration/Neo4jQueryAPIIntegrationTest.php index 8aa8aceb..dc763194 100644 --- a/tests/Integration/Neo4jQueryAPIIntegrationTest.php +++ b/tests/Integration/Neo4jQueryAPIIntegrationTest.php @@ -5,6 +5,7 @@ use GuzzleHttp\Exception\GuzzleException; use Neo4j\QueryAPI\Exception\Neo4jException; use Neo4j\QueryAPI\Neo4jQueryAPI; +use Neo4j\QueryAPI\Objects\ProfiledQueryPlan; use Neo4j\QueryAPI\Objects\Bookmarks; use Neo4j\QueryAPI\Objects\ResultCounters; use Neo4j\QueryAPI\Results\ResultRow; @@ -63,35 +64,162 @@ public function testCreateBookmarks(): void } -// -// public function testTransactionCommit(): void -// { -// // Begin a new transaction -// $tsx = $this->api->beginTransaction(); -// -// // Generate a random name for the node -// $name = (string)mt_rand(1, 100000); -// -// // Create a node within the transaction -// $tsx->run('CREATE (x:Human {name: $name})', ['name' => $name]); // Pass the array here -// -// // Validate that the node does not exist in the database before the transaction is committed -// $results = $this->api->run('MATCH (x:Human {name: $name}) RETURN x', ['name' => $name]); -// $this->assertCount(0, $results); -// -// // Validate that the node exists within the transaction -// $results = $tsx->run('MATCH (x:Human {name: $name}) RETURN x', ['name' => $name]); -// $this->assertCount(1, $results); -// -// // Commit the transaction -// $tsx->commit(); -// -// // Validate that the node now exists in the database -// $results = $this->api->run('MATCH (x:Human {name: $name}) RETURN x', ['name' => $name]); -// $this->assertCount(0, $results); -// } -// + 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 + { + // Define the CREATE query + $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"); + } + + 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->getChildren()); + + foreach ($profiledQueryPlan->getChildren() as $child) { + $this->assertInstanceOf(ProfiledQueryPlan::class, $child); + } + } + + public function testTransactionCommit(): void + { + // Begin a new transaction + $tsx = $this->api->beginTransaction(); + + // Generate a random name for the node + $name = (string)mt_rand(1, 100000); + + // Create a node within the transaction + $tsx->run("CREATE (x:Human {name: \$name})", ['name' => $name]); + + // Validate that the node does not exist in the database before the transaction is committed + $results = $this->api->run("MATCH (x:Human {name: \$name}) RETURN x", ['name' => $name]); + $this->assertCount(0, $results); + + // Validate that the node exists within the transaction + $results = $tsx->run("MATCH (x:Human {name: \$name}) RETURN x", ['name' => $name]); + $this->assertCount(1, $results); + + // Commit the transaction + $tsx->commit(); + + // Validate that the node now exists in the database + $results = $this->api->run("MATCH (x:Human {name: \$name}) RETURN x", ['name' => $name]); + $this->assertCount(1, $results); // Updated to expect 1 result + } /** @@ -113,11 +241,6 @@ private function populateTestData(): void } } - /** - * @throws GuzzleException - */ - - public function testInvalidQueryException(): void { try { @@ -130,9 +253,7 @@ public function testInvalidQueryException(): void $this->assertEquals('Expected parameter(s): invalidParam', $e->getMessage()); } } -// -// -// + public function testCreateDuplicateConstraintException(): void { try { diff --git a/tests/Unit/Neo4jQueryAPIUnitTest.php b/tests/Unit/Neo4jQueryAPIUnitTest.php index 291dd491..f03c16fb 100644 --- a/tests/Unit/Neo4jQueryAPIUnitTest.php +++ b/tests/Unit/Neo4jQueryAPIUnitTest.php @@ -8,6 +8,10 @@ use GuzzleHttp\HandlerStack; use GuzzleHttp\Psr7\Response; use Neo4j\QueryAPI\Neo4jQueryAPI; +use Neo4j\QueryAPI\Objects\Bookmarks; +use Neo4j\QueryAPI\Objects\ResultCounters; +use Neo4j\QueryAPI\Results\ResultRow; +use Neo4j\QueryAPI\Results\ResultSet; use PHPUnit\Framework\TestCase; class Neo4jQueryAPIUnitTest extends TestCase @@ -31,7 +35,7 @@ public function testCorrectClientSetup(): void $this->assertInstanceOf(Neo4jQueryAPI::class, $neo4jQueryAPI); - $clientReflection = new \ReflectionClass(Neo4jQueryAPIs::class); + $clientReflection = new \ReflectionClass(Neo4jQueryAPI::class); $clientProperty = $clientReflection->getProperty('client'); $client = $clientProperty->getValue($neo4jQueryAPI); @@ -49,7 +53,7 @@ public function testCorrectClientSetup(): void public function testRunSuccess(): void { $mock = new MockHandler([ - new Response(200, ['X-Foo' => 'Bar'], '{"hello":"world"}'), + new Response(200, ['X-Foo' => 'Bar'], '{"data": {"fields": ["hello"], "values": [[{"$type": "String", "_value": "world"}]]}}'), ]); $handlerStack = HandlerStack::create($mock); @@ -59,10 +63,8 @@ public function testRunSuccess(): void $cypherQuery = 'MATCH (n:Person) RETURN n LIMIT 5'; + $result = $neo4jQueryAPI->run($cypherQuery); - $result = $neo4jQueryAPI->run($cypherQuery, []); - - - $this->assertEquals(['hello' => 'world'], $result); + $this->assertEquals(new ResultSet([new ResultRow(['hello' => 'world'])], new ResultCounters(), new Bookmarks([])), $result); } } diff --git a/tests/Unit/ResultSetTest.php b/tests/Unit/ResultSetTest.php deleted file mode 100644 index 189a30bb..00000000 --- a/tests/Unit/ResultSetTest.php +++ /dev/null @@ -1,135 +0,0 @@ -expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('The keys array cannot be empty.'); - - $mockOgm = $this->createMock(OGM::class); - $resultSet = new ResultSet($mockOgm); - - // Call initialize with empty keys - $resultSet->initialize([], [ - [ - ['$type' => 'String', '_value' => 'Bob'], - ], - ]); - } - - /** - * Test that a valid ResultSet can be created and accessed. - */ - public function testValidResultSet(): void - { - $mockOgm = $this->createMock(OGM::class); - $mockOgm->method('map')->willReturnCallback(fn($value) => $value['_value'] ?? null); - - $resultSet = new ResultSet($mockOgm); - $resultSet->initialize( - ['name', 'age', 'email'], - [ - [ - ['$type' => 'String', '_value' => 'Bob'], - ['$type' => 'Integer', '_value' => 20], - ['$type' => 'String', '_value' => 'bob@example.com'], - ], - ], - $mockOgm - ); - - $rows = iterator_to_array($resultSet); - - // Assertions - $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')); - } - - /** - * Test accessing an invalid column throws an OutOfBoundsException. - */ - public function testInvalidColumnAccess(): void - { - $mockOgm = $this->createMock(OGM::class); - $mockOgm->method('map')->willReturnCallback(fn($value) => $value['_value'] ?? null); - - $resultSet = new ResultSet($mockOgm); - $resultSet->initialize( - ['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->expectExceptionMessage('Column phone not found.'); - - $rows[0]->get('phone'); - } - - /** - * Test that multiple rows are correctly handled in the ResultSet. - */ - public function testMultipleRows(): void - { - $mockOgm = $this->createMock(OGM::class); - $mockOgm->method('map')->willReturnCallback(fn($value) => $value['_value'] ?? null); - - $resultSet = new ResultSet($mockOgm); - $resultSet->initialize( - ['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); - - // Assertions for the first row - $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')); - - // Assertions for the second row - $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')); - } -}