Passed
Branch psalm-3 (d5d890)
by Wilmer
02:56
created

BatchQueryResult::reset()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 6
nc 2
nop 0
dl 0
loc 10
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Query;
6
7
use Iterator;
8
use PDOException;
9
use Throwable;
10
use Yiisoft\Db\Connection\ConnectionInterface;
11
use Yiisoft\Db\Exception\Exception;
12
use Yiisoft\Db\Exception\InvalidConfigException;
13
use Yiisoft\Db\Query\Data\DataReader;
14
15
use function current;
16
use function key;
17
use function next;
18
use function reset;
19
20
/**
21
 * BatchQueryResult represents a batch query from which you can retrieve data in batches.
22
 *
23
 * You usually do not instantiate BatchQueryResult directly. Instead, you obtain it by calling {@see Query::batch()} or
24
 * {@see Query::each()}. Because BatchQueryResult implements the {@see Iterator} interface, you can iterate it to
25
 * obtain a batch of data in each iteration.
26
 *
27
 * For example,
28
 *
29
 * ```php
30
 * $query = (new Query)->from('user');
31
 * foreach ($query->batch() as $i => $users) {
32
 *     // $users represents the rows in the $i-th batch
33
 * }
34
 * foreach ($query->each() as $user) {
35
 * }
36
 * ```
37
 */
38
class BatchQueryResult implements Iterator
39
{
40
    private int $batchSize = 100;
41
    private $key;
42
43
    /**
44
     * @var DataReader|null the data reader associated with this batch query.
45
     */
46
    private ?DataReader $dataReader = null;
47
48
    /**
49
     * @var array|null the data retrieved in the current batch
50
     */
51
    private ?array $batch = null;
52
53
    /**
54
     * @var mixed the value for the current iteration
55
     */
56
    private $value;
57
58
    /**
59
     * @var int MSSQL error code for exception that is thrown when last batch is size less than specified batch size
60
     *
61
     * {@see https://github.com/yiisoft/yii2/issues/10023}
62
     */
63
    private int $mssqlNoMoreRowsErrorCode = -13;
64
65
    public function __construct(
66
        private ConnectionInterface $db,
67
        private QueryInterface $query,
68
        private bool $each = false
69
    ) {
70
    }
71
72
    public function __destruct()
73
    {
74
        $this->reset();
75
    }
76
77
    /**
78
     * Resets the batch query.
79
     *
80
     * This method will clean up the existing batch query so that a new batch query can be performed.
81
     */
82
    public function reset(): void
83
    {
84
        if ($this->dataReader !== null) {
85
            $this->dataReader->close();
86
        }
87
88
        $this->dataReader = null;
89
        $this->batch = null;
90
        $this->value = null;
91
        $this->key = null;
92
    }
93
94
    /**
95
     * Resets the iterator to the initial state.
96
     *
97
     * This method is required by the interface {@see Iterator}.
98
     */
99
    public function rewind(): void
100
    {
101
        $this->reset();
102
        $this->next();
103
    }
104
105
    /**
106
     * Moves the internal pointer to the next dataset.
107
     *
108
     * This method is required by the interface {@see Iterator}.
109
     */
110
    public function next(): void
111
    {
112
        if ($this->batch === null || !$this->each || (next($this->batch) === false)) {
113
            $this->batch = $this->fetchData();
114
            reset($this->batch);
115
        }
116
117
        if ($this->each) {
118
            $this->value = current($this->batch);
119
120
            if ($this->query->getIndexBy() !== null) {
121
                $this->key = key($this->batch);
122
            } elseif (key($this->batch) !== null) {
123
                $this->key = $this->key === null ? 0 : $this->key + 1;
124
            } else {
125
                $this->key = null;
126
            }
127
        } else {
128
            $this->value = $this->batch;
129
            $this->key = $this->key === null ? 0 : $this->key + 1;
130
        }
131
    }
132
133
    /**
134
     * Fetches the next batch of data.
135
     *
136
     * @throws Exception|InvalidConfigException|Throwable
137
     *
138
     * @return array the data fetched.
139
     */
140
    protected function fetchData(): array
141
    {
142
        if ($this->dataReader === null) {
143
            $this->dataReader = $this->query->createCommand()->query();
144
        }
145
146
        $rows = $this->getRows();
147
148
        return $this->query->populate($rows);
149
    }
150
151
    /**
152
     * Reads and collects rows for batch.
153
     *
154
     * @return array
155
     */
156
    protected function getRows(): array
157
    {
158
        $rows = [];
159
        $count = 0;
160
161
        try {
162
            while ($count++ < $this->batchSize && ($row = $this->dataReader?->read())) {
163
                $rows[] = $row;
164
            }
165
        } catch (PDOException $e) {
166
            $errorCode = $e->errorInfo[1] ?? null;
167
168
            if ($this->getDbDriverName() !== 'sqlsrv' || $errorCode !== $this->mssqlNoMoreRowsErrorCode) {
169
                throw $e;
170
            }
171
        }
172
173
        return $rows;
174
    }
175
176
    /**
177
     * Returns the index of the current dataset.
178
     *
179
     * This method is required by the interface {@see Iterator}.
180
     *
181
     * @return int|string|null the index of the current row.
182
     */
183
    #[\ReturnTypeWillChange]
184
    public function key()
185
    {
186
        return $this->key;
187
    }
188
189
    /**
190
     * Returns the current dataset.
191
     *
192
     * This method is required by the interface {@see \Iterator}.
193
     *
194
     * @return mixed the current dataset.
195
     */
196
    #[\ReturnTypeWillChange]
197
    public function current()
198
    {
199
        return $this->value;
200
    }
201
202
    /**
203
     * Returns whether there is a valid dataset at the current position.
204
     *
205
     * This method is required by the interface {@see Iterator}.
206
     *
207
     * @return bool whether there is a valid dataset at the current position.
208
     */
209
    public function valid(): bool
210
    {
211
        return !empty($this->batch);
212
    }
213
214
    /**
215
     * Gets db driver name from the db connection that is passed to the `batch()` or `each()`.
216
     *
217
     * @return string|null
218
     */
219
    private function getDbDriverName(): ?string
220
    {
221
        return $this->db->getDriverName();
222
    }
223
224
    public function getQuery(): QueryInterface|null
225
    {
226
        return $this->query;
227
    }
228
229
    /**
230
     * {@see batchSize}
231
     *
232
     * @return int
233
     */
234
    public function getBatchSize(): int
235
    {
236
        return $this->batchSize;
237
    }
238
239
    /**
240
     * @param int $value the number of rows to be returned in each batch.
241
     *
242
     * @return $this
243
     */
244
    public function batchSize(int $value): self
245
    {
246
        $this->batchSize = $value;
247
248
        return $this;
249
    }
250
}
251