Passed
Push — 2.0 ( 21259a...8b13d0 )
by Sébastien
19:00
created

KeyValueSqlCompiler::compileExpressionColumn()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 18
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 5.0144

Importance

Changes 0
Metric Value
eloc 10
dl 0
loc 18
ccs 11
cts 12
cp 0.9167
rs 9.6111
c 0
b 0
f 0
cc 5
nc 5
nop 3
crap 5.0144
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 259
    protected function doCompileSelect(CompilableClause $query)
49
    {
50 259
        $sql = 'SELECT '.$this->compileProjection($query).' FROM '.$this->quoteIdentifier($query, $query->statements['table']).$this->compileWhere($query).$this->compileLimit($query);
51
52 259
        return $this->prepare($query, $sql);
53
    }
54
55
    /**
56
     * {@inheritdoc}
57
     */
58 322
    public function quoteIdentifier(CompilableClause $query, string $column): string
59
    {
60 322
        return $query->isQuoteIdentifier()
61 2
            ? $this->platform()->grammar()->quoteIdentifier($column)
62 322
            : $column
63 322
        ;
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 364
    public function getBindings(CompilableClause $query): array
78
    {
79 364
        $bindings = [];
80
81 364
        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 364
        foreach ($query->statements['where'] as $field => $value) {
88 343
            $bindings[] = $this->platform()->types()->toDatabase($value, $query->state()->compiledParts['types'][$field] ?? null);
89
        }
90
91 364
        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 364
        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 322
    private function compileWhere(CompilableClause $query)
108
    {
109 322
        if (isset($query->state()->compiledParts['where'])) {
110 63
            return $query->state()->compiledParts['where'];
111
        }
112
113 293
        if (empty($query->statements['where'])) {
114 31
            $query->state()->compiledParts['types'] = [];
115 31
            $query->state()->compiledParts['where'] = '';
116
117 31
            return '';
118
        }
119
120 263
        $sql = [];
121 263
        $types = [];
122
123 263
        foreach ($query->statements['where'] as $field => $value) {
124 263
            $type = true;
125
126 263
            $column = $query->preprocessor()->field($field, $type);
127
128 263
            $sql[] = $this->quoteIdentifier($query, $column).' = ?';
129
130 263
            if ($type !== true) {
131 263
                $types[$field] = $type;
132
            }
133
        }
134
135 263
        $query->state()->compiledParts['types'] = $types;
136
137 263
        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 259
    private function compileProjection(CompilableClause $query)
149
    {
150 259
        if (!empty($query->statements['aggregate'])) {
151 77
            return $this->compileAggregate($query, $query->statements['aggregate'][0], $query->statements['aggregate'][1]);
152
        }
153
154 200
        if (empty($query->statements['columns'])) {
155 162
            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 259
    private function compileLimit(CompilableClause $query)
239
    {
240 259
        if (!isset($query->statements['limit']) && !isset($query->statements['offset'])) {
241 226
            return '';
242
        }
243
244 44
        if (!isset($query->statements['offset'])) {
245 39
            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 322
    private function prepare(CompilableClause $query, $sql)
315
    {
316 322
        $query->state()->compiledParts['sql'] = $sql;
317
318 322
        if (isset($query->state()->compiledParts['prepared'][$sql])) {
319 56
            return $query->state()->compiledParts['prepared'][$sql];
320
        }
321
322 296
        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