Completed
Push — 2.x ( be80d1...0b4d06 )
by Aleksei
17s queued 15s
created

CompilerCache::hashParam()   B

Complexity

Conditions 7
Paths 8

Size

Total Lines 28
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 7

Importance

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