diff --git a/src/Parser/InsertParser.php b/src/Parser/InsertParser.php index 295573b3..73a5c26c 100644 --- a/src/Parser/InsertParser.php +++ b/src/Parser/InsertParser.php @@ -1,9 +1,10 @@ pointer, $query->setClause) = $p->parse(); break; + case 'SELECT': + $select_parser = new SelectParser($this->pointer, $this->tokens, $this->sql); + $query->selectQuery = $select_parser->parse(); + $this->pointer = $select_parser->getPointer(); + break; + default: throw new ParserException("Unexpected clause {$token->value}"); } @@ -168,7 +175,7 @@ public function parse() } else { if ($this->currentClause === 'COLUMN_LIST' && $needs_comma && $token->value === ')') { $needs_comma = false; - if (($this->tokens[$this->pointer + 1]->value ?? null) !== 'VALUES') { + if (!in_array($this->tokens[$this->pointer + 1]->value ?? null, ['VALUES', 'SELECT'])) { throw new ParserException("Expected VALUES after insert column list"); } break; @@ -203,7 +210,7 @@ public function parse() $this->pointer++; } - if ((!$query->insertColumns || !$query->values) && !$query->setClause) { + if ((!$query->insertColumns || !$query->values) && !$query->setClause && !$query->selectQuery) { throw new ParserException("Missing values to insert"); } return $query; diff --git a/src/Parser/SelectParser.php b/src/Parser/SelectParser.php index 6a84418b..1edcaecb 100644 --- a/src/Parser/SelectParser.php +++ b/src/Parser/SelectParser.php @@ -1,4 +1,5 @@ sql = $sql; } + public function getPointer(): int + { + return $this->pointer; + } + public function parse() : SelectQuery { // if the first part of this query is nested, we should be able to unwrap it safely diff --git a/src/Processor/InsertProcessor.php b/src/Processor/InsertProcessor.php index ede5fc60..6b36e76d 100644 --- a/src/Processor/InsertProcessor.php +++ b/src/Processor/InsertProcessor.php @@ -1,13 +1,18 @@ selectQuery) { + $selectResult = SelectProcessor::process( + $conn, + $scope, + $stmt->selectQuery + )->rows; + + foreach ($selectResult as $selected_param) { + $row = []; + foreach ($stmt->selectQuery->selectExpressions as $index => $expr) { + if (key_exists($index, $stmt->insertColumns) + && (is_string($selected_param[$expr->name]) || is_int($selected_param[$expr->name]))) { + $row[$stmt->insertColumns[$index]] = $selected_param[$expr->name]; + } + } + + $row = DataIntegrity::coerceToSchema($conn, $row, $table_definition); + + $result = DataIntegrity::checkUniqueConstraints($table, $row, $table_definition); + + if ($result !== null) { + throw new SQLFakeUniqueKeyViolation($result[0]); + } + + /** + * @psalm-suppress MixedAssignment + */ + foreach ($row as $column_name => $value) { + $column = $table_definition->columns[$column_name]; + + if ($column instanceof IntegerColumn && $column->isAutoIncrement() && is_int($value)) { + $last_incremented_value = $conn->getServer()->addAutoIncrementMinValue( + $database, + $table_name, + $column_name, + $value + ); + } + } + + $table[] = $row; + } + + $conn->getServer()->saveTable($database, $table_name, $table); + $conn->setLastInsertId((string)($last_incremented_value ?? 0)); + + return count($selectResult); + } + return $rows_affected; } } diff --git a/src/Query/InsertQuery.php b/src/Query/InsertQuery.php index 2b69c0de..ef6478ef 100644 --- a/src/Query/InsertQuery.php +++ b/src/Query/InsertQuery.php @@ -1,4 +1,5 @@ table = $table; diff --git a/tests/EndToEndTest.php b/tests/EndToEndTest.php index d2cd95a3..62a11449 100644 --- a/tests/EndToEndTest.php +++ b/tests/EndToEndTest.php @@ -1,6 +1,7 @@ prepare( + "INSERT INTO `video_game_characters` + (`name`, `type`, `profession`, `console`, `is_alive`, `powerups`, `skills`, `created_on`) + SELECT + 'wario','villain','plumber','nes','1','3','{\"magic\":0, \"speed\":0, \"strength\":0, \"weapons\":0}', NOW(), + FROM `video_game_characters`" + ); + + $query->execute(); + + self::assertEquals($pdo->lastInsertId(), 32); + + $query = $pdo->prepare( + 'SELECT `id`, `name` + FROM `video_game_characters` + ORDER BY `id` DESC + LIMIT 1' + ); + + $query->execute(); + + $this->assertSame( + [ + ['id' => 32, 'name' => 'wario'], + ], + $query->fetchAll(PDO::FETCH_ASSOC) + ); + } + public function testOrderBySecondDimensionAliased() { $pdo = self::getConnectionToFullDB(false);