Passed
Push — master ( 60c378...33ffe1 )
by Ondřej
02:20
created

ArrayRelation::__sleep()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
declare(strict_types=1);
3
namespace Ivory\Relation;
4
5
use Ivory\Connection\IConnection;
6
use Ivory\Exception\InvalidStateException;
7
use Ivory\Ivory;
8
use Ivory\Query\SqlPatternDefinitionMacros;
9
use Ivory\Type\ConnectionDependentObject;
10
use Ivory\Type\IConnectionDependentObject;
11
use Ivory\Type\IValueSerializer;
12
13
/**
14
 * Relation built from an array of rows.
15
 *
16
 * Immutable - once constructed, the data cannot be changed.
17
 */
18
class ArrayRelation extends RelationBase implements IConnectionDependentObject
19
{
20
    use ConnectionDependentObject {
21
        attachToConnection as traitAttachToConnection;
22
        detachFromConnection as traitDetachFromConnection;
23
    }
24
25
    private $typeMap;
26
    private $columns = null;
27
    private $colNameMap;
28
    private $rows;
29
    private $numRows;
30
31
    public function __sleep()
32
    {
33
        return ['typeMap', 'rows'];
34
    }
35
36
    public function __wakeup()
37
    {
38
        $this->init();
39
    }
40
41
    /**
42
     * Creates an array relation from a list of rows.
43
     *
44
     * Each row is interpreted as a map of column names to the corresponding values. Ideally, all rows should have the
45
     * same structure, although that is not really required:
46
     * - missing values are treated as `null` values,
47
     * - extra items are ignored,
48
     * - mutual order of the map entries is insignificant (except for the first row in case `$typeMap` is not given -
49
     *   see below).
50
     *
51
     * The relation columns may optionally be defined using the second parameter `$typeMap`. It is an ordered map of
52
     * column names to the specification of their types, which may either be:
53
     * - an {@link IType} or {@link IValueSerializer} object, or
54
     * - a string type specification (name or alias) as it would be used in an {@link SqlPattern} after the `%` sign,
55
     * - `null` to let the type dictionary infer the type automatically (more details below).
56
     *
57
     * If given, the `$typeMap` serves as the column definition list. Columns not mentioned in `$typeMap` are ignored in
58
     * `$rows`. Also, the order of relation columns is determined by `$typeMap`.
59
     *
60
     * If `$typeMap` is not given (or given as `null`), *the first row* from `$rows` takes the role of the relation
61
     * column definer, and the type of each column will be inferred automatically just as its type specification was
62
     * given as `null`.
63
     *
64
     * Note that there is no problem in using integers for column names. Actually, plain PHP lists (i.e., arrays with no
65
     * explicit keys specified) may be used, both for `$rows` and `$typeMap`.
66
     *
67
     * When inferring the type, the first non-`null` value of the column is used. If only `null`s are present in the
68
     * column, the type registered for `null` values is requested from the type dictionary. The autodetection is
69
     * performed using the type dictionary used by the connection which this relation gets attached to.
70
     *
71
     * @param array $rows list of data rows, each a map of column names to values
72
     * @param array $typeMap optional map specifying columns mutual order and types
73
     * @return ArrayRelation
74
     */
75
    public static function fromRows(array $rows, ?array $typeMap = null): ArrayRelation
76
    {
77
        if ($typeMap === null) {
78
            $typeMap = [];
79
            $firstRow = reset($rows);
80
            if ($firstRow !== false) { // in case no rows are there, we can infer nothing than an empty list of columns
81
                foreach ($firstRow as $columnName => $_) {
82
                    $typeMap[$columnName] = null;
83
                }
84
            }
85
        }
86
87
        return new ArrayRelation($rows, $typeMap);
88
    }
89
90
    /**
91
     * @param array $rows list: map: string column name => value
92
     * @param array $typeMap ordered map: column name => type specifier: {@link IType}, <tt>string</tt> or <tt>null</tt>
93
     */
94
    protected function __construct(array $rows, array $typeMap)
95
    {
96
        parent::__construct();
97
98
        $this->typeMap = $typeMap;
99
        $this->rows = $rows;
100
        $this->init();
101
    }
102
103
    private function init()
104
    {
105
        $this->numRows = count($this->rows);
106
        $this->colNameMap = array_flip(array_keys($this->typeMap));
107
    }
108
109
    public function attachToConnection(IConnection $connection): void
110
    {
111
        $this->traitAttachToConnection($connection);
112
        $this->buildColumns();
113
    }
114
115
    private function buildColumns(): void
116
    {
117
        $types = $this->inferTypes();
118
119
        $this->columns = [];
120
        foreach ($this->typeMap as $colName => $_) {
121
            $colOffset = count($this->columns);
122
            $col = new Column($this, $colOffset, (string)$colName, $types[$colName]);
123
            $this->columns[] = $col;
124
        }
125
    }
126
127
    /**
128
     * @return IValueSerializer[] unordered map: column name => type converter
129
     */
130
    private function inferTypes(): array
131
    {
132
        $result = [];
133
        $toParse = [];
134
        $toInfer = [];
135
        foreach ($this->typeMap as $colName => $typeSpec) {
136
            if ($typeSpec instanceof IValueSerializer) {
137
                $result[$colName] = $typeSpec;
138
            } elseif (is_string($typeSpec)) {
139
                $toParse[$colName] = $typeSpec;
140
            } elseif ($typeSpec === null) {
141
                $toInfer[$colName] = true;
142
            } else {
143
                throw new \UnexpectedValueException(
144
                    'Unexpected kind of column type specification: ' . get_class($typeSpec)
145
                );
146
            }
147
        }
148
149
        if ($toParse) {
150
            $typeDictionary = $this->getConnection()->getTypeDictionary();
151
            $parser = Ivory::getSqlPatternParser();
152
            foreach ($toParse as $colName => $typeSpecStr) {
153
                $pattern = $parser->parse('%' . $typeSpecStr);
154
                $placeholder = $pattern->getPositionalPlaceholders()[0];
155
                $result[$colName] = SqlPatternDefinitionMacros::getReferencedSerializer($placeholder, $typeDictionary);
156
            }
157
        }
158
159
        if ($toInfer) {
160
            $typeDictionary = $this->getConnection()->getTypeDictionary();
161
            foreach ($this->rows as $row) {
162
                foreach ($toInfer as $colName => $_) {
163
                    if (isset($row[$colName])) {
164
                        $result[$colName] = $typeDictionary->requireTypeByValue($row[$colName]);
165
                        unset($toInfer[$colName]);
166
                    }
167
                }
168
169
                if (!$toInfer) {
170
                    break;
171
                }
172
            }
173
            foreach ($toInfer as $colName => $_) {
174
                $result[$colName] = $typeDictionary->requireTypeByValue(null);
175
            }
176
        }
177
178
        return $result;
179
    }
180
181
    public function detachFromConnection(): void
182
    {
183
        $this->traitDetachFromConnection();
184
        $this->columns = null;
185
    }
186
187
    public function getColumns(): array
188
    {
189
        if ($this->columns === null) {
190
            throw new InvalidStateException('The relation has not been attached to any connection');
191
        }
192
        return $this->columns;
193
    }
194
195
    public function tuple(int $offset = 0): ITuple
196
    {
197
        if ($offset >= $this->numRows || $offset < -$this->numRows) {
198
            throw new \OutOfBoundsException("Offset $offset is out of the result bounds [0,{$this->numRows})");
199
        }
200
201
        $effectiveOffset = ($offset >= 0 ? $offset : $this->numRows + $offset);
202
        if (!isset($this->rows[$effectiveOffset])) {
203
            throw new \RuntimeException("Error fetching row at offset $offset");
204
        }
205
        $row = $this->rows[$effectiveOffset];
206
207
        $data = [];
208
        foreach ($this->colNameMap as $colName => $_) {
209
            $data[] = ($row[$colName] ?? null);
210
        }
211
212
        return new Tuple($data, $this->colNameMap);
1 ignored issue
show
Bug introduced by
It seems like $this->colNameMap can also be of type null; however, parameter $colNameMap of Ivory\Relation\Tuple::__construct() does only seem to accept 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

212
        return new Tuple($data, /** @scrutinizer ignore-type */ $this->colNameMap);
Loading history...
213
    }
214
215
    public function count()
216
    {
217
        return $this->numRows;
218
    }
219
}
220