Test Failed
Push — master ( d80a54...b07f16 )
by Konrad
04:15
created

InsertQueryHandler::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 2
c 0
b 0
f 0
nc 1
nop 2
dl 0
loc 6
ccs 4
cts 4
cp 1
crap 1
rs 10
1
<?php
2
3
/*
4
 * This file is part of the sweetrdf/InMemoryStoreSqlite package and licensed under
5
 * the terms of the GPL-3 license.
6
 *
7
 * (c) Konrad Abicht <[email protected]>
8
 * (c) Benjamin Nowak
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 sweetrdf\InMemoryStoreSqlite\Store\QueryHandler;
15
16
use function sweetrdf\InMemoryStoreSqlite\getNormalizedValue;
17
use sweetrdf\InMemoryStoreSqlite\KeyValueBag;
18
use sweetrdf\InMemoryStoreSqlite\Log\Logger;
19
use sweetrdf\InMemoryStoreSqlite\Store\InMemoryStoreSqlite;
20
21
class InsertQueryHandler extends QueryHandler
22
{
23
    /**
24
     * If true, store assumes one or more insert into SPARQL queries and will
25
     * skip certain DB operations to speed up insertion process.
26
     */
27
    private bool $bulkLoadModeIsActive = false;
28
29
    /**
30
     * Is used if $bulkLoadModeIsActive is true. Determines next term ID for
31
     * entries in id2val, s2val and o2val.
32
     */
33
    private int $bulkLoadModeNextTermId = 1;
34
35
    /**
36
     * When set it is used to store term information to speed up insert into operations.
37
     */
38
    private KeyValueBag $rowCache;
39
40
    /**
41
     * Is being used for blank nodes to generate a hash which is not only dependent on
42
     * blank node ID and graph, but also on a random value.
43
     * Otherwise blank nodes inserted in different "insert-sessions" will have the same reference.
44
     */
45 76
    private ?string $sessionId = null;
46
47 76
    public function __construct(InMemoryStoreSqlite $store, Logger $logger)
48 76
    {
49 76
        parent::__construct($store, $logger);
50
51 76
        // default value; will be overridden when running inside InMemoryStoreSqlite
52
        $this->rowCache = new KeyValueBag();
53 76
    }
54
55
    public function activateBulkLoadMode(int $bulkLoadModeNextTermId): void
56 76
    {
57
        $this->bulkLoadModeIsActive = true;
58 76
        $this->bulkLoadModeNextTermId = $bulkLoadModeNextTermId;
59 76
    }
60
61 76
    public function getBulkLoadModeNextTermId(): int
62
    {
63 76
        return $this->bulkLoadModeNextTermId;
64 76
    }
65
66 76
    public function setRowCache(KeyValueBag $rowCache): void
67 76
    {
68
        $this->rowCache = $rowCache;
69
    }
70 76
71
    public function runQuery(array $infos)
72 76
    {
73 76
        $this->sessionId = bin2hex(random_bytes(4));
74
        $this->store->getDBObject()->getPDO()->beginTransaction();
75
76
        foreach ($infos['query']['construct_triples'] as $triple) {
77
            $this->addTripleToGraph($triple, $infos['query']['target_graph']);
78 76
        }
79
80
        $this->store->getDBObject()->getPDO()->commit();
81
82
        $this->sessionId = null;
83
    }
84
85
    private function addTripleToGraph(array $triple, string $graph): void
86
    {
87 76
        /*
88
         * information:
89
         *
90
         *  + val_hash: hashed version of given value
91
         *  + val_type: type of the term; one of: bnode, uri, literal
92 76
         */
93 76
94 76
        $triple = $this->prepareTriple($triple, $graph);
95 76
96 76
        /*
97 76
         * graph
98
         */
99
        $graphId = $this->getIdOfExistingTerm($graph, 'id');
100
        if (null == $graphId) {
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $graphId of type integer|null against null; this is ambiguous if the integer can be zero. Consider using a strict comparison === instead.
Loading history...
101
            $graphId = $this->store->getDBObject()->insert('id2val', [
102
                'id' => $this->getMaxTermId(),
103
                'val' => $graph,
104 76
                'val_type' => 0, // = uri
105 76
            ]);
106 76
        }
107 76
108 76
        /*
109 76
         * s2val
110 76
         */
111
        $subjectId = $this->getIdOfExistingTerm($triple['s'], 'subject');
112
        if (null == $subjectId) {
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $subjectId of type integer|null against null; this is ambiguous if the integer can be zero. Consider using a strict comparison === instead.
Loading history...
113
            $subjectId = $this->getMaxTermId();
114
            $this->store->getDBObject()->insert('s2val', [
115
                'id' => $subjectId,
116
                'val' => $triple['s'],
117 76
                'val_hash' => $this->getValueHash($triple['s']),
118 76
            ]);
119 76
        }
120 76
121 76
        /*
122 76
         * predicate
123 76
         */
124
        $predicateId = $this->getIdOfExistingTerm($triple['p'], 'id');
125
        if (null == $predicateId) {
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $predicateId of type integer|null against null; this is ambiguous if the integer can be zero. Consider using a strict comparison === instead.
Loading history...
126
            $predicateId = $this->getMaxTermId();
127
            $this->store->getDBObject()->insert('id2val', [
128
                'id' => $predicateId,
129
                'val' => $triple['p'],
130 76
                'val_type' => 0, // = uri
131 76
            ]);
132 76
        }
133 76
134 76
        /*
135 76
         * o2val
136 76
         */
137
        $objectId = $this->getIdOfExistingTerm($triple['o'], 'object');
138
        if (null == $objectId) {
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $objectId of type integer|null against null; this is ambiguous if the integer can be zero. Consider using a strict comparison === instead.
Loading history...
139
            $objectId = $this->getMaxTermId();
140
            $this->store->getDBObject()->insert('o2val', [
141
                'id' => $objectId,
142
                'val' => $triple['o'],
143
                'val_hash' => $this->getValueHash($triple['o']),
144 76
            ]);
145 76
        }
146 76
147 76
        /*
148 76
         * o_lang_dt
149 76
         */
150 76
        // notice: only one of these two is set
151 76
        $oLangDt = $triple['o_datatype'].$triple['o_lang'];
152
        $oLangDtId = $this->getIdOfExistingTerm($oLangDt, 'id');
153
        if (null == $oLangDtId) {
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $oLangDtId of type integer|null against null; this is ambiguous if the integer can be zero. Consider using a strict comparison === instead.
Loading history...
154
            $oLangDtId = $this->getMaxTermId();
155
            $this->store->getDBObject()->insert('id2val', [
156
                'id' => $oLangDtId,
157
                'val' => $oLangDt,
158 76
                'val_type' => !empty($triple['o_datatype']) ? 0 : 2,
159 76
            ]);
160 76
        }
161 76
162 76
        /*
163 76
         * triple
164 76
         */
165 76
        $sql = 'SELECT * FROM triple WHERE s = ? AND p = ? AND o = ?';
166 76
        $check = $this->store->getDBObject()->fetchRow($sql, [$subjectId, $predicateId, $objectId]);
167 76
        if (false === $check) {
168 76
            $tripleId = $this->store->getDBObject()->insert('triple', [
169
                's' => $subjectId,
170
                's_type' => $triple['s_type_int'],
171 1
                'p' => $predicateId,
172
                'o' => $objectId,
173
                'o_type' => $triple['o_type_int'],
174
                'o_lang_dt' => $oLangDtId,
175
                'o_comp' => getNormalizedValue($triple['o']),
176
            ]);
177 76
        } else {
178 76
            $tripleId = $check['t'];
179 76
        }
180 76
181 76
        /*
182 76
         * triple to graph
183
         */
184
        $sql = 'SELECT * FROM g2t WHERE g = ? AND t = ?';
185 76
        $check = $this->store->getDBObject()->fetchRow($sql, [$graphId, $tripleId]);
186
        if (false == $check) {
187 76
            $this->store->getDBObject()->insert('g2t', [
188
                'g' => $graphId,
189
                't' => $tripleId,
190
            ]);
191
        }
192 76
    }
193 76
194 5
    private function prepareTriple(array $triple, string $graph): array
195 74
    {
196
        /*
197
         * subject: set type int
198
         */
199
        $triple['s_type_int'] = 0; // uri
200
        if ('bnode' == $triple['s_type']) {
201
            $triple['s_type_int'] = 1;
202 76
        } elseif ('literal' == $triple['s_type']) {
203
            $triple['s_type_int'] = 2;
204 5
        }
205
206 5
        /*
207 5
         * subject is a blank node
208
         */
209
        if ('bnode' == $triple['s_type']) {
210
            // transforms _:foo to _:b671320391_foo
211
            $s = $triple['s'];
212
            // TODO make bnode ID only unique for this session, not in general
213 76
            $triple['s'] = '_:b'.$this->getValueHash($this->sessionId.$graph.$s).'_';
214 76
            $triple['s'] .= substr($s, 2);
215 4
        }
216 75
217 58
        /*
218
         * object: set type int
219
         */
220
        $triple['o_type_int'] = 0; // uri
221
        if ('bnode' == $triple['o_type']) {
222
            $triple['o_type_int'] = 1;
223 76
        } elseif ('literal' == $triple['o_type']) {
224
            $triple['o_type_int'] = 2;
225 4
        }
226
227 4
        /*
228 4
         * object is a blank node
229
         */
230
        if ('bnode' == $triple['o_type']) {
231 76
            // transforms _:foo to _:b671320391_foo
232
            $o = $triple['o'];
233
            // TODO make bnode ID only unique for this session, not in general
234
            $triple['o'] = '_:b'.$this->getValueHash($this->sessionId.$graph.$o).'_';
235
            $triple['o'] .= substr($o, 2);
236
        }
237
238
        return $triple;
239 76
    }
240
241 76
    /**
242 76
     * Generates the next valid ID based on latest values in id2val, s2val and o2val.
243
     *
244
     * @return int returns 1 or higher
245
     */
246
    private function getMaxTermId(): int
247
    {
248
        if (true === $this->bulkLoadModeIsActive) {
249
            return $this->bulkLoadModeNextTermId++;
250
        } else {
251
            $sql = '';
252
            foreach (['id2val', 's2val', 'o2val'] as $table) {
253
                $sql .= !empty($sql) ? ' UNION ' : '';
254
                $sql .= 'SELECT MAX(id) as id FROM '.$table;
255
            }
256
            $result = 0;
257
258
            $rows = $this->store->getDBObject()->fetchList($sql);
259
260
            if (\is_array($rows)) {
0 ignored issues
show
introduced by
The condition is_array($rows) is always true.
Loading history...
261
                foreach ($rows as $row) {
262
                    $result = ($result < $row['id']) ? $row['id'] : $result;
263
                }
264
            }
265
266
            return $result + 1;
267
        }
268
    }
269 76
270
    /**
271
     * @param string $type     One of: bnode, uri, literal
272 76
     * @param string $quadPart One of: id, subject, object
273 76
     *
274
     * @return int 1 (or higher), if available, or null
275 76
     */
276 76
    private function getIdOfExistingTerm(string $value, string $quadPart): ?int
277 76
    {
278 76
        // id (predicate or graph)
279 44
        if ('id' == $quadPart) {
280
            $sql = 'SELECT id, val FROM id2val WHERE val = ?';
281
282
            $hashKey = md5($sql.json_encode([$value]));
283 76
            if (false === $this->rowCache->has($hashKey)) {
284
                $row = $this->store->getDBObject()->fetchRow($sql, [$value]);
285
                if (\is_array($row)) {
286 76
                    $this->rowCache->set($hashKey, $row);
287 44
                }
288
            }
289 76
290
            $entry = $this->rowCache->get($hashKey);
291
292
            // entry found, use its ID
293 76
            if (\is_array($entry)) {
294 76
                return $entry['id'];
295 76
            } else {
296
                return null;
297 76
            }
298 76
        } else {
299 76
            // subject or object
300 76
            $table = 'subject' == $quadPart ? 's2val' : 'o2val';
301 25
            $sql = 'SELECT id, val FROM '.$table.' WHERE val_hash = ?';
302
            $params = [$this->getValueHash($value)];
303
304
            $hashKey = md5($sql.json_encode($params));
305 76
            if (false === $this->rowCache->has($hashKey)) {
306
                $row = $this->store->getDBObject()->fetchRow($sql, $params);
307
                if (\is_array($row)) {
308 76
                    $this->rowCache->set($hashKey, $row);
309 25
                }
310
            }
311 76
312
            $entry = $this->rowCache->get($hashKey);
313
314
            // entry found, use its ID
315
            if (isset($entry['val']) && $entry['val'] == $value) {
316
                return $entry['id'];
317
            } else {
318
                return null;
319
            }
320
        }
321
    }
322
}
323