From 078dee819530a4236d0833d31fcc03f817474b71 Mon Sep 17 00:00:00 2001 From: yuha-yuha Date: Thu, 13 Mar 2025 17:37:14 +0900 Subject: [PATCH 1/2] feat: add str_to_datetime function --- .../Expression/FunctionEvaluator.php | 74 +++++++++++++++++++ tests/EndToEndTest.php | 32 ++++++++ tests/SelectParseTest.php | 13 ++++ tests/fixtures/bulk_character_insert.sql | 2 +- 4 files changed, 120 insertions(+), 1 deletion(-) diff --git a/src/Processor/Expression/FunctionEvaluator.php b/src/Processor/Expression/FunctionEvaluator.php index 8294c504..c6f995bb 100644 --- a/src/Processor/Expression/FunctionEvaluator.php +++ b/src/Processor/Expression/FunctionEvaluator.php @@ -114,6 +114,8 @@ public static function evaluate( return self::sqlInetAton($conn, $scope, $expr, $row, $result); case 'INET_NTOA': return self::sqlInetNtoa($conn, $scope, $expr, $row, $result); + case 'STR_TO_DATE': + return self::sqlStrToDate($conn, $scope, $expr, $row, $result); } throw new ProcessorException("Function " . $expr->functionName . " not implemented yet"); @@ -1533,4 +1535,76 @@ private static function getPhpIntervalFromExpression( throw new ProcessorException('MySQL INTERVAL unit ' . $expr->unit . ' not supported yet'); } } + + /** + * @param array $row + * @return string|null + */ + private static function sqlStrToDate( + FakePdoInterface $conn, + Scope $scope, + FunctionExpression $expr, + array $row, + QueryResult $result + ) : ?string { + $args = $expr->args; + + if (\count($args) !== 2) { + throw new ProcessorException("MySQL DATE_FORMAT() function must be called with one argument"); + } + + $subject = (string) Evaluator::evaluate($conn, $scope, $args[0], $row, $result); + $format = (string) Evaluator::evaluate($conn, $scope, $args[1], $row, $result); + + if (strpos($format, '%') === false) { + return null; + } + + $date_format_list = [ + "%b" => "M", "%c" => "n", "%d" => "d", "%D" => "jS", "%e" => "j", + "%m" => "m", "%M" => "F", "%y" => "y", "%Y" => "Y" + ]; + + $time_format_list = [ + "%h" => "h", "%H" => "H", "%i" => "i", "%I" => "h", "%k" => "G", + "%l" => "g", "%r" => "h:i:s A", "%s" => "s", "%S" => "s", "%T" => "H:i:s" + ]; + + $has_date_format = false; + $has_time_format = false; + preg_match_all("/(?:%[a-zA-Z])/u", $format, $matches); + foreach ($matches[0] as $match) { + $has_date_format = $has_date_format || in_array($match, array_keys($date_format_list)); + $has_time_format = $has_time_format || in_array($match, array_keys($time_format_list)); + } + + + $format = \str_replace( + array_keys($date_format_list + $time_format_list), + array_values($date_format_list + $time_format_list), + $format + ); + + if ($has_date_format && $has_time_format) { + $time = \DateTimeImmutable::createFromFormat($format, $subject); + if($time !== false) { + return $time->format('Y-m-d G:i:s'); + } + } + + if ($has_date_format) { + $time = \DateTimeImmutable::createFromFormat($format, $subject); + if($time !== false) { + return $time->format('Y-m-d'); + } + } + + if ($has_time_format) { + $time = \DateTimeImmutable::createFromFormat($format, $subject); + if($time !== false) { + return $time->format('G:i:s'); + } + } + return null; + } } diff --git a/tests/EndToEndTest.php b/tests/EndToEndTest.php index d2cd95a3..81cc4229 100644 --- a/tests/EndToEndTest.php +++ b/tests/EndToEndTest.php @@ -1242,4 +1242,36 @@ private static function getConnectionToFullDB(bool $emulate_prepares = true, boo return $pdo; } + + public function testStrToDateInSelectFunction() + { + $pdo = self::getConnectionToFullDB(false); + $query = $pdo->prepare("SELECT STR_TO_DATE('01,5,2013', '%d,%m,%Y') AS date"); + + $query->execute(); + + $d = mktime(0, 0, 0, 5, 1, 2013); + + $current_date = date('Y-m-d', $d); + + $this->assertSame( + [[ + 'date' => $current_date, + ]], + $query->fetchAll(\PDO::FETCH_ASSOC) + ); + } + + public function testStrToDateInWhereFunction() + { + $pdo = self::getConnectionToFullDB(false); + $query = $pdo->prepare("SELECT id FROM `video_game_characters` WHERE `created_on` = (STR_TO_DATE('26/3/2022', '%d/%m/%Y') - INTERVAL 2 MONTH)"); + + $query->execute(); + + $this->assertSame( + [['id' => 16,]], + $query->fetchAll(\PDO::FETCH_ASSOC) + ); + } } diff --git a/tests/SelectParseTest.php b/tests/SelectParseTest.php index e6f4e4a9..e2338db6 100644 --- a/tests/SelectParseTest.php +++ b/tests/SelectParseTest.php @@ -316,4 +316,17 @@ public function testBracketedFirstSelect() $select_query = \Vimeo\MysqlEngine\Parser\SQLParser::parse($sql); } + + public function testStrToDateFunction() + { + $sql = "SELECT STR_TO_DATE('01,5,2013', '%d,%m,%Y')"; + $select_query = \Vimeo\MysqlEngine\Parser\SQLParser::parse($sql); + $this->assertInstanceOf(SelectQuery::class, $select_query); + + $strToDateFunction = $select_query->selectExpressions[0]; + $this->assertTrue(isset($strToDateFunction->args[0])); + $this->assertTrue(isset($strToDateFunction->args[1])); + $this->assertEquals('01,5,2013', $strToDateFunction->args[0]->value); + $this->assertEquals('%d,%m,%Y', $strToDateFunction->args[1]->value); + } } diff --git a/tests/fixtures/bulk_character_insert.sql b/tests/fixtures/bulk_character_insert.sql index c5dec12c..1bede8a6 100644 --- a/tests/fixtures/bulk_character_insert.sql +++ b/tests/fixtures/bulk_character_insert.sql @@ -16,4 +16,4 @@ VALUES ('13','pac man', 'Ms Pac Man’s worse three-quarters', 'hero','yellow circle','atari','1','0','{"magic":0, "speed":0, "strength":0, "weapons":0}', NOW()), ('14','yoshi', 'Green machine', 'hero','dinosaur','super nintendo','1','0','{"magic":0, "speed":1, "strength":0, "weapons":0}', NOW()), ('15','link', 'Zelda? I hardly knew her!', 'hero','not sure','nes','1','0','{"magic":1, "speed":0, "strength":0, "weapons":1}', NOW()), - ('16','dude', 'Duuuuuude', 'hero','sure','sega genesis','1','0','{"magic":1, "speed":0, "strength":0, "weapons":1}', NOW()) + ('16','dude', 'Duuuuuude', 'hero','sure','sega genesis','1','0','{"magic":1, "speed":0, "strength":0, "weapons":1}', '2022-01-26 00:00:00') From 96c6c0cd29069c5a3811e33ac9073231c414fcd4 Mon Sep 17 00:00:00 2001 From: Aaron Marasco Date: Wed, 26 Mar 2025 15:40:06 -0400 Subject: [PATCH 2/2] update workflows to use actions/cache@v4 (#55) * Update phpunit.yml * use actions/cache@v4 --- .github/workflows/phpunit.yml | 2 +- .github/workflows/psalm.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index a1bbf1a6..b04aaa6d 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -23,7 +23,7 @@ jobs: echo "::set-output name=dir::$(composer config cache-files-dir)" - name: Cache Composer packages - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} diff --git a/.github/workflows/psalm.yml b/.github/workflows/psalm.yml index a7acc498..87311635 100644 --- a/.github/workflows/psalm.yml +++ b/.github/workflows/psalm.yml @@ -19,7 +19,7 @@ jobs: echo "::set-output name=dir::$(composer config cache-files-dir)" - name: Cache Composer packages - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}