Passed
Push — master ( 27ca4d...8d618a )
by Ondřej
02:45 queued 10s
created

Cursor::close()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 0
dl 0
loc 5
rs 10
c 0
b 0
f 0
1
<?php
2
declare(strict_types=1);
3
namespace Ivory\Relation;
4
5
use Ivory\Connection\IStatementExecution;
6
use Ivory\Exception\ClosedCursorException;
7
use Ivory\Query\SqlRelationDefinition;
8
9
/**
10
 * {@inheritDoc}
11
 *
12
 * Note the implementation checks its local _closed_ flag not only to save queries to the database, but also to prevent
13
 * from one `Cursor` object being used repeatedly for another, same-named cursor later.
14
 */
15
class Cursor implements ICursor
16
{
17
    private $stmtExec;
18
    private $name;
19
    private $properties;
20
    private $closed = null;
21
22
    /**
23
     * @param IStatementExecution $stmtExec
24
     * @param string $name
25
     * @param CursorProperties|null $properties known characteristics of the cursor, or <tt>null</tt> if unknown
26
     */
27
    public function __construct(IStatementExecution $stmtExec, string $name, ?CursorProperties $properties = null)
28
    {
29
        $this->stmtExec = $stmtExec;
30
        $this->name = $name;
31
        $this->properties = $properties;
32
    }
33
34
    public function getName(): string
35
    {
36
        return $this->name;
37
    }
38
39
    /**
40
     * {@inheritDoc}
41
     *
42
     * Note that finding out for the first time might invoke a database query. (For next time, it is cached.)
43
     */
44
    public function getProperties(): CursorProperties
45
    {
46
        if ($this->properties === null) {
47
            $t = $this->stmtExec->querySingleTuple(
48
                'SELECT is_holdable, is_scrollable, is_binary
49
                 FROM pg_catalog.pg_cursors
50
                 WHERE name = %s',
51
                $this->name
52
            );
53
            if ($t === null) {
54
                $this->closed = true;
55
                throw new ClosedCursorException($this->name);
56
            }
57
            $this->properties = new CursorProperties($t->is_holdable, $t->is_scrollable, $t->is_binary);
0 ignored issues
show
Bug Best Practice introduced by
The property is_scrollable does not exist on Ivory\Relation\ITuple. Since you implemented __get, consider adding a @property annotation.
Loading history...
Bug Best Practice introduced by
The property is_holdable does not exist on Ivory\Relation\ITuple. Since you implemented __get, consider adding a @property annotation.
Loading history...
Bug Best Practice introduced by
The property is_binary does not exist on Ivory\Relation\ITuple. Since you implemented __get, consider adding a @property annotation.
Loading history...
58
        }
59
        return $this->properties;
60
    }
61
62
    public function fetch(int $moveBy = 1): ?ITuple
63
    {
64
        $dirSql = $this->getRelativeDirectionSql($moveBy);
65
        return $this->fetchSingleImpl($dirSql);
66
    }
67
68
    public function fetchAt(int $position): ?ITuple
69
    {
70
        return $this->fetchSingleImpl("ABSOLUTE $position");
71
    }
72
73
    private function fetchSingleImpl(string $directionSql): ?ITuple
74
    {
75
        $this->assertOpen();
76
        return $this->stmtExec->querySingleTuple('FETCH %sql %ident', $directionSql, $this->name);
77
    }
78
79
    public function fetchMulti(int $count): IRelation
80
    {
81
        $this->assertOpen();
82
        $dirSql = $this->getCountingDirectionSql($count);
83
        return $this->stmtExec->query('FETCH %sql %ident', $dirSql, $this->name);
84
    }
85
86
    public function moveBy(int $offset): int
87
    {
88
        $dirSql = $this->getRelativeDirectionSql($offset);
89
        return $this->moveImpl($dirSql);
90
    }
91
92
    public function moveTo(int $position): int
93
    {
94
        return $this->moveImpl("ABSOLUTE $position");
95
    }
96
97
    public function moveAndCount(int $offset): int
98
    {
99
        $dirSql = $this->getCountingDirectionSql($offset);
100
        return $this->moveImpl($dirSql);
101
    }
102
103
    private function moveImpl(string $directionSql): int
104
    {
105
        $this->assertOpen();
106
107
        $res = $this->stmtExec->command('MOVE %sql %ident', $directionSql, $this->name);
108
109
        if (preg_match('~^MOVE (\d+)$~', $res->getCommandTag(), $m)) {
110
            return (int)$m[1];
111
        } else {
112
            throw new \RuntimeException('Unexpected command tag: ' . $res->getCommandTag());
113
        }
114
    }
115
116
    private function getRelativeDirectionSql(int $moveBy): string
117
    {
118
        if ($moveBy == 1) {
119
            return 'NEXT';
120
        } elseif ($moveBy == -1) {
121
            return 'PRIOR';
122
        } else {
123
            return "RELATIVE $moveBy";
124
        }
125
    }
126
127
    private function getCountingDirectionSql(int $count): string
128
    {
129
        if ($count == self::ALL_REMAINING) {
130
            return 'FORWARD ALL';
131
        } elseif ($count == self::ALL_FOREGOING) {
132
            return 'BACKWARD ALL';
133
        } elseif ($count >= 0) {
134
            return "FORWARD $count";
135
        } else {
136
            return 'BACKWARD ' . -$count;
137
        }
138
    }
139
    
140
    public function close(): void
141
    {
142
        $this->assertOpen();
143
        $this->stmtExec->command('CLOSE %ident', $this->name);
144
        $this->closed = true; // correctness: CLOSE is not rolled back upon rolling back a savepoint or transaction
145
    }
146
147
    /**
148
     * {@inheritDoc}
149
     *
150
     * Note that until the cursor is known to be closed, calling this method will invoke a database query.
151
     */
152
    public function isClosed(): bool
153
    {
154
        if ($this->closed) {
155
            return true;
156
        }
157
158
        // Use the situation we must query the database, and find out the cursor properties if unknown yet.
159
        $t = $this->stmtExec->querySingleTuple(
160
            'SELECT is_holdable, is_scrollable, is_binary
161
             FROM pg_catalog.pg_cursors
162
             WHERE name = %s',
163
            $this->name
164
        );
165
        if ($t === null) {
166
            $this->closed = true;
167
            return true;
168
        } else {
169
            if ($this->properties === null) {
170
                $this->properties = new CursorProperties($t->is_holdable, $t->is_scrollable, $t->is_binary);
0 ignored issues
show
Bug Best Practice introduced by
The property is_binary does not exist on Ivory\Relation\ITuple. Since you implemented __get, consider adding a @property annotation.
Loading history...
Bug Best Practice introduced by
The property is_scrollable does not exist on Ivory\Relation\ITuple. Since you implemented __get, consider adding a @property annotation.
Loading history...
Bug Best Practice introduced by
The property is_holdable does not exist on Ivory\Relation\ITuple. Since you implemented __get, consider adding a @property annotation.
Loading history...
171
            }
172
            return false;
173
        }
174
    }
175
176
    /**
177
     * {@inheritDoc}
178
     *
179
     * In the buffered mode, the implementation fetches the first batch of `$bufferSize` rows immediately, and while
180
     * iterating through the rows, it fetches another batch of rows in the background.
181
     */
182
    public function getIterator(int $bufferSize = 0): \Traversable
183
    {
184
        if ($bufferSize <= 0) {
185
            for ($i = 1; ; $i++) {
186
                $tuple = $this->fetchAt($i);
187
                if ($tuple === null) {
188
                    return;
189
                }
190
                yield $i => $tuple;
191
            }
192
        } else {
193
            $fetchSql = SqlRelationDefinition::fromPattern(
194
                'FETCH %sql %ident', $this->getCountingDirectionSql($bufferSize), $this->name
195
            );
196
            $this->moveTo(0);
197
            $i = 1;
198
            $buffer = $this->fetchMulti($bufferSize);
199
            while (true) {
200
                // start querying for the next batch in the background, if not at the end
201
                if (count($buffer) == $bufferSize) {
0 ignored issues
show
Bug introduced by
It seems like $buffer can also be of type Ivory\Result\IResult; however, parameter $var of count() does only seem to accept Countable|array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

201
                if (count(/** @scrutinizer ignore-type */ $buffer) == $bufferSize) {
Loading history...
202
                    $nextBatchGen = $this->stmtExec->executeStatementAsync($fetchSql);
203
                } else {
204
                    $nextBatchGen = null;
205
                }
206
                // read all the buffered tuples
207
                foreach ($buffer as $t) {
208
                    yield $i++ => $t;
209
                }
210
                // all read, check if we have fetched more data in the meantime, and put them in the buffer
211
                if ($nextBatchGen === null) {
212
                    return;
213
                }
214
                $buffer = $nextBatchGen->getResult();
215
            }
216
        }
217
    }
218
219
    private function assertOpen(): void
220
    {
221
        if ($this->closed) {
222
            throw new ClosedCursorException($this->name);
223
        }
224
    }
225
}
226