CompilerCache::hashParam()   B
last analyzed

Complexity

Conditions 7
Paths 8

Size

Total Lines 28
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 56

Importance

Changes 0
Metric Value
cc 7
eloc 15
nc 8
nop 2
dl 0
loc 28
ccs 0
cts 0
cp 0
crap 56
rs 8.8333
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * This file is part of Cycle ORM package.
5
 *
6
 * For the full copyright and license information, please view the LICENSE
7
 * file that was distributed with this source code.
8
 */
9
10
declare(strict_types=1);
11
12
namespace Cycle\Database\Driver;
13
14
use Cycle\Database\Injection\Expression;
15
use Cycle\Database\Injection\Fragment;
16
use Cycle\Database\Injection\FragmentInterface;
17
use Cycle\Database\Injection\JsonExpression;
18
use Cycle\Database\Injection\Parameter;
19
use Cycle\Database\Injection\ParameterInterface;
20
use Cycle\Database\Injection\SubQuery;
21
use Cycle\Database\Query\QueryInterface;
22
use Cycle\Database\Query\QueryParameters;
23
use Cycle\Database\Query\SelectQuery;
24
25
/**
26
 * Caches calculated queries. Code in this class is performance optimized.
27
 */
28
final class CompilerCache implements CompilerInterface
29
{
30 72
    private array $cache = [];
31
32
    public function __construct(
33 72
        private CachingCompilerInterface $compiler,
34
    ) {}
35
36
    /**
37
     * @psalm-param non-empty-string $identifier
38 2600
     */
39
    public function quoteIdentifier(string $identifier): string
40 2600
    {
41
        return $this->compiler->quoteIdentifier($identifier);
42
    }
43
44
    /**
45
     * @psalm-return non-empty-string
46 1946
     */
47
    public function compile(QueryParameters $params, string $prefix, FragmentInterface $fragment): string
48 1946
    {
49 1344
        if ($fragment->getType() === self::SELECT_QUERY) {
50
            $queryHash = $prefix . $this->hashSelectQuery($params, $fragment->getTokens());
51 1344
52 628
            if (isset($this->cache[$queryHash])) {
53
                return $this->cache[$queryHash];
54
            }
55 984
56 984
            return $this->cache[$queryHash] = $this->compiler->compile(
57
                new QueryParameters(),
58
                $prefix,
59
                $fragment,
60
            );
61
        }
62 860
63 348
        if ($fragment->getType() === self::INSERT_QUERY) {
64
            $tokens = $fragment->getTokens();
65 348
66 250
            if (\count($tokens['values']) === 1) {
67 250
                $queryHash = $prefix . $this->hashInsertQuery($params, $tokens);
68 200
                if (isset($this->cache[$queryHash])) {
69
                    return $this->cache[$queryHash];
70
                }
71 62
72 62
                return $this->cache[$queryHash] = $this->compiler->compile(
73
                    new QueryParameters(),
74
                    $prefix,
75
                    $fragment,
76
                );
77
            }
78
        }
79 636
80 636
        return $this->compiler->compile(
81
            $params,
82
            $prefix,
83
            $fragment,
84
        );
85
    }
86
87
    /**
88
     * @psalm-return non-empty-string
89 250
     */
90
    protected function hashInsertQuery(QueryParameters $params, array $tokens): string
91 250
    {
92 250
        foreach ((array) ($tokens['return'] ?? []) as $return) {
93 250
            if ($return instanceof FragmentInterface) {
94
                foreach ($return->getTokens()['parameters'] as $param) {
95
                    $params->push($param);
96
                }
97
            }
98
        }
99
100
        $hash = \sprintf(
101
            'i_%s%s_r%s',
102
            $tokens['table'],
103
            \implode('_', $tokens['columns']),
104 250
            \implode('_', (array) ($tokens['return'] ?? [])),
105
        );
106
        foreach ($tokens['values'] as $value) {
107
            if ($value instanceof FragmentInterface) {
108 250
                if ($value instanceof Expression || $value instanceof Fragment) {
109 250
                    foreach ($tokens['parameters'] as $param) {
110 250
                        $params->push($param);
111
                    }
112
                }
113
114 250
                $hash .= $value;
115 250
                continue;
116
            }
117
118 250
            if (!$value instanceof ParameterInterface) {
119
                $value = new Parameter($value);
120
            }
121 250
122 250
            if ($value->isArray()) {
123
                foreach ($value->getValue() as $child) {
124
                    if ($child instanceof FragmentInterface) {
125
                        if ($child instanceof \Stringable) {
126
                            $hash .= '_F_' . $child;
127
                        }
128
                        continue;
129 250
                    }
130
131
                    if (!$child instanceof ParameterInterface) {
132
                        $child = new Parameter($child);
133
                    }
134
135 1344
                    $params->push($child);
136
                }
137
138 1344
                $hash .= 'P?';
139 2
                continue;
140
            }
141 1344
142
            $params->push($value);
143
            $hash .= 'P?';
144 1344
        }
145 1344
146 6
        return $hash;
147 6
    }
148 6
149
    /**
150
     * @psalm-return non-empty-string
151 1344
     */
152
    protected function hashSelectQuery(QueryParameters $params, array $tokens): string
153
    {
154 1344
        // stable part of hash
155
        if (\is_array($tokens['distinct']) && isset($tokens['distinct']['on'])) {
156 1344
            $hash = 's_' . $tokens['forUpdate'] . '_on_' . $tokens['distinct']['on'];
157 210
        } else {
158
            $hash = 's_' . $tokens['forUpdate'] . '_' . $tokens['distinct'];
159 210
        }
160 6
161 6
        foreach ($tokens['from'] as $table) {
162 6
            if ($table instanceof SelectQuery) {
163
                $hash .= 's_' . ($table->getPrefix() ?? '');
164
                $hash .= $this->hashSelectQuery($params, $table->getTokens());
165 204
                continue;
166 204
            } elseif ($table instanceof SubQuery) {
167
                $hash .= 'sb_';
168
                $hash .= $this->hashSelectQuery($params, $table->getTokens());
169 1344
                continue;
170 648
            }
171
172
            $hash .= $table;
173 1344
        }
174 156
175
        $hash .= $this->hashColumns($params, $tokens['columns']);
176
177 1344
        foreach ($tokens['join'] as $join) {
178
            $hash .= 'j' . $join['alias'] . \str_replace(['JOIN', ' '], '', $join['type']);
179 1344
180 102
            if ($join['outer'] instanceof SelectQuery) {
181
                $hash .= $join['outer']->getPrefix() === null ? '' : 'p_' . $join['outer']->getPrefix();
182
                $hash .= $this->hashSelectQuery($params, $join['outer']->getTokens());
183 1344
            } else {
184
                $hash .= $join['outer'];
185 1344
            }
186 18
187 18
            $hash .= 'on' . $this->hashWhere($params, $join['on']);
188 18
        }
189 18
190 18
        if ($tokens['where'] !== []) {
191
            $hash .= 'w' . $this->hashWhere($params, $tokens['where']);
192
        }
193
194
        if ($tokens['having'] !== []) {
195
            $hash .= 'h' . $this->hashWhere($params, $tokens['having']);
196 1344
        }
197
198
        $hash .= \implode(',', $tokens['groupBy']);
199
200
        $hash .= $this->hashOrderBy($params, $tokens['orderBy']);
201
202 990
        $hash .= $this->compiler->hashLimit($params, $tokens);
203
204 990
        foreach ($tokens['union'] as $union) {
205 990
            $hash .= $union[0];
206
            if ($union[1] instanceof SelectQuery) {
207 990
                $hash .= $union[1]->getPrefix() === null ? '' : 'p_' . $union[1]->getPrefix();
208
                $hash .= $this->hashSelectQuery($params, $union[1]->getTokens());
209 990
                continue;
210 990
            }
211 180
212 180
            $hash .= $union[1];
213
        }
214
215 984
        foreach ($tokens['intersect'] as $intersect) {
216 6
            $hash .= $intersect[0];
217 6
            if ($intersect[1] instanceof SelectQuery) {
218 6
                $hash .= $intersect[1]->getPrefix() === null ? '' : 'i_' . $intersect[1]->getPrefix();
219
                $hash .= $this->hashSelectQuery($params, $intersect[1]->getTokens());
220
                continue;
221
            }
222 6
223 6
            $hash .= $intersect[1];
224
        }
225
226 984
        foreach ($tokens['except'] as $except) {
227 18
            $hash .= $except[0];
228 18
            if ($except[1] instanceof SelectQuery) {
229 984
                $hash .= $except[1]->getPrefix() === null ? '' : 'e_' . $except[1]->getPrefix();
230 6
                $hash .= $this->hashSelectQuery($params, $except[1]->getTokens());
231
                continue;
232 978
            }
233 12
234 12
            $hash .= $except[1];
235
        }
236
237
        return $hash;
238 978
    }
239
240
    /**
241
     * @psalm-return non-empty-string
242 984
     */
243 12
    protected function hashWhere(QueryParameters $params, array $where): string
244 6
    {
245
        $hash = '';
246
        foreach ($where as $condition) {
247
            // OR/AND keyword
248 984
            [$boolean, $context] = $condition;
249
250 984
            $hash .= $boolean;
251 30
            if (\is_string($context)) {
252 30
                $hash .= $context;
253 984
                continue;
254 816
            }
255
256 264
            if ($context instanceof FragmentInterface) {
257 264
                if (
258 12
                    $context instanceof Expression ||
259
                    $context instanceof Fragment ||
260
                    $context instanceof JsonExpression
261
                ) {
262 264
                    foreach ($context->getTokens()['parameters'] as $param) {
263
                        $params->push($param);
264
                    }
265 984
                }
266 48
267
                $hash .= $context;
268
                continue;
269 48
            }
270 48
271
            if ($context[0] instanceof QueryInterface) {
272
                $hash .= $context[0]->getPrefix() === null ? '' : 'p_' . $context[0]->getPrefix();
273
                $hash .= $this->hashSelectQuery($params, $context[0]->getTokens());
274
            } elseif ($context[0] instanceof ParameterInterface) {
275
                $hash .= $this->hashParam($params, $context[0]);
276
            } else {
277
                if ($context[0] instanceof Expression || $context[0] instanceof Fragment) {
278
                    foreach ($context[0]->getTokens()['parameters'] as $param) {
279
                        $params->push($param);
280
                    }
281
                }
282
283 990
                $hash .= $context[0];
284
            }
285
286
            // operator
287
            if ($context[1] instanceof Expression || $context[1] instanceof Fragment) {
288
                foreach ($context[1]->getTokens()['parameters'] as $param) {
289 1344
                    $params->push($param);
290
                }
291 1344
            }
292 1344
293 1344
            $hash .= $context[1];
294 48
295 12
            if ($context[2] instanceof QueryInterface) {
296
                $hash .= $context[2]->getPrefix() === null ? '' : 'p_' . $context[2]->getPrefix();
297
                $hash .= $this->hashSelectQuery($params, $context[2]->getTokens());
298
            } elseif ($context[2] instanceof ParameterInterface) {
299 1344
                $hash .= $this->hashParam($params, $context[2]);
300
            } else {
301
                if ($context[2] instanceof Expression || $context[2] instanceof Fragment) {
302 1344
                    foreach ($context[2]->getTokens()['parameters'] as $param) {
303
                        $params->push($param);
304
                    }
305
                }
306
307
                $hash .= $context[2];
308 816
            }
309
310 816
            if (isset($context[3])) {
311 24
                if ($context[3] instanceof QueryInterface) {
312
                    $hash .= $context[3]->getPrefix() === null ? '' : 'p_' . $context[3]->getPrefix();
313
                    $hash .= $this->hashSelectQuery($params, $context[3]->getTokens());
314 792
                } elseif ($context[3] instanceof ParameterInterface) {
315
                    $hash .= $this->hashParam($params, $context[3]);
316 792
                } else {
317 42
                    if ($context[3] instanceof Expression || $context[3] instanceof Fragment) {
318
                        foreach ($context[3]->getTokens()['parameters'] as $param) {
319
                            $params->push($param);
320 774
                        }
321
                    }
322
323
                    $hash .= $context[3];
324
                }
325
            }
326
        }
327
328
        return $hash;
329
    }
330
331
    /**
332
     * @psalm-return non-empty-string
333
     */
334
    protected function hashColumns(QueryParameters $params, array $columns): string
335
    {
336
        $hash = '';
337
        foreach ($columns as $column) {
338
            if ($column instanceof Expression || $column instanceof Fragment || $column instanceof SubQuery) {
339
                foreach ($column->getTokens()['parameters'] as $param) {
340
                    $params->push($param);
341
                }
342
            }
343
344
            $hash .= (string) $column . ',';
345
        }
346
347
        return $hash;
348
    }
349
350
    /**
351
     * @psalm-return non-empty-string
352
     */
353
    private function hashParam(QueryParameters $params, ParameterInterface $param): string
354
    {
355
        if ($param->isNull()) {
356
            return 'N';
357
        }
358
359
        if ($param->isArray()) {
360
            $simpleParams = [];
361
            foreach ($param->getValue() as $value) {
362
                if ($value instanceof FragmentInterface) {
363
                    foreach ($value->getTokens()['parameters'] as $fragmentParam) {
364
                        $params->push($fragmentParam);
365
                    }
366
                } else {
367
                    $simpleParams[] = $value;
368
                }
369
            }
370
371
            if ($simpleParams !== []) {
372
                $params->push(new Parameter($simpleParams));
373
            }
374
375
            return 'A' . \count($param->getValue());
376
        }
377
378
        $params->push($param);
379
380
        return '?';
381
    }
382
383
    private function hashOrderBy(QueryParameters $params, array $tokens): string
384
    {
385
        $hash = '';
386
        foreach ($tokens as $order) {
387
            if ($order[0] instanceof FragmentInterface) {
388
                foreach ($order[0]->getTokens()['parameters'] as $param) {
389
                    $params->push($param);
390
                }
391
            }
392
            $hash .= $order[0] . $order[1];
393
        }
394
395
        return $hash;
396
    }
397
}
398