1<?php
2
3/**
4 * `INSERT` statement.
5 */
6
7namespace PhpMyAdmin\SqlParser\Statements;
8
9use PhpMyAdmin\SqlParser\Components\ArrayObj;
10use PhpMyAdmin\SqlParser\Components\Array2d;
11use PhpMyAdmin\SqlParser\Components\IntoKeyword;
12use PhpMyAdmin\SqlParser\Components\OptionsArray;
13use PhpMyAdmin\SqlParser\Components\SetOperation;
14use PhpMyAdmin\SqlParser\Parser;
15use PhpMyAdmin\SqlParser\Statement;
16use PhpMyAdmin\SqlParser\Token;
17use PhpMyAdmin\SqlParser\TokensList;
18
19/**
20 * `INSERT` statement.
21 *
22 * INSERT [LOW_PRIORITY | DELAYED | HIGH_PRIORITY] [IGNORE]
23 *     [INTO] tbl_name
24 *     [PARTITION (partition_name,...)]
25 *     [(col_name,...)]
26 *     {VALUES | VALUE} ({expr | DEFAULT},...),(...),...
27 *     [ ON DUPLICATE KEY UPDATE
28 *       col_name=expr
29 *         [, col_name=expr] ... ]
30 *
31 * or
32 *
33 * INSERT [LOW_PRIORITY | DELAYED | HIGH_PRIORITY] [IGNORE]
34 *     [INTO] tbl_name
35 *     [PARTITION (partition_name,...)]
36 *     SET col_name={expr | DEFAULT}, ...
37 *     [ ON DUPLICATE KEY UPDATE
38 *       col_name=expr
39 *         [, col_name=expr] ... ]
40 *
41 * or
42 *
43 * INSERT [LOW_PRIORITY | HIGH_PRIORITY] [IGNORE]
44 *     [INTO] tbl_name
45 *     [PARTITION (partition_name,...)]
46 *     [(col_name,...)]
47 *     SELECT ...
48 *     [ ON DUPLICATE KEY UPDATE
49 *       col_name=expr
50 *         [, col_name=expr] ... ]
51 *
52 * @category   Statements
53 *
54 * @license    https://www.gnu.org/licenses/gpl-2.0.txt GPL-2.0+
55 */
56class InsertStatement extends Statement
57{
58    /**
59     * Options for `INSERT` statements.
60     *
61     * @var array
62     */
63    public static $OPTIONS = array(
64        'LOW_PRIORITY' => 1,
65        'DELAYED' => 2,
66        'HIGH_PRIORITY' => 3,
67        'IGNORE' => 4
68    );
69
70    /**
71     * Tables used as target for this statement.
72     *
73     * @var IntoKeyword
74     */
75    public $into;
76
77    /**
78     * Values to be inserted.
79     *
80     * @var ArrayObj[]|null
81     */
82    public $values;
83
84    /**
85     * If SET clause is present
86     * holds the SetOperation.
87     *
88     * @var SetOperation[]
89     */
90    public $set;
91
92    /**
93     * If SELECT clause is present
94     * holds the SelectStatement.
95     *
96     * @var SelectStatement
97     */
98    public $select;
99
100    /**
101     * If ON DUPLICATE KEY UPDATE clause is present
102     * holds the SetOperation.
103     *
104     * @var SetOperation[]
105     */
106    public $onDuplicateSet;
107
108    /**
109     * @return string
110     */
111    public function build()
112    {
113        $ret = 'INSERT ' . $this->options;
114        $ret = trim($ret) . ' INTO ' . $this->into;
115
116        if (! is_null($this->values) && count($this->values) > 0) {
117            $ret .= ' VALUES ' . Array2d::build($this->values);
118        } elseif (! is_null($this->set) && count($this->set) > 0) {
119            $ret .= ' SET ' . SetOperation::build($this->set);
120        } elseif (! is_null($this->select) && strlen($this->select) > 0) {
121            $ret .= ' ' . $this->select->build();
122        }
123
124        if (! is_null($this->onDuplicateSet) && count($this->onDuplicateSet) > 0) {
125            $ret .= ' ON DUPLICATE KEY UPDATE ' . SetOperation::build($this->onDuplicateSet);
126        }
127
128        return $ret;
129    }
130
131    /**
132     * @param Parser     $parser the instance that requests parsing
133     * @param TokensList $list   the list of tokens to be parsed
134     */
135    public function parse(Parser $parser, TokensList $list)
136    {
137        ++$list->idx; // Skipping `INSERT`.
138
139        // parse any options if provided
140        $this->options = OptionsArray::parse(
141            $parser,
142            $list,
143            static::$OPTIONS
144        );
145        ++$list->idx;
146
147        /**
148         * The state of the parser.
149         *
150         * Below are the states of the parser.
151         *
152         *      0 ---------------------------------[ INTO ]----------------------------------> 1
153         *
154         *      1 -------------------------[ VALUES/VALUE/SET/SELECT ]-----------------------> 2
155         *
156         *      2 -------------------------[ ON DUPLICATE KEY UPDATE ]-----------------------> 3
157         *
158         * @var int
159         */
160        $state = 0;
161
162        /**
163         * For keeping track of semi-states on encountering
164         * ON DUPLICATE KEY UPDATE ...
165         */
166        $miniState = 0;
167
168        for (; $list->idx < $list->count; ++$list->idx) {
169            /**
170             * Token parsed at this moment.
171             *
172             * @var Token
173             */
174            $token = $list->tokens[$list->idx];
175
176            // End of statement.
177            if ($token->type === Token::TYPE_DELIMITER) {
178                break;
179            }
180
181            // Skipping whitespaces and comments.
182            if (($token->type === Token::TYPE_WHITESPACE) || ($token->type === Token::TYPE_COMMENT)) {
183                continue;
184            }
185
186            if ($state === 0) {
187                if ($token->type === Token::TYPE_KEYWORD
188                    && $token->keyword !== 'INTO'
189                ) {
190                    $parser->error('Unexpected keyword.', $token);
191                    break;
192                }
193
194                ++$list->idx;
195                $this->into = IntoKeyword::parse(
196                    $parser,
197                    $list,
198                    array('fromInsert' => true)
199                );
200
201                $state = 1;
202            } elseif ($state === 1) {
203                if ($token->type === Token::TYPE_KEYWORD) {
204                    if ($token->keyword === 'VALUE'
205                        || $token->keyword === 'VALUES'
206                    ) {
207                        ++$list->idx; // skip VALUES
208
209                        $this->values = Array2d::parse($parser, $list);
210                    } elseif ($token->keyword === 'SET') {
211                        ++$list->idx; // skip SET
212
213                        $this->set = SetOperation::parse($parser, $list);
214                    } elseif ($token->keyword === 'SELECT') {
215                        $this->select = new SelectStatement($parser, $list);
216                    } else {
217                        $parser->error(
218                            'Unexpected keyword.',
219                            $token
220                        );
221                        break;
222                    }
223                    $state = 2;
224                    $miniState = 1;
225                } else {
226                    $parser->error(
227                        'Unexpected token.',
228                        $token
229                    );
230                    break;
231                }
232            } elseif ($state === 2) {
233                $lastCount = $miniState;
234
235                if ($miniState === 1 && $token->keyword === 'ON') {
236                    ++$miniState;
237                } elseif ($miniState === 2 && $token->keyword === 'DUPLICATE') {
238                    ++$miniState;
239                } elseif ($miniState === 3 && $token->keyword === 'KEY') {
240                    ++$miniState;
241                } elseif ($miniState === 4 && $token->keyword === 'UPDATE') {
242                    ++$miniState;
243                }
244
245                if ($lastCount === $miniState) {
246                    $parser->error(
247                        'Unexpected token.',
248                        $token
249                    );
250                    break;
251                }
252
253                if ($miniState === 5) {
254                    ++$list->idx;
255                    $this->onDuplicateSet = SetOperation::parse($parser, $list);
256                    $state = 3;
257                }
258            }
259        }
260
261        --$list->idx;
262    }
263}
264