Passed
Push — extract-store ( f24e42...0c7df1 )
by Konrad
04:35
created

SelectQueryHandler::getDefaultPatternSQL()   A

Complexity

Conditions 6
Paths 10

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 6

Importance

Changes 0
Metric Value
cc 6
eloc 7
nc 10
nop 2
dl 0
loc 11
ccs 8
cts 8
cp 1
crap 6
rs 9.2222
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 Exception;
17
use sweetrdf\InMemoryStoreSqlite\PDOSQLiteAdapter;
18
19
class SelectQueryHandler extends QueryHandler
20
{
21 102
    public function runQuery($infos)
22
    {
23 102
        $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...
24 102
        $this->infos['null_vars'] = [];
25 102
        $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...
26 102
        $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...
27 102
        $q_sql = $this->getSQL();
28
29
        /* create intermediate results (ID-based) */
30 102
        $tmp_tbl = $this->createTempTable($q_sql);
31
32
        /* join values */
33 102
        $r = $this->getFinalQueryResult($q_sql, $tmp_tbl);
34
35
        /* remove intermediate results */
36 102
        $this->store->getDBObject()->simpleQuery('DROP TABLE IF EXISTS '.$tmp_tbl);
37
38 102
        return $r;
39
    }
40
41 102
    public function getSQL()
42
    {
43 102
        $r = '';
44 102
        $nl = "\n";
45 102
        $this->buildInitialIndexes();
46 102
        foreach ($this->indexes as $i => $index) {
47 102
            $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...
48 102
            $this->analyzeIndex($this->getPattern('0'));
49 102
            $sub_r = $this->getQuerySQL();
50 102
            $r .= $r ? $nl.'UNION'.$this->getDistinctSQL().$nl : '';
51
52 102
            $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...
53 102
            $r .= $setBracket ? '('.$sub_r.')' : $sub_r;
54
55 102
            $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...
56
        }
57 102
        $r .= $this->is_union_query ? $this->getLIMITSQL() : '';
58 102
        $orderInfos = $this->infos['query']['order_infos'] ?? 0;
59 102
        if ($orderInfos) {
60 4
            $r = preg_replace('/SELECT(\s+DISTINCT)?\s*/', 'SELECT\\1 NULL AS `TMPPOS`, ', $r);
61
        }
62 102
        $pd_count = $this->problematicDependencies();
63 102
        if ($pd_count) {
64
            /* re-arranging the patterns sometimes reduces the LEFT JOIN dependencies */
65
            $set_sql = 0;
66
            if (!$this->pattern_order_offset) {
67
                $set_sql = 1;
68
            }
69
            if (!$set_sql && ($pd_count < $this->opt_sql_pd_count)) {
70
                $set_sql = 1;
71
            }
72
            if (!$set_sql && ($pd_count == $this->opt_sql_pd_count) && (\strlen($r) < \strlen($this->opt_sql))) {
73
                $set_sql = 1;
74
            }
75
            if ($set_sql) {
76
                $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...
77
                $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...
78
            }
79
            ++$this->pattern_order_offset;
80
            if ($this->pattern_order_offset > 5) {
81
                return $this->opt_sql;
82
            }
83
84
            return $this->getSQL();
85
        }
86
87 102
        return $r;
88
    }
89
90 102
    public function buildInitialIndexes()
91
    {
92 102
        $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...
93 102
        $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...
94
        // if no pattern is in the query, the index "pattern" is undefined, which leads to an error.
95
        // TODO throw an exception/raise an error and avoid "Undefined index: pattern" notification
96 102
        $this->buildIndex($this->infos['query']['pattern'], 0);
97 102
        $tmp = $this->index;
98 102
        $this->analyzeIndex($this->getPattern('0'));
99 102
        $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...
100 102
        $this->index = $tmp;
101 102
        $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...
102 102
        $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...
103 102
    }
104
105 102
    private function createTempTable($q_sql)
106
    {
107 102
        $tbl = 'Q'.md5($q_sql.time().uniqid(rand()));
108 102
        if (\strlen($tbl) > 64) {
109
            $tbl = 'Q'.md5($tbl);
110
        }
111
112 102
        $tmp_sql = 'CREATE TABLE '.$tbl.' ( '.$this->getTempTableDefForSQLite($q_sql).')';
113 102
        $tmpSql2 = str_replace('CREATE TEMPORARY', 'CREATE', $tmp_sql);
114
115
        if (
116 102
            !$this->store->getDBObject()->simpleQuery($tmp_sql)
117 102
            && !$this->store->getDBObject()->simpleQuery($tmpSql2)
118 102
            && !empty($this->store->getDBObject()->getErrorMessage())
119
        ) {
120
            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...
121
                $this->store->getDBObject()->getErrorMessage()
122
            );
123
        }
124 102
        if (false === $this->store->getDBObject()->exec('INSERT INTO '.$tbl.' '."\n".$q_sql)) {
125
            $this->store->getLogger()->error($this->store->getDBObject()->getErrorMessage());
126
        }
127
128 102
        return $tbl;
129
    }
130
131 102
    private function getEmptyIndex()
132
    {
133
        return [
134 102
            'from' => [],
135
            'join' => [],
136
            'left_join' => [],
137
            'vars' => [], 'graph_vars' => [], 'graph_uris' => [],
138
            'bnodes' => [],
139
            'triple_patterns' => [],
140
            'sub_joins' => [],
141
            'constraints' => [],
142
            'union_branches' => [],
143
            'patterns' => [],
144
            'havings' => [],
145
        ];
146
    }
147
148 102
    public function getTempTableDefForSQLite($q_sql)
149
    {
150 102
        $col_part = preg_replace('/^SELECT\s*(DISTINCT)?(.*)FROM.*$/s', '\\2', $q_sql);
151 102
        $parts = explode(',', $col_part);
152 102
        $has_order_infos = $this->infos['query']['order_infos'] ?? 0;
153 102
        $r = '';
154 102
        $added = [];
155 102
        foreach ($parts as $part) {
156 102
            if (preg_match('/\.?(.+)\s+AS\s+`(.+)`/U', trim($part), $m) && !isset($added[$m[2]])) {
157 102
                $alias = $m[2];
158 102
                if ('TMPPOS' == $alias) {
159 4
                    continue;
160
                }
161 102
                $r .= $r ? ',' : '';
162 102
                $r .= "\n `".$alias.'` INTEGER UNSIGNED';
163 102
                $added[$alias] = 1;
164
            }
165
        }
166 102
        if ($has_order_infos) {
167 4
            $r = "\n".'`TMPPOS` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, '.$r;
168
        }
169
170 102
        return $r ? $r."\n" : '';
171
    }
172
173 100
    public function getFinalQueryResult($q_sql, $tmp_tbl)
174
    {
175
        /* var names */
176 100
        $vars = [];
177 100
        $aggregate_vars = [];
178 100
        foreach ($this->infos['query']['result_vars'] as $entry) {
179 100
            if ($entry['aggregate']) {
180 10
                $vars[] = $entry['alias'];
181 10
                $aggregate_vars[] = $entry['alias'];
182
            } else {
183 99
                $vars[] = $entry['var'];
184
            }
185
        }
186
        /* result */
187 100
        $r = ['variables' => $vars];
188 100
        $v_sql = $this->getValueSQL($tmp_tbl, $q_sql);
189
190 100
        $entries = [];
0 ignored issues
show
Unused Code introduced by
The assignment to $entries is dead and can be removed.
Loading history...
191
        try {
192 100
            $entries = $this->store->getDBObject()->fetchList($v_sql);
193 1
        } catch (\Exception $e) {
194 1
            $this->store->getLogger()->error($e->getMessage());
195
        }
196
197 100
        $rows = [];
198 100
        $types = [0 => 'uri', 1 => 'bnode', 2 => 'literal'];
199 100
        if (0 < \count($entries)) {
200 89
            foreach ($entries as $pre_row) {
201 89
                $row = [];
202 89
                foreach ($vars as $var) {
203 89
                    if (isset($pre_row[$var])) {
204 89
                        $row[$var] = $pre_row[$var];
205 89
                        $row[$var.' type'] = isset($pre_row[$var.' type'])
206 86
                            ? $types[$pre_row[$var.' type']]
207
                            : (
208 42
                                \in_array($var, $aggregate_vars)
209 7
                                    ? 'literal'
210 42
                                    : 'uri'
211
                              );
212
                        if (
213 89
                            isset($pre_row[$var.' lang_dt'])
214 89
                            && ($lang_dt = $pre_row[$var.' lang_dt'])
215
                        ) {
216 13
                            if (preg_match('/^([a-z]+(\-[a-z0-9]+)*)$/i', $lang_dt)) {
217 6
                                $row[$var.' lang'] = $lang_dt;
218
                            } else {
219 9
                                $row[$var.' datatype'] = $lang_dt;
220
                            }
221
                        }
222
                    }
223
                }
224 89
                if ($row || !$vars) {
225 89
                    $rows[] = $row;
226
                }
227
            }
228
        }
229 100
        $r['rows'] = $rows;
230
231 100
        return $r;
232
    }
233
234 102
    public function buildIndex($pattern, $id)
235
    {
236 102
        $pattern['id'] = $id;
237 102
        $type = $pattern['type'] ?? '';
238 102
        $constraint = $pattern['constraint'] ?? 0;
239 102
        if ('filter' == $type && $constraint) {
240 23
            $sub_pattern = $pattern['constraint'];
241 23
            $sub_pattern['parent_id'] = $id;
242 23
            $sub_id = $id.'_0';
243 23
            $this->buildIndex($sub_pattern, $sub_id);
244 23
            $pattern['constraint'] = $sub_id;
245
        } else {
246 102
            $sub_patterns = $pattern['patterns'] ?? [];
247 102
            $keys = array_keys($sub_patterns);
248 102
            $spc = \count($sub_patterns);
249 102
            if ($spc > 4 && $this->pattern_order_offset) {
250
                $keys = [];
251
                for ($i = 0; $i < $spc; ++$i) {
252
                    $keys[$i] = $i + $this->pattern_order_offset;
253
                    while ($keys[$i] >= $spc) {
254
                        $keys[$i] -= $spc;
255
                    }
256
                }
257
            }
258 102
            foreach ($keys as $i => $key) {
259 102
                $sub_pattern = $sub_patterns[$key];
260 102
                $sub_pattern['parent_id'] = $id;
261 102
                $sub_id = $id.'_'.$key;
262 102
                $this->buildIndex($sub_pattern, $sub_id);
263 102
                $pattern['patterns'][$i] = $sub_id;
264 102
                if ('union' == $type) {
265 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...
266
                }
267
            }
268
        }
269 102
        $this->index['patterns'][$id] = $pattern;
270 102
    }
271
272 102
    public function analyzeIndex($pattern)
273
    {
274 102
        $type = $pattern['type'] ?? '';
275 102
        if (!$type) {
276
            return false;
277
        }
278 102
        $type = $pattern['type'];
279 102
        $id = $pattern['id'];
280
        /* triple */
281 102
        if ('triple' == $type) {
282 102
            foreach (['s', 'p', 'o'] as $term) {
283 102
                if ('var' == $pattern[$term.'_type']) {
284 102
                    $val = $pattern[$term];
285 102
                    $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...
286 102
                        $this->index['vars'][$val] ?? [],
287 102
                        [['table' => $pattern['id'], 'col' => $term]]
288
                    );
289
                }
290 102
                if ('bnode' == $pattern[$term.'_type']) {
291 11
                    $val = $pattern[$term];
292 11
                    $this->index['bnodes'][$val] = array_merge(
293 11
                        $this->index['bnodes'][$val] ?? [],
294 11
                        [['table' => $pattern['id'], 'col' => $term]]
295
                    );
296
                }
297
            }
298 102
            $this->index['triple_patterns'][] = $pattern['id'];
299
            /* joins */
300 102
            if ($this->isOptionalPattern($id)) {
301 2
                $this->index['left_join'][] = $id;
302 102
            } elseif (!$this->index['from']) {
303 102
                $this->index['from'][] = $id;
304 14
            } elseif (!$this->getJoinInfos($id)) {
305
                $this->index['from'][] = $id;
306
            } else {
307 14
                $this->index['join'][] = $id;
308
            }
309
            /* graph infos, graph vars */
310 102
            $this->index['patterns'][$id]['graph_infos'] = $this->getGraphInfos($id);
311 102
            foreach ($this->index['patterns'][$id]['graph_infos'] as $info) {
312 49
                if ('graph' == $info['type']) {
313
                    if ($info['var']) {
314
                        $val = $info['var']['value'];
315
                        $this->index['graph_vars'][$val] = array_merge(
316
                            $this->index['graph_vars'][$val] ?? [],
317
                            [['table' => $id]]
318
                        );
319
                    } elseif ($info['uri']) {
320
                        $val = $info['uri'];
321
                        $this->index['graph_uris'][$val] = array_merge(
322
                            $this->index['graph_uris'][$val] ?? [],
323
                            [['table' => $id]]
324
                        );
325
                    }
326
                }
327
            }
328
        }
329 102
        $sub_ids = $pattern['patterns'] ?? [];
330 102
        foreach ($sub_ids as $sub_id) {
331 102
            $this->analyzeIndex($this->getPattern($sub_id));
332
        }
333 102
    }
334
335 102
    public function getGraphInfos($id)
336
    {
337 102
        $r = [];
338 102
        if ($id) {
339 102
            $pattern = $this->index['patterns'][$id];
340 102
            $type = $pattern['type'];
341
            /* graph */
342 102
            if ('graph' == $type) {
343
                $r[] = ['type' => 'graph', 'var' => $pattern['var'], 'uri' => $pattern['uri']];
344
            }
345 102
            $p_pattern = $this->index['patterns'][$pattern['parent_id']];
346 102
            if (isset($p_pattern['graph_infos'])) {
347
                return array_merge($p_pattern['graph_infos'], $r);
348
            }
349
350 102
            return array_merge($this->getGraphInfos($pattern['parent_id']), $r);
351
        } else {
352
            /* FROM / FROM NAMED */
353 102
            if (isset($this->infos['query']['dataset'])) {
354 102
                foreach ($this->infos['query']['dataset'] as $set) {
355 49
                    $r[] = array_merge(['type' => 'dataset'], $set);
356
                }
357
            }
358
        }
359
360 102
        return $r;
361
    }
362
363 102
    public function getPattern($id): array
364
    {
365 102
        if (\is_array($id)) {
366
            return $id;
367
        }
368
369 102
        return $this->index['patterns'][$id] ?? [];
370
    }
371
372
    public function getInitialPattern($id): array
373
    {
374
        return $this->initial_index['patterns'][$id] ?? [];
375
    }
376
377 1
    public function getUnionIndexes($pre_index)
378
    {
379 1
        $r = [];
380 1
        $branches = [];
381 1
        $min_depth = 1000;
382
        /* only process branches with minimum depth */
383 1
        foreach ($pre_index['union_branches'] as $id) {
384 1
            $branches[$id] = \count(preg_split('/\_/', $id));
385 1
            $min_depth = min($min_depth, $branches[$id]);
386
        }
387 1
        foreach ($branches as $branch_id => $depth) {
388 1
            if ($depth == $min_depth) {
389 1
                $union_id = preg_replace('/\_[0-9]+$/', '', $branch_id);
390 1
                $index = [
391 1
                    'keeping' => $branch_id,
392
                    'union_branches' => [],
393 1
                    'patterns' => $pre_index['patterns'],
394
                ];
395 1
                $old_branches = $index['patterns'][$union_id]['patterns'];
396 1
                $skip_id = ($old_branches[0] == $branch_id) ? $old_branches[1] : $old_branches[0];
397 1
                $index['patterns'][$union_id]['type'] = 'group';
398 1
                $index['patterns'][$union_id]['patterns'] = [$branch_id];
399 1
                foreach ($index['patterns'] as $pattern_id => $pattern) {
400 1
                    if (preg_match('/^'.$skip_id.'/', $pattern_id)) {
401 1
                        unset($index['patterns'][$pattern_id]);
402 1
                    } elseif ('union' == $pattern['type']) {
403
                        foreach ($pattern['patterns'] as $sub_union_branch_id) {
404
                            $index['union_branches'][] = $sub_union_branch_id;
405
                        }
406
                    }
407
                }
408 1
                if ($index['union_branches']) {
409
                    $r = array_merge($r, $this->getUnionIndexes($index));
410
                } else {
411 1
                    $r[] = $index;
412
                }
413
            }
414
        }
415
416 1
        return $r;
417
    }
418
419 102
    public function isOptionalPattern($id)
420
    {
421 102
        $pattern = $this->getPattern($id);
422 102
        $type = $pattern['type'] ?? '';
423 102
        if ('optional' == $type) {
424 2
            return 1;
425
        }
426
427 102
        $parentId = $pattern['parent_id'] ?? '0';
428 102
        if ('0' == $parentId) {
429 102
            return 0;
430
        }
431
432 102
        return $this->isOptionalPattern($pattern['parent_id']);
433
    }
434
435 15
    public function getOptionalPattern($id)
436
    {
437 15
        $pn = $this->getPattern($id);
438
        do {
439 15
            $pn = $this->getPattern($pn['parent_id']);
440 15
        } while ($pn['parent_id'] && ('optional' != $pn['type']));
441
442 15
        return $pn['id'];
443
    }
444
445 2
    public function sameOptional($id, $id2)
446
    {
447 2
        return $this->getOptionalPattern($id) == $this->getOptionalPattern($id2);
448
    }
449
450
    public function isUnionPattern($id)
451
    {
452
        $pattern = $this->getPattern($id);
453
454
        $type = $pattern['type'] ?? '';
455
        if ('union' == $type) {
456
            return 1;
457
        }
458
459
        $parentId = $pattern['parent_id'] ?? '0';
460
        if ('0' == $parentId) {
461
            return 0;
462
        }
463
464
        return $this->isUnionPattern($parentId);
465
    }
466
467 99
    public function getValueTable($col)
468
    {
469 99
        return preg_match('/^(s|o)$/', $col) ? $col.'2val' : 'id2val';
470
    }
471
472 49
    public function getGraphTable()
473
    {
474 49
        return 'g2t';
475
    }
476
477 102
    public function getQuerySQL()
478
    {
479 102
        $nl = "\n";
480 102
        $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...
481 102
        $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...
482
483
        return ''.(
484 102
            $this->is_union_query
485 1
                ? 'SELECT'
486 102
                : 'SELECT'.$this->getDistinctSQL()).$nl.
487 102
                    $this->getResultVarsSQL().$nl. /* fills $index['sub_joins'] */
488 102
                    $this->getFROMSQL().
489 102
                    $this->getAllJoinsSQL().
490 102
                    $this->getWHERESQL().
491 102
                    $this->getGROUPSQL().
492 102
                    $this->getORDERSQL().
493 102
                    ($this->is_union_query
494 1
                        ? ''
495 102
                        : $this->getLIMITSQL()
496 102
                    ).$nl.'';
497
    }
498
499 102
    public function getDistinctSQL()
500
    {
501 102
        $distinct = $this->infos['query']['distinct'] ?? 0;
502 102
        $reduced = $this->infos['query']['reduced'] ?? 0;
503
504 102
        $check = $distinct || $reduced;
505 102
        if ($this->is_union_query) {
506 1
            return $check ? '' : ' ALL';
507
        }
508
509 101
        return $check ? ' DISTINCT' : '';
510
    }
511
512 102
    public function getResultVarsSQL()
513
    {
514 102
        $r = '';
515 102
        $vars = $this->infos['query']['result_vars'];
516 102
        $nl = "\n";
517 102
        $added = [];
518 102
        foreach ($vars as $var) {
519 102
            $var_name = $var['var'];
520 102
            $tbl_alias = '';
521 102
            if ($tbl_infos = $this->getVarTableInfos($var_name, 0)) {
522 100
                $tbl = $tbl_infos['table'];
523 100
                $col = $tbl_infos['col'];
524 100
                $tbl_alias = $tbl_infos['table_alias'];
525 3
            } elseif (1 == $var_name) {/* ASK query */
526 2
                $r .= '1 AS `success`';
527
            } else {
528 1
                $msg = 'Result variable "'.$var_name.'" not used in query.';
529 1
                $this->store->getLogger()->warning($msg);
530
            }
531
532 102
            if ($tbl_alias) {
533
                /* aggregate */
534 100
                if ($var['aggregate']) {
535 10
                    $conv_code = '';
536 10
                    if ('count' != strtolower($var['aggregate'])) {
537 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...
538 2
                        $conv_code = '0 + ';
539
                    }
540 10
                    if (!isset($added[$var['alias']])) {
541 10
                        $r .= $r ? ','.$nl.'  ' : '  ';
542
543 10
                        $distinct = $this->infos['query']['distinct'] ?? 0;
544 10
                        $distinct_code = ('count' == strtolower($var['aggregate']))
545 10
                            && $distinct ? 'DISTINCT ' : '';
546
547 10
                        $r .= $var['aggregate']
548 10
                            .'('.$conv_code.$distinct_code.$tbl_alias.') AS `'.$var['alias'].'`';
549 10
                        $added[$var['alias']] = 1;
550
                    }
551
                } else {
552
                    /* normal var */
553 99
                    if (!isset($added[$var_name])) {
554 99
                        $r .= $r ? ','.$nl.'  ' : '  ';
555 99
                        $r .= $tbl_alias.' AS `'.$var_name.'`';
556 99
                        $is_s = ('s' == $col);
557 99
                        $is_o = ('o' == $col);
558 99
                        if ('NULL' == $tbl_alias) {
559
                            /* type / add in UNION queries? */
560
                            if ($is_s || $is_o) {
561
                                $r .= ', '.$nl.'    NULL AS `'.$var_name.' type`';
562
                            }
563
                            /* lang_dt / always add it in UNION queries, the var may be used as s/p/o */
564
                            if ($is_o || $this->is_union_query) {
565
                                $r .= ', '.$nl.'    NULL AS `'.$var_name.' lang_dt`';
566
                            }
567
                        } else {
568
                            /* type */
569 99
                            if ($is_s || $is_o) {
570 97
                                $r .= ', '.$nl.'    '.$tbl_alias.'_type AS `'.$var_name.' type`';
571
                            }
572
                            /* lang_dt / always add it in UNION queries, the var may be used as s/p/o */
573 99
                            if ($is_o) {
574 89
                                $r .= ', '.$nl.'    '.$tbl_alias.'_lang_dt AS `'.$var_name.' lang_dt`';
575 73
                            } elseif ($this->is_union_query) {
576 1
                                $r .= ', '.$nl.'    NULL AS `'.$var_name.' lang_dt`';
577
                            }
578
                        }
579 99
                        $added[$var_name] = 1;
580
                    }
581
                }
582 100
                if (!\in_array($tbl_alias, $this->index['sub_joins'])) {
583 100
                    $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...
584
                }
585
            }
586
        }
587
588 102
        return $r ? $r : '1 AS `success`';
589
    }
590
591 102
    public function getVarTableInfos($var, $ignore_initial_index = 1)
592
    {
593 102
        if ('*' == $var) {
594 2
            return ['table' => '', 'col' => '', 'table_alias' => '*'];
595
        }
596 102
        if ($infos = $this->index['vars'][$var] ?? 0) {
597 100
            $infos[0]['table_alias'] = 'T_'.$infos[0]['table'].'.'.$infos[0]['col'];
598
599 100
            return $infos[0];
600
        }
601 4
        if ($infos = $this->index['graph_vars'][$var] ?? 0) {
602
            $infos[0]['col'] = 'g';
603
            $infos[0]['table_alias'] = 'G_'.$infos[0]['table'].'.'.$infos[0]['col'];
604
605
            return $infos[0];
606
        }
607 4
        if ($this->is_union_query && !$ignore_initial_index) {
608
            if (
609
                ($infos = $this->initial_index['vars'][$var] ?? 0)
610
                || ($infos = $this->initial_index['graph_vars'][$var] ?? 0)
611
            ) {
612
                if (!\in_array($var, $this->infos['null_vars'])) {
613
                    $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...
614
                }
615
                $infos[0]['table_alias'] = 'NULL';
616
                $infos[0]['col'] = !isset($infos[0]['col']) ? '' : $infos[0]['col'];
617
618
                return $infos[0];
619
            }
620
        }
621
622 4
        return 0;
623
    }
624
625 102
    public function getFROMSQL()
626
    {
627 102
        $from_ids = $this->index['from'];
628 102
        $r = '';
629 102
        foreach ($from_ids as $from_id) {
630 102
            $r .= $r ? ', ' : '';
631 102
            $r .= 'triple T_'.$from_id;
632
        }
633
634 102
        return $r ? 'FROM '.$r : '';
635
    }
636
637 21
    public function getOrderedJoinIDs()
638
    {
639 21
        return array_merge($this->index['from'], $this->index['join'], $this->index['left_join']);
640
    }
641
642 16
    public function getJoinInfos($id)
643
    {
644 16
        $r = [];
645 16
        $tbl_ids = $this->getOrderedJoinIDs();
646 16
        $pattern = $this->getPattern($id);
647 16
        foreach ($tbl_ids as $tbl_id) {
648 16
            $tbl_pattern = $this->getPattern($tbl_id);
649 16
            if ($tbl_id != $id) {
650 16
                foreach (['s', 'p', 'o'] as $tbl_term) {
651 16
                    foreach (['var', 'bnode', 'uri'] as $term_type) {
652 16
                        if ($tbl_pattern[$tbl_term.'_type'] == $term_type) {
653 16
                            foreach (['s', 'p', 'o'] as $term) {
654 16
                                if (($pattern[$term.'_type'] == $term_type) && ($tbl_pattern[$tbl_term] == $pattern[$term])) {
655 16
                                    $r[] = ['term' => $term, 'join_tbl' => $tbl_id, 'join_term' => $tbl_term];
656
                                }
657
                            }
658
                        }
659
                    }
660
                }
661
            }
662
        }
663
664 16
        return $r;
665
    }
666
667 102
    public function getAllJoinsSQL()
668
    {
669 102
        $js = $this->getJoins();
670 102
        $ljs = $this->getLeftJoins();
671 102
        $entries = array_merge($js, $ljs);
672 102
        $id2code = [];
673 102
        foreach ($entries as $entry) {
674 62
            if (preg_match('/([^\s]+) ON (.*)/s', $entry, $m)) {
675 62
                $id2code[$m[1]] = $entry;
676
            }
677
        }
678 102
        $deps = [];
679 102
        foreach ($id2code as $id => $code) {
680 62
            $deps[$id]['rank'] = 0;
681 62
            foreach ($id2code as $other_id => $other_code) {
682 62
                $deps[$id]['rank'] += ($id != $other_id) && preg_match('/'.$other_id.'/', $code) ? 1 : 0;
683 62
                $deps[$id][$other_id] = ($id != $other_id) && preg_match('/'.$other_id.'/', $code) ? 1 : 0;
684
            }
685
        }
686 102
        $r = '';
687
        do {
688
            /* get next 0-rank */
689 102
            $next_id = 0;
690 102
            foreach ($deps as $id => $infos) {
691 62
                if (0 == $infos['rank']) {
692 62
                    $next_id = $id;
693 62
                    break;
694
                }
695
            }
696 102
            if ($next_id) {
697 62
                $r .= "\n".$id2code[$next_id];
698 62
                unset($deps[$next_id]);
699 62
                foreach ($deps as $id => $infos) {
700 12
                    $deps[$id]['rank'] = 0;
701 12
                    unset($deps[$id][$next_id]);
702 12
                    foreach ($infos as $k => $v) {
703 12
                        if (!\in_array($k, ['rank', $next_id])) {
704 12
                            $deps[$id]['rank'] += $v;
705 12
                            $deps[$id][$k] = $v;
706
                        }
707
                    }
708
                }
709
            }
710 102
        } while ($next_id);
711
712 102
        return $r;
713
    }
714
715 102
    public function getJoins()
716
    {
717 102
        $r = [];
718 102
        $nl = "\n";
719 102
        foreach ($this->index['join'] as $id) {
720 13
            $sub_r = $this->getJoinConditionSQL($id);
721 13
            $r[] = 'JOIN triple T_'.$id.' ON ('.$sub_r.$nl.')';
722
        }
723 102
        foreach (array_merge($this->index['from'], $this->index['join']) as $id) {
724 102
            if ($sub_r = $this->getRequiredSubJoinSQL($id)) {
725 59
                $r[] = $sub_r;
726
            }
727
        }
728
729 102
        return $r;
730
    }
731
732 102
    public function getLeftJoins()
733
    {
734 102
        $r = [];
735 102
        $nl = "\n";
736 102
        foreach ($this->index['left_join'] as $id) {
737 2
            $sub_r = $this->getJoinConditionSQL($id);
738 2
            $r[] = 'LEFT JOIN triple T_'.$id.' ON ('.$sub_r.$nl.')';
739
        }
740 102
        foreach ($this->index['left_join'] as $id) {
741 2
            if ($sub_r = $this->getRequiredSubJoinSQL($id, 'LEFT')) {
742
                $r[] = $sub_r;
743
            }
744
        }
745
746 102
        return $r;
747
    }
748
749 15
    public function getJoinConditionSQL($id)
750
    {
751 15
        $r = '';
752 15
        $nl = "\n";
753 15
        $infos = $this->getJoinInfos($id);
754 15
        $pattern = $this->getPattern($id);
755
756 15
        $tbl = 'T_'.$id;
757
        /* core dependency */
758 15
        $d_tbls = $this->getDependentJoins($id);
759 15
        foreach ($d_tbls as $d_tbl) {
760 13
            if (preg_match('/^T_([0-9\_]+)\.[spo]+/', $d_tbl, $m) && ($m[1] != $id)) {
761
                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...
762
                    $r .= $r ? $nl.'  AND ' : $nl.'  ';
763
                    $r .= '('.$d_tbl.' IS NOT NULL)';
764
                }
765
                $this->logDependency($id, $d_tbl);
766
            }
767
        }
768
        /* triple-based join info */
769 15
        foreach ($infos as $info) {
770 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...
771 15
                $r .= $r ? $nl.'  AND ' : $nl.'  ';
772 15
                $r .= '('.$tbl.'.'.$info['term'].' = T_'.$info['join_tbl'].'.'.$info['join_term'].')';
773
            }
774
        }
775
        /* filters etc */
776 15
        if ($sub_r = $this->getPatternSQL($pattern, 'join__T_'.$id)) {
777 15
            $r .= $r ? $nl.'  AND '.$sub_r : $nl.'  '.'('.$sub_r.')';
778
        }
779
780 15
        return $r;
781
    }
782
783
    /**
784
     * A log of identified table join dependencies in getJoinConditionSQL.
785
     */
786
    public function logDependency($id, $tbl)
787
    {
788
        if (!isset($this->dependency_log[$id])) {
789
            $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...
790
        }
791
        if (!\in_array($tbl, $this->dependency_log[$id])) {
792
            $this->dependency_log[$id][] = $tbl;
793
        }
794
    }
795
796
    /**
797
     * checks whether entries in the dependecy log could perhaps be optimized
798
     * (triggers re-ordering of patterns.
799
     */
800 102
    public function problematicDependencies()
801
    {
802 102
        foreach ($this->dependency_log as $id => $tbls) {
803
            if (\count($tbls) > 1) {
804
                return \count($tbls);
805
            }
806
        }
807
808 102
        return 0;
809
    }
810
811 20
    public function isJoinedBefore($tbl_1, $tbl_2)
812
    {
813 20
        $tbl_ids = $this->getOrderedJoinIDs();
814 20
        foreach ($tbl_ids as $id) {
815 20
            if ($id == $tbl_1) {
816 20
                return 1;
817
            }
818 1
            if ($id == $tbl_2) {
819 1
                return 0;
820
            }
821
        }
822
    }
823
824 15
    public function joinDependsOn($id, $id2)
825
    {
826 15
        if (\in_array($id2, array_merge($this->index['from'], $this->index['join']))) {
827 15
            return 1;
828
        }
829 1
        $d_tbls = $this->getDependentJoins($id2);
830
        //echo $id . ' :: ' . $id2 . '=>' . print_r($d_tbls, 1);
831 1
        foreach ($d_tbls as $d_tbl) {
832 1
            if (preg_match('/^T_'.$id.'\./', $d_tbl)) {
833
                return 1;
834
            }
835
        }
836
837 1
        return 0;
838
    }
839
840 15
    public function getDependentJoins($id)
841
    {
842 15
        $r = [];
843
        /* sub joins */
844 15
        foreach ($this->index['sub_joins'] as $alias) {
845 13
            if (preg_match('/^(T|V|G)_'.$id.'/', $alias)) {
846 13
                $r[] = $alias;
847
            }
848
        }
849
        /* siblings in shared optional */
850 15
        $o_id = $this->getOptionalPattern($id);
851 15
        foreach ($this->index['sub_joins'] as $alias) {
852 13
            if (preg_match('/^(T|V|G)_'.$o_id.'/', $alias) && !\in_array($alias, $r)) {
853 11
                $r[] = $alias;
854
            }
855
        }
856 15
        foreach ($this->index['left_join'] as $alias) {
857 2
            if (preg_match('/^'.$o_id.'/', $alias) && !\in_array($alias, $r)) {
858 2
                $r[] = 'T_'.$alias.'.s';
859
            }
860
        }
861
862 15
        return $r;
863
    }
864
865 102
    public function getRequiredSubJoinSQL($id, $prefix = '')
866
    {
867
        /* id is a triple pattern id. Optional FILTERS and GRAPHs are getting added to the join directly */
868 102
        $nl = "\n";
869 102
        $r = '';
870 102
        foreach ($this->index['sub_joins'] as $alias) {
871 101
            if (preg_match('/^V_'.$id.'_([a-z\_]+)\.val$/', $alias, $m)) {
872 12
                $col = $m[1];
873 12
                $sub_r = '';
874 12
                if ($this->isOptionalPattern($id)) {
875
                    $pattern = $this->getPattern($id);
876
                    do {
877
                        $pattern = $this->getPattern($pattern['parent_id']);
878
                    } while ($pattern['parent_id'] && ('optional' != $pattern['type']));
879
                    $sub_r = $this->getPatternSQL($pattern, 'sub_join__V_'.$id);
880
                }
881 12
                $sub_r = $sub_r ? $nl.'  AND ('.$sub_r.')' : '';
882
                /* lang dt only on literals */
883 12
                if ('o_lang_dt' == $col) {
884 4
                    $sub_sub_r = 'T_'.$id.'.o_type = 2';
885 4
                    $sub_r .= $nl.'  AND ('.$sub_sub_r.')';
886
                }
887 12
                $cur_prefix = $prefix ? $prefix.' ' : '';
888 12
                if ('g' == $col) {
889
                    $r .= trim($cur_prefix.'JOIN '.$this->getValueTable($col).' V_'.$id.'_'.$col.' ON ('.$nl.'  (G_'.$id.'.'.$col.' = V_'.$id.'_'.$col.'.id) '.$sub_r.$nl.')');
890
                } else {
891 12
                    $r .= trim($cur_prefix.'JOIN '.$this->getValueTable($col).' V_'.$id.'_'.$col.' ON ('.$nl.'  (T_'.$id.'.'.$col.' = V_'.$id.'_'.$col.'.id) '.$sub_r.$nl.')');
892
                }
893 101
            } elseif (preg_match('/^G_'.$id.'\.g$/', $alias, $m)) {
894 49
                $pattern = $this->getPattern($id);
895 49
                $sub_r = $this->getPatternSQL($pattern, 'graph_sub_join__G_'.$id);
896 49
                $sub_r = $sub_r ? $nl.'  AND '.$sub_r : '';
897
                /* dataset restrictions */
898 49
                $gi = $this->getGraphInfos($id);
899 49
                $sub_sub_r = '';
900 49
                $added_gts = [];
901 49
                foreach ($gi as $set) {
902 49
                    if (isset($set['graph']) && !\in_array($set['graph'], $added_gts)) {
903 49
                        $sub_sub_r .= '' !== $sub_sub_r ? ',' : '';
904 49
                        $sub_sub_r .= $this->getTermID($set['graph'], 'g');
905 49
                        $added_gts[] = $set['graph'];
906
                    }
907
                }
908 49
                $sub_r .= ('' !== $sub_sub_r) ? $nl.' AND (G_'.$id.'.g IN ('.$sub_sub_r.'))' : '';
909
                /* other graph join conditions */
910 49
                foreach ($this->index['graph_vars'] as $var => $occurs) {
911
                    $occur_tbls = [];
912
                    foreach ($occurs as $occur) {
913
                        $occur_tbls[] = $occur['table'];
914
                        if ($occur['table'] == $id) {
915
                            break;
916
                        }
917
                    }
918
                    foreach ($occur_tbls as $tbl) {
919
                        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...
920
                            $sub_r .= $nl.'  AND (G_'.$id.'.g = G_'.$tbl.'.g)';
921
                        }
922
                    }
923
                }
924 49
                $cur_prefix = $prefix ? $prefix.' ' : '';
925 49
                $r .= trim($cur_prefix.'JOIN '.$this->getGraphTable().' G_'.$id.' ON ('.$nl.'  (T_'.$id.'.t = G_'.$id.'.t)'.$sub_r.$nl.')');
926
            }
927
        }
928
929 102
        return $r;
930
    }
931
932 102
    public function getWHERESQL()
933
    {
934 102
        $r = '';
935 102
        $nl = "\n";
936
        /* standard constraints */
937 102
        $sub_r = $this->getPatternSQL($this->getPattern('0'), 'where');
938
        /* additional constraints */
939 102
        foreach ($this->index['from'] as $id) {
940 102
            if ($sub_sub_r = $this->getConstraintSQL($id)) {
941
                $sub_r .= $sub_r ? $nl.' AND '.$sub_sub_r : $sub_sub_r;
942
            }
943
        }
944 102
        $r .= $sub_r ?: '';
945
        /* left join dependencies */
946 102
        foreach ($this->index['left_join'] as $id) {
947 2
            $d_joins = $this->getDependentJoins($id);
948 2
            $added = [];
949 2
            $d_aliases = [];
950 2
            $id_alias = 'T_'.$id.'.s';
951 2
            foreach ($d_joins as $alias) {
952 2
                if (preg_match('/^(T|V|G)_([0-9\_]+)(_[spo])?\.([a-z\_]+)/', $alias, $m)) {
953 2
                    $tbl_type = $m[1];
954 2
                    $tbl_pattern_id = $m[2];
955 2
                    $suffix = $m[3];
956
                    /* get rid of dependency permutations and nested optionals */
957 2
                    if (($tbl_pattern_id >= $id) && $this->sameOptional($tbl_pattern_id, $id)) {
958 2
                        if (!\in_array($tbl_type.'_'.$tbl_pattern_id.$suffix, $added)) {
959 2
                            $sub_r .= $sub_r ? ' AND ' : '';
960 2
                            $sub_r .= $alias.' IS NULL';
961 2
                            $d_aliases[] = $alias;
962 2
                            $added[] = $tbl_type.'_'.$tbl_pattern_id.$suffix;
963 2
                            $id_alias = ($tbl_pattern_id == $id) ? $alias : $id_alias;
964
                        }
965
                    }
966
                }
967
            }
968
            /* TODO fix this! */
969 2
            if (\count($d_aliases) > 2) {
970
                $sub_r1 = '  /* '.$id_alias.' dependencies */';
971
                $sub_r2 = '(('.$id_alias.' IS NULL) OR (CONCAT('.implode(', ', $d_aliases).') IS NOT NULL))';
972
                $r .= $r ? $nl.$sub_r1.$nl.'  AND '.$sub_r2 : $sub_r1.$nl.$sub_r2;
973
            }
974
        }
975
976 102
        return $r ? $nl.'WHERE '.$r : '';
977
    }
978
979
    public function addConstraintSQLEntry($id, $sql)
980
    {
981
        if (!isset($this->index['constraints'][$id])) {
982
            $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...
983
        }
984
        if (!\in_array($sql, $this->index['constraints'][$id])) {
985
            $this->index['constraints'][$id][] = $sql;
986
        }
987
    }
988
989 102
    public function getConstraintSQL($id)
990
    {
991 102
        $r = '';
992 102
        $nl = "\n";
993 102
        $constraints = $this->index['constraints'][$id] ?? [];
994 102
        foreach ($constraints as $constraint) {
995
            $r .= $r ? $nl.'  AND '.$constraint : $constraint;
996
        }
997
998 102
        return $r;
999
    }
1000
1001 102
    public function getPatternSQL($pattern, $context)
1002
    {
1003 102
        $type = $pattern['type'] ?? '';
1004 102
        if (!$type) {
1005
            return '';
1006
        }
1007
1008 102
        $m = 'get'.ucfirst($type).'PatternSQL';
1009
1010 102
        return method_exists($this, $m)
1011 102
            ? $this->$m($pattern, $context)
1012 102
            : $this->getDefaultPatternSQL($pattern, $context);
1013
    }
1014
1015 102
    public function getDefaultPatternSQL($pattern, $context)
1016
    {
1017 102
        $r = '';
1018 102
        $nl = "\n";
1019 102
        $sub_ids = $pattern['patterns'] ?? [];
1020 102
        foreach ($sub_ids as $sub_id) {
1021 102
            $sub_r = $this->getPatternSQL($this->getPattern($sub_id), $context);
1022 102
            $r .= ($r && $sub_r) ? $nl.'  AND ('.$sub_r.')' : ($sub_r ?: '');
1023
        }
1024
1025 102
        return $r ? $r : '';
1026
    }
1027
1028 102
    public function getTriplePatternSQL($pattern, $context)
1029
    {
1030 102
        $r = '';
1031 102
        $nl = "\n";
1032 102
        $id = $pattern['id'];
1033
        /* s p o */
1034 102
        $vars = [];
1035 102
        foreach (['s', 'p', 'o'] as $term) {
1036 102
            $sub_r = '';
1037 102
            $type = $pattern[$term.'_type'];
1038 102
            if ('uri' == $type) {
1039 72
                $term_id = $this->getTermID($pattern[$term], $term);
1040 72
                $sub_r = '(T_'.$id.'.'.$term.' = '.$term_id.') /* '
1041 72
                    .preg_replace('/[\#\*\>]/', '::', $pattern[$term]).' */';
1042 102
            } elseif ('literal' == $type) {
1043 6
                $term_id = $this->getTermID($pattern[$term], $term);
1044 6
                $sub_r = '(T_'.$id.'.'.$term.' = '.$term_id.') /* '
1045 6
                    .preg_replace('/[\#\n\*\>]/', ' ', $pattern[$term]).' */';
1046
                if (
1047 6
                    ($lang_dt = $pattern[$term.'_lang'] ?? '')
1048 6
                    || ($lang_dt = $pattern[$term.'_datatype'] ?? '')
1049
                ) {
1050 2
                    $lang_dt_id = $this->getTermID($lang_dt);
1051
                    $sub_r .= $nl
1052 2
                        .'  AND (T_'.$id.'.'.$term.'_lang_dt = '.$lang_dt_id.') /* '
1053 6
                        .preg_replace('/[\#\*\>]/', '::', $lang_dt).' */';
1054
                }
1055 102
            } elseif ('var' == $type) {
1056 102
                $val = $pattern[$term];
1057 102
                if (isset($vars[$val])) {
1058
                    /* repeated var in pattern */
1059
                    $sub_r = '(T_'.$id.'.'.$term.'='.'T_'.$id.'.'.$vars[$val].')';
1060
                }
1061 102
                $vars[$val] = $term;
1062 102
                if ($infos = $this->index['graph_vars'][$val] ?? 0) {
1063
                    /* graph var in triple pattern */
1064
                    $sub_r .= $sub_r ? $nl.'  AND ' : '';
1065
                    $tbl = $infos[0]['table'];
1066
                    $sub_r .= 'G_'.$tbl.'.g = T_'.$id.'.'.$term;
1067
                }
1068
            }
1069 102
            if ($sub_r) {
1070
                if (
1071 72
                    preg_match('/^(join)/', $context)
1072 72
                    || (preg_match('/^where/', $context) && \in_array($id, $this->index['from']))
1073
                ) {
1074 72
                    $r .= $r ? $nl.'  AND '.$sub_r : $sub_r;
1075
                }
1076
            }
1077
        }
1078
        /* g */
1079 102
        if ($infos = $pattern['graph_infos']) {
1080 49
            $tbl_alias = 'G_'.$id.'.g';
1081 49
            if (!\in_array($tbl_alias, $this->index['sub_joins'])) {
1082 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...
1083
            }
1084 49
            $sub_r = ['graph_var' => '', 'graph_uri' => '', 'from' => '', 'from_named' => ''];
1085 49
            foreach ($infos as $info) {
1086 49
                $type = $info['type'];
1087 49
                if ('graph' == $type) {
1088
                    if ($info['uri']) {
1089
                        $term_id = $this->getTermID($info['uri'], 'g');
1090
                        $sub_r['graph_uri'] .= $sub_r['graph_uri'] ? $nl.' AND ' : '';
1091
                        $sub_r['graph_uri'] .= '('.$tbl_alias.' = '.$term_id.') /* '
1092
                            .preg_replace('/[\#\*\>]/', '::', $info['uri']).' */';
1093
                    }
1094
                }
1095
            }
1096 49
            if ($sub_r['from'] && $sub_r['from_named']) {
1097
                $sub_r['from_named'] = '';
1098
            }
1099 49
            if (!$sub_r['from'] && !$sub_r['from_named']) {
1100 49
                $sub_r['graph_var'] = '';
1101
            }
1102 49
            if (preg_match('/^(graph_sub_join)/', $context)) {
1103 49
                foreach ($sub_r as $g_type => $g_sql) {
1104 49
                    if ($g_sql) {
1105
                        $r .= $r ? $nl.'  AND '.$g_sql : $g_sql;
1106
                    }
1107
                }
1108
            }
1109
        }
1110
        /* optional sibling filters? */
1111 102
        if (preg_match('/^(join|sub_join)/', $context) && $this->isOptionalPattern($id)) {
1112 2
            $o_pattern = $pattern;
1113
            do {
1114 2
                $o_pattern = $this->getPattern($o_pattern['parent_id']);
1115 2
            } while ($o_pattern['parent_id'] && ('optional' != $o_pattern['type']));
1116 2
            if ($sub_r = $this->getPatternSQL($o_pattern, 'optional_filter'.preg_replace('/^(.*)(__.*)$/', '\\2', $context))) {
1117
                $r .= $r ? $nl.'  AND '.$sub_r : $sub_r;
1118
            }
1119
            /* created constraints */
1120 2
            if ($sub_r = $this->getConstraintSQL($id)) {
1121
                $r .= $r ? $nl.'  AND '.$sub_r : $sub_r;
1122
            }
1123
        }
1124
        /* result */
1125 102
        if (preg_match('/^(where)/', $context) && $this->isOptionalPattern($id)) {
1126 2
            return '';
1127
        }
1128
1129 102
        return $r;
1130
    }
1131
1132 23
    public function getFilterPatternSQL($pattern, $context)
1133
    {
1134 23
        $r = '';
1135 23
        $id = $pattern['id'];
1136 23
        $constraint_id = $pattern['constraint'] ?? '';
1137 23
        $constraint = $this->getPattern($constraint_id);
1138 23
        $constraint_type = $constraint['type'];
1139 23
        if ('built_in_call' == $constraint_type) {
1140 14
            $r = $this->getBuiltInCallSQL($constraint, $context);
1141 9
        } elseif ('expression' == $constraint_type) {
1142 9
            $r = $this->getExpressionSQL($constraint, $context, '', 'filter');
1143
        } else {
1144
            $m = 'get'.ucfirst($constraint_type).'ExpressionSQL';
1145
            if (method_exists($this, $m)) {
1146
                $r = $this->$m($constraint, $context, '', 'filter');
1147
            }
1148
        }
1149 23
        if ($this->isOptionalPattern($id) && !preg_match('/^(join|optional_filter)/', $context)) {
1150
            return '';
1151
        }
1152
        /* unconnected vars in FILTERs eval to false */
1153 23
        $sub_r = $this->hasUnconnectedFilterVars($id);
1154 23
        if ($sub_r) {
1155
            if ('alias' == $sub_r) {
1156
                if (!\in_array($r, $this->index['havings'])) {
1157
                    $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...
1158
                }
1159
1160
                return '';
1161
            } elseif (preg_match('/^T([^\s]+\.)g (.*)$/s', $r, $m)) {/* graph filter */
1162
                return 'G'.$m[1].'t '.$m[2];
1163
            } elseif (preg_match('/^\(*V[^\s]+_g\.val .*$/s', $r, $m)) {
1164
                /* graph value filter, @@improveMe */
1165
            } else {
1166
                return 'FALSE';
1167
            }
1168
        }
1169
        /* some really ugly tweaks */
1170
        /* empty language filter: FILTER ( lang(?v) = '' ) */
1171 23
        $r = preg_replace(
1172 23
            '/\(\/\* language call \*\/ ([^\s]+) = ""\)/s', '((\\1 = "") OR (\\1 LIKE "%:%"))',
1173
            $r
1174
        );
1175
1176 23
        return $r;
1177
    }
1178
1179
    /**
1180
     * Checks if vars in the given (filter) pattern are used within the filter's scope.
1181
     */
1182 23
    public function hasUnconnectedFilterVars($filter_pattern_id)
1183
    {
1184 23
        $scope_id = $this->getFilterScope($filter_pattern_id);
1185 23
        $vars = $this->getFilterVars($filter_pattern_id);
1186 23
        $r = 0;
1187 23
        foreach ($vars as $var_name) {
1188 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...
1189 7
                continue;
1190
            }
1191
            if ($this->isAliasVar($var_name)) {
1192
                $r = 'alias';
1193
                break;
1194
            }
1195
            $r = 1;
1196
            break;
1197
        }
1198
1199 23
        return $r;
1200
    }
1201
1202
    /**
1203
     * Returns the given filter pattern's scope (the id of the parent group pattern).
1204
     */
1205 23
    public function getFilterScope($filter_pattern_id)
1206
    {
1207 23
        $patterns = $this->initial_index['patterns'];
1208 23
        $r = '';
1209 23
        foreach ($patterns as $id => $p) {
1210
            /* the id has to be sub-part of the given filter id */
1211 23
            if (!preg_match('/^'.$id.'.+/', $filter_pattern_id)) {
1212 23
                continue;
1213
            }
1214
            /* we are looking for a group or union */
1215 23
            if (!preg_match('/^(group|union)$/', $p['type'])) {
1216
                continue;
1217
            }
1218
            /* we are looking for the longest/deepest match */
1219 23
            if (\strlen($id) > \strlen($r)) {
1220 23
                $r = $id;
1221
            }
1222
        }
1223
1224 23
        return $r;
1225
    }
1226
1227
    /**
1228
     * Builds a list of vars used in the given (filter) pattern.
1229
     */
1230 23
    public function getFilterVars($filter_pattern_id)
1231
    {
1232 23
        $r = [];
1233 23
        $patterns = $this->initial_index['patterns'];
1234
        /* find vars in the given filter (i.e. the given id is part of their pattern id) */
1235 23
        foreach ($patterns as $id => $p) {
1236 23
            if (!preg_match('/^'.$filter_pattern_id.'.+/', $id)) {
1237 23
                continue;
1238
            }
1239 23
            $var_name = '';
1240 23
            if ('var' == $p['type']) {
1241 5
                $var_name = $p['value'];
1242 23
            } elseif (('built_in_call' == $p['type']) && ('bound' == $p['call'])) {
1243 2
                $var_name = $p['args'][0]['value'];
1244
            }
1245 23
            if ($var_name && !\in_array($var_name, $r)) {
1246 7
                $r[] = $var_name;
1247
            }
1248
        }
1249
1250 23
        return $r;
1251
    }
1252
1253
    /**
1254
     * Checks if $var_name appears as result projection alias.
1255
     */
1256
    public function isAliasVar($var_name)
1257
    {
1258
        foreach ($this->infos['query']['result_vars'] as $r_var) {
1259
            if ($r_var['alias'] == $var_name) {
1260
                return 1;
1261
            }
1262
        }
1263
1264
        return 0;
1265
    }
1266
1267
    /**
1268
     * Checks if $var_name is used in a triple pattern in the given scope.
1269
     */
1270 7
    public function isUsedTripleVar($var_name, $scope_id = '0')
1271
    {
1272 7
        $patterns = $this->initial_index['patterns'];
1273 7
        foreach ($patterns as $id => $p) {
1274 7
            if ('triple' != $p['type']) {
1275
                continue;
1276
            }
1277 7
            if (!preg_match('/^'.$scope_id.'.+/', $id)) {
1278
                continue;
1279
            }
1280 7
            foreach (['s', 'p', 'o'] as $term) {
1281 7
                if ('var' != $p[$term.'_type']) {
1282 7
                    continue;
1283
                }
1284 7
                if ($p[$term] == $var_name) {
1285 7
                    return 1;
1286
                }
1287
            }
1288
        }
1289
    }
1290
1291 13
    public function getExpressionSQL($pattern, $context, $val_type = '', $parent_type = '')
1292
    {
1293 13
        $r = '';
1294 13
        $type = $pattern['type'] ?? '';
1295 13
        $sub_type = $pattern['sub_type'] ?? $type;
1296 13
        if (preg_match('/^(and|or)$/', $sub_type)) {
1297 1
            foreach ($pattern['patterns'] as $sub_id) {
1298 1
                $sub_pattern = $this->getPattern($sub_id);
1299 1
                $sub_pattern_type = $sub_pattern['type'];
1300 1
                if ('built_in_call' == $sub_pattern_type) {
1301
                    $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

1301
                    /** @scrutinizer ignore-call */ 
1302
                    $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...
1302
                } else {
1303 1
                    $sub_r = $this->getExpressionSQL($sub_pattern, $context, '', $parent_type);
1304
                }
1305 1
                if ($sub_r) {
1306 1
                    $r .= $r ? ' '.strtoupper($sub_type).' ('.$sub_r.')' : '('.$sub_r.')';
1307
                }
1308
            }
1309 13
        } elseif ('built_in_call' == $sub_type) {
1310
            $r = $this->getBuiltInCallSQL($pattern, $context, $val_type, $parent_type);
1311 13
        } elseif (preg_match('/literal/', $sub_type)) {
1312 4
            $r = $this->getLiteralExpressionSQL($pattern, $context, $val_type, $parent_type);
1313 11
        } elseif ($sub_type) {
1314 11
            $m = 'get'.ucfirst($sub_type).'ExpressionSQL';
1315 11
            if (method_exists($this, $m)) {
1316 11
                $r = $this->$m($pattern, $context, '', $parent_type);
1317
            }
1318
        }
1319
        /* skip expressions that reference non-yet-joined tables */
1320 13
        if (preg_match('/__(T|V|G)_(.+)$/', $context, $m)) {
1321
            $context_pattern_id = $m[2];
1322
            $context_table_type = $m[1];
1323
            if (preg_match_all('/((T|V|G)(\_[0-9])+)/', $r, $m)) {
1324
                $aliases = $m[1];
1325
                $keep = 1;
1326
                foreach ($aliases as $alias) {
1327
                    if (preg_match('/(T|V|G)_(.*)$/', $alias, $m)) {
1328
                        $tbl_type = $m[1];
1329
                        $tbl = $m[2];
1330
                        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...
1331
                            $keep = 0;
1332
                        } elseif (($context_pattern_id == $tbl) && preg_match('/(TV)/', $context_table_type.$tbl_type)) {
1333
                            $keep = 0;
1334
                        }
1335
                    }
1336
                }
1337
                $r = $keep ? $r : '';
1338
            }
1339
        }
1340
1341 13
        return $r ? '('.$r.')' : $r;
1342
    }
1343
1344 9
    public function detectExpressionValueType($pattern_ids)
1345
    {
1346 9
        foreach ($pattern_ids as $id) {
1347 9
            $pattern = $this->getPattern($id);
1348 9
            $type = $pattern['type'] ?? '';
1349 9
            if (('literal' == $type) && isset($pattern['datatype'])) {
1350 5
                $numericDatatypes = [$this->xsd.'integer', $this->xsd.'float', $this->xsd.'double'];
1351 5
                if (\in_array($pattern['datatype'], $numericDatatypes)) {
1352 5
                    return 'numeric';
1353
                }
1354
            }
1355
        }
1356
1357 4
        return '';
1358
    }
1359
1360 9
    public function getRelationalExpressionSQL($pattern, $context, $val_type = '', $parent_type = '')
1361
    {
1362 9
        $r = '';
1363 9
        $val_type = $this->detectExpressionValueType($pattern['patterns']);
1364 9
        $op = $pattern['operator'];
1365 9
        foreach ($pattern['patterns'] as $sub_id) {
1366 9
            $sub_pattern = $this->getPattern($sub_id);
1367 9
            $sub_pattern['parent_op'] = $op;
1368 9
            $sub_type = $sub_pattern['type'];
1369 9
            $m = ('built_in_call' == $sub_type) ? 'getBuiltInCallSQL' : 'get'.ucfirst($sub_type).'ExpressionSQL';
1370 9
            $m = str_replace('ExpressionExpression', 'Expression', $m);
1371 9
            $sub_r = method_exists($this, $m) ? $this->$m($sub_pattern, $context, $val_type, 'relational') : '';
1372 9
            $r .= $r ? ' '.$op.' '.$sub_r : $sub_r;
1373
        }
1374
1375
        /*
1376
         * SQLite related adaption for relational expressions like ?w < 100
1377
         *
1378
         * We have to cast the variable behind ?w to a number otherwise we don't get
1379
         * meaningful results.
1380
         */
1381 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...
1382
            // Regex to catch things like: ?w < 100, ?w > 20
1383 9
            $regex = '/([T\_0-9]+\.o_comp)\s*[<>]{1}\s*[0-9]+/si';
1384 9
            if (0 < preg_match_all($regex, $r, $matches)) {
1385 3
                foreach ($matches[1] as $variable) {
1386 3
                    $r = str_replace($variable, 'CAST ('.$variable.' as float)', $r);
1387
                }
1388
            }
1389
        }
1390
1391 9
        return $r ? '('.$r.')' : $r;
1392
    }
1393
1394
    public function getAdditiveExpressionSQL($pattern, $context, $val_type = '')
1395
    {
1396
        $r = '';
1397
        $val_type = $this->detectExpressionValueType($pattern['patterns']);
1398
        foreach ($pattern['patterns'] as $sub_id) {
1399
            $sub_pattern = $this->getPattern($sub_id);
1400
            $sub_type = $sub_pattern['type'] ?? '';
1401
            $m = ('built_in_call' == $sub_type)
1402
                ? 'getBuiltInCallSQL'
1403
                : 'get'.ucfirst($sub_type).'ExpressionSQL';
1404
            $m = str_replace('ExpressionExpression', 'Expression', $m);
1405
            $sub_r = method_exists($this, $m)
1406
                ? $this->$m($sub_pattern, $context, $val_type, 'additive')
1407
                : '';
1408
            $r .= $r ? ' '.$sub_r : $sub_r;
1409
        }
1410
1411
        return $r;
1412
    }
1413
1414
    public function getMultiplicativeExpressionSQL($pattern, $context, $val_type = '', $parent_type = '')
1415
    {
1416
        $r = '';
1417
        $val_type = $this->detectExpressionValueType($pattern['patterns']);
1418
        foreach ($pattern['patterns'] as $sub_id) {
1419
            $sub_pattern = $this->getPattern($sub_id);
1420
            $sub_type = $sub_pattern['type'];
1421
            $m = ('built_in_call' == $sub_type)
1422
                ? 'getBuiltInCallSQL'
1423
                : 'get'.ucfirst($sub_type).'ExpressionSQL';
1424
            $m = str_replace('ExpressionExpression', 'Expression', $m);
1425
            $sub_r = method_exists($this, $m)
1426
                ? $this->$m($sub_pattern, $context, $val_type, 'multiplicative')
1427
                : '';
1428
            $r .= $r ? ' '.$sub_r : $sub_r;
1429
        }
1430
1431
        return $r;
1432
    }
1433
1434 13
    public function getVarExpressionSQL($pattern, $context, $val_type = '', $parent_type = '')
1435
    {
1436 13
        $var = $pattern['value'];
1437 13
        $info = $this->getVarTableInfos($var);
1438
1439 13
        $tbl = false;
1440 13
        if (isset($info['table'])) {
1441 12
            $tbl = $info['table'];
1442
        }
1443
1444 13
        if (!$tbl) {
1445
            /* might be an aggregate var */
1446 1
            $vars = $this->infos['query']['result_vars'];
1447 1
            foreach ($vars as $test_var) {
1448 1
                if ($test_var['alias'] == $pattern['value']) {
1449 1
                    return '`'.$pattern['value'].'`';
1450
                }
1451
            }
1452
1453
            return '';
1454
        }
1455 12
        $col = $info['col'];
1456 12
        $parentOp = $pattern['parent_op'] ?? '';
1457 12
        if ('order' == $context && 'o' == $col) {
1458 3
            $tbl_alias = 'T_'.$tbl.'.o_comp';
1459 9
        } elseif ('sameterm' == $context) {
1460
            $tbl_alias = 'T_'.$tbl.'.'.$col;
1461 9
        } elseif ('relational' == $parent_type && 'o' == $col && preg_match('/[\<\>]/', $parentOp)) {
1462 3
            $tbl_alias = 'T_'.$tbl.'.o_comp';
1463
        } else {
1464 6
            $tbl_alias = 'V_'.$tbl.'_'.$col.'.val';
1465 6
            if (!\in_array($tbl_alias, $this->index['sub_joins'])) {
1466 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...
1467
            }
1468
        }
1469
1470 12
        $op = $pattern['operator'] ?? '';
1471 12
        if (preg_match('/^(filter|and)/', $parent_type)) {
1472
            if ('!' == $op) {
1473
                $r = '((('.$tbl_alias.' = 0) AND (CONCAT("1", '.$tbl_alias.') != 1))'; /* 0 and no string */
1474
                $r .= ' OR ('.$tbl_alias.' IN ("", "false")))'; /* or "", or "false" */
1475
            } else {
1476
                $r = '(('.$tbl_alias.' != 0)'; /* not null */
1477
                $r .= ' OR ((CONCAT("1", '.$tbl_alias.') = 1) AND ('.$tbl_alias.' NOT IN ("", "false"))))'; /* string, and not "" or "false" */
1478
            }
1479
        } else {
1480 12
            $r = trim($op.' '.$tbl_alias);
1481 12
            if ('numeric' == $val_type) {
1482 5
                if (preg_match('/__(T|V|G)_(.+)$/', $context, $m)) {
1483
                    $context_pattern_id = $m[2];
1484
                    $context_table_type = $m[1];
1485
                } else {
1486 5
                    $context_pattern_id = $pattern['id'];
1487 5
                    $context_table_type = 'T';
1488
                }
1489 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...
1490 5
                    $add = ($tbl != $context_pattern_id) ? 1 : 0;
1491 5
                    $add = (!$add && ('V' == $context_table_type)) ? 1 : 0;
1492 5
                    if ($add) {
1493
                        $this->addConstraintSQLEntry($context_pattern_id, '('.$r.' = "0" OR '.$r.'*1.0 != 0)');
1494
                    }
1495
                }
1496
            }
1497
        }
1498
1499 12
        return $r;
1500
    }
1501
1502 1
    public function getUriExpressionSQL($pattern)
1503
    {
1504 1
        $val = $pattern['uri'];
1505 1
        $r = $pattern['operator'];
1506 1
        $r .= is_numeric($val) ? ' '.$val : ' "'.$this->store->getDBObject()->escape($val).'"';
1507
1508 1
        return $r;
1509
    }
1510
1511 12
    public function getLiteralExpressionSQL($pattern, $context, $val_type = '', $parent_type = '')
1512
    {
1513 12
        $val = $pattern['value'];
1514 12
        $r = $pattern['operator'];
1515 12
        $datatype = $pattern['datatype'] ?? '';
1516
1517 12
        if (is_numeric($val) && ($pattern['datatype'] ?? 0)) {
1518 5
            $r .= ' '.$val;
1519
        } elseif (
1520 7
            preg_match('/^(true|false)$/i', $val)
1521 7
            && 'http://www.w3.org/2001/XMLSchema#boolean' == $datatype
1522
        ) {
1523
            $r .= ' '.strtoupper($val);
1524 7
        } elseif ('regex' == $parent_type) {
1525 2
            $sub_r = $this->store->getDBObject()->escape($val);
1526 2
            $r .= ' "'.preg_replace('/\x5c\x5c/', '\\', $sub_r).'"';
1527
        } else {
1528 5
            $r .= ' "'.$this->store->getDBObject()->escape($val).'"';
1529
        }
1530
1531 12
        $lang_dt = $pattern['lang'] ?? $pattern['datatype'] ?? '';
1532 12
        if ($lang_dt) {
1533
            /* try table/alias via var in siblings */
1534 5
            if ($var = $this->findSiblingVarExpression($pattern['id'])) {
1535 5
                if (isset($this->index['vars'][$var])) {
1536 5
                    $infos = $this->index['vars'][$var];
1537 5
                    foreach ($infos as $info) {
1538 5
                        if ('o' == $info['col']) {
1539 5
                            $tbl = $info['table'];
1540 5
                            $term_id = $this->getTermID($lang_dt);
1541 5
                            if ('!=' != $pattern['operator']) {
1542 5
                                if (preg_match('/__(T|V|G)_(.+)$/', $context, $m)) {
1543
                                    $context_pattern_id = $m[2];
1544
                                    $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...
1545 5
                                } elseif ('where' == $context) {
1546 5
                                    $context_pattern_id = $tbl;
1547
                                } else {
1548
                                    $context_pattern_id = $pattern['id'];
1549
                                }
1550
                                // TODO better dependency check
1551 5
                                if ($tbl == $context_pattern_id) {
1552 5
                                    if ($term_id || ('http://www.w3.org/2001/XMLSchema#integer' != $lang_dt)) {
1553
                                        /* skip, if simple int, but no id */
1554
                                        $this->addConstraintSQLEntry($context_pattern_id, 'T_'.$tbl.'.o_lang_dt = '.$term_id.' /* '.preg_replace('/[\#\*\>]/', '::', $lang_dt).' */');
1555
                                    }
1556
                                }
1557
                            }
1558 5
                            break;
1559
                        }
1560
                    }
1561
                }
1562
            }
1563
        }
1564
1565 12
        return trim($r);
1566
    }
1567
1568 5
    public function findSiblingVarExpression($id)
1569
    {
1570 5
        $pattern = $this->getPattern($id);
1571
        do {
1572 5
            $pattern = $this->getPattern($pattern['parent_id']);
1573 5
        } while ($pattern['parent_id'] && ('expression' != $pattern['type']));
1574
1575 5
        $sub_patterns = $pattern['patterns'] ?? [];
1576 5
        foreach ($sub_patterns as $sub_id) {
1577 5
            $sub_pattern = $this->getPattern($sub_id);
1578 5
            if ('var' == $sub_pattern['type']) {
1579 5
                return $sub_pattern['value'];
1580
            }
1581
        }
1582
1583
        return '';
1584
    }
1585
1586
    public function getFunctionExpressionSQL($pattern, $context, $val_type = '', $parent_type = '')
1587
    {
1588
        $fnc_uri = $pattern['uri'];
1589
        $op = $pattern['operator'] ?? '';
1590
        if ($op) {
1591
            $op .= ' ';
1592
        }
1593
1594
        /* simple type conversions */
1595
        if (0 === strpos($fnc_uri, 'http://www.w3.org/2001/XMLSchema#')) {
1596
            return $op.$this->getExpressionSQL($pattern['args'][0], $context, $val_type, $parent_type);
1597
        }
1598
1599
        return '';
1600
    }
1601
1602 18
    public function getBuiltInCallSQL($pattern, $context)
1603
    {
1604 18
        $call = $pattern['call'];
1605 18
        $m = 'get'.ucfirst($call).'CallSQL';
1606 18
        if (method_exists($this, $m)) {
1607 18
            return $this->$m($pattern, $context);
1608
        } else {
1609
            throw new Exception('Unknown built-in call "'.$call.'"');
1610
        }
1611
1612
        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...
1613
    }
1614
1615 2
    public function getBoundCallSQL($pattern, $context)
1616
    {
1617 2
        $r = '';
0 ignored issues
show
Unused Code introduced by
The assignment to $r is dead and can be removed.
Loading history...
1618 2
        $var = $pattern['args'][0]['value'];
1619 2
        $info = $this->getVarTableInfos($var);
1620 2
        if (!$tbl = $info['table']) {
1621
            return '';
1622
        }
1623 2
        $col = $info['col'];
1624 2
        $tbl_alias = 'T_'.$tbl.'.'.$col;
1625 2
        if ('!' == $pattern['operator']) {
1626
            return $tbl_alias.' IS NULL';
1627
        }
1628
1629 2
        return $tbl_alias.' IS NOT NULL';
1630
    }
1631
1632 8
    public function getHasTypeCallSQL($pattern, $context, $type)
1633
    {
1634 8
        $r = '';
0 ignored issues
show
Unused Code introduced by
The assignment to $r is dead and can be removed.
Loading history...
1635 8
        $var = $pattern['args'][0]['value'];
1636 8
        $info = $this->getVarTableInfos($var);
1637 8
        if (!$tbl = $info['table']) {
1638
            return '';
1639
        }
1640 8
        $col = $info['col'];
1641 8
        $tbl_alias = 'T_'.$tbl.'.'.$col.'_type';
1642
1643 8
        $operator = $pattern['operator'] ?? '';
1644
1645 8
        return $tbl_alias.' '.$operator.'= '.$type;
1646
    }
1647
1648 2
    public function getIsliteralCallSQL($pattern, $context)
1649
    {
1650 2
        return $this->getHasTypeCallSQL($pattern, $context, 2);
1651
    }
1652
1653 2
    public function getIsblankCallSQL($pattern, $context)
1654
    {
1655 2
        return $this->getHasTypeCallSQL($pattern, $context, 1);
1656
    }
1657
1658 2
    public function getIsiriCallSQL($pattern, $context)
1659
    {
1660 2
        return $this->getHasTypeCallSQL($pattern, $context, 0);
1661
    }
1662
1663 2
    public function getIsuriCallSQL($pattern, $context)
1664
    {
1665 2
        return $this->getHasTypeCallSQL($pattern, $context, 0);
1666
    }
1667
1668 2
    public function getStrCallSQL($pattern, $context)
1669
    {
1670 2
        $sub_pattern = $pattern['args'][0];
1671 2
        $sub_type = $sub_pattern['type'];
1672 2
        $m = 'get'.ucfirst($sub_type).'ExpressionSQL';
1673 2
        if (method_exists($this, $m)) {
1674 2
            return $this->$m($sub_pattern, $context);
1675
        }
1676
    }
1677
1678
    public function getFunctionCallSQL($pattern, $context)
1679
    {
1680
        $f_uri = $pattern['uri'];
1681
        if (preg_match('/(integer|double|float|string)$/', $f_uri)) {/* skip conversions */
1682
            $sub_pattern = $pattern['args'][0];
1683
            $sub_type = $sub_pattern['type'];
1684
            $m = 'get'.ucfirst($sub_type).'ExpressionSQL';
1685
            if (method_exists($this, $m)) {
1686
                return $this->$m($sub_pattern, $context);
1687
            }
1688
        }
1689
    }
1690
1691 4
    public function getLangDatatypeCallSQL($pattern, $context)
1692
    {
1693 4
        $r = '';
0 ignored issues
show
Unused Code introduced by
The assignment to $r is dead and can be removed.
Loading history...
1694 4
        if (isset($pattern['patterns'])) { /* proceed with first argument only (assumed as base type for type promotion) */
1695
            $sub_pattern = ['args' => [$pattern['patterns'][0]]];
1696
1697
            return $this->getLangDatatypeCallSQL($sub_pattern, $context);
1698
        }
1699 4
        if (!isset($pattern['args'])) {
1700
            return 'FALSE';
1701
        }
1702 4
        $sub_type = $pattern['args'][0]['type'];
1703 4
        if ('var' != $sub_type) {
1704
            return $this->getLangDatatypeCallSQL($pattern['args'][0], $context);
1705
        }
1706 4
        $var = $pattern['args'][0]['value'];
1707 4
        $info = $this->getVarTableInfos($var);
1708 4
        if (!$tbl = $info['table']) {
1709
            return '';
1710
        }
1711 4
        $col = 'o_lang_dt';
1712 4
        $tbl_alias = 'V_'.$tbl.'_'.$col.'.val';
1713 4
        if (!\in_array($tbl_alias, $this->index['sub_joins'])) {
1714 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...
1715
        }
1716 4
        $op = $pattern['operator'] ?? '';
1717 4
        $r = trim($op.' '.$tbl_alias);
1718
1719 4
        return $r;
1720
    }
1721
1722 1
    public function getDatatypeCallSQL($pattern, $context)
1723
    {
1724 1
        return '/* datatype call */ '.$this->getLangDatatypeCallSQL($pattern, $context);
1725
    }
1726
1727 3
    public function getLangCallSQL($pattern, $context)
1728
    {
1729 3
        return '/* language call */ '.$this->getLangDatatypeCallSQL($pattern, $context);
1730
    }
1731
1732 2
    public function getLangmatchesCallSQL($pattern, $context)
1733
    {
1734 2
        if (2 == \count($pattern['args'])) {
1735 2
            $arg_1 = $pattern['args'][0];
1736 2
            $arg_2 = $pattern['args'][1];
1737 2
            $sub_r_1 = $this->getBuiltInCallSQL($arg_1, $context); /* adds value join */
1738 2
            $sub_r_2 = $this->getExpressionSQL($arg_2, $context);
1739 2
            $op = $pattern['operator'] ?? '';
1740 2
            if (preg_match('/^([\"\'])([^\'\"]+)/', $sub_r_2, $m)) {
1741
                if ('*' == $m[2]) {
1742
                    $r = '!' == $op
1743
                        ? 'NOT ('.$sub_r_1.' REGEXP "^[a-zA-Z\-]+$"'.')'
1744
                        : $sub_r_1.' REGEXP "^[a-zA-Z\-]+$"';
1745
                } else {
1746
                    $r = ('!' == $op) ? $sub_r_1.' NOT LIKE '.$m[1].$m[2].'%'.$m[1] : $sub_r_1.' LIKE '.$m[1].$m[2].'%'.$m[1];
1747
                }
1748
            } else {
1749 2
                $r = ('!' == $op) ? $sub_r_1.' NOT LIKE CONCAT('.$sub_r_2.', "%")' : $sub_r_1.' LIKE CONCAT('.$sub_r_2.', "%")';
1750
            }
1751
1752 2
            return $r;
1753
        }
1754
1755
        return '';
1756
    }
1757
1758
    /**
1759
     * @todo not in use, so remove?
1760
     */
1761
    public function getSametermCallSQL($pattern, $context)
1762
    {
1763
        if (2 == \count($pattern['args'])) {
1764
            $arg_1 = $pattern['args'][0];
1765
            $arg_2 = $pattern['args'][1];
1766
            $sub_r_1 = $this->getExpressionSQL($arg_1, 'sameterm');
1767
            $sub_r_2 = $this->getExpressionSQL($arg_2, 'sameterm');
1768
            $op = $pattern['operator'] ?? '';
1769
            $r = $sub_r_1.' '.$op.'= '.$sub_r_2;
1770
1771
            return $r;
1772
        }
1773
1774
        return '';
1775
    }
1776
1777 2
    public function getRegexCallSQL($pattern, $context)
1778
    {
1779 2
        $ac = \count($pattern['args']);
1780 2
        if ($ac >= 2) {
1781 2
            foreach ($pattern['args'] as $i => $arg) {
1782 2
                $var = 'sub_r_'.($i + 1);
1783 2
                $$var = $this->getExpressionSQL($arg, $context, '', 'regex');
1784
            }
1785 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...
1786 2
            $operator = $pattern['operator'] ?? '';
1787 2
            $op = '!' == $operator ? ' NOT' : '';
1788 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...
1789
                return '';
1790
            }
1791 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...
1792 2
            $is_simple_search = preg_match('/^[\(\"]+(\^)?([^\\\*\[\]\}\{\(\)\"\'\?\+\.]+)(\$)?[\)\"]+$/is', $sub_r_2, $m);
1793 2
            $is_o_search = preg_match('/o\.val\)*$/', $sub_r_1);
1794
            /* fulltext search (may have "|") */
1795 2
            if ($is_simple_search && $is_o_search && !$op && (\strlen($m[2]) > 8)) {
1796
                /* MATCH variations */
1797
                if (($val_parts = preg_split('/\|/', $m[2]))) {
1798
                    return 'MATCH('.trim($sub_r_1, '()').') AGAINST("'.implode(' ', $val_parts).'")';
1799
                } else {
1800
                    return 'MATCH('.trim($sub_r_1, '()').') AGAINST("'.$m[2].'")';
1801
                }
1802
            }
1803 2
            if (preg_match('/\|/', $sub_r_2)) {
1804
                $is_simple_search = 0;
1805
            }
1806
            /* LIKE */
1807 2
            if ($is_simple_search && ('i' == $sub_r_3)) {
1808 1
                $sub_r_2 = $m[1] ? $m[2] : '%'.$m[2];
1809 1
                $sub_r_2 .= isset($m[3]) && $m[3] ? '' : '%';
1810
1811 1
                return $sub_r_1.$op.' LIKE "'.$sub_r_2.'"';
1812
            }
1813
            /* REGEXP */
1814 1
            $opt = '';
1815 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...
1816
                $opt = ('i' == $sub_r_3) ? '' : 'BINARY ';
1817
            }
1818
1819 1
            return $sub_r_1.$op.' REGEXP '.$opt.$sub_r_2;
1820
        }
1821
1822
        return '';
1823
    }
1824
1825 102
    public function getGROUPSQL()
1826
    {
1827 102
        $r = '';
1828 102
        $nl = "\n";
1829 102
        $infos = $this->infos['query']['group_infos'] ?? [];
1830 102
        foreach ($infos as $info) {
1831 5
            $var = $info['value'];
1832 5
            if ($tbl_infos = $this->getVarTableInfos($var, 0)) {
1833 5
                $tbl_alias = $tbl_infos['table_alias'];
1834 5
                $r .= $r ? ', ' : 'GROUP BY ';
1835 5
                $r .= $tbl_alias;
1836
            }
1837
        }
1838 102
        $hr = '';
1839 102
        foreach ($this->index['havings'] as $having) {
1840
            $hr .= $hr ? ' AND' : ' HAVING';
1841
            $hr .= '('.$having.')';
1842
        }
1843 102
        $r .= $hr;
1844
1845 102
        return $r ? $nl.$r : $r;
1846
    }
1847
1848 102
    public function getORDERSQL()
1849
    {
1850 102
        $r = '';
1851 102
        $nl = "\n";
1852 102
        $infos = $this->infos['query']['order_infos'] ?? [];
1853 102
        foreach ($infos as $info) {
1854 4
            $type = $info['type'];
1855 4
            $ms = [
1856
                'expression' => 'getExpressionSQL',
1857
                'built_in_call' => 'getBuiltInCallSQL',
1858
                'function_call' => 'getFunctionCallSQL',
1859
            ];
1860 4
            $m = isset($ms[$type]) ? $ms[$type] : 'get'.ucfirst($type).'ExpressionSQL';
1861 4
            if (method_exists($this, $m)) {
1862 4
                $sub_r = '('.$this->$m($info, 'order').')';
1863 4
                $direction = $info['direction'] ?? '';
1864 4
                $sub_r .= 'desc' == $direction ? ' DESC' : '';
1865 4
                $r .= $r ? ','.$nl.$sub_r : $sub_r;
1866
            }
1867
        }
1868
1869 102
        return $r ? $nl.'ORDER BY '.$r : '';
1870
    }
1871
1872 102
    public function getLIMITSQL()
1873
    {
1874 102
        $r = '';
1875 102
        $nl = "\n";
1876 102
        $limit = $this->infos['query']['limit'] ?? -1;
1877 102
        $offset = $this->infos['query']['offset'] ?? -1;
1878 102
        if (-1 != $limit) {
1879 6
            $offset = (-1 == $offset) ? 0 : $this->store->getDBObject()->escape($offset);
1880 6
            $r = 'LIMIT '.$offset.','.$limit;
1881 96
        } elseif (-1 != $offset) {
1882
            // TODO is that if-else required with SQLite?
1883
            // mysql doesn't support stand-alone offsets
1884 1
            $r = 'LIMIT '.$this->store->getDBObject()->escape($offset).',999999999999';
1885
        }
1886
1887 102
        return $r ? $nl.$r : '';
1888
    }
1889
1890 100
    public function getValueSQL($q_tbl, $q_sql)
1891
    {
1892 100
        $r = '';
1893
        /* result vars */
1894 100
        $vars = $this->infos['query']['result_vars'];
1895 100
        $nl = "\n";
1896 100
        $v_tbls = ['JOIN' => [], 'LEFT JOIN' => []];
1897 100
        $vc = 1;
1898 100
        foreach ($vars as $var) {
1899 100
            $var_name = $var['var'];
1900 100
            $r .= $r ? ','.$nl.'  ' : '  ';
1901 100
            $col = '';
1902 100
            $tbl = '';
1903 100
            if ('*' != $var_name) {
1904 100
                if (\in_array($var_name, $this->infos['null_vars'])) {
1905
                    if (isset($this->initial_index['vars'][$var_name])) {
1906
                        $col = $this->initial_index['vars'][$var_name][0]['col'];
1907
                        $tbl = $this->initial_index['vars'][$var_name][0]['table'];
1908
                    }
1909
                    if (isset($this->initial_index['graph_vars'][$var_name])) {
1910
                        $col = 'g';
1911
                        $tbl = $this->initial_index['graph_vars'][$var_name][0]['table'];
1912
                    }
1913 100
                } elseif (isset($this->index['vars'][$var_name])) {
1914 100
                    $col = $this->index['vars'][$var_name][0]['col'];
1915 100
                    $tbl = $this->index['vars'][$var_name][0]['table'];
1916
                }
1917
            }
1918 100
            if ($var['aggregate']) {
1919 10
                $r .= 'TMP.`'.$var['alias'].'`';
1920
            } else {
1921 99
                $join_type = \in_array($tbl, array_merge($this->index['from'], $this->index['join'])) ? 'JOIN' : 'LEFT JOIN'; /* val may be NULL */
1922 99
                $v_tbls[$join_type][] = ['t_col' => $col, 'q_col' => $var_name, 'vc' => $vc];
1923 99
                $r .= 'V'.$vc.'.val AS `'.$var_name.'`';
1924 99
                if (\in_array($col, ['s', 'o'])) {
1925 97
                    if (strpos($q_sql, '`'.$var_name.' type`')) {
1926 97
                        $r .= ', '.$nl.'    TMP.`'.$var_name.' type` AS `'.$var_name.' type`';
1927
                    //$r .= ', ' . $nl . '    CASE TMP.`' . $var_name . ' type` WHEN 2 THEN "literal" WHEN 1 THEN "bnode" ELSE "uri" END AS `' . $var_name . ' type`';
1928
                    } else {
1929
                        $r .= ', '.$nl.'    NULL AS `'.$var_name.' type`';
1930
                    }
1931
                }
1932 99
                ++$vc;
1933 99
                if ('o' == $col) {
1934 89
                    $v_tbls[$join_type][] = ['t_col' => 'id', 'q_col' => $var_name.' lang_dt', 'vc' => $vc];
1935 89
                    if (strpos($q_sql, '`'.$var_name.' lang_dt`')) {
1936 89
                        $r .= ', '.$nl.'    V'.$vc.'.val AS `'.$var_name.' lang_dt`';
1937 89
                        ++$vc;
1938
                    } else {
1939
                        $r .= ', '.$nl.'    NULL AS `'.$var_name.' lang_dt`';
1940
                    }
1941
                }
1942
            }
1943
        }
1944 100
        if (!$r) {
1945 2
            $r = '*';
1946
        }
1947
        /* from */
1948 100
        $r .= $nl.'FROM ('.$q_tbl.' TMP)';
1949 100
        foreach (['JOIN', 'LEFT JOIN'] as $join_type) {
1950 100
            foreach ($v_tbls[$join_type] as $v_tbl) {
1951 99
                $tbl = $this->getValueTable($v_tbl['t_col']);
1952 99
                $var_name = preg_replace('/^([^\s]+)(.*)$/', '\\1', $v_tbl['q_col']);
1953 99
                $cur_join_type = \in_array($var_name, $this->infos['null_vars']) ? 'LEFT JOIN' : $join_type;
1954 99
                if (!strpos($q_sql, '`'.$v_tbl['q_col'].'`')) {
1955 1
                    continue;
1956
                }
1957
                $r .= $nl
1958 99
                    .' '.$cur_join_type
1959 99
                    .' '.$tbl.' V'.$v_tbl['vc']
1960 99
                    .' ON ((V'.$v_tbl['vc'].'.id = TMP.`'.$v_tbl['q_col'].'`))';
1961
            }
1962
        }
1963
        /* create pos columns, id needed */
1964 100
        $orderInfos = $this->infos['query']['order_infos'] ?? [];
1965 100
        if ($orderInfos) {
1966 4
            $r .= $nl.' ORDER BY TMPPOS';
1967
        }
1968
1969 100
        return 'SELECT'.$nl.$r;
1970
    }
1971
}
1972