Passed
Push — 2.1 ( e72b46...572524 )
by Sébastien
23:54 queued 17:51
created

KeyValueSqlCompiler   B

Complexity

Total Complexity 50

Size/Duplication

Total Lines 304
Duplicated Lines 0 %

Test Coverage

Coverage 91.23%

Importance

Changes 0
Metric Value
wmc 50
eloc 101
dl 0
loc 304
ccs 104
cts 114
cp 0.9123
rs 8.4
c 0
b 0
f 0

14 Methods

Rating   Name   Duplication   Size   Complexity  
B compileLimit() 0 25 7
A quote() 0 3 1
A quoteIdentifier() 0 5 2
A doCompileInsert() 0 3 1
A compileProjection() 0 17 4
A doCompileSelect() 0 5 1
A doCompileUpdate() 0 3 1
A prepare() 0 9 2
A doCompileDelete() 0 3 1
A compileWhere() 0 31 5
B getBindings() 0 20 7
B compileAggregate() 0 18 8
A compileValues() 0 31 5
A compileExpressionColumn() 0 18 5

How to fix   Complexity   

Complex Class

Complex classes like KeyValueSqlCompiler often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use KeyValueSqlCompiler, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Bdf\Prime\Query\Custom\KeyValue;
4
5
use Bdf\Prime\Exception\PrimeException;
6
use Bdf\Prime\Query\CompilableClause;
7
use Bdf\Prime\Query\Compiler\AbstractCompiler;
8
use Bdf\Prime\Query\Compiler\QuoteCompilerInterface;
9
use Bdf\Prime\Query\Contract\Compilable;
10
use Bdf\Prime\Query\Expression\ExpressionInterface;
11
use Doctrine\DBAL\Statement;
12
13
/**
14
 * SQL compiler for KeyValueQuery
15
 *
16
 * @extends AbstractCompiler<KeyValueQuery, \Doctrine\DBAL\Connection&\Bdf\Prime\Connection\ConnectionInterface>
17
 * @implements QuoteCompilerInterface<KeyValueQuery>
18
 */
19
class KeyValueSqlCompiler extends AbstractCompiler implements QuoteCompilerInterface
20
{
21
    /**
22
     * {@inheritdoc}
23
     */
24
    protected function doCompileInsert(CompilableClause $query)
25
    {
26
        throw new \BadMethodCallException('INSERT operation is not supported on key value query');
27
    }
28
29
    /**
30
     * {@inheritdoc}
31
     */
32 68
    protected function doCompileUpdate(CompilableClause $query)
33
    {
34 68
        return $this->prepare($query, 'UPDATE '.$this->quoteIdentifier($query, $query->statements['table']).$this->compileValues($query).$this->compileWhere($query));
35
    }
36
37
    /**
38
     * {@inheritdoc}
39
     */
40 53
    protected function doCompileDelete(CompilableClause $query)
41
    {
42 53
        return $this->prepare($query, 'DELETE FROM '.$this->quoteIdentifier($query, $query->statements['table']).$this->compileWhere($query));
43
    }
44
45
    /**
46
     * {@inheritdoc}
47
     */
48 260
    protected function doCompileSelect(CompilableClause $query)
49
    {
50 260
        $sql = 'SELECT '.$this->compileProjection($query).' FROM '.$this->quoteIdentifier($query, $query->statements['table']).$this->compileWhere($query).$this->compileLimit($query);
51
52 260
        return $this->prepare($query, $sql);
53
    }
54
55
    /**
56
     * {@inheritdoc}
57
     */
58 323
    public function quoteIdentifier(CompilableClause $query, string $column): string
59
    {
60 323
        return $query->isQuoteIdentifier()
61 2
            ? $this->platform()->grammar()->quoteIdentifier($column)
62 323
            : $column
63 323
        ;
64
    }
65
66
    /**
67
     * {@inheritdoc}
68
     */
69
    public function quote($value)
70
    {
71
        return $this->connection->quote($value);
0 ignored issues
show
Bug introduced by
The method quote() does not exist on Bdf\Prime\Connection\ConnectionInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Bdf\Prime\Connection\ConnectionInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

71
        return $this->connection->/** @scrutinizer ignore-call */ quote($value);
Loading history...
72
    }
73
74
    /**
75
     * {@inheritdoc}
76
     */
77 365
    public function getBindings(CompilableClause $query): array
78
    {
79 365
        $bindings = [];
80
81 365
        if ($query->type() === Compilable::TYPE_UPDATE) {
0 ignored issues
show
Bug introduced by
The method type() does not exist on Bdf\Prime\Query\CompilableClause. It seems like you code against a sub-type of said class. However, the method does not exist in Bdf\Prime\Sharding\Query\ShardingInsertQuery. Are you sure you never get one of those? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

81
        if ($query->/** @scrutinizer ignore-call */ type() === Compilable::TYPE_UPDATE) {
Loading history...
82 66
            foreach ($query->statements['values']['data'] as $column => $value) {
83 66
                $bindings[] = $this->platform()->types()->toDatabase($value, $query->state()->compiledParts['values']['types'][$column] ?? null);
84
            }
85
        }
86
87 365
        foreach ($query->statements['where'] as $field => $value) {
88 344
            $bindings[] = $this->platform()->types()->toDatabase($value, $query->state()->compiledParts['types'][$field] ?? null);
89
        }
90
91 365
        if ($query->type() === Compilable::TYPE_SELECT && isset($query->statements['limit']) && isset($query->statements['offset'])) {
92 3
            $bindings[] = $query->statements['limit'];
93 3
            $bindings[] = $query->statements['offset'];
94
        }
95
96 365
        return $bindings;
97
    }
98
99
    /**
100
     * Compile the primary key condition
101
     *
102
     * @param CompilableClause $query
103
     *
104
     * @return string
105
     * @throws PrimeException
106
     */
107 323
    private function compileWhere(CompilableClause $query)
108
    {
109 323
        if (isset($query->state()->compiledParts['where'])) {
110 63
            return $query->state()->compiledParts['where'];
111
        }
112
113 294
        if (empty($query->statements['where'])) {
114 31
            $query->state()->compiledParts['types'] = [];
115 31
            $query->state()->compiledParts['where'] = '';
116
117 31
            return '';
118
        }
119
120 264
        $sql = [];
121 264
        $types = [];
122
123 264
        foreach ($query->statements['where'] as $field => $value) {
124 264
            $type = true;
125
126 264
            $column = $query->preprocessor()->field($field, $type);
127
128 264
            $sql[] = $this->quoteIdentifier($query, $column).' = ?';
129
130 264
            if ($type !== true) {
131 264
                $types[$field] = $type;
132
            }
133
        }
134
135 264
        $query->state()->compiledParts['types'] = $types;
136
137 264
        return $query->state()->compiledParts['where'] = ' WHERE '.implode(' AND ', $sql);
138
    }
139
140
    /**
141
     * Compile the projection columns
142
     *
143
     * @param CompilableClause $query
144
     *
145
     * @return string
146
     * @throws PrimeException
147
     */
148 260
    private function compileProjection(CompilableClause $query)
149
    {
150 260
        if (!empty($query->statements['aggregate'])) {
151 77
            return $this->compileAggregate($query, $query->statements['aggregate'][0], $query->statements['aggregate'][1]);
152
        }
153
154 201
        if (empty($query->statements['columns'])) {
155 163
            return '*';
156
        }
157
158 43
        $sql = [];
159
160 43
        foreach ($query->statements['columns'] as $column) {
161 43
            $sql[] = $this->compileExpressionColumn($query, $column['column'], $column['alias']);
162
        }
163
164 43
        return implode(', ', $sql);
165
    }
166
167
    /**
168
     * Compile a SQL function
169
     *
170
     * @param CompilableClause $query
171
     * @param string $function  The sql function
172
     * @param string $column    The column to aggregate
173
     *
174
     * @return string
175
     * @throws PrimeException
176
     */
177 77
    private function compileAggregate(CompilableClause $query, $function, $column)
178
    {
179 77
        if ($column !== '*') {
180 2
            $column = $query->preprocessor()->field($column);
181 2
            $column = $this->quoteIdentifier($query, $column);
182
        }
183
184
        switch ($function) {
185 77
            case 'avg'  :      return $this->platform()->grammar()->getAvgExpression($column).' AS aggregate';
0 ignored issues
show
Deprecated Code introduced by
The function Doctrine\DBAL\Platforms\...orm::getAvgExpression() has been deprecated: Use AVG() in SQL instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

185
            case 'avg'  :      return /** @scrutinizer ignore-deprecated */ $this->platform()->grammar()->getAvgExpression($column).' AS aggregate';

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
186 77
            case 'count':      return $this->platform()->grammar()->getCountExpression($column).' AS aggregate';
0 ignored issues
show
Deprecated Code introduced by
The function Doctrine\DBAL\Platforms\...m::getCountExpression() has been deprecated: Use COUNT() in SQL instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

186
            case 'count':      return /** @scrutinizer ignore-deprecated */ $this->platform()->grammar()->getCountExpression($column).' AS aggregate';

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
187 2
            case 'max'  :      return $this->platform()->grammar()->getMaxExpression($column).' AS aggregate';
0 ignored issues
show
Deprecated Code introduced by
The function Doctrine\DBAL\Platforms\...orm::getMaxExpression() has been deprecated: Use MAX() in SQL instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

187
            case 'max'  :      return /** @scrutinizer ignore-deprecated */ $this->platform()->grammar()->getMaxExpression($column).' AS aggregate';

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
188 2
            case 'min'  :      return $this->platform()->grammar()->getMinExpression($column).' AS aggregate';
0 ignored issues
show
Deprecated Code introduced by
The function Doctrine\DBAL\Platforms\...orm::getMinExpression() has been deprecated: Use MIN() in SQL instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

188
            case 'min'  :      return /** @scrutinizer ignore-deprecated */ $this->platform()->grammar()->getMinExpression($column).' AS aggregate';

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
189 2
            case 'pagination': return $this->platform()->grammar()->getCountExpression($column).' AS aggregate';
0 ignored issues
show
Deprecated Code introduced by
The function Doctrine\DBAL\Platforms\...m::getCountExpression() has been deprecated: Use COUNT() in SQL instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

189
            case 'pagination': return /** @scrutinizer ignore-deprecated */ $this->platform()->grammar()->getCountExpression($column).' AS aggregate';

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
190 2
            case 'sum'  :      return $this->platform()->grammar()->getSumExpression($column).' AS aggregate';
0 ignored issues
show
Deprecated Code introduced by
The function Doctrine\DBAL\Platforms\...orm::getSumExpression() has been deprecated: Use SUM() in SQL instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

190
            case 'sum'  :      return /** @scrutinizer ignore-deprecated */ $this->platform()->grammar()->getSumExpression($column).' AS aggregate';

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
191
192
            default:
193
                $method = 'get'.ucfirst($function).'Expression';
194
                return $this->platform()->grammar()->{$method}($column).' AS aggregate';
195
        }
196
    }
197
198
    /**
199
     * Compile expression column
200
     *
201
     * @param CompilableClause $query
202
     * @param mixed $column
203
     * @param string $alias
204
     *
205
     * @return string
206
     * @throws PrimeException
207
     */
208 43
    private function compileExpressionColumn(CompilableClause $query, $column, $alias = null)
209
    {
210 43
        if ($column instanceof ExpressionInterface) {
211 1
            return $alias !== null
212 1
                ? $column->build($query, $this).' as '.$this->quoteIdentifier($query, $alias)
213 1
                : $column->build($query, $this)
214 1
            ;
215
        }
216
217 42
        $column = $query->preprocessor()->field($column);
218
219 42
        if (strpos($column, '*') !== false) {
220
            return $column;
221
        }
222
223 42
        return $alias !== null
224 21
            ? $this->quoteIdentifier($query, $column).' as '.$this->quoteIdentifier($query, $alias)
225 42
            : $this->quoteIdentifier($query, $column)
226 42
        ;
227
    }
228
229
    /**
230
     * Compile the LIMIT clause, using placeholder for pagination
231
     * /!\ Compatible only with SGBD which supports LIMIT and OFFSET keyword (MySQL, SQLite and PgSQL)
232
     *
233
     * @param CompilableClause $query
234
     *
235
     * @return string
236
     * @throws PrimeException
237
     */
238 260
    private function compileLimit(CompilableClause $query)
239
    {
240 260
        if (!isset($query->statements['limit']) && !isset($query->statements['offset'])) {
241 226
            return '';
242
        }
243
244 45
        if (!isset($query->statements['offset'])) {
245 40
            return ' LIMIT '.$query->statements['limit'];
246
        }
247
248 5
        if (!isset($query->statements['limit'])) {
249 2
            switch ($this->platform()->name()) {
250 2
                case 'sqlite':
251 2
                    return ' LIMIT -1 OFFSET '.$query->statements['offset'];
252
253
                case 'mysql':
254
                    return ' LIMIT 18446744073709551615 OFFSET '.$query->statements['offset'];
255
256
                default:
257
                    return ' OFFSET '.$query->statements['offset'];
258
            }
259
        }
260
261
        // Use prepared only for pagination
262 3
        return ' LIMIT ? OFFSET ?';
263
    }
264
265
    /**
266
     * Compile SET clause on UPDATE query
267
     *
268
     * @param CompilableClause $query
269
     *
270
     * @return string
271
     * @throws PrimeException
272
     */
273 68
    private function compileValues(CompilableClause $query): string
274
    {
275 68
        if (isset($query->state()->compiledParts['values'])) {
276 2
            return $query->state()->compiledParts['values']['sql'];
277
        }
278
279 68
        $types = $query->statements['values']['types'];
280 68
        $sql = null;
281
282 68
        foreach ($query->statements['values']['data'] as $column => $value) {
283 67
            $type = $types[$column] ?? true;
284
285 67
            if ($sql === null) {
286 67
                $sql = ' SET ';
287
            } else {
288 50
                $sql .= ', ';
289
            }
290
291 67
            $sql .= $this->quoteIdentifier($query, $query->preprocessor()->field($column, $type)).' = ?';
292
293 67
            if ($type !== true) {
294 67
                $types[$column] = $type;
295
            }
296
        }
297
298 68
        $query->state()->compiledParts['values'] = [
299 68
            'sql'   => $sql,
300 68
            'types' => $types
301 68
        ];
302
303 68
        return (string) $sql;
304
    }
305
306
    /**
307
     * Prepare the SQL statement
308
     *
309
     * @param CompilableClause $query
310
     * @param string $sql
311
     *
312
     * @return Statement
313
     */
314 323
    private function prepare(CompilableClause $query, $sql)
315
    {
316 323
        $query->state()->compiledParts['sql'] = $sql;
317
318 323
        if (isset($query->state()->compiledParts['prepared'][$sql])) {
319 56
            return $query->state()->compiledParts['prepared'][$sql];
320
        }
321
322 297
        return $query->state()->compiledParts['prepared'][$sql] = $this->connection->prepare($sql);
0 ignored issues
show
Bug introduced by
The method prepare() does not exist on Bdf\Prime\Connection\ConnectionInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Bdf\Prime\Connection\ConnectionInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

322
        return $query->state()->compiledParts['prepared'][$sql] = $this->connection->/** @scrutinizer ignore-call */ prepare($sql);
Loading history...
323
    }
324
}
325