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

SelectQueryHandler::buildIndex()   B

Complexity

Conditions 9
Paths 3

Size

Total Lines 35
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 22
CRAP Score 9.5145

Importance

Changes 0
Metric Value
cc 9
eloc 27
nc 3
nop 2
dl 0
loc 35
ccs 22
cts 27
cp 0.8148
crap 9.5145
rs 8.0555
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 Nowack
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 sweetrdf\InMemoryStoreSqlite\PDOSQLiteAdapter;
17
18
class SelectQueryHandler extends QueryHandler
19
{
20 101
    public function runQuery($infos)
21
    {
22 101
        $this->infos = $infos;
0 ignored issues
show
Bug Best Practice introduced by
The property infos does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
23 101
        $this->infos['null_vars'] = [];
24 101
        $this->indexes = [];
0 ignored issues
show
Bug Best Practice introduced by
The property indexes does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
25 101
        $this->pattern_order_offset = 0;
0 ignored issues
show
Bug Best Practice introduced by
The property pattern_order_offset does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
26 101
        $q_sql = $this->getSQL();
27
28
        /* create intermediate results (ID-based) */
29 101
        $tmp_tbl = $this->createTempTable($q_sql);
30
31
        /* join values */
32 101
        $r = $this->getFinalQueryResult($q_sql, $tmp_tbl);
33
34
        /* remove intermediate results */
35 101
        $this->store->getDBObject()->simpleQuery('DROP TABLE IF EXISTS '.$tmp_tbl);
36
37 101
        return $r;
38
    }
39
40 101
    public function getSQL()
41
    {
42 101
        $r = '';
43 101
        $nl = "\n";
44 101
        $this->buildInitialIndexes();
45 101
        foreach ($this->indexes as $i => $index) {
46 101
            $this->index = array_merge($this->getEmptyIndex(), $index);
0 ignored issues
show
Bug Best Practice introduced by
The property index does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
47 101
            $this->analyzeIndex($this->getPattern('0'));
48 101
            $sub_r = $this->getQuerySQL();
49 101
            $r .= $r ? $nl.'UNION'.$this->getDistinctSQL().$nl : '';
50
51 101
            $setBracket = $this->is_union_query && !$this->store->getDBObject() instanceof PDOSQLiteAdapter;
0 ignored issues
show
introduced by
$this->store->getDBObject() is always a sub-type of sweetrdf\InMemoryStoreSqlite\PDOSQLiteAdapter.
Loading history...
52 101
            $r .= $setBracket ? '('.$sub_r.')' : $sub_r;
53
54 101
            $this->indexes[$i] = $this->index;
0 ignored issues
show
Bug Best Practice introduced by
The property indexes does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
55
        }
56 101
        $r .= $this->is_union_query ? $this->getLIMITSQL() : '';
57 101
        if ($this->v('order_infos', 0, $this->infos['query'])) {
58 4
            $r = preg_replace('/SELECT(\s+DISTINCT)?\s*/', 'SELECT\\1 NULL AS `TMPPOS`, ', $r);
59
        }
60 101
        $pd_count = $this->problematicDependencies();
61 101
        if ($pd_count) {
62
            /* re-arranging the patterns sometimes reduces the LEFT JOIN dependencies */
63
            $set_sql = 0;
64
            if (!$this->pattern_order_offset) {
65
                $set_sql = 1;
66
            }
67
            if (!$set_sql && ($pd_count < $this->opt_sql_pd_count)) {
68
                $set_sql = 1;
69
            }
70
            if (!$set_sql && ($pd_count == $this->opt_sql_pd_count) && (\strlen($r) < \strlen($this->opt_sql))) {
71
                $set_sql = 1;
72
            }
73
            if ($set_sql) {
74
                $this->opt_sql = $r;
0 ignored issues
show
Bug Best Practice introduced by
The property opt_sql does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
75
                $this->opt_sql_pd_count = $pd_count;
0 ignored issues
show
Bug Best Practice introduced by
The property opt_sql_pd_count does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
76
            }
77
            ++$this->pattern_order_offset;
78
            if ($this->pattern_order_offset > 5) {
79
                return $this->opt_sql;
80
            }
81
82
            return $this->getSQL();
83
        }
84
85 101
        return $r;
86
    }
87
88 101
    public function buildInitialIndexes()
89
    {
90 101
        $this->dependency_log = [];
0 ignored issues
show
Bug Best Practice introduced by
The property dependency_log does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
91 101
        $this->index = $this->getEmptyIndex();
0 ignored issues
show
Bug Best Practice introduced by
The property index does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
92
        // if no pattern is in the query, the index "pattern" is undefined, which leads to an error.
93
        // TODO throw an exception/raise an error and avoid "Undefined index: pattern" notification
94 101
        $this->buildIndex($this->infos['query']['pattern'], 0);
95 101
        $tmp = $this->index;
96 101
        $this->analyzeIndex($this->getPattern('0'));
97 101
        $this->initial_index = $this->index;
0 ignored issues
show
Bug Best Practice introduced by
The property initial_index does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
98 101
        $this->index = $tmp;
99 101
        $this->is_union_query = $this->index['union_branches'] ? 1 : 0;
0 ignored issues
show
Bug Best Practice introduced by
The property is_union_query does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
100 101
        $this->indexes = $this->is_union_query ? $this->getUnionIndexes($this->index) : [$this->index];
0 ignored issues
show
Bug Best Practice introduced by
The property indexes does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
101 101
    }
102
103 101
    private function createTempTable($q_sql)
104
    {
105 101
        $tbl = 'Q'.md5($q_sql.time().uniqid(rand()));
106 101
        if (\strlen($tbl) > 64) {
107
            $tbl = 'Q'.md5($tbl);
108
        }
109
110 101
        $tmp_sql = 'CREATE TABLE '.$tbl.' ( '.$this->getTempTableDefForSQLite($q_sql).')';
111 101
        $tmpSql2 = str_replace('CREATE TEMPORARY', 'CREATE', $tmp_sql);
112
113
        if (
114 101
            !$this->store->getDBObject()->simpleQuery($tmp_sql)
115 101
            && !$this->store->getDBObject()->simpleQuery($tmpSql2)
116 101
            && !empty($this->store->getDBObject()->getErrorMessage())
117
        ) {
118
            return $this->store->getLogger()->error(
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->store->getLogger(...t()->getErrorMessage()) targeting sweetrdf\InMemoryStoreSqlite\Logger::error() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
119
                $this->store->getDBObject()->getErrorMessage()
120
            );
121
        }
122 101
        if (false === $this->store->getDBObject()->exec('INSERT INTO '.$tbl.' '."\n".$q_sql)) {
123
            $this->store->getLogger()->error($this->store->getDBObject()->getErrorMessage());
124
        }
125
126 101
        return $tbl;
127
    }
128
129 101
    private function getEmptyIndex()
130
    {
131
        return [
132 101
            'from' => [],
133
            'join' => [],
134
            'left_join' => [],
135
            'vars' => [], 'graph_vars' => [], 'graph_uris' => [],
136
            'bnodes' => [],
137
            'triple_patterns' => [],
138
            'sub_joins' => [],
139
            'constraints' => [],
140
            'union_branches' => [],
141
            'patterns' => [],
142
            'havings' => [],
143
        ];
144
    }
145
146 101
    public function getTempTableDefForSQLite($q_sql)
147
    {
148 101
        $col_part = preg_replace('/^SELECT\s*(DISTINCT)?(.*)FROM.*$/s', '\\2', $q_sql);
149 101
        $parts = explode(',', $col_part);
150 101
        $has_order_infos = $this->v('order_infos', 0, $this->infos['query']);
151 101
        $r = '';
152 101
        $added = [];
153 101
        foreach ($parts as $part) {
154 101
            if (preg_match('/\.?(.+)\s+AS\s+`(.+)`/U', trim($part), $m) && !isset($added[$m[2]])) {
155 101
                $alias = $m[2];
156 101
                if ('TMPPOS' == $alias) {
157 4
                    continue;
158
                }
159 101
                $r .= $r ? ',' : '';
160 101
                $r .= "\n `".$alias.'` INTEGER UNSIGNED';
161 101
                $added[$alias] = 1;
162
            }
163
        }
164 101
        if ($has_order_infos) {
165 4
            $r = "\n".'`TMPPOS` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, '.$r;
166
        }
167
168 101
        return $r ? $r."\n" : '';
169
    }
170
171 99
    public function getFinalQueryResult($q_sql, $tmp_tbl)
172
    {
173
        /* var names */
174 99
        $vars = [];
175 99
        $aggregate_vars = [];
176 99
        foreach ($this->infos['query']['result_vars'] as $entry) {
177 99
            if ($entry['aggregate']) {
178 10
                $vars[] = $entry['alias'];
179 10
                $aggregate_vars[] = $entry['alias'];
180
            } else {
181 98
                $vars[] = $entry['var'];
182
            }
183
        }
184
        /* result */
185 99
        $r = ['variables' => $vars];
186 99
        $v_sql = $this->getValueSQL($tmp_tbl, $q_sql);
187
188 99
        $entries = [];
0 ignored issues
show
Unused Code introduced by
The assignment to $entries is dead and can be removed.
Loading history...
189
        try {
190 99
            $entries = $this->store->getDBObject()->fetchList($v_sql);
191 1
        } catch (\Exception $e) {
192 1
            $this->store->getLogger()->error($e->getMessage());
193
        }
194
195 99
        $rows = [];
196 99
        $types = [0 => 'uri', 1 => 'bnode', 2 => 'literal'];
197 99
        if (0 < \count($entries)) {
198 88
            foreach ($entries as $pre_row) {
199 88
                $row = [];
200 88
                foreach ($vars as $var) {
201 88
                    if (isset($pre_row[$var])) {
202 88
                        $row[$var] = $pre_row[$var];
203 88
                        $row[$var.' type'] = isset($pre_row[$var.' type'])
204 85
                            ? $types[$pre_row[$var.' type']]
205
                            : (
206 42
                                \in_array($var, $aggregate_vars)
207 7
                                    ? 'literal'
208 42
                                    : 'uri'
209
                              );
210
                        if (
211 88
                            isset($pre_row[$var.' lang_dt'])
212 88
                            && ($lang_dt = $pre_row[$var.' lang_dt'])
213
                        ) {
214 13
                            if (preg_match('/^([a-z]+(\-[a-z0-9]+)*)$/i', $lang_dt)) {
215 6
                                $row[$var.' lang'] = $lang_dt;
216
                            } else {
217 9
                                $row[$var.' datatype'] = $lang_dt;
218
                            }
219
                        }
220
                    }
221
                }
222 88
                if ($row || !$vars) {
223 88
                    $rows[] = $row;
224
                }
225
            }
226
        }
227 99
        $r['rows'] = $rows;
228
229 99
        return $r;
230
    }
231
232 101
    public function buildIndex($pattern, $id)
233
    {
234 101
        $pattern['id'] = $id;
235 101
        $type = $this->v('type', '', $pattern);
236 101
        if (('filter' == $type) && $this->v('constraint', 0, $pattern)) {
237 23
            $sub_pattern = $pattern['constraint'];
238 23
            $sub_pattern['parent_id'] = $id;
239 23
            $sub_id = $id.'_0';
240 23
            $this->buildIndex($sub_pattern, $sub_id);
241 23
            $pattern['constraint'] = $sub_id;
242
        } else {
243 101
            $sub_patterns = $this->v('patterns', [], $pattern);
244 101
            $keys = array_keys($sub_patterns);
245 101
            $spc = \count($sub_patterns);
246 101
            if ($spc > 4 && $this->pattern_order_offset) {
247
                $keys = [];
248
                for ($i = 0; $i < $spc; ++$i) {
249
                    $keys[$i] = $i + $this->pattern_order_offset;
250
                    while ($keys[$i] >= $spc) {
251
                        $keys[$i] -= $spc;
252
                    }
253
                }
254
            }
255 101
            foreach ($keys as $i => $key) {
256 101
                $sub_pattern = $sub_patterns[$key];
257 101
                $sub_pattern['parent_id'] = $id;
258 101
                $sub_id = $id.'_'.$key;
259 101
                $this->buildIndex($sub_pattern, $sub_id);
260 101
                $pattern['patterns'][$i] = $sub_id;
261 101
                if ('union' == $type) {
262 1
                    $this->index['union_branches'][] = $sub_id;
0 ignored issues
show
Bug Best Practice introduced by
The property index does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
263
                }
264
            }
265
        }
266 101
        $this->index['patterns'][$id] = $pattern;
267 101
    }
268
269 101
    public function analyzeIndex($pattern)
270
    {
271 101
        $type = $this->v('type', '', $pattern);
272 101
        if (!$type) {
273
            return false;
274
        }
275 101
        $type = $pattern['type'];
276 101
        $id = $pattern['id'];
277
        /* triple */
278 101
        if ('triple' == $type) {
279 101
            foreach (['s', 'p', 'o'] as $term) {
280 101
                if ('var' == $pattern[$term.'_type']) {
281 101
                    $val = $pattern[$term];
282 101
                    $this->index['vars'][$val] = array_merge(
0 ignored issues
show
Bug Best Practice introduced by
The property index does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
283 101
                        $this->v($val, [], $this->index['vars']),
284 101
                        [['table' => $pattern['id'], 'col' => $term]]
285
                    );
286
                }
287 101
                if ('bnode' == $pattern[$term.'_type']) {
288 11
                    $val = $pattern[$term];
289 11
                    $this->index['bnodes'][$val] = array_merge(
290 11
                        $this->v($val, [], $this->index['bnodes']),
291 11
                        [['table' => $pattern['id'], 'col' => $term]]
292
                    );
293
                }
294
            }
295 101
            $this->index['triple_patterns'][] = $pattern['id'];
296
            /* joins */
297 101
            if ($this->isOptionalPattern($id)) {
298 2
                $this->index['left_join'][] = $id;
299 101
            } elseif (!$this->index['from']) {
300 101
                $this->index['from'][] = $id;
301 14
            } elseif (!$this->getJoinInfos($id)) {
302
                $this->index['from'][] = $id;
303
            } else {
304 14
                $this->index['join'][] = $id;
305
            }
306
            /* graph infos, graph vars */
307 101
            $this->index['patterns'][$id]['graph_infos'] = $this->getGraphInfos($id);
308 101
            foreach ($this->index['patterns'][$id]['graph_infos'] as $info) {
309 49
                if ('graph' == $info['type']) {
310
                    if ($info['var']) {
311
                        $val = $info['var']['value'];
312
                        $this->index['graph_vars'][$val] = array_merge(
313
                            $this->v($val, [], $this->index['graph_vars']),
314
                            [['table' => $id]]
315
                        );
316
                    } elseif ($info['uri']) {
317
                        $val = $info['uri'];
318
                        $this->index['graph_uris'][$val] = array_merge(
319
                            $this->v($val, [], $this->index['graph_uris']),
320
                            [['table' => $id]]
321
                        );
322
                    }
323
                }
324
            }
325
        }
326 101
        $sub_ids = $this->v('patterns', [], $pattern);
327 101
        foreach ($sub_ids as $sub_id) {
328 101
            $this->analyzeIndex($this->getPattern($sub_id));
329
        }
330 101
    }
331
332 101
    public function getGraphInfos($id)
333
    {
334 101
        $r = [];
335 101
        if ($id) {
336 101
            $pattern = $this->index['patterns'][$id];
337 101
            $type = $pattern['type'];
338
            /* graph */
339 101
            if ('graph' == $type) {
340
                $r[] = ['type' => 'graph', 'var' => $pattern['var'], 'uri' => $pattern['uri']];
341
            }
342 101
            $p_pattern = $this->index['patterns'][$pattern['parent_id']];
343 101
            if (isset($p_pattern['graph_infos'])) {
344
                return array_merge($p_pattern['graph_infos'], $r);
345
            }
346
347 101
            return array_merge($this->getGraphInfos($pattern['parent_id']), $r);
348
        } else {
349
            /* FROM / FROM NAMED */
350 101
            if (isset($this->infos['query']['dataset'])) {
351 101
                foreach ($this->infos['query']['dataset'] as $set) {
352 49
                    $r[] = array_merge(['type' => 'dataset'], $set);
353
                }
354
            }
355
        }
356
357 101
        return $r;
358
    }
359
360 101
    public function getPattern($id)
361
    {
362 101
        if (\is_array($id)) {
363
            return $id;
364
        }
365
366 101
        return $this->v($id, [], $this->index['patterns']);
367
    }
368
369
    public function getInitialPattern($id)
370
    {
371
        return $this->v($id, [], $this->initial_index['patterns']);
372
    }
373
374 1
    public function getUnionIndexes($pre_index)
375
    {
376 1
        $r = [];
377 1
        $branches = [];
378 1
        $min_depth = 1000;
379
        /* only process branches with minimum depth */
380 1
        foreach ($pre_index['union_branches'] as $id) {
381 1
            $branches[$id] = \count(preg_split('/\_/', $id));
382 1
            $min_depth = min($min_depth, $branches[$id]);
383
        }
384 1
        foreach ($branches as $branch_id => $depth) {
385 1
            if ($depth == $min_depth) {
386 1
                $union_id = preg_replace('/\_[0-9]+$/', '', $branch_id);
387 1
                $index = [
388 1
                    'keeping' => $branch_id,
389
                    'union_branches' => [],
390 1
                    'patterns' => $pre_index['patterns'],
391
                ];
392 1
                $old_branches = $index['patterns'][$union_id]['patterns'];
393 1
                $skip_id = ($old_branches[0] == $branch_id) ? $old_branches[1] : $old_branches[0];
394 1
                $index['patterns'][$union_id]['type'] = 'group';
395 1
                $index['patterns'][$union_id]['patterns'] = [$branch_id];
396 1
                foreach ($index['patterns'] as $pattern_id => $pattern) {
397 1
                    if (preg_match('/^'.$skip_id.'/', $pattern_id)) {
398 1
                        unset($index['patterns'][$pattern_id]);
399 1
                    } elseif ('union' == $pattern['type']) {
400
                        foreach ($pattern['patterns'] as $sub_union_branch_id) {
401
                            $index['union_branches'][] = $sub_union_branch_id;
402
                        }
403
                    }
404
                }
405 1
                if ($index['union_branches']) {
406
                    $r = array_merge($r, $this->getUnionIndexes($index));
407
                } else {
408 1
                    $r[] = $index;
409
                }
410
            }
411
        }
412
413 1
        return $r;
414
    }
415
416 101
    public function isOptionalPattern($id)
417
    {
418 101
        $pattern = $this->getPattern($id);
419 101
        if ('optional' == $this->v('type', '', $pattern)) {
420 2
            return 1;
421
        }
422 101
        if ('0' == $this->v('parent_id', '0', $pattern)) {
423 101
            return 0;
424
        }
425
426 101
        return $this->isOptionalPattern($pattern['parent_id']);
427
    }
428
429 15
    public function getOptionalPattern($id)
430
    {
431 15
        $pn = $this->getPattern($id);
432
        do {
433 15
            $pn = $this->getPattern($pn['parent_id']);
434 15
        } while ($pn['parent_id'] && ('optional' != $pn['type']));
435
436 15
        return $pn['id'];
437
    }
438
439 2
    public function sameOptional($id, $id2)
440
    {
441 2
        return $this->getOptionalPattern($id) == $this->getOptionalPattern($id2);
442
    }
443
444
    public function isUnionPattern($id)
445
    {
446
        $pattern = $this->getPattern($id);
447
        if ('union' == $this->v('type', '', $pattern)) {
448
            return 1;
449
        }
450
        if ('0' == $this->v('parent_id', '0', $pattern)) {
451
            return 0;
452
        }
453
454
        return $this->isUnionPattern($pattern['parent_id']);
455
    }
456
457 98
    public function getValueTable($col)
458
    {
459 98
        return preg_match('/^(s|o)$/', $col) ? $col.'2val' : 'id2val';
460
    }
461
462 49
    public function getGraphTable()
463
    {
464 49
        return 'g2t';
465
    }
466
467 101
    public function getQuerySQL()
468
    {
469 101
        $nl = "\n";
470 101
        $where_sql = $this->getWHERESQL();  /* pre-fills $index['sub_joins'] $index['constraints'] */
0 ignored issues
show
Unused Code introduced by
The assignment to $where_sql is dead and can be removed.
Loading history...
471 101
        $order_sql = $this->getORDERSQL();  /* pre-fills $index['sub_joins'] $index['constraints'] */
0 ignored issues
show
Unused Code introduced by
The assignment to $order_sql is dead and can be removed.
Loading history...
472
473
        return ''.(
474 101
            $this->is_union_query
475 1
                ? 'SELECT'
476 101
                : 'SELECT'.$this->getDistinctSQL()).$nl.
477 101
                    $this->getResultVarsSQL().$nl. /* fills $index['sub_joins'] */
478 101
                    $this->getFROMSQL().
479 101
                    $this->getAllJoinsSQL().
480 101
                    $this->getWHERESQL().
481 101
                    $this->getGROUPSQL().
482 101
                    $this->getORDERSQL().
483 101
                    ($this->is_union_query
484 1
                        ? ''
485 101
                        : $this->getLIMITSQL()
486 101
                    ).$nl.'';
487
    }
488
489 101
    public function getDistinctSQL()
490
    {
491 101
        if ($this->is_union_query) {
492 1
            $check = $this->v('distinct', 0, $this->infos['query'])
493 1
                || $this->v('reduced', 0, $this->infos['query']);
494
495 1
            return $check ? '' : ' ALL';
496
        }
497
498 100
        $check = $this->v('distinct', 0, $this->infos['query'])
499 97
            || $this->v('reduced', 0, $this->infos['query']);
500
501 100
        return $check ? ' DISTINCT' : '';
502
    }
503
504 101
    public function getResultVarsSQL()
505
    {
506 101
        $r = '';
507 101
        $vars = $this->infos['query']['result_vars'];
508 101
        $nl = "\n";
509 101
        $added = [];
510 101
        foreach ($vars as $var) {
511 101
            $var_name = $var['var'];
512 101
            $tbl_alias = '';
513 101
            if ($tbl_infos = $this->getVarTableInfos($var_name, 0)) {
514 99
                $tbl = $tbl_infos['table'];
515 99
                $col = $tbl_infos['col'];
516 99
                $tbl_alias = $tbl_infos['table_alias'];
517 3
            } elseif (1 == $var_name) {/* ASK query */
518 2
                $r .= '1 AS `success`';
519
            } else {
520 1
                $msg = 'Result variable "'.$var_name.'" not used in query.';
521 1
                $this->store->getLogger()->warning($msg);
522
            }
523
524 101
            if ($tbl_alias) {
525
                /* aggregate */
526 99
                if ($var['aggregate']) {
527 10
                    $conv_code = '';
528 10
                    if ('count' != strtolower($var['aggregate'])) {
529 2
                        $tbl_alias = 'V_'.$tbl.'_'.$col.'.val';
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $col does not seem to be defined for all execution paths leading up to this point.
Loading history...
Comprehensibility Best Practice introduced by
The variable $tbl does not seem to be defined for all execution paths leading up to this point.
Loading history...
530 2
                        $conv_code = '0 + ';
531
                    }
532 10
                    if (!isset($added[$var['alias']])) {
533 10
                        $r .= $r ? ','.$nl.'  ' : '  ';
534 10
                        $distinct_code = ('count' == strtolower($var['aggregate'])) && $this->v('distinct', 0, $this->infos['query']) ? 'DISTINCT ' : '';
535 10
                        $r .= $var['aggregate'].'('.$conv_code.$distinct_code.$tbl_alias.') AS `'.$var['alias'].'`';
536 10
                        $added[$var['alias']] = 1;
537
                    }
538
                }
539
                /* normal var */
540
                else {
541 98
                    if (!isset($added[$var_name])) {
542 98
                        $r .= $r ? ','.$nl.'  ' : '  ';
543 98
                        $r .= $tbl_alias.' AS `'.$var_name.'`';
544 98
                        $is_s = ('s' == $col);
545 98
                        $is_o = ('o' == $col);
546 98
                        if ('NULL' == $tbl_alias) {
547
                            /* type / add in UNION queries? */
548
                            if ($is_s || $is_o) {
549
                                $r .= ', '.$nl.'    NULL AS `'.$var_name.' type`';
550
                            }
551
                            /* lang_dt / always add it in UNION queries, the var may be used as s/p/o */
552
                            if ($is_o || $this->is_union_query) {
553
                                $r .= ', '.$nl.'    NULL AS `'.$var_name.' lang_dt`';
554
                            }
555
                        } else {
556
                            /* type */
557 98
                            if ($is_s || $is_o) {
558 96
                                $r .= ', '.$nl.'    '.$tbl_alias.'_type AS `'.$var_name.' type`';
559
                            }
560
                            /* lang_dt / always add it in UNION queries, the var may be used as s/p/o */
561 98
                            if ($is_o) {
562 88
                                $r .= ', '.$nl.'    '.$tbl_alias.'_lang_dt AS `'.$var_name.' lang_dt`';
563 73
                            } elseif ($this->is_union_query) {
564 1
                                $r .= ', '.$nl.'    NULL AS `'.$var_name.' lang_dt`';
565
                            }
566
                        }
567 98
                        $added[$var_name] = 1;
568
                    }
569
                }
570 99
                if (!\in_array($tbl_alias, $this->index['sub_joins'])) {
571 99
                    $this->index['sub_joins'][] = $tbl_alias;
0 ignored issues
show
Bug Best Practice introduced by
The property index does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
572
                }
573
            }
574
        }
575
576 101
        return $r ? $r : '1 AS `success`';
577
    }
578
579 101
    public function getVarTableInfos($var, $ignore_initial_index = 1)
580
    {
581 101
        if ('*' == $var) {
582 2
            return ['table' => '', 'col' => '', 'table_alias' => '*'];
583
        }
584 101
        if ($infos = $this->v($var, 0, $this->index['vars'])) {
585 99
            $infos[0]['table_alias'] = 'T_'.$infos[0]['table'].'.'.$infos[0]['col'];
586
587 99
            return $infos[0];
588
        }
589 4
        if ($infos = $this->v($var, 0, $this->index['graph_vars'])) {
590
            $infos[0]['col'] = 'g';
591
            $infos[0]['table_alias'] = 'G_'.$infos[0]['table'].'.'.$infos[0]['col'];
592
593
            return $infos[0];
594
        }
595 4
        if ($this->is_union_query && !$ignore_initial_index) {
596
            if (($infos = $this->v($var, 0, $this->initial_index['vars'])) || ($infos = $this->v($var, 0, $this->initial_index['graph_vars']))) {
597
                if (!\in_array($var, $this->infos['null_vars'])) {
598
                    $this->infos['null_vars'][] = $var;
0 ignored issues
show
Bug Best Practice introduced by
The property infos does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
599
                }
600
                $infos[0]['table_alias'] = 'NULL';
601
                $infos[0]['col'] = !isset($infos[0]['col']) ? '' : $infos[0]['col'];
602
603
                return $infos[0];
604
            }
605
        }
606
607 4
        return 0;
608
    }
609
610 101
    public function getFROMSQL()
611
    {
612 101
        $from_ids = $this->index['from'];
613 101
        $r = '';
614 101
        foreach ($from_ids as $from_id) {
615 101
            $r .= $r ? ', ' : '';
616 101
            $r .= 'triple T_'.$from_id;
617
        }
618
619 101
        return $r ? 'FROM '.$r : '';
620
    }
621
622 21
    public function getOrderedJoinIDs()
623
    {
624 21
        return array_merge($this->index['from'], $this->index['join'], $this->index['left_join']);
625
    }
626
627 16
    public function getJoinInfos($id)
628
    {
629 16
        $r = [];
630 16
        $tbl_ids = $this->getOrderedJoinIDs();
631 16
        $pattern = $this->getPattern($id);
632 16
        foreach ($tbl_ids as $tbl_id) {
633 16
            $tbl_pattern = $this->getPattern($tbl_id);
634 16
            if ($tbl_id != $id) {
635 16
                foreach (['s', 'p', 'o'] as $tbl_term) {
636 16
                    foreach (['var', 'bnode', 'uri'] as $term_type) {
637 16
                        if ($tbl_pattern[$tbl_term.'_type'] == $term_type) {
638 16
                            foreach (['s', 'p', 'o'] as $term) {
639 16
                                if (($pattern[$term.'_type'] == $term_type) && ($tbl_pattern[$tbl_term] == $pattern[$term])) {
640 16
                                    $r[] = ['term' => $term, 'join_tbl' => $tbl_id, 'join_term' => $tbl_term];
641
                                }
642
                            }
643
                        }
644
                    }
645
                }
646
            }
647
        }
648
649 16
        return $r;
650
    }
651
652 101
    public function getAllJoinsSQL()
653
    {
654 101
        $js = $this->getJoins();
655 101
        $ljs = $this->getLeftJoins();
656 101
        $entries = array_merge($js, $ljs);
657 101
        $id2code = [];
658 101
        foreach ($entries as $entry) {
659 62
            if (preg_match('/([^\s]+) ON (.*)/s', $entry, $m)) {
660 62
                $id2code[$m[1]] = $entry;
661
            }
662
        }
663 101
        $deps = [];
664 101
        foreach ($id2code as $id => $code) {
665 62
            $deps[$id]['rank'] = 0;
666 62
            foreach ($id2code as $other_id => $other_code) {
667 62
                $deps[$id]['rank'] += ($id != $other_id) && preg_match('/'.$other_id.'/', $code) ? 1 : 0;
668 62
                $deps[$id][$other_id] = ($id != $other_id) && preg_match('/'.$other_id.'/', $code) ? 1 : 0;
669
            }
670
        }
671 101
        $r = '';
672
        do {
673
            /* get next 0-rank */
674 101
            $next_id = 0;
675 101
            foreach ($deps as $id => $infos) {
676 62
                if (0 == $infos['rank']) {
677 62
                    $next_id = $id;
678 62
                    break;
679
                }
680
            }
681 101
            if ($next_id) {
682 62
                $r .= "\n".$id2code[$next_id];
683 62
                unset($deps[$next_id]);
684 62
                foreach ($deps as $id => $infos) {
685 12
                    $deps[$id]['rank'] = 0;
686 12
                    unset($deps[$id][$next_id]);
687 12
                    foreach ($infos as $k => $v) {
688 12
                        if (!\in_array($k, ['rank', $next_id])) {
689 12
                            $deps[$id]['rank'] += $v;
690 12
                            $deps[$id][$k] = $v;
691
                        }
692
                    }
693
                }
694
            }
695 101
        } while ($next_id);
696
697 101
        return $r;
698
    }
699
700 101
    public function getJoins()
701
    {
702 101
        $r = [];
703 101
        $nl = "\n";
704 101
        foreach ($this->index['join'] as $id) {
705 13
            $sub_r = $this->getJoinConditionSQL($id);
706 13
            $r[] = 'JOIN triple T_'.$id.' ON ('.$sub_r.$nl.')';
707
        }
708 101
        foreach (array_merge($this->index['from'], $this->index['join']) as $id) {
709 101
            if ($sub_r = $this->getRequiredSubJoinSQL($id)) {
710 59
                $r[] = $sub_r;
711
            }
712
        }
713
714 101
        return $r;
715
    }
716
717 101
    public function getLeftJoins()
718
    {
719 101
        $r = [];
720 101
        $nl = "\n";
721 101
        foreach ($this->index['left_join'] as $id) {
722 2
            $sub_r = $this->getJoinConditionSQL($id);
723 2
            $r[] = 'LEFT JOIN triple T_'.$id.' ON ('.$sub_r.$nl.')';
724
        }
725 101
        foreach ($this->index['left_join'] as $id) {
726 2
            if ($sub_r = $this->getRequiredSubJoinSQL($id, 'LEFT')) {
727
                $r[] = $sub_r;
728
            }
729
        }
730
731 101
        return $r;
732
    }
733
734 15
    public function getJoinConditionSQL($id)
735
    {
736 15
        $r = '';
737 15
        $nl = "\n";
738 15
        $infos = $this->getJoinInfos($id);
739 15
        $pattern = $this->getPattern($id);
740
741 15
        $tbl = 'T_'.$id;
742
        /* core dependency */
743 15
        $d_tbls = $this->getDependentJoins($id);
744 15
        foreach ($d_tbls as $d_tbl) {
745 13
            if (preg_match('/^T_([0-9\_]+)\.[spo]+/', $d_tbl, $m) && ($m[1] != $id)) {
746
                if ($this->isJoinedBefore($m[1], $id) && !\in_array($m[1], array_merge($this->index['from'], $this->index['join']))) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->isJoinedBefore($m[1], $id) of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
747
                    $r .= $r ? $nl.'  AND ' : $nl.'  ';
748
                    $r .= '('.$d_tbl.' IS NOT NULL)';
749
                }
750
                $this->logDependency($id, $d_tbl);
751
            }
752
        }
753
        /* triple-based join info */
754 15
        foreach ($infos as $info) {
755 15
            if ($this->isJoinedBefore($info['join_tbl'], $id) && $this->joinDependsOn($id, $info['join_tbl'])) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->isJoinedBefore($info['join_tbl'], $id) of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
756 15
                $r .= $r ? $nl.'  AND ' : $nl.'  ';
757 15
                $r .= '('.$tbl.'.'.$info['term'].' = T_'.$info['join_tbl'].'.'.$info['join_term'].')';
758
            }
759
        }
760
        /* filters etc */
761 15
        if ($sub_r = $this->getPatternSQL($pattern, 'join__T_'.$id)) {
762 15
            $r .= $r ? $nl.'  AND '.$sub_r : $nl.'  '.'('.$sub_r.')';
763
        }
764
765 15
        return $r;
766
    }
767
768
    /**
769
     * A log of identified table join dependencies in getJoinConditionSQL.
770
     */
771
    public function logDependency($id, $tbl)
772
    {
773
        if (!isset($this->dependency_log[$id])) {
774
            $this->dependency_log[$id] = [];
0 ignored issues
show
Bug Best Practice introduced by
The property dependency_log does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
775
        }
776
        if (!\in_array($tbl, $this->dependency_log[$id])) {
777
            $this->dependency_log[$id][] = $tbl;
778
        }
779
    }
780
781
    /**
782
     * checks whether entries in the dependecy log could perhaps be optimized
783
     * (triggers re-ordering of patterns.
784
     */
785 101
    public function problematicDependencies()
786
    {
787 101
        foreach ($this->dependency_log as $id => $tbls) {
788
            if (\count($tbls) > 1) {
789
                return \count($tbls);
790
            }
791
        }
792
793 101
        return 0;
794
    }
795
796 20
    public function isJoinedBefore($tbl_1, $tbl_2)
797
    {
798 20
        $tbl_ids = $this->getOrderedJoinIDs();
799 20
        foreach ($tbl_ids as $id) {
800 20
            if ($id == $tbl_1) {
801 20
                return 1;
802
            }
803 1
            if ($id == $tbl_2) {
804 1
                return 0;
805
            }
806
        }
807
    }
808
809 15
    public function joinDependsOn($id, $id2)
810
    {
811 15
        if (\in_array($id2, array_merge($this->index['from'], $this->index['join']))) {
812 15
            return 1;
813
        }
814 1
        $d_tbls = $this->getDependentJoins($id2);
815
        //echo $id . ' :: ' . $id2 . '=>' . print_r($d_tbls, 1);
816 1
        foreach ($d_tbls as $d_tbl) {
817 1
            if (preg_match('/^T_'.$id.'\./', $d_tbl)) {
818
                return 1;
819
            }
820
        }
821
822 1
        return 0;
823
    }
824
825 15
    public function getDependentJoins($id)
826
    {
827 15
        $r = [];
828
        /* sub joins */
829 15
        foreach ($this->index['sub_joins'] as $alias) {
830 13
            if (preg_match('/^(T|V|G)_'.$id.'/', $alias)) {
831 13
                $r[] = $alias;
832
            }
833
        }
834
        /* siblings in shared optional */
835 15
        $o_id = $this->getOptionalPattern($id);
836 15
        foreach ($this->index['sub_joins'] as $alias) {
837 13
            if (preg_match('/^(T|V|G)_'.$o_id.'/', $alias) && !\in_array($alias, $r)) {
838 11
                $r[] = $alias;
839
            }
840
        }
841 15
        foreach ($this->index['left_join'] as $alias) {
842 2
            if (preg_match('/^'.$o_id.'/', $alias) && !\in_array($alias, $r)) {
843 2
                $r[] = 'T_'.$alias.'.s';
844
            }
845
        }
846
847 15
        return $r;
848
    }
849
850 101
    public function getRequiredSubJoinSQL($id, $prefix = '')
851
    {
852
        /* id is a triple pattern id. Optional FILTERS and GRAPHs are getting added to the join directly */
853 101
        $nl = "\n";
854 101
        $r = '';
855 101
        foreach ($this->index['sub_joins'] as $alias) {
856 100
            if (preg_match('/^V_'.$id.'_([a-z\_]+)\.val$/', $alias, $m)) {
857 12
                $col = $m[1];
858 12
                $sub_r = '';
859 12
                if ($this->isOptionalPattern($id)) {
860
                    $pattern = $this->getPattern($id);
861
                    do {
862
                        $pattern = $this->getPattern($pattern['parent_id']);
863
                    } while ($pattern['parent_id'] && ('optional' != $pattern['type']));
864
                    $sub_r = $this->getPatternSQL($pattern, 'sub_join__V_'.$id);
865
                }
866 12
                $sub_r = $sub_r ? $nl.'  AND ('.$sub_r.')' : '';
867
                /* lang dt only on literals */
868 12
                if ('o_lang_dt' == $col) {
869 4
                    $sub_sub_r = 'T_'.$id.'.o_type = 2';
870 4
                    $sub_r .= $nl.'  AND ('.$sub_sub_r.')';
871
                }
872 12
                $cur_prefix = $prefix ? $prefix.' ' : '';
873 12
                if ('g' == $col) {
874
                    $r .= trim($cur_prefix.'JOIN '.$this->getValueTable($col).' V_'.$id.'_'.$col.' ON ('.$nl.'  (G_'.$id.'.'.$col.' = V_'.$id.'_'.$col.'.id) '.$sub_r.$nl.')');
875
                } else {
876 12
                    $r .= trim($cur_prefix.'JOIN '.$this->getValueTable($col).' V_'.$id.'_'.$col.' ON ('.$nl.'  (T_'.$id.'.'.$col.' = V_'.$id.'_'.$col.'.id) '.$sub_r.$nl.')');
877
                }
878 100
            } elseif (preg_match('/^G_'.$id.'\.g$/', $alias, $m)) {
879 49
                $pattern = $this->getPattern($id);
880 49
                $sub_r = $this->getPatternSQL($pattern, 'graph_sub_join__G_'.$id);
881 49
                $sub_r = $sub_r ? $nl.'  AND '.$sub_r : '';
882
                /* dataset restrictions */
883 49
                $gi = $this->getGraphInfos($id);
884 49
                $sub_sub_r = '';
885 49
                $added_gts = [];
886 49
                foreach ($gi as $set) {
887 49
                    if (isset($set['graph']) && !\in_array($set['graph'], $added_gts)) {
888 49
                        $sub_sub_r .= '' !== $sub_sub_r ? ',' : '';
889 49
                        $sub_sub_r .= $this->getTermID($set['graph'], 'g');
890 49
                        $added_gts[] = $set['graph'];
891
                    }
892
                }
893 49
                $sub_r .= ('' !== $sub_sub_r) ? $nl.' AND (G_'.$id.'.g IN ('.$sub_sub_r.'))' : '';
894
                /* other graph join conditions */
895 49
                foreach ($this->index['graph_vars'] as $var => $occurs) {
896
                    $occur_tbls = [];
897
                    foreach ($occurs as $occur) {
898
                        $occur_tbls[] = $occur['table'];
899
                        if ($occur['table'] == $id) {
900
                            break;
901
                        }
902
                    }
903
                    foreach ($occur_tbls as $tbl) {
904
                        if (($tbl != $id) && \in_array($id, $occur_tbls) && $this->isJoinedBefore($tbl, $id)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->isJoinedBefore($tbl, $id) of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
905
                            $sub_r .= $nl.'  AND (G_'.$id.'.g = G_'.$tbl.'.g)';
906
                        }
907
                    }
908
                }
909 49
                $cur_prefix = $prefix ? $prefix.' ' : '';
910 49
                $r .= trim($cur_prefix.'JOIN '.$this->getGraphTable().' G_'.$id.' ON ('.$nl.'  (T_'.$id.'.t = G_'.$id.'.t)'.$sub_r.$nl.')');
911
            }
912
        }
913
914 101
        return $r;
915
    }
916
917 101
    public function getWHERESQL()
918
    {
919 101
        $r = '';
920 101
        $nl = "\n";
921
        /* standard constraints */
922 101
        $sub_r = $this->getPatternSQL($this->getPattern('0'), 'where');
923
        /* additional constraints */
924 101
        foreach ($this->index['from'] as $id) {
925 101
            if ($sub_sub_r = $this->getConstraintSQL($id)) {
926
                $sub_r .= $sub_r ? $nl.' AND '.$sub_sub_r : $sub_sub_r;
927
            }
928
        }
929 101
        $r .= $sub_r ?: '';
930
        /* left join dependencies */
931 101
        foreach ($this->index['left_join'] as $id) {
932 2
            $d_joins = $this->getDependentJoins($id);
933 2
            $added = [];
934 2
            $d_aliases = [];
935 2
            $id_alias = 'T_'.$id.'.s';
936 2
            foreach ($d_joins as $alias) {
937 2
                if (preg_match('/^(T|V|G)_([0-9\_]+)(_[spo])?\.([a-z\_]+)/', $alias, $m)) {
938 2
                    $tbl_type = $m[1];
939 2
                    $tbl_pattern_id = $m[2];
940 2
                    $suffix = $m[3];
941
                    /* get rid of dependency permutations and nested optionals */
942 2
                    if (($tbl_pattern_id >= $id) && $this->sameOptional($tbl_pattern_id, $id)) {
943 2
                        if (!\in_array($tbl_type.'_'.$tbl_pattern_id.$suffix, $added)) {
944 2
                            $sub_r .= $sub_r ? ' AND ' : '';
945 2
                            $sub_r .= $alias.' IS NULL';
946 2
                            $d_aliases[] = $alias;
947 2
                            $added[] = $tbl_type.'_'.$tbl_pattern_id.$suffix;
948 2
                            $id_alias = ($tbl_pattern_id == $id) ? $alias : $id_alias;
949
                        }
950
                    }
951
                }
952
            }
953
            /* TODO fix this! */
954 2
            if (\count($d_aliases) > 2) {
955
                $sub_r1 = '  /* '.$id_alias.' dependencies */';
956
                $sub_r2 = '(('.$id_alias.' IS NULL) OR (CONCAT('.implode(', ', $d_aliases).') IS NOT NULL))';
957
                $r .= $r ? $nl.$sub_r1.$nl.'  AND '.$sub_r2 : $sub_r1.$nl.$sub_r2;
958
            }
959
        }
960
961 101
        return $r ? $nl.'WHERE '.$r : '';
962
    }
963
964
    public function addConstraintSQLEntry($id, $sql)
965
    {
966
        if (!isset($this->index['constraints'][$id])) {
967
            $this->index['constraints'][$id] = [];
0 ignored issues
show
Bug Best Practice introduced by
The property index does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
968
        }
969
        if (!\in_array($sql, $this->index['constraints'][$id])) {
970
            $this->index['constraints'][$id][] = $sql;
971
        }
972
    }
973
974 101
    public function getConstraintSQL($id)
975
    {
976 101
        $r = '';
977 101
        $nl = "\n";
978 101
        $constraints = $this->v($id, [], $this->index['constraints']);
979 101
        foreach ($constraints as $constraint) {
980
            $r .= $r ? $nl.'  AND '.$constraint : $constraint;
981
        }
982
983 101
        return $r;
984
    }
985
986 101
    public function getPatternSQL($pattern, $context)
987
    {
988 101
        $type = $this->v('type', '', $pattern);
989 101
        if (!$type) {
990
            return '';
991
        }
992 101
        $m = 'get'.ucfirst($type).'PatternSQL';
993
994 101
        return method_exists($this, $m)
995 101
            ? $this->$m($pattern, $context)
996 101
            : $this->getDefaultPatternSQL($pattern, $context);
997
    }
998
999 101
    public function getDefaultPatternSQL($pattern, $context)
1000
    {
1001 101
        $r = '';
1002 101
        $nl = "\n";
1003 101
        $sub_ids = $this->v('patterns', [], $pattern);
1004 101
        foreach ($sub_ids as $sub_id) {
1005 101
            $sub_r = $this->getPatternSQL($this->getPattern($sub_id), $context);
1006 101
            $r .= ($r && $sub_r) ? $nl.'  AND ('.$sub_r.')' : ($sub_r ?: '');
1007
        }
1008
1009 101
        return $r ? $r : '';
1010
    }
1011
1012 101
    public function getTriplePatternSQL($pattern, $context)
1013
    {
1014 101
        $r = '';
1015 101
        $nl = "\n";
1016 101
        $id = $pattern['id'];
1017
        /* s p o */
1018 101
        $vars = [];
1019 101
        foreach (['s', 'p', 'o'] as $term) {
1020 101
            $sub_r = '';
1021 101
            $type = $pattern[$term.'_type'];
1022 101
            if ('uri' == $type) {
1023 71
                $term_id = $this->getTermID($pattern[$term], $term);
1024 71
                $sub_r = '(T_'.$id.'.'.$term.' = '.$term_id.') /* '.preg_replace('/[\#\*\>]/', '::', $pattern[$term]).' */';
1025 101
            } elseif ('literal' == $type) {
1026 6
                $term_id = $this->getTermID($pattern[$term], $term);
1027 6
                $sub_r = '(T_'.$id.'.'.$term.' = '.$term_id.') /* '.preg_replace('/[\#\n\*\>]/', ' ', $pattern[$term]).' */';
1028 6
                if (($lang_dt = $this->v1($term.'_lang', '', $pattern)) || ($lang_dt = $this->v1($term.'_datatype', '', $pattern))) {
1029 2
                    $lang_dt_id = $this->getTermID($lang_dt);
1030 6
                    $sub_r .= $nl.'  AND (T_'.$id.'.'.$term.'_lang_dt = '.$lang_dt_id.') /* '.preg_replace('/[\#\*\>]/', '::', $lang_dt).' */';
1031
                }
1032 101
            } elseif ('var' == $type) {
1033 101
                $val = $pattern[$term];
1034 101
                if (isset($vars[$val])) {/* repeated var in pattern */
1035
                    $sub_r = '(T_'.$id.'.'.$term.'='.'T_'.$id.'.'.$vars[$val].')';
1036
                }
1037 101
                $vars[$val] = $term;
1038 101
                if ($infos = $this->v($val, 0, $this->index['graph_vars'])) {/* graph var in triple pattern */
1039
                    $sub_r .= $sub_r ? $nl.'  AND ' : '';
1040
                    $tbl = $infos[0]['table'];
1041
                    $sub_r .= 'G_'.$tbl.'.g = T_'.$id.'.'.$term;
1042
                }
1043
            }
1044 101
            if ($sub_r) {
1045 71
                if (preg_match('/^(join)/', $context) || (preg_match('/^where/', $context) && \in_array($id, $this->index['from']))) {
1046 71
                    $r .= $r ? $nl.'  AND '.$sub_r : $sub_r;
1047
                }
1048
            }
1049
        }
1050
        /* g */
1051 101
        if ($infos = $pattern['graph_infos']) {
1052 49
            $tbl_alias = 'G_'.$id.'.g';
1053 49
            if (!\in_array($tbl_alias, $this->index['sub_joins'])) {
1054 49
                $this->index['sub_joins'][] = $tbl_alias;
0 ignored issues
show
Bug Best Practice introduced by
The property index does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
1055
            }
1056 49
            $sub_r = ['graph_var' => '', 'graph_uri' => '', 'from' => '', 'from_named' => ''];
1057 49
            foreach ($infos as $info) {
1058 49
                $type = $info['type'];
1059 49
                if ('graph' == $type) {
1060
                    if ($info['uri']) {
1061
                        $term_id = $this->getTermID($info['uri'], 'g');
1062
                        $sub_r['graph_uri'] .= $sub_r['graph_uri'] ? $nl.' AND ' : '';
1063
                        $sub_r['graph_uri'] .= '('.$tbl_alias.' = '.$term_id.') /* '.preg_replace('/[\#\*\>]/', '::', $info['uri']).' */';
1064
                    }
1065
                }
1066
            }
1067 49
            if ($sub_r['from'] && $sub_r['from_named']) {
1068
                $sub_r['from_named'] = '';
1069
            }
1070 49
            if (!$sub_r['from'] && !$sub_r['from_named']) {
1071 49
                $sub_r['graph_var'] = '';
1072
            }
1073 49
            if (preg_match('/^(graph_sub_join)/', $context)) {
1074 49
                foreach ($sub_r as $g_type => $g_sql) {
1075 49
                    if ($g_sql) {
1076
                        $r .= $r ? $nl.'  AND '.$g_sql : $g_sql;
1077
                    }
1078
                }
1079
            }
1080
        }
1081
        /* optional sibling filters? */
1082 101
        if (preg_match('/^(join|sub_join)/', $context) && $this->isOptionalPattern($id)) {
1083 2
            $o_pattern = $pattern;
1084
            do {
1085 2
                $o_pattern = $this->getPattern($o_pattern['parent_id']);
1086 2
            } while ($o_pattern['parent_id'] && ('optional' != $o_pattern['type']));
1087 2
            if ($sub_r = $this->getPatternSQL($o_pattern, 'optional_filter'.preg_replace('/^(.*)(__.*)$/', '\\2', $context))) {
1088
                $r .= $r ? $nl.'  AND '.$sub_r : $sub_r;
1089
            }
1090
            /* created constraints */
1091 2
            if ($sub_r = $this->getConstraintSQL($id)) {
1092
                $r .= $r ? $nl.'  AND '.$sub_r : $sub_r;
1093
            }
1094
        }
1095
        /* result */
1096 101
        if (preg_match('/^(where)/', $context) && $this->isOptionalPattern($id)) {
1097 2
            return '';
1098
        }
1099
1100 101
        return $r;
1101
    }
1102
1103 23
    public function getFilterPatternSQL($pattern, $context)
1104
    {
1105 23
        $r = '';
1106 23
        $id = $pattern['id'];
1107 23
        $constraint_id = $this->v1('constraint', '', $pattern);
1108 23
        $constraint = $this->getPattern($constraint_id);
1109 23
        $constraint_type = $constraint['type'];
1110 23
        if ('built_in_call' == $constraint_type) {
1111 14
            $r = $this->getBuiltInCallSQL($constraint, $context);
1112 9
        } elseif ('expression' == $constraint_type) {
1113 9
            $r = $this->getExpressionSQL($constraint, $context, '', 'filter');
1114
        } else {
1115
            $m = 'get'.ucfirst($constraint_type).'ExpressionSQL';
1116
            if (method_exists($this, $m)) {
1117
                $r = $this->$m($constraint, $context, '', 'filter');
1118
            }
1119
        }
1120 23
        if ($this->isOptionalPattern($id) && !preg_match('/^(join|optional_filter)/', $context)) {
1121
            return '';
1122
        }
1123
        /* unconnected vars in FILTERs eval to false */
1124 23
        $sub_r = $this->hasUnconnectedFilterVars($id);
1125 23
        if ($sub_r) {
1126
            if ('alias' == $sub_r) {
1127
                if (!\in_array($r, $this->index['havings'])) {
1128
                    $this->index['havings'][] = $r;
0 ignored issues
show
Bug Best Practice introduced by
The property index does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
1129
                }
1130
1131
                return '';
1132
            } elseif (preg_match('/^T([^\s]+\.)g (.*)$/s', $r, $m)) {/* graph filter */
1133
                return 'G'.$m[1].'t '.$m[2];
1134
            } elseif (preg_match('/^\(*V[^\s]+_g\.val .*$/s', $r, $m)) {
1135
                /* graph value filter, @@improveMe */
1136
            } else {
1137
                return 'FALSE';
1138
            }
1139
        }
1140
        /* some really ugly tweaks */
1141
        /* empty language filter: FILTER ( lang(?v) = '' ) */
1142 23
        $r = preg_replace(
1143 23
            '/\(\/\* language call \*\/ ([^\s]+) = ""\)/s', '((\\1 = "") OR (\\1 LIKE "%:%"))',
1144
            $r
1145
        );
1146
1147 23
        return $r;
1148
    }
1149
1150
    /**
1151
     * Checks if vars in the given (filter) pattern are used within the filter's scope.
1152
     */
1153 23
    public function hasUnconnectedFilterVars($filter_pattern_id)
1154
    {
1155 23
        $scope_id = $this->getFilterScope($filter_pattern_id);
1156 23
        $vars = $this->getFilterVars($filter_pattern_id);
1157 23
        $r = 0;
1158 23
        foreach ($vars as $var_name) {
1159 7
            if ($this->isUsedTripleVar($var_name, $scope_id)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->isUsedTripleVar($var_name, $scope_id) of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
1160 7
                continue;
1161
            }
1162
            if ($this->isAliasVar($var_name)) {
1163
                $r = 'alias';
1164
                break;
1165
            }
1166
            $r = 1;
1167
            break;
1168
        }
1169
1170 23
        return $r;
1171
    }
1172
1173
    /**
1174
     * Returns the given filter pattern's scope (the id of the parent group pattern).
1175
     */
1176 23
    public function getFilterScope($filter_pattern_id)
1177
    {
1178 23
        $patterns = $this->initial_index['patterns'];
1179 23
        $r = '';
1180 23
        foreach ($patterns as $id => $p) {
1181
            /* the id has to be sub-part of the given filter id */
1182 23
            if (!preg_match('/^'.$id.'.+/', $filter_pattern_id)) {
1183 23
                continue;
1184
            }
1185
            /* we are looking for a group or union */
1186 23
            if (!preg_match('/^(group|union)$/', $p['type'])) {
1187
                continue;
1188
            }
1189
            /* we are looking for the longest/deepest match */
1190 23
            if (\strlen($id) > \strlen($r)) {
1191 23
                $r = $id;
1192
            }
1193
        }
1194
1195 23
        return $r;
1196
    }
1197
1198
    /**
1199
     * Builds a list of vars used in the given (filter) pattern.
1200
     */
1201 23
    public function getFilterVars($filter_pattern_id)
1202
    {
1203 23
        $r = [];
1204 23
        $patterns = $this->initial_index['patterns'];
1205
        /* find vars in the given filter (i.e. the given id is part of their pattern id) */
1206 23
        foreach ($patterns as $id => $p) {
1207 23
            if (!preg_match('/^'.$filter_pattern_id.'.+/', $id)) {
1208 23
                continue;
1209
            }
1210 23
            $var_name = '';
1211 23
            if ('var' == $p['type']) {
1212 5
                $var_name = $p['value'];
1213 23
            } elseif (('built_in_call' == $p['type']) && ('bound' == $p['call'])) {
1214 2
                $var_name = $p['args'][0]['value'];
1215
            }
1216 23
            if ($var_name && !\in_array($var_name, $r)) {
1217 7
                $r[] = $var_name;
1218
            }
1219
        }
1220
1221 23
        return $r;
1222
    }
1223
1224
    /**
1225
     * Checks if $var_name appears as result projection alias.
1226
     */
1227
    public function isAliasVar($var_name)
1228
    {
1229
        foreach ($this->infos['query']['result_vars'] as $r_var) {
1230
            if ($r_var['alias'] == $var_name) {
1231
                return 1;
1232
            }
1233
        }
1234
1235
        return 0;
1236
    }
1237
1238
    /**
1239
     * Checks if $var_name is used in a triple pattern in the given scope.
1240
     */
1241 7
    public function isUsedTripleVar($var_name, $scope_id = '0')
1242
    {
1243 7
        $patterns = $this->initial_index['patterns'];
1244 7
        foreach ($patterns as $id => $p) {
1245 7
            if ('triple' != $p['type']) {
1246
                continue;
1247
            }
1248 7
            if (!preg_match('/^'.$scope_id.'.+/', $id)) {
1249
                continue;
1250
            }
1251 7
            foreach (['s', 'p', 'o'] as $term) {
1252 7
                if ('var' != $p[$term.'_type']) {
1253 7
                    continue;
1254
                }
1255 7
                if ($p[$term] == $var_name) {
1256 7
                    return 1;
1257
                }
1258
            }
1259
        }
1260
    }
1261
1262 13
    public function getExpressionSQL($pattern, $context, $val_type = '', $parent_type = '')
1263
    {
1264 13
        $r = '';
1265 13
        $nl = "\n";
0 ignored issues
show
Unused Code introduced by
The assignment to $nl is dead and can be removed.
Loading history...
1266 13
        $type = $this->v1('type', '', $pattern);
1267 13
        $sub_type = $this->v1('sub_type', $type, $pattern);
1268 13
        if (preg_match('/^(and|or)$/', $sub_type)) {
1269 1
            foreach ($pattern['patterns'] as $sub_id) {
1270 1
                $sub_pattern = $this->getPattern($sub_id);
1271 1
                $sub_pattern_type = $sub_pattern['type'];
1272 1
                if ('built_in_call' == $sub_pattern_type) {
1273
                    $sub_r = $this->getBuiltInCallSQL($sub_pattern, $context, '', $parent_type);
0 ignored issues
show
Unused Code introduced by
The call to sweetrdf\InMemoryStoreSq...er::getBuiltInCallSQL() has too many arguments starting with ''. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1273
                    /** @scrutinizer ignore-call */ 
1274
                    $sub_r = $this->getBuiltInCallSQL($sub_pattern, $context, '', $parent_type);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
1274
                } else {
1275 1
                    $sub_r = $this->getExpressionSQL($sub_pattern, $context, '', $parent_type);
1276
                }
1277 1
                if ($sub_r) {
1278 1
                    $r .= $r ? ' '.strtoupper($sub_type).' ('.$sub_r.')' : '('.$sub_r.')';
1279
                }
1280
            }
1281 13
        } elseif ('built_in_call' == $sub_type) {
1282
            $r = $this->getBuiltInCallSQL($pattern, $context, $val_type, $parent_type);
1283 13
        } elseif (preg_match('/literal/', $sub_type)) {
1284 4
            $r = $this->getLiteralExpressionSQL($pattern, $context, $val_type, $parent_type);
1285 11
        } elseif ($sub_type) {
1286 11
            $m = 'get'.ucfirst($sub_type).'ExpressionSQL';
1287 11
            if (method_exists($this, $m)) {
1288 11
                $r = $this->$m($pattern, $context, '', $parent_type);
1289
            }
1290
        }
1291
        /* skip expressions that reference non-yet-joined tables */
1292 13
        if (preg_match('/__(T|V|G)_(.+)$/', $context, $m)) {
1293
            $context_pattern_id = $m[2];
1294
            $context_table_type = $m[1];
1295
            if (preg_match_all('/((T|V|G)(\_[0-9])+)/', $r, $m)) {
1296
                $aliases = $m[1];
1297
                $keep = 1;
1298
                foreach ($aliases as $alias) {
1299
                    if (preg_match('/(T|V|G)_(.*)$/', $alias, $m)) {
1300
                        $tbl_type = $m[1];
1301
                        $tbl = $m[2];
1302
                        if (!$this->isJoinedBefore($tbl, $context_pattern_id)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->isJoinedBefore($tbl, $context_pattern_id) of type integer|null is loosely compared to false; this is ambiguous if the integer can be 0. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
1303
                            $keep = 0;
1304
                        } elseif (($context_pattern_id == $tbl) && preg_match('/(TV)/', $context_table_type.$tbl_type)) {
1305
                            $keep = 0;
1306
                        }
1307
                    }
1308
                }
1309
                $r = $keep ? $r : '';
1310
            }
1311
        }
1312
1313 13
        return $r ? '('.$r.')' : $r;
1314
    }
1315
1316 9
    public function detectExpressionValueType($pattern_ids)
1317
    {
1318 9
        foreach ($pattern_ids as $id) {
1319 9
            $pattern = $this->getPattern($id);
1320 9
            $type = $this->v('type', '', $pattern);
1321 9
            if (('literal' == $type) && isset($pattern['datatype'])) {
1322 5
                if (\in_array($pattern['datatype'], [$this->xsd.'integer', $this->xsd.'float', $this->xsd.'double'])) {
1323 5
                    return 'numeric';
1324
                }
1325
            }
1326
        }
1327
1328 4
        return '';
1329
    }
1330
1331 9
    public function getRelationalExpressionSQL($pattern, $context, $val_type = '', $parent_type = '')
1332
    {
1333 9
        $r = '';
1334 9
        $val_type = $this->detectExpressionValueType($pattern['patterns']);
1335 9
        $op = $pattern['operator'];
1336 9
        foreach ($pattern['patterns'] as $sub_id) {
1337 9
            $sub_pattern = $this->getPattern($sub_id);
1338 9
            $sub_pattern['parent_op'] = $op;
1339 9
            $sub_type = $sub_pattern['type'];
1340 9
            $m = ('built_in_call' == $sub_type) ? 'getBuiltInCallSQL' : 'get'.ucfirst($sub_type).'ExpressionSQL';
1341 9
            $m = str_replace('ExpressionExpression', 'Expression', $m);
1342 9
            $sub_r = method_exists($this, $m) ? $this->$m($sub_pattern, $context, $val_type, 'relational') : '';
1343 9
            $r .= $r ? ' '.$op.' '.$sub_r : $sub_r;
1344
        }
1345
1346
        /*
1347
         * SQLite related adaption for relational expressions like ?w < 100
1348
         *
1349
         * We have to cast the variable behind ?w to a number otherwise we don't get
1350
         * meaningful results.
1351
         */
1352 9
        if ($this->store->getDBObject() instanceof PDOSQLiteAdapter) {
0 ignored issues
show
introduced by
$this->store->getDBObject() is always a sub-type of sweetrdf\InMemoryStoreSqlite\PDOSQLiteAdapter.
Loading history...
1353
            // Regex to catch things like: ?w < 100, ?w > 20
1354 9
            $regex = '/([T\_0-9]+\.o_comp)\s*[<>]{1}\s*[0-9]+/si';
1355 9
            if (0 < preg_match_all($regex, $r, $matches)) {
1356 3
                foreach ($matches[1] as $variable) {
1357 3
                    $r = str_replace($variable, 'CAST ('.$variable.' as float)', $r);
1358
                }
1359
            }
1360
        }
1361
1362 9
        return $r ? '('.$r.')' : $r;
1363
    }
1364
1365
    /**
1366
     * @todo not in use, so remove?
1367
     */
1368
    public function getAdditiveExpressionSQL($pattern, $context, $val_type = '', $parent_type = '')
1369
    {
1370
        $r = '';
1371
        $val_type = $this->detectExpressionValueType($pattern['patterns']);
1372
        foreach ($pattern['patterns'] as $sub_id) {
1373
            $sub_pattern = $this->getPattern($sub_id);
1374
            $sub_type = $this->v('type', '', $sub_pattern);
1375
            $m = ('built_in_call' == $sub_type) ? 'getBuiltInCallSQL' : 'get'.ucfirst($sub_type).'ExpressionSQL';
1376
            $m = str_replace('ExpressionExpression', 'Expression', $m);
1377
            $sub_r = method_exists($this, $m) ? $this->$m($sub_pattern, $context, $val_type, 'additive') : '';
1378
            $r .= $r ? ' '.$sub_r : $sub_r;
1379
        }
1380
1381
        return $r;
1382
    }
1383
1384
    /**
1385
     * @todo not in use, so remove?
1386
     */
1387
    public function getMultiplicativeExpressionSQL($pattern, $context, $val_type = '', $parent_type = '')
1388
    {
1389
        $r = '';
1390
        $val_type = $this->detectExpressionValueType($pattern['patterns']);
1391
        foreach ($pattern['patterns'] as $sub_id) {
1392
            $sub_pattern = $this->getPattern($sub_id);
1393
            $sub_type = $sub_pattern['type'];
1394
            $m = ('built_in_call' == $sub_type) ? 'getBuiltInCallSQL' : 'get'.ucfirst($sub_type).'ExpressionSQL';
1395
            $m = str_replace('ExpressionExpression', 'Expression', $m);
1396
            $sub_r = method_exists($this, $m) ? $this->$m($sub_pattern, $context, $val_type, 'multiplicative') : '';
1397
            $r .= $r ? ' '.$sub_r : $sub_r;
1398
        }
1399
1400
        return $r;
1401
    }
1402
1403 13
    public function getVarExpressionSQL($pattern, $context, $val_type = '', $parent_type = '')
1404
    {
1405 13
        $var = $pattern['value'];
1406 13
        $info = $this->getVarTableInfos($var);
1407
1408 13
        $tbl = false;
1409 13
        if (isset($info['table'])) {
1410 12
            $tbl = $info['table'];
1411
        }
1412
1413 13
        if (!$tbl) {
1414
            /* might be an aggregate var */
1415 1
            $vars = $this->infos['query']['result_vars'];
1416 1
            foreach ($vars as $test_var) {
1417 1
                if ($test_var['alias'] == $pattern['value']) {
1418 1
                    return '`'.$pattern['value'].'`';
1419
                }
1420
            }
1421
1422
            return '';
1423
        }
1424 12
        $col = $info['col'];
1425 12
        if (('order' == $context) && ('o' == $col)) {
1426 3
            $tbl_alias = 'T_'.$tbl.'.o_comp';
1427 9
        } elseif ('sameterm' == $context) {
1428
            $tbl_alias = 'T_'.$tbl.'.'.$col;
1429
        } elseif (
1430 9
            ('relational' == $parent_type)
1431 9
            && 'o' == $col
1432 9
            && (preg_match('/[\<\>]/', $this->v('parent_op', '', $pattern)))) {
1433 3
            $tbl_alias = 'T_'.$tbl.'.o_comp';
1434
        } else {
1435 6
            $tbl_alias = 'V_'.$tbl.'_'.$col.'.val';
1436 6
            if (!\in_array($tbl_alias, $this->index['sub_joins'])) {
1437 6
                $this->index['sub_joins'][] = $tbl_alias;
0 ignored issues
show
Bug Best Practice introduced by
The property index does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
1438
            }
1439
        }
1440 12
        $op = $this->v('operator', '', $pattern);
1441 12
        if (preg_match('/^(filter|and)/', $parent_type)) {
1442
            if ('!' == $op) {
1443
                $r = '((('.$tbl_alias.' = 0) AND (CONCAT("1", '.$tbl_alias.') != 1))'; /* 0 and no string */
1444
                $r .= ' OR ('.$tbl_alias.' IN ("", "false")))'; /* or "", or "false" */
1445
            } else {
1446
                $r = '(('.$tbl_alias.' != 0)'; /* not null */
1447
                $r .= ' OR ((CONCAT("1", '.$tbl_alias.') = 1) AND ('.$tbl_alias.' NOT IN ("", "false"))))'; /* string, and not "" or "false" */
1448
            }
1449
        } else {
1450 12
            $r = trim($op.' '.$tbl_alias);
1451 12
            if ('numeric' == $val_type) {
1452 5
                if (preg_match('/__(T|V|G)_(.+)$/', $context, $m)) {
1453
                    $context_pattern_id = $m[2];
1454
                    $context_table_type = $m[1];
1455
                } else {
1456 5
                    $context_pattern_id = $pattern['id'];
1457 5
                    $context_table_type = 'T';
1458
                }
1459 5
                if ($this->isJoinedBefore($tbl, $context_pattern_id)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->isJoinedBefore($tbl, $context_pattern_id) of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
1460 5
                    $add = ($tbl != $context_pattern_id) ? 1 : 0;
1461 5
                    $add = (!$add && ('V' == $context_table_type)) ? 1 : 0;
1462 5
                    if ($add) {
1463
                        $this->addConstraintSQLEntry($context_pattern_id, '('.$r.' = "0" OR '.$r.'*1.0 != 0)');
1464
                    }
1465
                }
1466
            }
1467
        }
1468
1469 12
        return $r;
1470
    }
1471
1472 1
    public function getUriExpressionSQL($pattern, $context, $val_type = '')
1473
    {
1474 1
        $val = $pattern['uri'];
1475 1
        $r = $pattern['operator'];
1476 1
        $r .= is_numeric($val) ? ' '.$val : ' "'.$this->store->getDBObject()->escape($val).'"';
1477
1478 1
        return $r;
1479
    }
1480
1481 12
    public function getLiteralExpressionSQL($pattern, $context, $val_type = '', $parent_type = '')
1482
    {
1483 12
        $val = $pattern['value'];
1484 12
        $r = $pattern['operator'];
1485 12
        if (is_numeric($val) && $this->v('datatype', 0, $pattern)) {
1486 5
            $r .= ' '.$val;
1487 7
        } elseif (preg_match('/^(true|false)$/i', $val) && ('http://www.w3.org/2001/XMLSchema#boolean' == $this->v1('datatype', '', $pattern))) {
1488
            $r .= ' '.strtoupper($val);
1489 7
        } elseif ('regex' == $parent_type) {
1490 2
            $sub_r = $this->store->getDBObject()->escape($val);
1491 2
            $r .= ' "'.preg_replace('/\x5c\x5c/', '\\', $sub_r).'"';
1492
        } else {
1493 5
            $r .= ' "'.$this->store->getDBObject()->escape($val).'"';
1494
        }
1495 12
        if (($lang_dt = $this->v1('lang', '', $pattern)) || ($lang_dt = $this->v1('datatype', '', $pattern))) {
1496
            /* try table/alias via var in siblings */
1497 5
            if ($var = $this->findSiblingVarExpression($pattern['id'])) {
1498 5
                if (isset($this->index['vars'][$var])) {
1499 5
                    $infos = $this->index['vars'][$var];
1500 5
                    foreach ($infos as $info) {
1501 5
                        if ('o' == $info['col']) {
1502 5
                            $tbl = $info['table'];
1503 5
                            $term_id = $this->getTermID($lang_dt);
1504 5
                            if ('!=' != $pattern['operator']) {
1505 5
                                if (preg_match('/__(T|V|G)_(.+)$/', $context, $m)) {
1506
                                    $context_pattern_id = $m[2];
1507
                                    $context_table_type = $m[1];
0 ignored issues
show
Unused Code introduced by
The assignment to $context_table_type is dead and can be removed.
Loading history...
1508 5
                                } elseif ('where' == $context) {
1509 5
                                    $context_pattern_id = $tbl;
1510
                                } else {
1511
                                    $context_pattern_id = $pattern['id'];
1512
                                }
1513
                                // TODO better dependency check
1514 5
                                if ($tbl == $context_pattern_id) {
1515 5
                                    if ($term_id || ('http://www.w3.org/2001/XMLSchema#integer' != $lang_dt)) {
1516
                                        /* skip, if simple int, but no id */
1517
                                        $this->addConstraintSQLEntry($context_pattern_id, 'T_'.$tbl.'.o_lang_dt = '.$term_id.' /* '.preg_replace('/[\#\*\>]/', '::', $lang_dt).' */');
1518
                                    }
1519
                                }
1520
                            }
1521 5
                            break;
1522
                        }
1523
                    }
1524
                }
1525
            }
1526
        }
1527
1528 12
        return trim($r);
1529
    }
1530
1531 5
    public function findSiblingVarExpression($id)
1532
    {
1533 5
        $pattern = $this->getPattern($id);
1534
        do {
1535 5
            $pattern = $this->getPattern($pattern['parent_id']);
1536 5
        } while ($pattern['parent_id'] && ('expression' != $pattern['type']));
1537 5
        $sub_patterns = $this->v('patterns', [], $pattern);
1538 5
        foreach ($sub_patterns as $sub_id) {
1539 5
            $sub_pattern = $this->getPattern($sub_id);
1540 5
            if ('var' == $sub_pattern['type']) {
1541 5
                return $sub_pattern['value'];
1542
            }
1543
        }
1544
1545
        return '';
1546
    }
1547
1548
    public function getFunctionExpressionSQL($pattern, $context, $val_type = '', $parent_type = '')
1549
    {
1550
        $fnc_uri = $pattern['uri'];
1551
        $op = $this->v('operator', '', $pattern);
1552
        if ($op) {
1553
            $op .= ' ';
1554
        }
1555
        /* simple type conversions */
1556
        if (0 === strpos($fnc_uri, 'http://www.w3.org/2001/XMLSchema#')) {
1557
            return $op.$this->getExpressionSQL($pattern['args'][0], $context, $val_type, $parent_type);
1558
        }
1559
1560
        return '';
1561
    }
1562
1563 18
    public function getBuiltInCallSQL($pattern, $context)
1564
    {
1565 18
        $call = $pattern['call'];
1566 18
        $m = 'get'.ucfirst($call).'CallSQL';
1567 18
        if (method_exists($this, $m)) {
1568 18
            return $this->$m($pattern, $context);
1569
        } else {
1570
            throw new Exception('Unknown built-in call "'.$call.'"');
0 ignored issues
show
Bug introduced by
The type sweetrdf\InMemoryStoreSq...\QueryHandler\Exception was not found. Did you mean Exception? If so, make sure to prefix the type with \.
Loading history...
1571
        }
1572
1573
        return '';
0 ignored issues
show
Unused Code introduced by
return '' is not reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
1574
    }
1575
1576 2
    public function getBoundCallSQL($pattern, $context)
1577
    {
1578 2
        $r = '';
0 ignored issues
show
Unused Code introduced by
The assignment to $r is dead and can be removed.
Loading history...
1579 2
        $var = $pattern['args'][0]['value'];
1580 2
        $info = $this->getVarTableInfos($var);
1581 2
        if (!$tbl = $info['table']) {
1582
            return '';
1583
        }
1584 2
        $col = $info['col'];
1585 2
        $tbl_alias = 'T_'.$tbl.'.'.$col;
1586 2
        if ('!' == $pattern['operator']) {
1587
            return $tbl_alias.' IS NULL';
1588
        }
1589
1590 2
        return $tbl_alias.' IS NOT NULL';
1591
    }
1592
1593 8
    public function getHasTypeCallSQL($pattern, $context, $type)
1594
    {
1595 8
        $r = '';
0 ignored issues
show
Unused Code introduced by
The assignment to $r is dead and can be removed.
Loading history...
1596 8
        $var = $pattern['args'][0]['value'];
1597 8
        $info = $this->getVarTableInfos($var);
1598 8
        if (!$tbl = $info['table']) {
1599
            return '';
1600
        }
1601 8
        $col = $info['col'];
1602 8
        $tbl_alias = 'T_'.$tbl.'.'.$col.'_type';
1603
1604 8
        return $tbl_alias.' '.$this->v('operator', '', $pattern).'= '.$type;
1605
    }
1606
1607 2
    public function getIsliteralCallSQL($pattern, $context)
1608
    {
1609 2
        return $this->getHasTypeCallSQL($pattern, $context, 2);
1610
    }
1611
1612 2
    public function getIsblankCallSQL($pattern, $context)
1613
    {
1614 2
        return $this->getHasTypeCallSQL($pattern, $context, 1);
1615
    }
1616
1617 2
    public function getIsiriCallSQL($pattern, $context)
1618
    {
1619 2
        return $this->getHasTypeCallSQL($pattern, $context, 0);
1620
    }
1621
1622 2
    public function getIsuriCallSQL($pattern, $context)
1623
    {
1624 2
        return $this->getHasTypeCallSQL($pattern, $context, 0);
1625
    }
1626
1627 2
    public function getStrCallSQL($pattern, $context)
1628
    {
1629 2
        $sub_pattern = $pattern['args'][0];
1630 2
        $sub_type = $sub_pattern['type'];
1631 2
        $m = 'get'.ucfirst($sub_type).'ExpressionSQL';
1632 2
        if (method_exists($this, $m)) {
1633 2
            return $this->$m($sub_pattern, $context);
1634
        }
1635
    }
1636
1637
    public function getFunctionCallSQL($pattern, $context)
1638
    {
1639
        $f_uri = $pattern['uri'];
1640
        if (preg_match('/(integer|double|float|string)$/', $f_uri)) {/* skip conversions */
1641
            $sub_pattern = $pattern['args'][0];
1642
            $sub_type = $sub_pattern['type'];
1643
            $m = 'get'.ucfirst($sub_type).'ExpressionSQL';
1644
            if (method_exists($this, $m)) {
1645
                return $this->$m($sub_pattern, $context);
1646
            }
1647
        }
1648
    }
1649
1650 4
    public function getLangDatatypeCallSQL($pattern, $context)
1651
    {
1652 4
        $r = '';
0 ignored issues
show
Unused Code introduced by
The assignment to $r is dead and can be removed.
Loading history...
1653 4
        if (isset($pattern['patterns'])) { /* proceed with first argument only (assumed as base type for type promotion) */
1654
            $sub_pattern = ['args' => [$pattern['patterns'][0]]];
1655
1656
            return $this->getLangDatatypeCallSQL($sub_pattern, $context);
1657
        }
1658 4
        if (!isset($pattern['args'])) {
1659
            return 'FALSE';
1660
        }
1661 4
        $sub_type = $pattern['args'][0]['type'];
1662 4
        if ('var' != $sub_type) {
1663
            return $this->getLangDatatypeCallSQL($pattern['args'][0], $context);
1664
        }
1665 4
        $var = $pattern['args'][0]['value'];
1666 4
        $info = $this->getVarTableInfos($var);
1667 4
        if (!$tbl = $info['table']) {
1668
            return '';
1669
        }
1670 4
        $col = 'o_lang_dt';
1671 4
        $tbl_alias = 'V_'.$tbl.'_'.$col.'.val';
1672 4
        if (!\in_array($tbl_alias, $this->index['sub_joins'])) {
1673 4
            $this->index['sub_joins'][] = $tbl_alias;
0 ignored issues
show
Bug Best Practice introduced by
The property index does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
1674
        }
1675 4
        $op = $this->v('operator', '', $pattern);
1676 4
        $r = trim($op.' '.$tbl_alias);
1677
1678 4
        return $r;
1679
    }
1680
1681 1
    public function getDatatypeCallSQL($pattern, $context)
1682
    {
1683 1
        return '/* datatype call */ '.$this->getLangDatatypeCallSQL($pattern, $context);
1684
    }
1685
1686 3
    public function getLangCallSQL($pattern, $context)
1687
    {
1688 3
        return '/* language call */ '.$this->getLangDatatypeCallSQL($pattern, $context);
1689
    }
1690
1691 2
    public function getLangmatchesCallSQL($pattern, $context)
1692
    {
1693 2
        if (2 == \count($pattern['args'])) {
1694 2
            $arg_1 = $pattern['args'][0];
1695 2
            $arg_2 = $pattern['args'][1];
1696 2
            $sub_r_1 = $this->getBuiltInCallSQL($arg_1, $context); /* adds value join */
1697 2
            $sub_r_2 = $this->getExpressionSQL($arg_2, $context);
1698 2
            $op = $this->v('operator', '', $pattern);
1699 2
            if (preg_match('/^([\"\'])([^\'\"]+)/', $sub_r_2, $m)) {
1700
                if ('*' == $m[2]) {
1701
                    $r = '!' == $op
1702
                        ? 'NOT ('.$sub_r_1.' REGEXP "^[a-zA-Z\-]+$"'.')'
1703
                        : $sub_r_1.' REGEXP "^[a-zA-Z\-]+$"';
1704
                } else {
1705
                    $r = ('!' == $op) ? $sub_r_1.' NOT LIKE '.$m[1].$m[2].'%'.$m[1] : $sub_r_1.' LIKE '.$m[1].$m[2].'%'.$m[1];
1706
                }
1707
            } else {
1708 2
                $r = ('!' == $op) ? $sub_r_1.' NOT LIKE CONCAT('.$sub_r_2.', "%")' : $sub_r_1.' LIKE CONCAT('.$sub_r_2.', "%")';
1709
            }
1710
1711 2
            return $r;
1712
        }
1713
1714
        return '';
1715
    }
1716
1717
    /**
1718
     * @todo not in use, so remove?
1719
     */
1720
    public function getSametermCallSQL($pattern, $context)
1721
    {
1722
        if (2 == \count($pattern['args'])) {
1723
            $arg_1 = $pattern['args'][0];
1724
            $arg_2 = $pattern['args'][1];
1725
            $sub_r_1 = $this->getExpressionSQL($arg_1, 'sameterm');
1726
            $sub_r_2 = $this->getExpressionSQL($arg_2, 'sameterm');
1727
            $op = $this->v('operator', '', $pattern);
1728
            $r = $sub_r_1.' '.$op.'= '.$sub_r_2;
1729
1730
            return $r;
1731
        }
1732
1733
        return '';
1734
    }
1735
1736 2
    public function getRegexCallSQL($pattern, $context)
1737
    {
1738 2
        $ac = \count($pattern['args']);
1739 2
        if ($ac >= 2) {
1740 2
            foreach ($pattern['args'] as $i => $arg) {
1741 2
                $var = 'sub_r_'.($i + 1);
1742 2
                $$var = $this->getExpressionSQL($arg, $context, '', 'regex');
1743
            }
1744 2
            $sub_r_3 = (isset($sub_r_3) && preg_match('/[\"\'](.+)[\"\']/', $sub_r_3, $m)) ? strtolower($m[1]) : '';
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $sub_r_3 seems to never exist and therefore isset should always be false.
Loading history...
1745 2
            $op = ('!' == $this->v('operator', '', $pattern)) ? ' NOT' : '';
1746 2
            if (!$sub_r_1 || !$sub_r_2) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $sub_r_2 seems to be never defined.
Loading history...
Comprehensibility Best Practice introduced by
The variable $sub_r_1 does not exist. Did you maybe mean $sub_r_3?
Loading history...
1747
                return '';
1748
            }
1749 2
            $is_simple_search = preg_match('/^[\(\"]+(\^)?([a-z0-9\_\-\s]+)(\$)?[\)\"]+$/is', $sub_r_2, $m);
0 ignored issues
show
Unused Code introduced by
The assignment to $is_simple_search is dead and can be removed.
Loading history...
1750 2
            $is_simple_search = preg_match('/^[\(\"]+(\^)?([^\\\*\[\]\}\{\(\)\"\'\?\+\.]+)(\$)?[\)\"]+$/is', $sub_r_2, $m);
1751 2
            $is_o_search = preg_match('/o\.val\)*$/', $sub_r_1);
1752
            /* fulltext search (may have "|") */
1753 2
            if ($is_simple_search && $is_o_search && !$op && (\strlen($m[2]) > 8) && $this->store->hasFulltextIndex()) {
0 ignored issues
show
Bug introduced by
The method hasFulltextIndex() does not exist on sweetrdf\InMemoryStoreSq...ore\InMemoryStoreSqlite. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1753
            if ($is_simple_search && $is_o_search && !$op && (\strlen($m[2]) > 8) && $this->store->/** @scrutinizer ignore-call */ hasFulltextIndex()) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
1754
                /* MATCH variations */
1755
                if (($val_parts = preg_split('/\|/', $m[2]))) {
1756
                    return 'MATCH('.trim($sub_r_1, '()').') AGAINST("'.implode(' ', $val_parts).'")';
1757
                } else {
1758
                    return 'MATCH('.trim($sub_r_1, '()').') AGAINST("'.$m[2].'")';
1759
                }
1760
            }
1761 2
            if (preg_match('/\|/', $sub_r_2)) {
1762
                $is_simple_search = 0;
1763
            }
1764
            /* LIKE */
1765 2
            if ($is_simple_search && ('i' == $sub_r_3)) {
1766 1
                $sub_r_2 = $m[1] ? $m[2] : '%'.$m[2];
1767 1
                $sub_r_2 .= isset($m[3]) && $m[3] ? '' : '%';
1768
1769 1
                return $sub_r_1.$op.' LIKE "'.$sub_r_2.'"';
1770
            }
1771
            /* REGEXP */
1772 1
            $opt = '';
1773 1
            if (!$this->store->getDBObject() instanceof PDOSQLiteAdapter) {
0 ignored issues
show
introduced by
$this->store->getDBObject() is always a sub-type of sweetrdf\InMemoryStoreSqlite\PDOSQLiteAdapter.
Loading history...
1774
                $opt = ('i' == $sub_r_3) ? '' : 'BINARY ';
1775
            }
1776
1777 1
            return $sub_r_1.$op.' REGEXP '.$opt.$sub_r_2;
1778
        }
1779
1780
        return '';
1781
    }
1782
1783 101
    public function getGROUPSQL()
1784
    {
1785 101
        $r = '';
1786 101
        $nl = "\n";
1787 101
        $infos = $this->v('group_infos', [], $this->infos['query']);
1788 101
        foreach ($infos as $info) {
1789 5
            $var = $info['value'];
1790 5
            if ($tbl_infos = $this->getVarTableInfos($var, 0)) {
1791 5
                $tbl_alias = $tbl_infos['table_alias'];
1792 5
                $r .= $r ? ', ' : 'GROUP BY ';
1793 5
                $r .= $tbl_alias;
1794
            }
1795
        }
1796 101
        $hr = '';
1797 101
        foreach ($this->index['havings'] as $having) {
1798
            $hr .= $hr ? ' AND' : ' HAVING';
1799
            $hr .= '('.$having.')';
1800
        }
1801 101
        $r .= $hr;
1802
1803 101
        return $r ? $nl.$r : $r;
1804
    }
1805
1806 101
    public function getORDERSQL()
1807
    {
1808 101
        $r = '';
1809 101
        $nl = "\n";
1810 101
        $infos = $this->v('order_infos', [], $this->infos['query']);
1811 101
        foreach ($infos as $info) {
1812 4
            $type = $info['type'];
1813 4
            $ms = ['expression' => 'getExpressionSQL', 'built_in_call' => 'getBuiltInCallSQL', 'function_call' => 'getFunctionCallSQL'];
1814 4
            $m = isset($ms[$type]) ? $ms[$type] : 'get'.ucfirst($type).'ExpressionSQL';
1815 4
            if (method_exists($this, $m)) {
1816 4
                $sub_r = '('.$this->$m($info, 'order').')';
1817 4
                $sub_r .= 'desc' == $this->v('direction', '', $info) ? ' DESC' : '';
1818 4
                $r .= $r ? ','.$nl.$sub_r : $sub_r;
1819
            }
1820
        }
1821
1822 101
        return $r ? $nl.'ORDER BY '.$r : '';
1823
    }
1824
1825 101
    public function getLIMITSQL()
1826
    {
1827 101
        $r = '';
1828 101
        $nl = "\n";
1829 101
        $limit = $this->v('limit', -1, $this->infos['query']);
1830 101
        $offset = $this->v('offset', -1, $this->infos['query']);
1831 101
        if (-1 != $limit) {
1832 6
            $offset = (-1 == $offset) ? 0 : $this->store->getDBObject()->escape($offset);
1833 6
            $r = 'LIMIT '.$offset.','.$limit;
1834 95
        } elseif (-1 != $offset) {
1835
            // mysql doesn't support stand-alone offsets
1836 1
            $r = 'LIMIT '.$this->store->getDBObject()->escape($offset).',999999999999';
1837
        }
1838
1839 101
        return $r ? $nl.$r : '';
1840
    }
1841
1842 99
    public function getValueSQL($q_tbl, $q_sql)
1843
    {
1844 99
        $r = '';
1845
        /* result vars */
1846 99
        $vars = $this->infos['query']['result_vars'];
1847 99
        $nl = "\n";
1848 99
        $v_tbls = ['JOIN' => [], 'LEFT JOIN' => []];
1849 99
        $vc = 1;
1850 99
        foreach ($vars as $var) {
1851 99
            $var_name = $var['var'];
1852 99
            $r .= $r ? ','.$nl.'  ' : '  ';
1853 99
            $col = '';
1854 99
            $tbl = '';
1855 99
            if ('*' != $var_name) {
1856 99
                if (\in_array($var_name, $this->infos['null_vars'])) {
1857
                    if (isset($this->initial_index['vars'][$var_name])) {
1858
                        $col = $this->initial_index['vars'][$var_name][0]['col'];
1859
                        $tbl = $this->initial_index['vars'][$var_name][0]['table'];
1860
                    }
1861
                    if (isset($this->initial_index['graph_vars'][$var_name])) {
1862
                        $col = 'g';
1863
                        $tbl = $this->initial_index['graph_vars'][$var_name][0]['table'];
1864
                    }
1865 99
                } elseif (isset($this->index['vars'][$var_name])) {
1866 99
                    $col = $this->index['vars'][$var_name][0]['col'];
1867 99
                    $tbl = $this->index['vars'][$var_name][0]['table'];
1868
                }
1869
            }
1870 99
            if ($var['aggregate']) {
1871 10
                $r .= 'TMP.`'.$var['alias'].'`';
1872
            } else {
1873 98
                $join_type = \in_array($tbl, array_merge($this->index['from'], $this->index['join'])) ? 'JOIN' : 'LEFT JOIN'; /* val may be NULL */
1874 98
                $v_tbls[$join_type][] = ['t_col' => $col, 'q_col' => $var_name, 'vc' => $vc];
1875 98
                $r .= 'V'.$vc.'.val AS `'.$var_name.'`';
1876 98
                if (\in_array($col, ['s', 'o'])) {
1877 96
                    if (strpos($q_sql, '`'.$var_name.' type`')) {
1878 96
                        $r .= ', '.$nl.'    TMP.`'.$var_name.' type` AS `'.$var_name.' type`';
1879
                    //$r .= ', ' . $nl . '    CASE TMP.`' . $var_name . ' type` WHEN 2 THEN "literal" WHEN 1 THEN "bnode" ELSE "uri" END AS `' . $var_name . ' type`';
1880
                    } else {
1881
                        $r .= ', '.$nl.'    NULL AS `'.$var_name.' type`';
1882
                    }
1883
                }
1884 98
                ++$vc;
1885 98
                if ('o' == $col) {
1886 88
                    $v_tbls[$join_type][] = ['t_col' => 'id', 'q_col' => $var_name.' lang_dt', 'vc' => $vc];
1887 88
                    if (strpos($q_sql, '`'.$var_name.' lang_dt`')) {
1888 88
                        $r .= ', '.$nl.'    V'.$vc.'.val AS `'.$var_name.' lang_dt`';
1889 88
                        ++$vc;
1890
                    } else {
1891
                        $r .= ', '.$nl.'    NULL AS `'.$var_name.' lang_dt`';
1892
                    }
1893
                }
1894
            }
1895
        }
1896 99
        if (!$r) {
1897 2
            $r = '*';
1898
        }
1899
        /* from */
1900 99
        $r .= $nl.'FROM ('.$q_tbl.' TMP)';
1901 99
        foreach (['JOIN', 'LEFT JOIN'] as $join_type) {
1902 99
            foreach ($v_tbls[$join_type] as $v_tbl) {
1903 98
                $tbl = $this->getValueTable($v_tbl['t_col']);
1904 98
                $var_name = preg_replace('/^([^\s]+)(.*)$/', '\\1', $v_tbl['q_col']);
1905 98
                $cur_join_type = \in_array($var_name, $this->infos['null_vars']) ? 'LEFT JOIN' : $join_type;
1906 98
                if (!strpos($q_sql, '`'.$v_tbl['q_col'].'`')) {
1907 1
                    continue;
1908
                }
1909 98
                $r .= $nl.' '.$cur_join_type.' '.$tbl.' V'.$v_tbl['vc'].' ON (
1910 98
            (V'.$v_tbl['vc'].'.id = TMP.`'.$v_tbl['q_col'].'`)
1911
        )';
1912
            }
1913
        }
1914
        /* create pos columns, id needed */
1915 99
        if ($this->v('order_infos', [], $this->infos['query'])) {
1916 4
            $r .= $nl.' ORDER BY TMPPOS';
1917
        }
1918
1919 99
        return 'SELECT'.$nl.$r;
1920
    }
1921
}
1922