From 6736b1febb00cde25b18fa781a7d5da44c169378 Mon Sep 17 00:00:00 2001 From: p123-stack Date: Thu, 2 Jan 2025 12:34:01 +0530 Subject: [PATCH 1/4] Transaction query run returned in Resultset format --- src/Transaction.php | 93 ++++++++++--------- src/query-api-test.php | 3 - src/run_neo4j_query.php | 82 ---------------- src/runtransaction.php | 4 +- .../Neo4jQueryAPIIntegrationTest.php | 11 ++- tests/Integration/Neo4jTransactionTest.php | 79 ---------------- 6 files changed, 55 insertions(+), 217 deletions(-) delete mode 100644 src/run_neo4j_query.php delete mode 100644 tests/Integration/Neo4jTransactionTest.php diff --git a/src/Transaction.php b/src/Transaction.php index 82a2d12f..214767f8 100644 --- a/src/Transaction.php +++ b/src/Transaction.php @@ -5,72 +5,75 @@ use GuzzleHttp\Client; use Neo4j\QueryAPI\Exception\Neo4jException; use Neo4j\QueryAPI\Results\ResultRow; +use Neo4j\QueryAPI\Results\ResultSet; use Psr\Http\Client\ClientInterface; -use Psr\Http\Client\RequestExceptionInterface; use stdClass; class Transaction { - - public function __construct(private ClientInterface $client, private string $clusterAffinity, private string $transactionId) - { + public function __construct( + private ClientInterface $client, + private string $clusterAffinity, + private string $transactionId + ) { } /** - * Create a node in Neo4j with a specified label and properties. + * Execute a Cypher query within the transaction. * * @param string $query The Cypher query to be executed. - * @param $parameters - * @return array The response data from Neo4j. + * @param array $parameters Parameters for the query. + * @return ResultSet The result rows in ResultSet format. + * @throws Neo4jException If the response structure is invalid. */ - public function run(string $query, array $parameters): array + public function run(string $query, array $parameters): ResultSet { - // Execute the request to the Neo4j server - $response = $this->client->post("/db/neo4j/query/v2/tx", [ - 'headers' => [ - 'neo4j-cluster-affinity' => $this->clusterAffinity, - ], - 'json' => [ + $response = $this->client->post("/db/neo4j/query/v2/tx/{$this->transactionId}", [ + 'headers' => [ + 'neo4j-cluster-affinity' => $this->clusterAffinity, + ], + 'json' => [ + 'statement' => $query, + 'parameters' => empty($parameters) ? new stdClass() : $parameters, + ], + ]); - 'statement' => $query, - 'parameters' => empty($parameters) ? new stdClass() : $parameters, // Pass the parameters array here + $responseBody = $response->getBody()->getContents(); + $data = json_decode($responseBody, true); - ], + if (!isset($data['data']['fields'], $data['data']['values'])) { + throw new Neo4jException([ + 'message' => 'Unexpected response structure from Neo4j', + 'response' => $data, ]); - - // Decode the response body - $data = json_decode($response->getBody()->getContents(), true); - - // Initialize the OGM (Object Graph Mapping) class - $ogm = new OGM(); - - // Extract keys (field names) and values (actual data) - $keys = $data['results'][0]['columns']; - $values = $data['results'][0]['data']; - - // Process each row of the result and map them using OGM - $rows = array_map(function ($resultRow) use ($ogm, $keys) { - $data = []; - foreach ($keys as $index => $key) { - $fieldData = $resultRow['row'][$index] ?? null; - $data[$key] = $ogm->map($fieldData); // Map the field data to the appropriate object format - } - return new ResultRow($data); // Wrap the mapped data in a ResultRow object - }, $values); - - return $rows; // Return the processed rows as an array of ResultRow objects - - + } + + $keys = $data['data']['fields']; + $values = $data['data']['values']; + + if (empty($values)) { + return new ResultSet([]); + } + + $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); } - - public function commit(): void { $this->client->post("/db/neo4j/query/v2/tx/{$this->transactionId}/commit", [ 'headers' => [ 'neo4j-cluster-affinity' => $this->clusterAffinity, - ] + ], ]); } @@ -79,7 +82,7 @@ public function rollback(): void $this->client->delete("/db/neo4j/query/v2/tx/{$this->transactionId}", [ 'headers' => [ 'neo4j-cluster-affinity' => $this->clusterAffinity, - ] + ], ]); } } diff --git a/src/query-api-test.php b/src/query-api-test.php index 779db7ef..0e1d0996 100644 --- a/src/query-api-test.php +++ b/src/query-api-test.php @@ -5,15 +5,12 @@ require __DIR__ . '/../vendor/autoload.php'; - -// Login to the Neo4j instance $api = Transaction::login( 'https://bb79fe35.databases.neo4j.io', 'neo4j', 'OXDRMgdWFKMcBRCBrIwXnKkwLgDlmFxipnywT6t_AK0' ); -// Run a query to fetch results $query = 'MATCH (n:Person) RETURN n.name LIMIT 10'; $results = $api->run($query, []); diff --git a/src/run_neo4j_query.php b/src/run_neo4j_query.php deleted file mode 100644 index 83faf532..00000000 --- a/src/run_neo4j_query.php +++ /dev/null @@ -1,82 +0,0 @@ - [] - ]); - - curl_setopt($ch, CURLOPT_POSTFIELDS, $payload); - - $response = curl_exec($ch); - if(curl_errno($ch)) { - echo 'Error starting transaction: ' . curl_error($ch); - return; - } - - echo "Transaction Start Response: "; - print_r($response); - - $http_status = curl_getinfo($ch, CURLINFO_HTTP_CODE); - if ($http_status === 403) { - echo "403 Forbidden: Check credentials and permissions.\n"; - } - - $transaction_data = json_decode($response, true); - - if (!isset($transaction_data['results'])) { - echo "Transaction creation failed or missing results. Response: "; - print_r($transaction_data); - return; - } - - $query_data = [ - "statements" => [ - [ - "statement" => $query - ] - ] - ]; - - curl_setopt($ch, CURLOPT_URL, $neo4j_url . '/db/neo4j/query/v2/tx'); - curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($query_data)); - - $response = curl_exec($ch); - if(curl_errno($ch)) { - echo 'Error running query: ' . curl_error($ch); - return; - } - - $commit_data = json_decode($response, true); - if (isset($commit_data['errors']) && count($commit_data['errors']) > 0) { - echo "Query error: " . $commit_data['errors'][0]['message']; - return; - } - - echo "Transaction successful. Data returned: "; - print_r($commit_data); - - curl_close($ch); -} - -$query = 'MATCH (n) RETURN n LIMIT 5'; - -runNeo4jTransaction($query); \ No newline at end of file diff --git a/src/runtransaction.php b/src/runtransaction.php index 7ff5a9f9..92ea990c 100644 --- a/src/runtransaction.php +++ b/src/runtransaction.php @@ -4,7 +4,6 @@ use Neo4j\QueryAPI\Neo4jQueryAPI; - $neo4jUrl = 'https://6f72daa1.databases.neo4j.io/'; $username = 'neo4j'; $password = '9lWmptqBgxBOz8NVcTJjgs3cHPyYmsy63ui6Spmw1d0'; @@ -15,9 +14,8 @@ $query = 'CREATE (n:Person {name: "Bobby"}) RETURN n'; -$response = $transaction->run($query); +$response = $transaction->run($query, []); print_r($response); $transaction->commit(); - diff --git a/tests/Integration/Neo4jQueryAPIIntegrationTest.php b/tests/Integration/Neo4jQueryAPIIntegrationTest.php index 7557778a..9f11ba68 100644 --- a/tests/Integration/Neo4jQueryAPIIntegrationTest.php +++ b/tests/Integration/Neo4jQueryAPIIntegrationTest.php @@ -47,26 +47,27 @@ public function testTransactionCommit(): void $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 + $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]); + $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]); + $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); + $results = $this->api->run("MATCH (x:Human {name: \$name}) RETURN x", ['name' => $name]); + $this->assertCount(1, $results); // Updated to expect 1 result } + /** * @throws GuzzleException */ diff --git a/tests/Integration/Neo4jTransactionTest.php b/tests/Integration/Neo4jTransactionTest.php deleted file mode 100644 index 4bc5ecdc..00000000 --- a/tests/Integration/Neo4jTransactionTest.php +++ /dev/null @@ -1,79 +0,0 @@ -transaction = new Transaction($this->neo4jUrl, $this->username, $this->password); - } - - /** - * Test case for committing a transaction. - */ - public function testTransactionCommit(): void - { - $transactionData = $this->transaction->startTransaction(); - $transactionId = $transactionData['transactionId']; - $clusterAffinity = $transactionData['clusterAffinity']; - - $name = 'TestHuman_' . mt_rand(1, 100000); - $query = "CREATE (x:Human {name: '$name'})"; - $this->transaction->run($query); - - $query = "MATCH (x:Human {name: '$name'}) RETURN x"; - $results = $this->transaction->run($query); - $this->assertCount(2, $results); - - $query = "MATCH (x:Human {name: '$name'}) RETURN x"; - $results = $this->transaction->run($query); - $this->assertCount(2, $results); - - $this->transaction->commit($transactionId, $clusterAffinity); - - $results = $this->transaction->run("MATCH (x:Human {name: '$name'}) RETURN x"); - $this->assertCount(2, $results); - } - - /** - * Test case for rolling back a transaction. - */ - public function testTransactionRollback(): void - { - - $transactionData = $this->transaction->startTransaction(); - $transactionId = $transactionData['transactionId']; - $clusterAffinity = $transactionData['clusterAffinity']; - - $name = 'TestHuman_' . mt_rand(1, 100000); - $query = "CREATE (x:Human {name: '$name'})"; - $this->transaction->run($query); - - - $query = "MATCH (x:Human {name: '$name'}) RETURN x"; - $results = $this->transaction->run($query); - $this->assertCount(2, $results); - - $query = "MATCH (x:Human {name: '$name'}) RETURN x"; - $results = $this->transaction->run($query); - $this->assertCount(2, $results); - - $rollbackResponse = $this->transaction->rollback($transactionId, $clusterAffinity); - $this->assertArrayHasKey('status', $rollbackResponse); - - $results = $this->transaction->run("MATCH (x:Human {name: '$name'}) RETURN x"); - $this->assertCount(2, $results); - } -} From d76b13e448d53b54642fd6c467563d8cac57e1bf Mon Sep 17 00:00:00 2001 From: p123-stack Date: Thu, 2 Jan 2025 17:54:43 +0530 Subject: [PATCH 2/4] Query Profile implementation script --- src/Query_Profile.php | 44 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 src/Query_Profile.php diff --git a/src/Query_Profile.php b/src/Query_Profile.php new file mode 100644 index 00000000..7242ae08 --- /dev/null +++ b/src/Query_Profile.php @@ -0,0 +1,44 @@ +post($neo4jUrl, [ + 'auth' => [$username, $password], + 'json' => [ + 'statement' => $query + ] +]); + +$body = $response->getBody(); +$data = json_decode($body, true); + +$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']; +} + +echo json_encode($output, JSON_PRETTY_PRINT); From 1e35cbbd2dbf8cc5fd0141e3a58499c978004fb0 Mon Sep 17 00:00:00 2001 From: p123-stack Date: Fri, 3 Jan 2025 17:42:22 +0530 Subject: [PATCH 3/4] Query Profile code --- .gitignore | 2 +- src/Profile.php | 61 ++++++++++++++++++++++++++ src/Query_Profile_run.php | 18 ++++++++ tests/Integration/ProfileTest.php | 73 +++++++++++++++++++++++++++++++ 4 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 src/Profile.php create mode 100644 src/Query_Profile_run.php create mode 100644 tests/Integration/ProfileTest.php diff --git a/.gitignore b/.gitignore index 2ace794d..9ac6f606 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,4 @@ vendor phpunit.xml test -.phpunit.result.cache \ No newline at end of file +.phpunit.result.cache diff --git a/src/Profile.php b/src/Profile.php new file mode 100644 index 00000000..7e0ba556 --- /dev/null +++ b/src/Profile.php @@ -0,0 +1,61 @@ +neo4jUrl = $url; + $this->username = $username; + $this->password = $password; + $this->client = new Client(); + } + + public function executeQuery($query) + { + $response = $this->client->post($this->neo4jUrl, [ + 'auth' => [$this->username, $this->password], + 'json' => [ + 'statement' => $query + ] + ]); + + 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/Query_Profile_run.php b/src/Query_Profile_run.php new file mode 100644 index 00000000..00cdfd08 --- /dev/null +++ b/src/Query_Profile_run.php @@ -0,0 +1,18 @@ +executeQuery($query); +$formattedResponse = $client->formatResponse($data); + +echo json_encode($formattedResponse, JSON_PRETTY_PRINT); + diff --git a/tests/Integration/ProfileTest.php b/tests/Integration/ProfileTest.php new file mode 100644 index 00000000..c2ed7116 --- /dev/null +++ b/tests/Integration/ProfileTest.php @@ -0,0 +1,73 @@ + [ + 'columns' => ['name'], + 'rows' => [['John Doe']], + ], + 'bookmarks' => ['bookmark1'], + ]; + + + $mock = new MockHandler([ + new Response(200, [], json_encode($mockResponseData)) + ]); + $handlerStack = HandlerStack::create($mock); + + $mockClient = new Client(['handler' => $handlerStack]); + + $profile = new Profile('http://mock-neo4j-url', 'user', 'password'); + $reflection = new \ReflectionClass(Profile::class); + $clientProperty = $reflection->getProperty('client'); + $clientProperty->setValue($profile, $mockClient); + + $query = 'MATCH (n:Person) RETURN n.name'; + $result = $profile->executeQuery($query); + + $this->assertIsArray($result); + $this->assertEquals($mockResponseData, $result); + } + + public function testFormatResponse(): void + { + $mockInputData = [ + 'result' => [ + 'columns' => ['name'], + 'rows' => [['John Doe']], + ], + 'profiledQueryPlan' => [ + 'plan' => 'Mock Plan', + ], + 'bookmarks' => ['bookmark1'], + ]; + + $expectedOutput = [ + 'data' => [ + 'fields' => ['name'], + 'values' => [['John Doe']], + ], + 'profiledQueryPlan' => [ + 'plan' => 'Mock Plan', + ], + 'bookmarks' => ['bookmark1'], + ]; + + $profile = new Profile('http://mock-neo4j-url', 'user', 'password'); + + $formattedResponse = $profile->formatResponse($mockInputData); + $this->assertEquals($expectedOutput, $formattedResponse); + } +} From ab0531130aea9a8c7ac1be251aeda27b175dec24 Mon Sep 17 00:00:00 2001 From: p123-stack Date: Mon, 6 Jan 2025 12:17:15 +0530 Subject: [PATCH 4/4] temp --- src/Profile.php | 5 +++-- src/Query_Profile_run.php | 10 +++++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/Profile.php b/src/Profile.php index 7e0ba556..c6a7b0ff 100644 --- a/src/Profile.php +++ b/src/Profile.php @@ -21,12 +21,13 @@ public function __construct($url, $username, $password) $this->client = new Client(); } - public function executeQuery($query) + public function executeQuery($query ,$parameters=[]) { $response = $this->client->post($this->neo4jUrl, [ 'auth' => [$this->username, $this->password], 'json' => [ - 'statement' => $query + 'statement' => $query, + 'parameters'=>$parameters ] ]); diff --git a/src/Query_Profile_run.php b/src/Query_Profile_run.php index 00cdfd08..d6f33081 100644 --- a/src/Query_Profile_run.php +++ b/src/Query_Profile_run.php @@ -10,9 +10,13 @@ $client = new Profile($neo4jUrl, $username, $password); -$query = "PROFILE MATCH (n:Person) RETURN n"; -$data = $client->executeQuery($query); +$params = ['name' => 'Alice']; + +$query = "PROFILE MATCH (n:Person {name: \$name}) RETURN n.name"; + +$data = $client->executeQuery($query, $params); + + $formattedResponse = $client->formatResponse($data); echo json_encode($formattedResponse, JSON_PRETTY_PRINT); -