SelectQueryHandler::getLeftJoins()   A
last analyzed

Complexity

Conditions 4
Paths 6

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 4.016

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 9
c 1
b 0
f 0
nc 6
nop 0
dl 0
loc 15
ccs 9
cts 10
cp 0.9
crap 4.016
rs 9.9666
1
<?php
2
3
/**
4
 * This file is part of the sweetrdf/InMemoryStoreSqlite package and licensed under
5
 * the terms of the GPL-2 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
    private int $cache_results;
0 ignored issues
show
introduced by
The private property $cache_results is not used, and could be removed.
Loading history...
22
23
    /**
24
     * @var array<mixed>
25
     */
26
    private array $dependency_log;
27
28
    private string $engine_type;
0 ignored issues
show
introduced by
The private property $engine_type is not used, and could be removed.
Loading history...
29
30
    /**
31
     * @var array<miyed>
32
     */
33
    private array $index;
34
35
    /**
36
     * @var array<miyed>
37
     */
38
    private array $indexes;
39
40
    /**
41
     * @var array<miyed>
42
     */
43
    private array $initial_index;
44
45
    private int $is_union_query;
46
47
    /**
48
     * @var array<miyed>
49
     */
50
    protected array $infos;
51
52
    private $opt_sql;
53
54
    private int $opt_sql_pd_count;
55
56
    private int $pattern_order_offset;
57
58 92
    public function runQuery($infos)
59
    {
60 92
        $this->infos = $infos;
61 92
        $this->infos['null_vars'] = [];
62 92
        $this->indexes = [];
63 92
        $this->pattern_order_offset = 0;
64 92
        $q_sql = $this->getSQL();
65
66
        /* create intermediate results (ID-based) */
67 92
        $tmp_tbl = $this->createTempTable($q_sql);
68
69
        /* join values */
70 92
        $r = $this->getFinalQueryResult($q_sql, $tmp_tbl);
71
72
        /* remove intermediate results */
73 92
        $this->store->getDBObject()->exec('DROP TABLE IF EXISTS '.$tmp_tbl);
74
75 92
        return $r;
76
    }
77
78 92
    private function getSQL()
79
    {
80 92
        $r = '';
81 92
        $nl = "\n";
82 92
        $this->buildInitialIndexes();
83 92
        foreach ($this->indexes as $i => $index) {
84 92
            $this->index = array_merge($this->getEmptyIndex(), $index);
85 92
            $this->analyzeIndex($this->getPattern('0'));
86 92
            $sub_r = $this->getQuerySQL();
87 92
            $r .= $r ? $nl.'UNION'.$this->getDistinctSQL().$nl : '';
88
89 92
            $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...
90 92
            $r .= $setBracket ? '('.$sub_r.')' : $sub_r;
91
92 92
            $this->indexes[$i] = $this->index;
93
        }
94 92
        $r .= $this->is_union_query ? $this->getLIMITSQL() : '';
95 92
        $orderInfos = $this->infos['query']['order_infos'] ?? 0;
96 92
        if ($orderInfos) {
97 4
            $r = preg_replace('/SELECT(\s+DISTINCT)?\s*/', 'SELECT\\1 NULL AS `TMPPOS`, ', $r);
98
        }
99 92
        $pd_count = $this->problematicDependencies();
100 92
        if ($pd_count) {
101
            /* re-arranging the patterns sometimes reduces the LEFT JOIN dependencies */
102
            $set_sql = 0;
103
            if (!$this->pattern_order_offset) {
104
                $set_sql = 1;
105
            }
106
            if (!$set_sql && ($pd_count < $this->opt_sql_pd_count)) {
107
                $set_sql = 1;
108
            }
109
            if (!$set_sql && ($pd_count == $this->opt_sql_pd_count) && (\strlen($r) < \strlen($this->opt_sql))) {
110
                $set_sql = 1;
111
            }
112
            if ($set_sql) {
113
                $this->opt_sql = $r;
114
                $this->opt_sql_pd_count = $pd_count;
115
            }
116
            ++$this->pattern_order_offset;
117
            if ($this->pattern_order_offset > 5) {
118
                return $this->opt_sql;
119
            }
120
121
            return $this->getSQL();
122
        }
123
124 92
        return $r;
125
    }
126
127 92
    private function buildInitialIndexes()
128
    {
129 92
        $this->dependency_log = [];
130 92
        $this->index = $this->getEmptyIndex();
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->getEmptyIndex() of type array<string,array> is incompatible with the declared type sweetrdf\InMemoryStoreSq...re\QueryHandler\miyed[] of property $index.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

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

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