Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
76 / 76
100.00% covered (success)
100.00%
10 / 10
CRAP
100.00% covered (success)
100.00%
1 / 1
Insert
100.00% covered (success)
100.00%
76 / 76
100.00% covered (success)
100.00%
10 / 10
28
100.00% covered (success)
100.00%
1 / 1
 renderOptions
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
5
 into
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 renderInto
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 columns
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 renderColumns
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 onDuplicateKeyUpdate
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 renderOnDuplicateKeyUpdate
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 checkRowStatementsConflict
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 sql
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
7
 run
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php declare(strict_types=1);
2/*
3 * This file is part of Aplus Framework Database Library.
4 *
5 * (c) Natan Felles <natanfelles@gmail.com>
6 *
7 * For the full copyright and license information, please view the LICENSE
8 * file that was distributed with this source code.
9 */
10namespace Framework\Database\Manipulation;
11
12use Closure;
13use InvalidArgumentException;
14use LogicException;
15
16/**
17 * Class Insert.
18 *
19 * @see https://mariadb.com/kb/en/insert/
20 *
21 * @package database
22 */
23class Insert extends Statement
24{
25    use Traits\Select;
26    use Traits\Set;
27    use Traits\Values;
28
29    /**
30     * @see https://mariadb.com/kb/en/insert-delayed/
31     *
32     * @var string
33     */
34    public const OPT_DELAYED = 'DELAYED';
35    /**
36     * Convert errors to warnings, which will not stop inserts of additional rows.
37     *
38     * @see https://mariadb.com/kb/en/insert-ignore/
39     *
40     * @var string
41     */
42    public const OPT_IGNORE = 'IGNORE';
43    /**
44     * @see https://mariadb.com/kb/en/high_priority-and-low_priority/
45     *
46     * @var string
47     */
48    public const OPT_HIGH_PRIORITY = 'HIGH_PRIORITY';
49    /**
50     * @see https://mariadb.com/kb/en/high_priority-and-low_priority/
51     *
52     * @var string
53     */
54    public const OPT_LOW_PRIORITY = 'LOW_PRIORITY';
55
56    protected function renderOptions() : ?string
57    {
58        if ( ! $this->hasOptions()) {
59            return null;
60        }
61        $options = $this->sql['options'];
62        foreach ($options as &$option) {
63            $input = $option;
64            $option = \strtoupper($option);
65            if ( ! \in_array($option, [
66                static::OPT_DELAYED,
67                static::OPT_IGNORE,
68                static::OPT_LOW_PRIORITY,
69                static::OPT_HIGH_PRIORITY,
70            ], true)) {
71                throw new InvalidArgumentException("Invalid option: {$input}");
72            }
73        }
74        unset($option);
75        $intersection = \array_intersect(
76            $options,
77            [static::OPT_DELAYED, static::OPT_HIGH_PRIORITY, static::OPT_LOW_PRIORITY]
78        );
79        if (\count($intersection) > 1) {
80            throw new LogicException(
81                'Options LOW_PRIORITY, DELAYED or HIGH_PRIORITY can not be used together'
82            );
83        }
84        $options = \implode(' ', $options);
85        return " {$options}";
86    }
87
88    /**
89     * Sets the INTO table.
90     *
91     * @param string $table Table name
92     *
93     * @return static
94     */
95    public function into(string $table) : static
96    {
97        $this->sql['into'] = $table;
98        return $this;
99    }
100
101    /**
102     * Renders the "INTO $table" clause.
103     *
104     * @throws LogicException if INTO was not set
105     *
106     * @return string
107     */
108    protected function renderInto() : string
109    {
110        if ( ! isset($this->sql['into'])) {
111            throw new LogicException('INTO table must be set');
112        }
113        return ' INTO ' . $this->renderIdentifier($this->sql['into']);
114    }
115
116    /**
117     * Sets the INTO columns.
118     *
119     * @param string $column Column name
120     * @param string ...$columns Extra column names
121     *
122     * @return static
123     */
124    public function columns(string $column, string ...$columns) : static
125    {
126        $this->sql['columns'] = [$column, ...$columns];
127        return $this;
128    }
129
130    /**
131     * Renders the INTO $table "(...$columns)" part.
132     *
133     * @return string|null The imploded columns or null if none was set
134     */
135    protected function renderColumns() : ?string
136    {
137        if ( ! isset($this->sql['columns'])) {
138            return null;
139        }
140        $columns = [];
141        foreach ($this->sql['columns'] as $column) {
142            $columns[] = $this->renderIdentifier($column);
143        }
144        $columns = \implode(', ', $columns);
145        return " ({$columns})";
146    }
147
148    /**
149     * Sets the ON DUPLICATE KEY UPDATE part.
150     *
151     * @param array<string,Closure|float|int|string|null>|object $columns Column name
152     * as key/property, column value/expression as value
153     *
154     * @see https://mariadb.com/kb/en/insert-on-duplicate-key-update/
155     *
156     * @return static
157     */
158    public function onDuplicateKeyUpdate(array | object $columns) : static
159    {
160        $this->sql['on_duplicate'] = (array) $columns;
161        return $this;
162    }
163
164    /**
165     * Renders the ON DUPLICATE KEY UPDATE part.
166     *
167     * @return string|null The part or null if it was not set
168     */
169    protected function renderOnDuplicateKeyUpdate() : ?string
170    {
171        if ( ! isset($this->sql['on_duplicate'])) {
172            return null;
173        }
174        $onDuplicate = [];
175        foreach ($this->sql['on_duplicate'] as $column => $value) {
176            $onDuplicate[] = $this->renderAssignment($column, $value);
177        }
178        $onDuplicate = \implode(', ', $onDuplicate);
179        return " ON DUPLICATE KEY UPDATE {$onDuplicate}";
180    }
181
182    /**
183     * Check for conflicts in the INSERT statement.
184     *
185     * @throws LogicException if it has conflicts
186     */
187    protected function checkRowStatementsConflict() : void
188    {
189        if ( ! isset($this->sql['values'])
190            && ! isset($this->sql['select'])
191            && ! $this->hasSet()
192        ) {
193            throw new LogicException(
194                'The INSERT INTO must be followed by VALUES, SET or SELECT statement'
195            );
196        }
197    }
198
199    /**
200     * Renders the INSERT statement.
201     *
202     * @return string
203     */
204    public function sql() : string
205    {
206        $sql = 'INSERT' . \PHP_EOL;
207        $part = $this->renderOptions();
208        if ($part) {
209            $sql .= $part . \PHP_EOL;
210        }
211        $sql .= $this->renderInto() . \PHP_EOL;
212        $part = $this->renderColumns();
213        if ($part) {
214            $sql .= $part . \PHP_EOL;
215        }
216        $this->checkRowStatementsConflict();
217        $part = $this->renderValues();
218        if ($part) {
219            $sql .= $part . \PHP_EOL;
220        }
221        $part = $this->renderSetCheckingConflicts();
222        if ($part) {
223            $sql .= $part . \PHP_EOL;
224        }
225        $part = $this->renderSelect();
226        if ($part) {
227            $sql .= $part . \PHP_EOL;
228        }
229        $part = $this->renderOnDuplicateKeyUpdate();
230        if ($part) {
231            $sql .= $part . \PHP_EOL;
232        }
233        return $sql;
234    }
235
236    /**
237     * Runs the INSERT statement.
238     *
239     * @return int|string The number of affected rows
240     */
241    public function run() : int|string
242    {
243        return $this->database->exec($this->sql());
244    }
245}