Skip to content

Commit f3c3652

Browse files
authored
Fixed iteration tests (#303)
1 parent 15e7078 commit f3c3652

5 files changed

Lines changed: 133 additions & 30 deletions

File tree

src/Bolt/BoltConnection.php

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -336,13 +336,12 @@ public function protocol(): V4_4|V5|V5_1|V5_2|V5_3|V5_4
336336
* Pulls a result set.
337337
*
338338
* Any of the preconditioned states are: 'TX_READY', 'INTERRUPTED'.
339-
*
340-
* @return non-empty-list<list>
341339
*/
342-
public function pull(?int $qid, ?int $fetchSize): array
340+
public function pull(?int $qid, ?int $fetchSize): PullResult
343341
{
344342
$extra = $this->buildResultExtra($fetchSize, $qid);
345343

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

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

359358
foreach ($message->send()->getResponses() as $response) {
360-
$this->assertNoFailure($response);
359+
if ($response->signature === Signature::FAILURE) {
360+
$this->logger?->log(LogLevel::ERROR, 'FAILURE', $response->content);
361+
$this->subscribedResults = [];
362+
$failure = Neo4jException::fromBoltResponse($response);
363+
if ($tbr !== []) {
364+
$this->reset();
365+
366+
return PullResult::withDeferredFailure($tbr, $failure);
367+
}
368+
369+
throw $failure;
370+
}
371+
361372
$tbr[] = $response->content;
362373
}
363374

364375
$this->restoreOriginalTimeout();
365376

366-
/** @var non-empty-list<list> */
367-
return $tbr;
377+
if ($tbr === []) {
378+
throw new Exception('PULL returned no responses');
379+
}
380+
381+
return PullResult::complete($tbr);
368382
} catch (Throwable $e) {
369383
$this->restoreOriginalTimeout();
370384
// If we've received some records before the disconnect, return them so first next() succeeds.
371385
// Second next() must pull again and fail with a connection error (TestKit exit_after_record scripts).
372386
// Do not append []: BoltResult treats trailing empty SUCCESS as stream completion, so the iterator
373387
// would stop cleanly instead of surfacing the disconnect. A synthetic has_more:true means "not done".
374-
if (!empty($tbr)) {
388+
if ($tbr !== []) {
375389
$tbr[] = ['has_more' => true];
376390

377-
/** @var non-empty-list<list> */
378-
return $tbr;
391+
return PullResult::complete($tbr);
379392
}
380393
throw $e;
381394
}

src/Bolt/BoltResult.php

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,32 +14,30 @@
1414
namespace Laudis\Neo4j\Bolt;
1515

1616
use function array_key_exists;
17-
use function array_splice;
1817

1918
use Bolt\error\BoltException;
2019
use Bolt\error\ConnectException as BoltConnectException;
21-
22-
use function count;
23-
2420
use Generator;
2521

2622
use function in_array;
2723
use function is_array;
2824

2925
use Iterator;
26+
use Laudis\Neo4j\Exception\Neo4jException;
3027
use Laudis\Neo4j\Formatter\SummarizedResultFormatter;
3128
use Throwable;
3229

3330
/**
3431
* @psalm-import-type BoltCypherStats from SummarizedResultFormatter
3532
*
36-
* @implements Iterator<int, list<mixed>>
33+
* @implements Iterator<int, array<array-key, mixed>>
3734
*/
3835
final class BoltResult implements Iterator
3936
{
40-
/** @var list<list> */
37+
/** @var list<array<array-key, mixed>> */
4138
private array $rows = [];
4239
private ?array $meta = null;
40+
private ?Neo4jException $deferredFailure = null;
4341
/** @var list<(callable(array):void)> */
4442
private array $finishedCallbacks = [];
4543

@@ -92,7 +90,7 @@ public function addFinishedCallback(callable $finishedCallback): void
9290
}
9391

9492
/**
95-
* @return Generator<int, list>
93+
* @return Generator<int, array<array-key, mixed>>
9694
*/
9795
public function getIt(): Generator
9896
{
@@ -104,7 +102,7 @@ public function getIt(): Generator
104102
}
105103

106104
/**
107-
* @return Generator<int, list<mixed>>
105+
* @return Generator<int, array<array-key, mixed>>
108106
*/
109107
public function iterator(): Generator
110108
{
@@ -143,24 +141,32 @@ public function consume(): array
143141

144142
private function fetchResults(): void
145143
{
144+
if ($this->deferredFailure !== null) {
145+
throw $this->deferredFailure;
146+
}
147+
146148
$this->networkPullOccurred = true;
147149

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

159-
/** @var list<list> $rows */
160-
$rows = array_splice($meta, 0, count($meta) - 1);
161-
$this->rows = $rows;
160+
$this->rows = $pullResult->getRecordRows();
161+
162+
$deferredFailure = $pullResult->getDeferredFailure();
163+
if ($deferredFailure !== null) {
164+
$this->deferredFailure = $deferredFailure;
165+
166+
return;
167+
}
162168

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

171177
$summaryEmpty = $summarySlot === [];
172-
$hasDataRows = $rows !== [];
178+
$hasDataRows = $this->rows !== [];
173179

174180
if ($summaryEmpty && !$hasDataRows) {
175181
// Normal completion with no records
@@ -188,7 +194,7 @@ private function fetchResults(): void
188194
/**
189195
* @psalm-suppress InvalidNullableReturnType
190196
*
191-
* @return list<mixed>
197+
* @return array<array-key, mixed>
192198
*/
193199
public function current(): array
194200
{

src/Bolt/PullResult.php

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of the Neo4j PHP Client and Driver package.
7+
*
8+
* (c) Nagels <https://nagels.tech>
9+
*
10+
* For the full copyright and license information, please view the LICENSE
11+
* file that was distributed with this source code.
12+
*/
13+
14+
namespace Laudis\Neo4j\Bolt;
15+
16+
use Laudis\Neo4j\Exception\Neo4jException;
17+
18+
/**
19+
* Internal result of a Bolt PULL. May carry a deferred server failure when
20+
* RECORD rows were received before a Bolt FAILURE in the same pull response.
21+
*
22+
* @internal
23+
*/
24+
final class PullResult
25+
{
26+
/**
27+
* @param list<array<array-key, mixed>> $recordRows
28+
* @param array<string, mixed>|null $summary
29+
*/
30+
private function __construct(
31+
private readonly array $recordRows,
32+
private readonly ?array $summary,
33+
private readonly ?Neo4jException $deferredFailure,
34+
) {
35+
}
36+
37+
/**
38+
* @param non-empty-list<array<array-key, mixed>> $content record rows followed by a summary map
39+
*/
40+
public static function complete(array $content): self
41+
{
42+
/** @var array<string, mixed> $summary */
43+
$summary = array_pop($content);
44+
45+
return new self($content, $summary, null);
46+
}
47+
48+
/**
49+
* @param non-empty-list<array<array-key, mixed>> $bufferedRows
50+
*/
51+
public static function withDeferredFailure(array $bufferedRows, Neo4jException $failure): self
52+
{
53+
return new self($bufferedRows, null, $failure);
54+
}
55+
56+
/**
57+
* @return list<array<array-key, mixed>>
58+
*/
59+
public function getRecordRows(): array
60+
{
61+
return $this->recordRows;
62+
}
63+
64+
/**
65+
* @return array<string, mixed>|null
66+
*/
67+
public function getSummary(): ?array
68+
{
69+
return $this->summary;
70+
}
71+
72+
public function getDeferredFailure(): ?Neo4jException
73+
{
74+
return $this->deferredFailure;
75+
}
76+
}

testkit-backend/features.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@
9999
// notified when the server reports a token expired.
100100
'Feature:Auth:Managed' => false,
101101
// The driver supports Bolt protocol version 3
102-
'Feature:Bolt:3.0' => true,
102+
'Feature:Bolt:3.0' => false,
103103
// The driver supports Bolt protocol version 4.1
104104
'Feature:Bolt:4.1' => true,
105105
// The driver supports Bolt protocol version 4.2

testkit-backend/testkit.sh

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ echo ""
4343

4444
### Failing/error tests
4545
#python3 -m unittest -vvv \
46+
# tests.stub.iteration.test_iteration_session_run.TestIterationSessionRun.test_error
4647
# tests.stub.datatypes.test_vector_types.TestVectorTypes.test_vector \
4748
# tests.stub.datatypes.test_unsupported_type.TestUnsupportedTypes.test_unsupported_type \
4849
# tests.stub.datatypes.test_temporal_types.TestTemporalTypesV4x4.test_date_time_with_patch \
@@ -71,7 +72,7 @@ echo ""
7172
#
7273
# Passing tests (commented out)
7374
# python3 -m unittest -vvv \
74-
75+
#
7576
python3 -m unittest -vvv \
7677
tests.neo4j.test_authentication.TestAuthenticationBasic.test_error_on_incorrect_credentials \
7778
tests.neo4j.test_authentication.TestAuthenticationBasic.test_success_on_basic_token \
@@ -179,13 +180,20 @@ python3 -m unittest -vvv \
179180
tests.stub.iteration.test_result_peek.TestResultPeek.test_result_peek_with_0_records \
180181
tests.stub.iteration.test_result_peek.TestResultPeek.test_result_peek_with_1_records \
181182
tests.stub.iteration.test_result_peek.TestResultPeek.test_result_peek_with_2_records \
183+
tests.stub.iteration.test_iteration_session_run.TestIterationSessionRun.test_error \
184+
tests.stub.iteration.test_iteration_tx_run.TestIterationTxRun.test_batch \
185+
tests.stub.iteration.test_iteration_tx_run.TestIterationTxRun.test_all \
186+
tests.stub.iteration.test_iteration_tx_run.TestIterationTxRun.test_all_slow_connection \
187+
tests.stub.iteration.test_iteration_tx_run.TestIterationTxRun.test_nested \
188+
tests.stub.iteration.test_iteration_tx_run.TestIterationTxRun.test_nested_using_list \
182189
tests.stub.iteration.test_iteration_session_run.TestIterationSessionRun.test_full_batch \
183190
tests.stub.iteration.test_iteration_session_run.TestIterationSessionRun.test_half_batch \
184191
tests.stub.iteration.test_iteration_session_run.TestIterationSessionRun.test_empty_batch \
192+
tests.stub.iteration.test_iteration_session_run.TestIterationSessionRun.test_error \
185193
tests.stub.iteration.test_iteration_session_run.TestIterationSessionRun.test_all \
186-
tests.stub.iteration.test_iteration_tx_run.TestIterationTxRun.test_batch \
187-
tests.stub.iteration.test_iteration_tx_run.TestIterationTxRun.test_all \
188-
tests.stub.iteration.test_iteration_tx_run.TestIterationTxRun.test_nested \
194+
tests.stub.iteration.test_iteration_session_run.TestIterationSessionRun.test_all_slow_connection \
195+
tests.stub.iteration.test_iteration_session_run.TestIterationSessionRun.test_nested \
196+
tests.stub.iteration.test_iteration_session_run.TestIterationSessionRun.test_nested_using_list \
189197
tests.stub.driver_parameters.telemetry.test_telemetry.TestTelemetry.test_execute_read \
190198
tests.stub.driver_parameters.telemetry.test_telemetry.TestTelemetry.test_execute_write \
191199
tests.stub.driver_parameters.telemetry.test_telemetry.TestTelemetry.test_begin_transaction \

0 commit comments

Comments
 (0)