Passed
Branch extract-store (f24e42)
by Konrad
04:37
created

InsertQueryHandler::getIdOfExistingTerm()   A

Complexity

Conditions 6
Paths 6

Size

Total Lines 25
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 6

Importance

Changes 0
Metric Value
cc 6
eloc 16
nc 6
nop 2
dl 0
loc 25
ccs 14
cts 14
cp 1
crap 6
rs 9.1111
c 0
b 0
f 0
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
class InsertQueryHandler extends QueryHandler
17
{
18
    /**
19
     * Is being used for blank nodes to generate a hash which is not only dependent on
20
     * blank node ID and graph, but also on a random value.
21
     * Otherwise blank nodes inserted in different "insert-sessions" will have the same reference.
22
     */
23
    private ?string $sessionId = null;
24
25 77
    public function runQuery(array $infos)
26
    {
27 77
        $this->addTriplesToGraph(
28 77
            $infos['query']['construct_triples'],
29 77
            $infos['query']['target_graph']
30
        );
31 77
    }
32
33 77
    public function addTriplesToGraph(array $triples, string $graph): void
34
    {
35 77
        $this->sessionId = bin2hex(random_bytes(8));
36
37 77
        foreach ($triples as $triple) {
38 77
            $this->addTripleToGraph($triple, $graph);
39
        }
40
41 77
        $this->sessionId = null;
42 77
    }
43
44
    /**
45
     * @todo cache once loaded triples/quads
46
     */
47 77
    private function addTripleToGraph(array $triple, string $graph): void
48
    {
49
        /*
50
         * information:
51
         *
52
         *  + val_hash: hashed version of given value
53
         *  + val_type: type of the term; one of: bnode, uri, literal
54
         */
55
56 77
        $triple = $this->prepareTriple($triple, $graph);
57
58
        /*
59
         * graph
60
         */
61 77
        $graphId = $this->getIdOfExistingTerm($graph, 'id');
62 77
        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...
63 77
            $graphId = $this->store->getDBObject()->insert('id2val', [
64 77
                'id' => $this->getMaxTermId(),
65 77
                'val' => $graph,
66 77
                'val_type' => 0, // = uri
67
            ]);
68
        }
69
70
        /*
71
         * s2val
72
         */
73 77
        $subjectId = $this->getIdOfExistingTerm($triple['s'], 'subject');
74 77
        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...
75 77
            $subjectId = $this->getMaxTermId();
76 77
            $this->store->getDBObject()->insert('s2val', [
77 77
                'id' => $subjectId,
78 77
                'val' => $triple['s'],
79 77
                'val_hash' => $this->store->getValueHash($triple['s']),
80
            ]);
81
        }
82
83
        /*
84
         * predicate
85
         */
86 77
        $predicateId = $this->getIdOfExistingTerm($triple['p'], 'id');
87 77
        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...
88 77
            $predicateId = $this->getMaxTermId();
89 77
            $this->store->getDBObject()->insert('id2val', [
90 77
                'id' => $predicateId,
91 77
                'val' => $triple['p'],
92 77
                'val_type' => 0, // = uri
93
            ]);
94
        }
95
96
        /*
97
         * o2val
98
         */
99 77
        $objectId = $this->getIdOfExistingTerm($triple['o'], 'object');
100 77
        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...
101 77
            $objectId = $this->getMaxTermId();
102 77
            $this->store->getDBObject()->insert('o2val', [
103 77
                'id' => $objectId,
104 77
                'val' => $triple['o'],
105 77
                'val_hash' => $this->store->getValueHash($triple['o']),
106
            ]);
107
        }
108
109
        /*
110
         * o_lang_dt
111
         */
112
        // notice: only one of these two is set
113 77
        $oLangDt = $triple['o_datatype'].$triple['o_lang'];
114 77
        $oLangDtId = $this->getIdOfExistingTerm($oLangDt, 'id');
115 77
        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...
116 77
            $oLangDtId = $this->getMaxTermId();
117 77
            $this->store->getDBObject()->insert('id2val', [
118 77
                'id' => $oLangDtId,
119 77
                'val' => $oLangDt,
120 77
                'val_type' => !empty($triple['o_datatype']) ? 0 : 2,
121
            ]);
122
        }
123
124
        /*
125
         * triple
126
         */
127 77
        $sql = 'SELECT * FROM triple WHERE s = ? AND p = ? AND o = ?';
128 77
        $check = $this->store->getDBObject()->fetchRow($sql, [$subjectId, $predicateId, $objectId]);
129 77
        if (false === $check) {
130 77
            $tripleId = $this->store->getDBObject()->insert('triple', [
131 77
                's' => $subjectId,
132 77
                's_type' => $triple['s_type_int'],
133 77
                'p' => $predicateId,
134 77
                'o' => $objectId,
135 77
                'o_type' => $triple['o_type_int'],
136 77
                'o_lang_dt' => $oLangDtId,
137 77
                'o_comp' => $this->getOComp($triple['o']),
138
            ]);
139
        } else {
140 1
            $tripleId = $check['t'];
141
        }
142
143
        /*
144
         * triple to graph
145
         */
146 77
        $sql = 'SELECT * FROM g2t WHERE g = ? AND t = ?';
147 77
        $check = $this->store->getDBObject()->fetchRow($sql, [$graphId, $tripleId]);
148 77
        if (false == $check) {
149 77
            $this->store->getDBObject()->insert('g2t', [
150 77
                'g' => $graphId,
151 77
                't' => $tripleId,
152
            ]);
153
        }
154 77
    }
155
156 77
    private function prepareTriple(array $triple, string $graph): array
157
    {
158
        /*
159
         * subject: set type int
160
         */
161 77
        $triple['s_type_int'] = 0; // uri
162 77
        if ('bnode' == $triple['s_type']) {
163 4
            $triple['s_type_int'] = 1;
164 75
        } elseif ('literal' == $triple['s_type']) {
165
            $triple['s_type_int'] = 2;
166
        }
167
168
        /*
169
         * subject is a blank node
170
         */
171 77
        if ('bnode' == $triple['s_type']) {
172
            // transforms _:foo to _:b671320391_foo
173 4
            $s = $triple['s'];
174
            // TODO make bnode ID only unique for this session, not in general
175 4
            $triple['s'] = '_:b'.$this->store->getValueHash($this->sessionId.$graph.$s).'_';
176 4
            $triple['s'] .= substr($s, 2);
177
        }
178
179
        /*
180
         * object: set type int
181
         */
182 77
        $triple['o_type_int'] = 0; // uri
183 77
        if ('bnode' == $triple['o_type']) {
184 4
            $triple['o_type_int'] = 1;
185 76
        } elseif ('literal' == $triple['o_type']) {
186 59
            $triple['o_type_int'] = 2;
187
        }
188
189
        /*
190
         * object is a blank node
191
         */
192 77
        if ('bnode' == $triple['o_type']) {
193
            // transforms _:foo to _:b671320391_foo
194 4
            $o = $triple['o'];
195
            // TODO make bnode ID only unique for this session, not in general
196 4
            $triple['o'] = '_:b'.$this->store->getValueHash($this->sessionId.$graph.$o).'_';
197 4
            $triple['o'] .= substr($o, 2);
198
        }
199
200 77
        return $triple;
201
    }
202
203
    /**
204
     * Get normalized value for ORDER BY operations.
205
     */
206 77
    private function getOComp($val): string
207
    {
208
        /* try date (e.g. 21 August 2007) */
209
        if (
210 77
            preg_match('/^[0-9]{1,2}\s+[a-z]+\s+[0-9]{4}/i', $val)
211 77
            && ($uts = strtotime($val))
212 77
            && (-1 !== $uts)
213
        ) {
214 1
            return date("Y-m-d\TH:i:s", $uts);
215
        }
216
217
        /* xsd date (e.g. 2009-05-28T18:03:38+09:00 2009-05-28T18:03:38GMT) */
218 77
        if (true === (bool) strtotime($val)) {
219 3
            return date('Y-m-d\TH:i:s\Z', strtotime($val));
220
        }
221
222 76
        if (is_numeric($val)) {
223 23
            $val = sprintf('%f', $val);
224 23
            if (preg_match("/([\-\+])([0-9]*)\.([0-9]*)/", $val, $m)) {
225
                return $m[1].sprintf('%018s', $m[2]).'.'.sprintf('%-015s', $m[3]);
226
            }
227 23
            if (preg_match("/([0-9]*)\.([0-9]*)/", $val, $m)) {
228 23
                return '+'.sprintf('%018s', $m[1]).'.'.sprintf('%-015s', $m[2]);
229
            }
230
231
            return $val;
232
        }
233
234
        /* any other string: remove tags, linebreaks etc., but keep MB-chars */
235
        // [\PL\s]+ ( = non-Letters) kills digits
236 57
        $re = '/[\PL\s]+/isu';
0 ignored issues
show
Unused Code introduced by
The assignment to $re is dead and can be removed.
Loading history...
237 57
        $re = '/[\s\'\"\´\`]+/is';
238 57
        $val = trim(preg_replace($re, '-', strip_tags($val)));
239 57
        if (\strlen($val) > 35) {
240 5
            $fnc = \function_exists('mb_substr') ? 'mb_substr' : 'substr';
241 5
            $val = $fnc($val, 0, 17).'-'.$fnc($val, -17);
242
        }
243
244 57
        return $val;
245
    }
246
247
    /**
248
     * Generates the next valid ID based on latest values in id2val, s2val and o2val.
249
     *
250
     * @return int returns 1 or higher
251
     */
252 77
    private function getMaxTermId(): int
253
    {
254 77
        $sql = '';
255 77
        foreach (['id2val', 's2val', 'o2val'] as $table) {
256 77
            $sql .= !empty($sql) ? ' UNION ' : '';
257 77
            $sql .= 'SELECT MAX(id) as id FROM '.$table;
258
        }
259 77
        $result = 0;
260
261 77
        $rows = $this->store->getDBObject()->fetchList($sql);
262
263 77
        if (\is_array($rows)) {
0 ignored issues
show
introduced by
The condition is_array($rows) is always true.
Loading history...
264 77
            foreach ($rows as $row) {
265 77
                $result = ($result < $row['id']) ? $row['id'] : $result;
266
            }
267
        }
268
269 77
        return $result + 1;
270
    }
271
272
    /**
273
     * @param string $type     One of: bnode, uri, literal
274
     * @param string $quadPart One of: id, subject, object
275
     *
276
     * @return int 1 (or higher), if available, or null
277
     */
278 77
    private function getIdOfExistingTerm(string $value, string $quadPart): ?int
279
    {
280
        // id (predicate or graph)
281 77
        if ('id' == $quadPart) {
282 77
            $sql = 'SELECT id, val FROM id2val WHERE val = ?';
283 77
            $entry = $this->store->getDBObject()->fetchRow($sql, [$value]);
284
285
            // entry found, use its ID
286 77
            if (\is_array($entry)) {
287 44
                return $entry['id'];
288
            } else {
289 77
                return null;
290
            }
291
        } else {
292
            // subject or object
293 77
            $table = 'subject' == $quadPart ? 's2val' : 'o2val';
294 77
            $sql = 'SELECT id, val FROM '.$table.' WHERE val_hash = ?';
295 77
            $params = [$this->store->getValueHash($value)];
296 77
            $entry = $this->store->getDBObject()->fetchRow($sql, $params);
297
298
            // entry found, use its ID
299 77
            if (isset($entry['val']) && $entry['val'] == $value) {
300 26
                return $entry['id'];
301
            } else {
302 77
                return null;
303
            }
304
        }
305
    }
306
}
307