diff --git a/src/Formatter/SummarizedResultFormatter.php b/src/Formatter/SummarizedResultFormatter.php index e56c093b..6a6d8531 100644 --- a/src/Formatter/SummarizedResultFormatter.php +++ b/src/Formatter/SummarizedResultFormatter.php @@ -86,8 +86,8 @@ * constraints-added?: int, * constraints-removed?: int, * contains-updates?: bool, - * contains-system-updates?: bool, - * system-updates?: int, + * contains-system-updates?: bool|int, + * system-updates?: int|bool, * db?: string * } * @psalm-type CypherError = array{code: string, message: string} @@ -138,6 +138,21 @@ public function formatBoltStats(array $response): SummaryCounters } } + $systemUpdates = $stats['system-updates'] ?? 0; + if (is_bool($systemUpdates)) { + $systemUpdates = (int) $systemUpdates; + } + + $containsSystemUpdates = $stats['contains-system-updates'] ?? null; + + if ($containsSystemUpdates === null) { + $containsSystemUpdates = $systemUpdates > 0; + } else { + if (!is_bool($containsSystemUpdates)) { + $containsSystemUpdates = (bool) $containsSystemUpdates; + } + } + return new SummaryCounters( $stats['nodes-created'] ?? 0, $stats['nodes-deleted'] ?? 0, @@ -151,8 +166,8 @@ public function formatBoltStats(array $response): SummaryCounters $stats['constraints-added'] ?? 0, $stats['constraints-removed'] ?? 0, $updateCount > 0, - ($stats['contains-system-updates'] ?? $stats['system-updates'] ?? 0) >= 1, - $stats['system-updates'] ?? 0 + $containsSystemUpdates, + $systemUpdates ); } @@ -195,10 +210,11 @@ function (mixed $response) use ($connection, $statement, $runStart, $resultAvail $formattedResult = $this->processBoltResult($meta, $result, $connection, $holder); - /** - * @var SummarizedResult> - */ - return new SummarizedResult($summary, (new CypherList($formattedResult))->withCacheLimit($result->getFetchSize())); + /** @var SummarizedResult */ + $result = (new CypherList($formattedResult))->withCacheLimit($result->getFetchSize()); + // $keys = $meta['fields']; + + return new SummarizedResult($summary, $result); } public function formatArgs(array $profiledPlanData): PlanArguments @@ -255,7 +271,7 @@ private function formatProfiledPlan(array $profiledPlanData): ProfiledQueryPlan pageCacheHitRatio: (float) ($profiledPlanData['pageCacheHitRatio'] ?? 0.0), time: (int) ($profiledPlanData['time'] ?? 0), operatorType: $profiledPlanData['operatorType'] ?? '', - children: array_map([$this, 'formatProfiledPlan'], $profiledPlanData['children'] ?? []), + children: array_values(array_map([$this, 'formatProfiledPlan'], $profiledPlanData['children'] ?? [])), identifiers: $profiledPlanData['identifiers'] ?? [] ); } @@ -309,7 +325,7 @@ private function formatPlan(array $plan): Plan { return new Plan( $this->formatArgs($plan['args']), - array_map($this->formatPlan(...), $plan['children'] ?? []), + array_values(array_map($this->formatPlan(...), $plan['children'] ?? [])), $plan['identifiers'] ?? [], $plan['operatorType'] ?? '' ); diff --git a/tests/Integration/SummarizedResultFormatterTest.php b/tests/Integration/SummarizedResultFormatterTest.php index 0bc86131..1d83014a 100644 --- a/tests/Integration/SummarizedResultFormatterTest.php +++ b/tests/Integration/SummarizedResultFormatterTest.php @@ -24,6 +24,8 @@ use Laudis\Neo4j\Contracts\TransactionInterface; use Laudis\Neo4j\Databags\SummarizedResult; use Laudis\Neo4j\Databags\SummaryCounters; +use Laudis\Neo4j\Formatter\Specialised\BoltOGMTranslator; +use Laudis\Neo4j\Formatter\SummarizedResultFormatter; use Laudis\Neo4j\Tests\EnvironmentAwareIntegrationTest; use Laudis\Neo4j\Types\CartesianPoint; use Laudis\Neo4j\Types\CypherList; @@ -521,4 +523,98 @@ private function articlesQuery(): string article.readingTime = duration(articleProperties.readingTime) CYPHER; } + + public function testFormatBoltStatsWithFalseSystemUpdates(): void + { + $formatter = new SummarizedResultFormatter(new BoltOGMTranslator()); + + $response = [ + 'stats' => [ + 'nodes-created' => 1, + 'nodes-deleted' => 0, + 'relationships-created' => 0, + 'relationships-deleted' => 0, + 'properties-set' => 2, + 'labels-added' => 1, + 'labels-removed' => 0, + 'indexes-added' => 0, + 'indexes-removed' => 0, + 'constraints-added' => 0, + 'constraints-removed' => 0, + 'contains-updates' => true, + 'contains-system-updates' => false, + 'system-updates' => false, + ], + ]; + + $counters = $formatter->formatBoltStats($response); + + self::assertInstanceOf(SummaryCounters::class, $counters); + self::assertEquals(1, $counters->nodesCreated()); + self::assertEquals(2, $counters->propertiesSet()); + self::assertSame(0, $counters->systemUpdates()); + } + + public function testSystemUpdatesWithPotentialFalseValues(): void + { + $this->getSession()->run('CREATE INDEX duplicate_test_index IF NOT EXISTS FOR (n:TestSystemUpdates) ON (n.duplicateProperty)'); + $result = $this->getSession()->run('CREATE INDEX duplicate_test_index IF NOT EXISTS FOR (n:TestSystemUpdates) ON (n.duplicateProperty)'); + + $summary = $result->getSummary(); + $counters = $summary->getCounters(); + + // For duplicate index creation (IF NOT EXISTS), might not create system updates + $this->assertGreaterThanOrEqual(0, $counters->systemUpdates()); + // containsSystemUpdates should be consistent with systemUpdates count + $this->assertEquals($counters->systemUpdates() > 0, $counters->containsSystemUpdates()); + + $result2 = $this->getSession()->run('DROP INDEX non_existent_test_index IF EXISTS'); + + $summary2 = $result2->getSummary(); + $counters2 = $summary2->getCounters(); + + // Dropping non-existent index should not create system updates + $this->assertEquals(0, $counters2->systemUpdates()); + $this->assertFalse($counters2->containsSystemUpdates()); + + $this->getSession()->run('DROP INDEX duplicate_test_index IF EXISTS'); + } + + public function testMultipleSystemOperationsForBug(): void + { + $operations = [ + 'CREATE INDEX multi_test_1 IF NOT EXISTS FOR (n:MultiTestNode) ON (n.prop1)', + 'CREATE INDEX multi_test_2 IF NOT EXISTS FOR (n:MultiTestNode) ON (n.prop2)', + 'CREATE CONSTRAINT multi_test_constraint IF NOT EXISTS FOR (n:MultiTestNode) REQUIRE n.id IS UNIQUE', + 'DROP INDEX multi_test_1 IF EXISTS', + 'DROP INDEX multi_test_2 IF EXISTS', + 'DROP CONSTRAINT multi_test_constraint IF EXISTS', + ]; + + foreach ($operations as $operation) { + $result = $this->getSession()->run($operation); + + $summary = $result->getSummary(); + $counters = $summary->getCounters(); + + // Test that system operations properly track system updates + $this->assertGreaterThanOrEqual(0, $counters->systemUpdates()); + // Verify consistency between systemUpdates count and containsSystemUpdates flag + $this->assertEquals($counters->systemUpdates() > 0, $counters->containsSystemUpdates()); + } + } + + public function testRegularDataOperationsStillWork(): void + { + $result = $this->getSession()->run('CREATE (n:RegularTestNode {name: "test", id: $id}) RETURN n', ['id' => bin2hex(random_bytes(8))]); + + $summary = $result->getSummary(); + $counters = $summary->getCounters(); + + // Regular data operations should not involve system updates + $this->assertEquals(0, $counters->systemUpdates()); + $this->assertFalse($counters->containsSystemUpdates()); + + $this->getSession()->run('MATCH (n:RegularTestNode) DELETE n'); + } }