From 4baa1866300494fefa8bffb367c6e397a6d9792e Mon Sep 17 00:00:00 2001 From: Javakky Date: Fri, 31 Jan 2025 12:51:15 +0900 Subject: [PATCH 1/2] feat: INSERT...SELECT syntax implementation. --- src/Parser/InsertParser.php | 13 ++++++++++--- src/Parser/SelectParser.php | 6 ++++++ src/Processor/InsertProcessor.php | 25 +++++++++++++++++++++++- src/Query/InsertQuery.php | 4 ++++ tests/EndToEndTest.php | 32 +++++++++++++++++++++++++++++++ 5 files changed, 76 insertions(+), 4 deletions(-) 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..fcd47544 100644 --- a/src/Processor/InsertProcessor.php +++ b/src/Processor/InsertProcessor.php @@ -1,8 +1,8 @@ selectQuery) { + $selectResult = SelectProcessor::process( + $conn, + $scope, + $stmt->selectQuery + )->rows; + + $row = []; + foreach ($selectResult as $value) { + foreach ($stmt->selectQuery->selectExpressions as $index => $expr) { + if (key_exists($index, $stmt->insertColumns) + && (is_string($value[$expr->name]) || is_int($value[$expr->name]))) { + $row[$stmt->insertColumns[$index]] = $value[$expr->name]; + } + } + } + + $table[] = $row; + + $conn->getServer()->saveTable($database, $table_name, $table); + 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..a78ca5b5 100644 --- a/tests/EndToEndTest.php +++ b/tests/EndToEndTest.php @@ -1,6 +1,7 @@ prepare( + "INSERT INTO `video_game_characters` + (`id`, `name`, `type`, `profession`, `console`, `is_alive`, `powerups`, `skills`, `created_on`) + SELECT + 19+`id`, 'wario','villain','plumber','nes','1','3','{\"magic\":0, \"speed\":0, \"strength\":0, \"weapons\":0}', NOW(), + FROM `video_game_characters`" + ); + + $query->execute(); + + $query = $pdo->prepare( + 'SELECT `id`, `name` + FROM `video_game_characters` + ORDER BY `id` DESC + LIMIT 1' + ); + + $query->execute(); + + $this->assertSame( + [ + ['id' => 35, 'name' => 'wario'], + ], + $query->fetchAll(PDO::FETCH_ASSOC) + ); + } + public function testOrderBySecondDimensionAliased() { $pdo = self::getConnectionToFullDB(false); From 754176f4cc8828c64b40c684e00f5a5ccf144cef Mon Sep 17 00:00:00 2001 From: Javakky Date: Mon, 3 Feb 2025 17:26:38 +0900 Subject: [PATCH 2/2] fix: support default_value and last_insert_id --- src/Processor/InsertProcessor.php | 43 ++++++++++++++++++++++++++----- tests/EndToEndTest.php | 8 +++--- 2 files changed, 42 insertions(+), 9 deletions(-) diff --git a/src/Processor/InsertProcessor.php b/src/Processor/InsertProcessor.php index fcd47544..6b36e76d 100644 --- a/src/Processor/InsertProcessor.php +++ b/src/Processor/InsertProcessor.php @@ -8,6 +8,11 @@ final class InsertProcessor extends Processor { + /** + * @throws SQLFakeUniqueKeyViolation + * @throws ProcessorException + * @throws \Exception + */ public static function process( \Vimeo\MysqlEngine\FakePdoInterface $conn, Scope $scope, @@ -115,19 +120,45 @@ public static function process( $stmt->selectQuery )->rows; - $row = []; - foreach ($selectResult as $value) { + foreach ($selectResult as $selected_param) { + $row = []; foreach ($stmt->selectQuery->selectExpressions as $index => $expr) { if (key_exists($index, $stmt->insertColumns) - && (is_string($value[$expr->name]) || is_int($value[$expr->name]))) { - $row[$stmt->insertColumns[$index]] = $value[$expr->name]; + && (is_string($selected_param[$expr->name]) || is_int($selected_param[$expr->name]))) { + $row[$stmt->insertColumns[$index]] = $selected_param[$expr->name]; } } - } - $table[] = $row; + $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); } diff --git a/tests/EndToEndTest.php b/tests/EndToEndTest.php index a78ca5b5..62a11449 100644 --- a/tests/EndToEndTest.php +++ b/tests/EndToEndTest.php @@ -918,14 +918,16 @@ public function testInsertWithSelect(): void $query = $pdo->prepare( "INSERT INTO `video_game_characters` - (`id`, `name`, `type`, `profession`, `console`, `is_alive`, `powerups`, `skills`, `created_on`) + (`name`, `type`, `profession`, `console`, `is_alive`, `powerups`, `skills`, `created_on`) SELECT - 19+`id`, 'wario','villain','plumber','nes','1','3','{\"magic\":0, \"speed\":0, \"strength\":0, \"weapons\":0}', NOW(), + '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` @@ -937,7 +939,7 @@ public function testInsertWithSelect(): void $this->assertSame( [ - ['id' => 35, 'name' => 'wario'], + ['id' => 32, 'name' => 'wario'], ], $query->fetchAll(PDO::FETCH_ASSOC) );