Passed
Push — master ( 67a964...2aff1a )
by Konrad
04:08
created

InsertQueryHandler   A

Complexity

Total Complexity 38

Size/Duplication

Total Lines 293
Duplicated Lines 0 %

Test Coverage

Coverage 91.54%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 127
dl 0
loc 293
rs 9.36
c 1
b 0
f 0
ccs 119
cts 130
cp 0.9154
wmc 38

8 Methods

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