Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
eb0772e
temp commit
p123-stack Apr 7, 2026
2875987
feat(bolt,testkit): TestKit result iteration, SummarizedResult::list(…
p123-stack Apr 8, 2026
9d7010d
Fixed iteration tests
p123-stack Apr 8, 2026
d515b97
fix(bolt): only consumeResults on STREAMING, not TX_STREAMING (nested…
p123-stack Apr 15, 2026
cb0e8b0
Fixed psalm errors
p123-stack Apr 15, 2026
1171cd8
Add CypherList::peek() for non-consuming next record; document in README
p123-stack Apr 27, 2026
4aba77a
fixed psalm errors
p123-stack Apr 28, 2026
ca4fbce
fixed code standards
p123-stack Apr 28, 2026
4b6c132
fix: use Bolt Path indices and require stefanak-michal/bolt ^7.4
p123-stack May 4, 2026
315d344
refactor(bolt): remove Bolt protocol v3 handling and related TestKit …
p123-stack May 4, 2026
c4103c1
fixed merged conflicts and solved failed tests
p123-stack May 6, 2026
9058cd1
fix(testkit-backend): repair ResultSingle and duplicate returns in Re…
p123-stack May 6, 2026
d496c9e
fixed merge conflicts and solved psalm errors
p123-stack May 29, 2026
df24959
fix: clean up merge conflicts in ConnectionPool and TestKit tests
p123-stack Jun 1, 2026
186586c
Merge remote-tracking branch 'origin/main' into stub-iteration-1
p123-stack Jun 16, 2026
ae3fd95
Add TestKit stub support for Bolt datatypes (#305)
p123-stack Jun 20, 2026
f5b6dd6
Fixed iteration tests
p123-stack Apr 8, 2026
e469139
Fixed psalm errors
p123-stack Apr 15, 2026
bf53937
refactor(bolt): remove Bolt protocol v3 handling and related TestKit …
p123-stack May 4, 2026
7253c4c
fixed merged conflicts and solved failed tests
p123-stack May 6, 2026
3650555
fix: clean up merge conflicts in ConnectionPool and TestKit tests
p123-stack Jun 1, 2026
6721281
temp commit
p123-stack Jun 22, 2026
3e259c4
fixed merge conflicts
p123-stack Jun 22, 2026
657cd44
fixed merge conflicts
p123-stack Jun 22, 2026
928835b
mend
p123-stack Jun 22, 2026
f7a1df3
Fix partial PULL failure handling in BoltResult for TestKit iteration…
p123-stack Jun 22, 2026
3452ab8
Merge remote-tracking branch 'origin/main' into stub-iteration-1
p123-stack Jun 22, 2026
9cf6d1c
fixed merge conflicts
p123-stack Jun 22, 2026
45b9157
temp commit
p123-stack Jul 1, 2026
e3331a7
Fix TestKit iteration error handling when PULL returns records follow…
p123-stack Jul 1, 2026
d4578b8
Remove redundant row normalization from PullResult and type records a…
p123-stack Jul 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 22 additions & 9 deletions src/Bolt/BoltConnection.php
Original file line number Diff line number Diff line change
Expand Up @@ -336,13 +336,12 @@ public function protocol(): V4_4|V5|V5_1|V5_2|V5_3|V5_4
* Pulls a result set.
*
* Any of the preconditioned states are: 'TX_READY', 'INTERRUPTED'.
*
* @return non-empty-list<list>
*/
public function pull(?int $qid, ?int $fetchSize): array
public function pull(?int $qid, ?int $fetchSize): PullResult
{
$extra = $this->buildResultExtra($fetchSize, $qid);

/** @var list<array<array-key, mixed>> $tbr */
$tbr = [];
$message = $this->messageFactory->createPullMessage($extra);

Expand All @@ -357,25 +356,39 @@ public function pull(?int $qid, ?int $fetchSize): array
}

foreach ($message->send()->getResponses() as $response) {
$this->assertNoFailure($response);
if ($response->signature === Signature::FAILURE) {
$this->logger?->log(LogLevel::ERROR, 'FAILURE', $response->content);
$this->subscribedResults = [];
$failure = Neo4jException::fromBoltResponse($response);
if ($tbr !== []) {
$this->reset();

return PullResult::withDeferredFailure($tbr, $failure);
}

throw $failure;
}

$tbr[] = $response->content;
}

$this->restoreOriginalTimeout();

/** @var non-empty-list<list> */
return $tbr;
if ($tbr === []) {
throw new Exception('PULL returned no responses');
}

return PullResult::complete($tbr);
} catch (Throwable $e) {
$this->restoreOriginalTimeout();
// If we've received some records before the disconnect, return them so first next() succeeds.
// Second next() must pull again and fail with a connection error (TestKit exit_after_record scripts).
// Do not append []: BoltResult treats trailing empty SUCCESS as stream completion, so the iterator
// would stop cleanly instead of surfacing the disconnect. A synthetic has_more:true means "not done".
if (!empty($tbr)) {
if ($tbr !== []) {
$tbr[] = ['has_more' => true];

/** @var non-empty-list<list> */
return $tbr;
return PullResult::complete($tbr);
}
throw $e;
}
Expand Down
38 changes: 22 additions & 16 deletions src/Bolt/BoltResult.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,32 +14,30 @@
namespace Laudis\Neo4j\Bolt;

use function array_key_exists;
use function array_splice;

use Bolt\error\BoltException;
use Bolt\error\ConnectException as BoltConnectException;

use function count;

use Generator;

use function in_array;
use function is_array;

use Iterator;
use Laudis\Neo4j\Exception\Neo4jException;
use Laudis\Neo4j\Formatter\SummarizedResultFormatter;
use Throwable;

/**
* @psalm-import-type BoltCypherStats from SummarizedResultFormatter
*
* @implements Iterator<int, list<mixed>>
* @implements Iterator<int, array<array-key, mixed>>
*/
final class BoltResult implements Iterator
{
/** @var list<list> */
/** @var list<array<array-key, mixed>> */
private array $rows = [];
private ?array $meta = null;
private ?Neo4jException $deferredFailure = null;
/** @var list<(callable(array):void)> */
private array $finishedCallbacks = [];

Expand Down Expand Up @@ -92,7 +90,7 @@ public function addFinishedCallback(callable $finishedCallback): void
}

/**
* @return Generator<int, list>
* @return Generator<int, array<array-key, mixed>>
*/
public function getIt(): Generator
{
Expand All @@ -104,7 +102,7 @@ public function getIt(): Generator
}

/**
* @return Generator<int, list<mixed>>
* @return Generator<int, array<array-key, mixed>>
*/
public function iterator(): Generator
{
Expand Down Expand Up @@ -143,24 +141,32 @@ public function consume(): array

private function fetchResults(): void
{
if ($this->deferredFailure !== null) {
throw $this->deferredFailure;
}

$this->networkPullOccurred = true;

try {
$meta = $this->connection->pull($this->qid, $this->effectivePullSize());
$pullResult = $this->connection->pull($this->qid, $this->effectivePullSize());
} catch (BoltConnectException|BoltException $e) {
// Invalidate connection on socket/network errors so pool does not reuse it.
// Rethrow as-is - Session retry logic inspects the actual exception via isConnectionError().
$this->connection->invalidate();
throw $e;
}
// Neo4jException and other Throwable propagate naturally - no invalidate needed for server errors
// $meta is non-empty: {@see BoltConnection::pull()} is contractually non-empty-list<list>.

/** @var list<list> $rows */
$rows = array_splice($meta, 0, count($meta) - 1);
$this->rows = $rows;
$this->rows = $pullResult->getRecordRows();

$deferredFailure = $pullResult->getDeferredFailure();
if ($deferredFailure !== null) {
$this->deferredFailure = $deferredFailure;

return;
}

$summarySlot = $meta[0] ?? null;
$summarySlot = $pullResult->getSummary();
if (!is_array($summarySlot)) {
// No summary received (connection closed before summary)
$this->meta = null;
Expand All @@ -169,7 +175,7 @@ private function fetchResults(): void
}

$summaryEmpty = $summarySlot === [];
$hasDataRows = $rows !== [];
$hasDataRows = $this->rows !== [];

if ($summaryEmpty && !$hasDataRows) {
// Normal completion with no records
Expand All @@ -188,7 +194,7 @@ private function fetchResults(): void
/**
* @psalm-suppress InvalidNullableReturnType
*
* @return list<mixed>
* @return array<array-key, mixed>
*/
public function current(): array
{
Expand Down
76 changes: 76 additions & 0 deletions src/Bolt/PullResult.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php

declare(strict_types=1);

/*
* This file is part of the Neo4j PHP Client and Driver package.
*
* (c) Nagels <https://nagels.tech>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Laudis\Neo4j\Bolt;

use Laudis\Neo4j\Exception\Neo4jException;

/**
* Internal result of a Bolt PULL. May carry a deferred server failure when
* RECORD rows were received before a Bolt FAILURE in the same pull response.
*
* @internal
*/
final class PullResult
{
/**
* @param list<array<array-key, mixed>> $recordRows
* @param array<string, mixed>|null $summary
*/
private function __construct(
private readonly array $recordRows,
private readonly ?array $summary,
private readonly ?Neo4jException $deferredFailure,
) {
}

/**
* @param non-empty-list<array<array-key, mixed>> $content record rows followed by a summary map
*/
public static function complete(array $content): self
{
/** @var array<string, mixed> $summary */
$summary = array_pop($content);

return new self($content, $summary, null);
}

/**
* @param non-empty-list<array<array-key, mixed>> $bufferedRows
*/
public static function withDeferredFailure(array $bufferedRows, Neo4jException $failure): self
{
return new self($bufferedRows, null, $failure);
}

/**
* @return list<array<array-key, mixed>>
*/
public function getRecordRows(): array
{
return $this->recordRows;
}

/**
* @return array<string, mixed>|null
*/
public function getSummary(): ?array
{
return $this->summary;
}

public function getDeferredFailure(): ?Neo4jException
{
return $this->deferredFailure;
}
}
2 changes: 1 addition & 1 deletion testkit-backend/features.php
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@
// notified when the server reports a token expired.
'Feature:Auth:Managed' => false,
// The driver supports Bolt protocol version 3
'Feature:Bolt:3.0' => true,
'Feature:Bolt:3.0' => false,
// The driver supports Bolt protocol version 4.1
'Feature:Bolt:4.1' => true,
// The driver supports Bolt protocol version 4.2
Expand Down
16 changes: 12 additions & 4 deletions testkit-backend/testkit.sh
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ echo ""

### Failing/error tests
#python3 -m unittest -vvv \
# tests.stub.iteration.test_iteration_session_run.TestIterationSessionRun.test_error
# tests.stub.datatypes.test_vector_types.TestVectorTypes.test_vector \
# tests.stub.datatypes.test_unsupported_type.TestUnsupportedTypes.test_unsupported_type \
# tests.stub.datatypes.test_temporal_types.TestTemporalTypesV4x4.test_date_time_with_patch \
Expand Down Expand Up @@ -71,7 +72,7 @@ echo ""
#
# Passing tests (commented out)
# python3 -m unittest -vvv \

#
python3 -m unittest -vvv \
tests.neo4j.test_authentication.TestAuthenticationBasic.test_error_on_incorrect_credentials \
tests.neo4j.test_authentication.TestAuthenticationBasic.test_success_on_basic_token \
Expand Down Expand Up @@ -179,13 +180,20 @@ python3 -m unittest -vvv \
tests.stub.iteration.test_result_peek.TestResultPeek.test_result_peek_with_0_records \
tests.stub.iteration.test_result_peek.TestResultPeek.test_result_peek_with_1_records \
tests.stub.iteration.test_result_peek.TestResultPeek.test_result_peek_with_2_records \
tests.stub.iteration.test_iteration_session_run.TestIterationSessionRun.test_error \
tests.stub.iteration.test_iteration_tx_run.TestIterationTxRun.test_batch \
tests.stub.iteration.test_iteration_tx_run.TestIterationTxRun.test_all \
tests.stub.iteration.test_iteration_tx_run.TestIterationTxRun.test_all_slow_connection \
tests.stub.iteration.test_iteration_tx_run.TestIterationTxRun.test_nested \
tests.stub.iteration.test_iteration_tx_run.TestIterationTxRun.test_nested_using_list \
tests.stub.iteration.test_iteration_session_run.TestIterationSessionRun.test_full_batch \
tests.stub.iteration.test_iteration_session_run.TestIterationSessionRun.test_half_batch \
tests.stub.iteration.test_iteration_session_run.TestIterationSessionRun.test_empty_batch \
tests.stub.iteration.test_iteration_session_run.TestIterationSessionRun.test_error \
tests.stub.iteration.test_iteration_session_run.TestIterationSessionRun.test_all \
tests.stub.iteration.test_iteration_tx_run.TestIterationTxRun.test_batch \
tests.stub.iteration.test_iteration_tx_run.TestIterationTxRun.test_all \
tests.stub.iteration.test_iteration_tx_run.TestIterationTxRun.test_nested \
tests.stub.iteration.test_iteration_session_run.TestIterationSessionRun.test_all_slow_connection \
tests.stub.iteration.test_iteration_session_run.TestIterationSessionRun.test_nested \
tests.stub.iteration.test_iteration_session_run.TestIterationSessionRun.test_nested_using_list \
tests.stub.driver_parameters.telemetry.test_telemetry.TestTelemetry.test_execute_read \
tests.stub.driver_parameters.telemetry.test_telemetry.TestTelemetry.test_execute_write \
tests.stub.driver_parameters.telemetry.test_telemetry.TestTelemetry.test_begin_transaction \
Expand Down
Loading