Passed
Push — master ( 161f89...e4642f )
by Ondřej
03:54
created

QueryResult::tuple()   C

Complexity

Conditions 8
Paths 10

Size

Total Lines 26
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 26
rs 5.3846
c 0
b 0
f 0
cc 8
eloc 16
nc 10
nop 1
1
<?php
2
namespace Ivory\Result;
3
4
use Ivory\Connection\ITypeControl;
5
use Ivory\Exception\NotImplementedException;
6
use Ivory\Exception\ResultException;
7
use Ivory\Relation\Column;
8
use Ivory\Relation\FilteredRelation;
9
use Ivory\Relation\IRelation;
10
use Ivory\Relation\ITuple;
11
use Ivory\Relation\ProjectedRelation;
12
use Ivory\Relation\RelationMacros;
13
use Ivory\Relation\RenamedRelation;
14
use Ivory\Relation\Tuple;
15
use Ivory\Type\IType;
16
17
class QueryResult extends Result implements IQueryResult
18
{
19
    use RelationMacros;
20
21
    private $numRows;
22
    /** @var Column[] */
23
    private $columns;
24
    /** @var array map: column name => offset of the first column of the name, or {@link Tuple::AMBIGUOUS_COL} */
25
    private $colNameMap;
26
    /** @var IType[] */
27
    private $colTypes;
28
29
30
    /**
31
     * @param resource $resultHandler the result, with the internal pointer at the beginning
32
     * @param ITypeControl $typeControl
33
     * @param string|null $lastNotice last notice captured on the connection
34
     */
35
    public function __construct($resultHandler, ITypeControl $typeControl, string $lastNotice = null)
36
    {
37
        parent::__construct($resultHandler, $lastNotice);
38
39
        $this->numRows = $this->fetchNumRows();
40
        $this->initCols($typeControl);
41
    }
42
43
    private function fetchNumRows(): int
44
    {
45
        $numRows = pg_num_rows($this->handler);
46
        if ($numRows >= 0 && $numRows !== null) { // NOTE: besides -1, pg_num_rows() might return NULL on error
47
            return $numRows;
48
        } else {
49
            throw new ResultException('Error retrieving number of rows of the result.');
50
        }
51
    }
52
53
    private function initCols(ITypeControl $typeControl)
54
    {
55
        $numFields = pg_num_fields($this->handler);
56
        if ($numFields < 0 || $numFields === null) {
57
            throw new ResultException('Error retrieving number of fields of the result.');
58
        }
59
        $this->columns = [];
60
        $this->colNameMap = [];
61
        $this->colTypes = [];
62
        for ($i = 0; $i < $numFields; $i++) {
63
            /* NOTE: pg_field_type() cannot be used for simplicity - multiple types of the same name might exist in
64
             *       different schemas. Thus, the only reasonable way to recognize the types is using their OIDs,
65
             *       returned by pg_field_type_oid(). Up to some extreme cases, within a given database, the same OID
66
             *       will always refer to the same data type.
67
             */
68
            $name = pg_field_name($this->handler, $i);
69
            if ($name === false || $name === null) { // NOTE: besides false, pg_field_name() might return NULL on error
70
                throw new ResultException("Error retrieving name of result column $i.");
71
            }
72
            if ($name == '?column?') {
73
                $name = null;
74
            }
75
            $typeOid = pg_field_type_oid($this->handler, $i);
76
            if ($typeOid === false || $typeOid === null) { // NOTE: besides false, pg_field_type_oid() might return NULL on error
77
                throw new ResultException("Error retrieving type OID of result column $i.");
78
            }
79
            // NOTE: the type dictionary may change during the iterations, so taky a fresh one every time
80
            $typeDictionary = $typeControl->getTypeDictionary();
81
            $type = $typeDictionary->requireTypeByOid($typeOid);
82
83
            $this->columns[] = new Column($this, $i, $name, $type);
84
            if ($name !== null) {
85
                $this->colNameMap[$name] = (isset($this->colNameMap[$name]) ? Tuple::AMBIGUOUS_COL : $i);
86
            }
87
88
            $this->colTypes[] = $type;
89
        }
90
    }
91
92
93
    //region IRelation
94
95
    public function getColumns()
96
    {
97
        return $this->columns;
98
    }
99
100
    public function filter($decider): IRelation
101
    {
102
        return new FilteredRelation($this, $decider);
103
    }
104
105
    public function project($columns): IRelation
106
    {
107
        return new ProjectedRelation($this, $columns);
108
    }
109
110
    public function rename($renamePairs): IRelation
111
    {
112
        return new RenamedRelation($this, $renamePairs);
113
    }
114
115
    public function uniq($hasher = null, $comparator = null): IRelation
116
    {
117
        throw new NotImplementedException();
118
    }
119
120
    public function tuple(int $offset = 0): ITuple
121
    {
122
        if ($offset >= 0) {
123
            if ($offset >= $this->numRows) {
124
                throw new \OutOfBoundsException("Offset $offset is out of the result bounds [0,{$this->numRows})");
125
            }
126
            $rawData = pg_fetch_row($this->handler, $offset);
127
        } else {
128
            if ($offset < -$this->numRows) {
129
                throw new \OutOfBoundsException("Offset $offset is out of the result bounds [0,{$this->numRows})");
130
            }
131
            $rawData = pg_fetch_row($this->handler, $this->numRows + $offset);
132
        }
133
134
        if ($rawData === false || $rawData === null) {
135
            throw new ResultException("Error fetching row at offset $offset");
136
        }
137
138
        $data = [];
139
        foreach ($this->colTypes as $i => $type) {
140
            $v = $rawData[$i];
141
            $data[$i] = ($v !== null ? $type->parseValue($v) : null);
142
        }
143
144
        return new Tuple($data, $this->colNameMap);
145
    }
146
147
    //endregion
148
149
    //region \Countable
150
151
    public function count()
152
    {
153
        return $this->numRows;
154
    }
155
156
    //endregion
157
}
158