Lines 96.53% 195 / 202
Methods 88.23% 30 / 34
Classes 0.00% 0 / 1
Name Lines Methods CRAP
 wrap 100.00% 1 / 1 100.00% 1 / 1 1
 compileEmptyInsert 100.00% 1 / 1 100.00% 1 / 1 1
 compileOffset 80.00% 4 / 5 0.00% 0 / 1 3.07
 compileLock 100.00% 4 / 4 100.00% 1 / 1 4
 [BlueprintAU\Radiant\Database\Grammars\Grammar] wrapSegments 100.00% 6 / 6 100.00% 1 / 1 3
 [BlueprintAU\Radiant\Database\Grammars\Grammar] wrapTable 80.00% 4 / 5 0.00% 0 / 1 3.07
 [BlueprintAU\Radiant\Database\Grammars\Grammar] wrapColumn 100.00% 13 / 13 100.00% 1 / 1 6
 [BlueprintAU\Radiant\Database\Grammars\Grammar] wrapAggregateInner 66.66% 8 / 12 0.00% 0 / 1 5.93
 [BlueprintAU\Radiant\Database\Grammars\Grammar] columnize 100.00% 1 / 1 100.00% 1 / 1 1
 [BlueprintAU\Radiant\Database\Grammars\Grammar] parameter 100.00% 5 / 5 100.00% 1 / 1 3
 [BlueprintAU\Radiant\Database\Grammars\Grammar] parameterize 100.00% 1 / 1 100.00% 1 / 1 1
 [BlueprintAU\Radiant\Database\Grammars\Grammar] compileSelect 100.00% 16 / 16 100.00% 1 / 1 2
 [BlueprintAU\Radiant\Database\Grammars\Grammar] compileInsert 100.00% 14 / 14 100.00% 1 / 1 3
 [BlueprintAU\Radiant\Database\Grammars\Grammar] usesReturning 100.00% 1 / 1 100.00% 1 / 1 1
 [BlueprintAU\Radiant\Database\Grammars\Grammar] withReturning 100.00% 3 / 3 100.00% 1 / 1 3
 [BlueprintAU\Radiant\Database\Grammars\Grammar] compileInsertForId 100.00% 4 / 4 100.00% 1 / 1 1
 [BlueprintAU\Radiant\Database\Grammars\Grammar] compileUpdate 100.00% 9 / 9 100.00% 1 / 1 2
 [BlueprintAU\Radiant\Database\Grammars\Grammar] compileDelete 100.00% 5 / 5 100.00% 1 / 1 2
 [BlueprintAU\Radiant\Database\Grammars\Grammar] compileColumns 100.00% 4 / 4 100.00% 1 / 1 2
 [BlueprintAU\Radiant\Database\Grammars\Grammar] compileFrom 100.00% 6 / 6 100.00% 1 / 1 2
 [BlueprintAU\Radiant\Database\Grammars\Grammar] compileJoins 100.00% 10 / 10 100.00% 1 / 1 7
 [BlueprintAU\Radiant\Database\Grammars\Grammar] compileJoinWheres 100.00% 5 / 5 100.00% 1 / 1 3
 [BlueprintAU\Radiant\Database\Grammars\Grammar] compileWheres 100.00% 4 / 4 100.00% 1 / 1 2
 [BlueprintAU\Radiant\Database\Grammars\Grammar] compileWhereGroup 100.00% 5 / 5 100.00% 1 / 1 3
 [BlueprintAU\Radiant\Database\Grammars\Grammar] compileWhere 100.00% 12 / 12 100.00% 1 / 1 11
 [BlueprintAU\Radiant\Database\Grammars\Grammar] compileBasicWhere 100.00% 12 / 12 100.00% 1 / 1 8
 [BlueprintAU\Radiant\Database\Grammars\Grammar] compileBetweenWhere 100.00% 3 / 3 100.00% 1 / 1 1
 [BlueprintAU\Radiant\Database\Grammars\Grammar] compileNullWhere 100.00% 3 / 3 100.00% 1 / 1 1
 [BlueprintAU\Radiant\Database\Grammars\Grammar] compileGroups 100.00% 4 / 4 100.00% 1 / 1 2
 [BlueprintAU\Radiant\Database\Grammars\Grammar] compileHavings 100.00% 9 / 9 100.00% 1 / 1 4
 [BlueprintAU\Radiant\Database\Grammars\Grammar] compileOrders 100.00% 8 / 8 100.00% 1 / 1 4
 [BlueprintAU\Radiant\Database\Grammars\Grammar] compileLimit 100.00% 1 / 1 100.00% 1 / 1 2
 [BlueprintAU\Radiant\Database\Grammars\Grammar] compileUnions 100.00% 6 / 6 100.00% 1 / 1 3
 [BlueprintAU\Radiant\Database\Grammars\Grammar] wrapFromTable 75.00% 3 / 4 0.00% 0 / 1 2.06
20final class MySqlGrammar extends Grammar
21{
22    /**
23     * Wrap an identifier in MySQL backticks.
24     *
25     * @param  string  $value
26     * @return string
27     */
28    protected function wrap(string $value): string
29    {
30        return '`' . str_replace('`', '``', $value) . '`';
31    }
32
33    /**
34     * Compile the empty-row insert — the one-row `VALUES ()` fallback.
35     *
36     * MySQL has no `INSERT ... DEFAULT VALUES` form; it accepts the one-row
37     * `VALUES ()` and applies the column defaults.
38     *
39     * @param  QueryBuilder  $builder
40     * @return string
41     */
42    #[\Override]
43    protected function compileEmptyInsert(QueryBuilder $builder): string
44    {
45        return "INSERT INTO {$this->wrapFromTable($builder)} () VALUES ()";
46    }
47
48    /**
49     * Compile the offset clause, padding a bare offset with the max limit.
50     *
51     * @param  QueryBuilder  $builder
52     * @return string
53     */
54    protected function compileOffset(QueryBuilder $builder): string
55    {
56        if ($builder->getOffset() === null) {
57            return '';
58        }
59        if ($builder->getLimit() === null) {
60            return 'LIMIT 18446744073709551615 OFFSET ' . $builder->getOffset();
61        }
62        return 'OFFSET ' . $builder->getOffset();
63    }
64
65    /**
66     * Compile the row lock for MySQL.
67     *
68     * @param  QueryBuilder  $builder
69     * @return string
70     */
71    protected function compileLock(QueryBuilder $builder): string
72    {
73        return match ($builder->getLock()) {
74            null => '',
75            LockType::Update => 'FOR UPDATE',
76            LockType::Shared => 'LOCK IN SHARE MODE',
77        };
78    }
79}

Inherited from BlueprintAU\Radiant\Database\Grammars\Grammar

61    protected function wrapSegments(string|Expression $value): string
62    {
63        if ($value instanceof Expression) {
64            return $value->value;
65        }
66        return implode('.', array_map(
67            fn($segment) => $segment === '*' ? '*' : $this->wrap($segment),
68            explode('.', $value)
69        ));
70    }
78    protected function wrapTable(string|Expression $table): string
79    {
80        if ($table instanceof Expression) {
81            return $table->value;
82        }
83        if (preg_match('/^(.+?)(?:\s+as\s+)(.+)$/i', $table, $m)) {
84            return $this->wrapSegments($m[1]) . ' AS ' . $this->wrapSegments($m[2]);
85        }
86        return $this->wrapSegments($table);
87    }
99    protected function wrapColumn(string|Expression|Aggregate|SubquerySelect $column): string
100    {
101        if ($column instanceof Expression) {
102            return $column->value;
103        }
104
105        if ($column instanceof SubquerySelect) {
106            return '(' . $this->compileSelect($column->query) . ') AS '
107                . $this->wrapSegments($column->alias);
108        }
109
110        if ($column instanceof Aggregate) {
111            $rendered = $column->function . '(' . $this->wrapAggregateInner($column->column) . ')';
112
113            // Only an EXPLICIT alias renders an AS — the derived result key
114            // (e.g. `count(*)`) is for reading the row back, not for SQL.
115            return $column->alias === null
116                ? $rendered
117                : $rendered . ' AS ' . $this->wrapSegments($column->alias);
118        }
119
120        if (preg_match('/^(.+?)(?:\s+as\s+)(.+)$/i', $column, $m)) {
121            return $this->wrapSegments($m[1]) . ' AS ' . $this->wrapSegments($m[2]);
122        }
123        
124        return $this->wrapSegments($column);
125    }
139    protected function wrapAggregateInner(string|Expression $inner): string
140    {
141        if ($inner instanceof Expression) {
142            return $inner->value;
143        }
144        if (preg_match('/^distinct\s+(.+)$/i', $inner, $m)) {
145            return 'DISTINCT ' . $this->wrapSegments($m[1]);
146        }
147        if ($inner === '*') {
148            return '*';
149        }
150
151        // Strict single-identifier path: `a`, `a.b`, `a.*` — no spaces,
152        // commas, parentheses, or other expression machinery. Anything
153        // more complex is not a supported aggregate argument.
154        if (preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*|\.\*)?$/', $inner) !== 1) {
155            throw new UnsupportedFeatureException(
156                'Aggregate arguments support only a single column (optionally schema-qualified '
157                    . "or `distinct col`); got [{$inner}]. Use a raw Expression for complex arguments.",
158            );
159        }
160
161        return $this->wrapSegments($inner);
162    }
170    protected function columnize(array $columns): string
171    {
172        return implode(', ', array_map(fn($column) => $this->wrapColumn($column), $columns));
173    }
186    protected function parameter(mixed $value): string
187    {
188        if ($value instanceof Expression) {
189            return $value->value;
190        }
191        if ($value instanceof ToSqlValue) {
192            return $this->quoteLiteral($value->toSqlValue());
193        }
194        return '?';
195    }
203    protected function parameterize(array $values): string
204    {
205        return implode(', ', array_map(fn($value) => $this->parameter($value), $values));
206    }
218    final public function compileSelect(QueryBuilder $builder): string
219    {
220        $sql = $this->concatenate([
221            'SELECT',
222            $builder->isDistinct() ? 'DISTINCT' : '',
223            $this->compileColumns($builder),
224            'FROM',
225            $this->compileFrom($builder),
226            $this->compileJoins($builder),
227            $this->compileWheres($builder),
228            $this->compileGroups($builder),
229            $this->compileHavings($builder),
230            $this->compileOrders($builder),
231            $this->compileLimit($builder),
232            $this->compileOffset($builder),
233            $this->compileLock($builder),
234        ]);
235
236        return $this->compileUnions($builder, $sql);
237    }
249    final public function compileInsert(QueryBuilder $builder, array $values, ?string $pk = null): string
250    {
251        $rows = $this->normalizeInsertRows($values);
252
253        // Ragged rows cannot compile: the column list comes from row 0 and
254        // each row's placeholder group is sized by its own arity. Fail fast
255        // rather than emit a malformed statement (or silently write NULL).
256        $this->assertUniformInsertRows($rows);
257
258        // An EMPTY row (a model with no set properties, a DEFAULTS-only
259        // insert) cannot compile to the degenerate `INSERT INTO t () VALUES
260        // ()` — invalid SQL on every dialect. The dialect owns the form via
261        // {@see compileEmptyInsert()} (the SQL-standard DEFAULT VALUES by
262        // default; MySQL overrides with the one-row `VALUES ()` it
263        // accepts).
264        if (isset($rows[0]) && $rows[0] === []) {
265            return $this->withReturning(
266                $this->compileEmptyInsert($builder),
267                $pk,
268            );
269        }
270
271        $columns = implode(', ', array_map(fn($column) => $this->wrapSegments($column), array_keys($rows[0])));
272        $placeholders = implode(', ', array_map(
273            fn($row) => '(' . implode(', ', array_fill(0, count($row), '?')) . ')',
274            $rows
275        ));
276        $sql = "INSERT INTO {$this->wrapFromTable($builder)} ({$columns}) VALUES {$placeholders}";
277
278        return $this->withReturning($sql, $pk);
279    }
301    protected function usesReturning(): bool
302    {
303        return false;
304    }
314    protected function withReturning(string $sql, ?string $pk): string
315    {
316        if ($pk !== null && $this->usesReturning()) {
317            return $sql . ' RETURNING ' . $this->wrapSegments($pk);
318        }
319
320        return $sql;
321    }
334    final public function compileInsertForId(QueryBuilder $builder, array $values, string $pk): array
335    {
336        return [
337            'sql' => $this->compileInsert($builder, $values, $pk),
338            'returnsKey' => $this->usesReturning(),
339        ];
340    }
351    final public function compileUpdate(QueryBuilder $builder, array $values): string
352    {
353        $sets = implode(', ', array_map(
354            fn($column) => $this->wrapSegments($column) . ' = ?',
355            array_keys($values)
356        ));
357        $sql = "UPDATE {$this->wrapFromTable($builder)} SET {$sets}";
358        $wheres = $this->compileWheres($builder);
359        if ($wheres !== '') {
360            $sql .= ' ' . $wheres;
361        }
362        return $sql;
363    }
373    final public function compileDelete(QueryBuilder $builder): string
374    {
375        $sql = "DELETE FROM {$this->wrapFromTable($builder)}";
376        $wheres = $this->compileWheres($builder);
377        if ($wheres !== '') {
378            $sql .= ' ' . $wheres;
379        }
380        return $sql;
381    }
391    protected function compileColumns(QueryBuilder $builder): string
392    {
393        $columns = $builder->getColumns();
394        if ($columns === ['*']) {
395            return '*';
396        }
397        return $this->columnize($columns);
398    }
406    protected function compileFrom(QueryBuilder $builder): string
407    {
408        $from = $builder->getFrom();
409        if ($from instanceof QueryBuilder) {
410            $alias = $builder->getFromAlias();
411            $subSql = $this->compileSelect($from);
412            return '(' . $subSql . ') AS ' . $this->wrapSegments($alias ?? '');
413        }
414        return $this->wrapTable($from);
415    }
423    protected function compileJoins(QueryBuilder $builder): string
424    {
425        $segments = [];
426        foreach ($builder->getJoins() as $join) {
427            $keyword = match ($join['type']) {
428                JoinType::Inner => 'INNER JOIN',
429                JoinType::Left => 'LEFT JOIN',
430                JoinType::Right => 'RIGHT JOIN',
431                JoinType::Cross => 'CROSS JOIN',
432            };
433            $on = $this->compileJoinWheres($join['wheres']);
434            $segments[] = $keyword . ' ' . $this->wrapTable($join['table']) . ($on !== '' ? ' ON ' . $on : '');
435        }
436        return implode(' ', $segments);
437    }
445    protected function compileJoinWheres(array $wheres): string
446    {
447        $segments = [];
448        foreach ($wheres as $i => $where) {
449            $boolean = $i === 0 ? '' : strtoupper($where['boolean']->value) . ' ';
450            $segments[] = $boolean . $this->wrapSegments($where['first']) . ' ' . $where['operator']->value . ' ' . $this->wrapSegments($where['second']);
451        }
452        return implode(' ', $segments);
453    }
461    protected function compileWheres(QueryBuilder $builder): string
462    {
463        $wheres = $builder->getWheres();
464        if ($wheres === []) {
465            return '';
466        }
467        return 'WHERE ' . $this->compileWhereGroup($wheres);
468    }
476    protected function compileWhereGroup(array $wheres): string
477    {
478        $segments = [];
479        foreach ($wheres as $i => $where) {
480            $boolean = $i === 0 ? '' : strtoupper($where['boolean']->value) . ' ';
481            $segments[] = $boolean . $this->compileWhere($where);
482        }
483        return implode(' ', $segments);
484    }
492    protected function compileWhere(array $where): string
493    {
494        return match ($where['type']) {
495            WhereType::Basic => $this->compileBasicWhere($where),
496            WhereType::Between => $this->compileBetweenWhere($where),
497            WhereType::Null => $this->compileNullWhere($where),
498            WhereType::Raw => $where['sql'],
499            WhereType::Column => $this->wrapSegments($where['first']) . ' ' . $where['operator']->value . ' ' . $this->wrapSegments($where['second']),
500            WhereType::Nested => '(' . $this->compileWhereGroup($where['group']->wheres) . ')',
501            WhereType::Exists => ($where['negated'] ? 'NOT ' : '')
502                . 'EXISTS (' . $this->compileSelect($where['query']) . ')',
503            WhereType::InSub => $this->wrapSegments($where['column'])
504                . ($where['negated'] ? ' NOT' : '') . ' IN ('
505                . $this->compileSelect($where['query']) . ')',
506        };
507    }
515    protected function compileBasicWhere(array $where): string
516    {
517        $column = $this->wrapSegments($where['column']);
518        $operator = $where['operator'];
519        return match ($operator) {
520            WhereOperator::Null => "{$column} IS NULL",
521            WhereOperator::NotNull => "{$column} IS NOT NULL",
522            WhereOperator::In => "{$column} IN (" . $this->parameterize($where['value']) . ')',
523            WhereOperator::NotIn => "{$column} NOT IN (" . $this->parameterize($where['value']) . ')',
524            WhereOperator::Between => "{$column} BETWEEN " . $this->parameterize($where['value']),
525            WhereOperator::NotBetween => "{$column} NOT BETWEEN " . $this->parameterize($where['value']),
526            WhereOperator::Eq, WhereOperator::NotEq, WhereOperator::Lt, WhereOperator::LtEq,
527            WhereOperator::Gt, WhereOperator::GtEq, WhereOperator::Like, WhereOperator::NotLike,
528            WhereOperator::Is, WhereOperator::IsNot => "{$column} {$operator->value} " . $this->parameter($where['value']),
529        };
530    }
538    protected function compileBetweenWhere(array $where): string
539    {
540        $column = $this->wrapSegments($where['column']);
541        $operator = $where['operator'];
542        return "{$column} {$operator->value} " . $this->parameter($where['value'][0]) . ' AND ' . $this->parameter($where['value'][1]);
543    }
551    protected function compileNullWhere(array $where): string
552    {
553        $column = $this->wrapSegments($where['column']);
554        $operator = $where['operator'];
555        return "{$column} IS {$operator->value}";
556    }
564    protected function compileGroups(QueryBuilder $builder): string
565    {
566        $groups = $builder->getGroups();
567        if ($groups === []) {
568            return '';
569        }
570        return 'GROUP BY ' . implode(', ', array_map(fn($group) => $this->wrapSegments($group), $groups));
571    }
579    protected function compileHavings(QueryBuilder $builder): string
580    {
581        $havings = $builder->getHavings();
582        if ($havings === []) {
583            return '';
584        }
585        $segments = [];
586        foreach ($havings as $i => $having) {
587            $boolean = $i === 0 ? '' : 'AND ';
588            $column = $this->wrapColumn($having['column']);
589            $segments[] = $boolean . $column . ' ' . $having['operator']->value . ' ' . $this->parameter($having['value']);
590        }
591        return 'HAVING ' . implode(' ', $segments);
592    }
600    protected function compileOrders(QueryBuilder $builder): string
601    {
602        $orders = $builder->getOrders();
603        if ($orders === []) {
604            return '';
605        }
606        $segments = [];
607        foreach ($orders as $order) {
608            $column = $this->wrapColumn($order['column']);
609            $segments[] = $order['direction'] !== null ? $column . ' ' . $order['direction']->value : $column;
610        }
611        return 'ORDER BY ' . implode(', ', $segments);
612    }
620    protected function compileLimit(QueryBuilder $builder): string
621    {
622        return $builder->getLimit() !== null ? 'LIMIT ' . $builder->getLimit() : '';
623    }
662    protected function compileUnions(QueryBuilder $builder, string $sql): string
663    {
664        foreach ($builder->getUnions() as $union) {
665            $keyword = $union['all'] ? 'UNION ALL' : 'UNION';
666            $sub = $union['query'];
667            $unionSql = $this->compileSelect($sub);
668            $sql .= ' ' . $keyword . ' (' . $unionSql . ')';
669        }
670        return $sql;
671    }
680    protected function wrapFromTable(QueryBuilder $builder): string
681    {
682        $from = $builder->getFrom();
683        if ($from instanceof QueryBuilder) {
684            throw new UnsupportedFeatureException('Insert/update/delete cannot target a subquery.');
685        }
686        return $this->wrapTable($from);
687    }