CompilerCache::hashColumns()   A
last analyzed

Complexity

Conditions 5
Paths 3

Size

Total Lines 14
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

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