Passed
Push — 2.x ( dac6da...5763d4 )
by Aleksei
30:52 queued 10:52
created

CompilerCache::hashColumns()   A

Complexity

Conditions 5
Paths 3

Size

Total Lines 14
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 5

Importance

Changes 0
Metric Value
cc 5
eloc 7
nc 3
nop 2
dl 0
loc 14
ccs 7
cts 7
cp 1
crap 5
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
    /**
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
                        continue;
126
                    }
127
128
                    if (!$child instanceof ParameterInterface) {
129 250
                        $child = new Parameter($child);
130
                    }
131
132
                    $params->push($child);
133
                }
134
135 1344
                $hash .= 'P?';
136
                continue;
137
            }
138 1344
139 2
            $params->push($value);
140
            $hash .= 'P?';
141 1344
        }
142
143
        return $hash;
144 1344
    }
145 1344
146 6
    /**
147 6
     * @psalm-return non-empty-string
148 6
     */
149
    protected function hashSelectQuery(QueryParameters $params, array $tokens): string
150
    {
151 1344
        // stable part of hash
152
        if (is_array($tokens['distinct']) && isset($tokens['distinct']['on'])) {
153
            $hash = 's_' . $tokens['forUpdate'] . '_on_' . $tokens['distinct']['on'];
154 1344
        } else {
155
            $hash = 's_' . $tokens['forUpdate'] . '_' . $tokens['distinct'];
156 1344
        }
157 210
158
        foreach ($tokens['from'] as $table) {
159 210
            if ($table instanceof SelectQuery) {
160 6
                $hash .= 's_' . ($table->getPrefix() ?? '');
161 6
                $hash .= $this->hashSelectQuery($params, $table->getTokens());
162 6
                continue;
163
            }
164
165 204
            $hash .= $table;
166 204
        }
167
168
        $hash .= $this->hashColumns($params, $tokens['columns']);
169 1344
170 648
        foreach ($tokens['join'] as $join) {
171
            $hash .= 'j' . $join['alias'] . $join['type'];
172
173 1344
            if ($join['outer'] instanceof SelectQuery) {
174 156
                $hash .= $join['outer']->getPrefix() === null ? '' : 'p_' . $join['outer']->getPrefix();
175
                $hash .= $this->hashSelectQuery($params, $join['outer']->getTokens());
176
            } else {
177 1344
                $hash .= $join['outer'];
178
            }
179 1344
180 102
            $hash .= 'on' . $this->hashWhere($params, $join['on']);
181
        }
182
183 1344
        if ($tokens['where'] !== []) {
184
            $hash .= 'w' . $this->hashWhere($params, $tokens['where']);
185 1344
        }
186 18
187 18
        if ($tokens['having'] !== []) {
188 18
            $hash .= 'h' . $this->hashWhere($params, $tokens['having']);
189 18
        }
190 18
191
        $hash .= implode(',', $tokens['groupBy']);
192
193
        foreach ($tokens['orderBy'] as $order) {
194
            $hash .= $order[0] . $order[1];
195
        }
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
        return $hash;
211 180
    }
212 180
213
    /**
214
     * @psalm-return non-empty-string
215 984
     */
216 6
    protected function hashWhere(QueryParameters $params, array $where): string
217 6
    {
218 6
        $hash = '';
219
        foreach ($where as $condition) {
220
            // OR/AND keyword
221
            [$boolean, $context] = $condition;
222 6
223 6
            $hash .= $boolean;
224
            if (is_string($context)) {
225
                $hash .= $context;
226 984
                continue;
227 18
            }
228 18
229 984
            if ($context instanceof FragmentInterface) {
230 6
                if (
231
                    $context instanceof Expression ||
232 978
                    $context instanceof Fragment ||
233 12
                    $context instanceof JsonExpression
234 12
                ) {
235
                    foreach ($context->getTokens()['parameters'] as $param) {
236
                        $params->push($param);
237
                    }
238 978
                }
239
240
                $hash .= $context;
241
                continue;
242 984
            }
243 12
244 6
            if ($context[0] instanceof QueryInterface) {
245
                $hash .= $context[0]->getPrefix() === null ? '' : 'p_' . $context[0]->getPrefix();
246
                $hash .= $this->hashSelectQuery($params, $context[0]->getTokens());
247
            } elseif ($context[0] instanceof ParameterInterface) {
248 984
                $hash .= $this->hashParam($params, $context[0]);
249
            } else {
250 984
                if ($context[0] instanceof Expression || $context[0] instanceof Fragment) {
251 30
                    foreach ($context[0]->getTokens()['parameters'] as $param) {
252 30
                        $params->push($param);
253 984
                    }
254 816
                }
255
256 264
                $hash .= $context[0];
257 264
            }
258 12
259
            // operator
260
            if ($context[1] instanceof Expression || $context[1] instanceof Fragment) {
261
                foreach ($context[1]->getTokens()['parameters'] as $param) {
262 264
                    $params->push($param);
263
                }
264
            }
265 984
266 48
            $hash .= $context[1];
267
268
            if ($context[2] instanceof QueryInterface) {
269 48
                $hash .= $context[2]->getPrefix() === null ? '' : 'p_' . $context[2]->getPrefix();
270 48
                $hash .= $this->hashSelectQuery($params, $context[2]->getTokens());
271
            } elseif ($context[2] instanceof ParameterInterface) {
272
                $hash .= $this->hashParam($params, $context[2]);
273
            } else {
274
                if ($context[2] instanceof Expression || $context[2] instanceof Fragment) {
275
                    foreach ($context[2]->getTokens()['parameters'] as $param) {
276
                        $params->push($param);
277
                    }
278
                }
279
280
                $hash .= $context[2];
281
            }
282
283 990
            if (isset($context[3])) {
284
                if ($context[3] instanceof QueryInterface) {
285
                    $hash .= $context[3]->getPrefix() === null ? '' : 'p_' . $context[3]->getPrefix();
286
                    $hash .= $this->hashSelectQuery($params, $context[3]->getTokens());
287
                } elseif ($context[3] instanceof ParameterInterface) {
288
                    $hash .= $this->hashParam($params, $context[3]);
289 1344
                } else {
290
                    if ($context[3] instanceof Expression || $context[3] instanceof Fragment) {
291 1344
                        foreach ($context[3]->getTokens()['parameters'] as $param) {
292 1344
                            $params->push($param);
293 1344
                        }
294 48
                    }
295 12
296
                    $hash .= $context[3];
297
                }
298
            }
299 1344
        }
300
301
        return $hash;
302 1344
    }
303
304
    /**
305
     * @psalm-return non-empty-string
306
     */
307
    protected function hashColumns(QueryParameters $params, array $columns): string
308 816
    {
309
        $hash = '';
310 816
        foreach ($columns as $column) {
311 24
            if ($column instanceof Expression || $column instanceof Fragment) {
312
                foreach ($column->getTokens()['parameters'] as $param) {
313
                    $params->push($param);
314 792
                }
315
            }
316 792
317 42
            $hash .= (string) $column . ',';
318
        }
319
320 774
        return $hash;
321
    }
322
323
    /**
324
     * @psalm-return non-empty-string
325
     */
326
    private function hashParam(QueryParameters $params, ParameterInterface $param): string
327
    {
328
        if ($param->isNull()) {
329
            return 'N';
330
        }
331
332
        if ($param->isArray()) {
333
            $simpleParams = [];
334
            foreach ($param->getValue() as $value) {
335
                if ($value instanceof FragmentInterface) {
336
                    foreach ($value->getTokens()['parameters'] as $fragmentParam) {
337
                        $params->push($fragmentParam);
338
                    }
339
                } else {
340
                    $simpleParams[] = $value;
341
                }
342
            }
343
344
            if ($simpleParams !== []) {
345
                $params->push(new Parameter($simpleParams));
346
            }
347
348
            return 'A' . count($param->getValue());
349
        }
350
351
        $params->push($param);
352
353
        return '?';
354
    }
355
}
356