Skip to content

Commit aede233

Browse files
tarasomTaras Omelianchuk
authored andcommitted
feat: Add support for CONVERT_TZ() function
1 parent 226e0fe commit aede233

File tree

2 files changed

+98
-0
lines changed

2 files changed

+98
-0
lines changed

src/Processor/Expression/FunctionEvaluator.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ public static function evaluate(
9999
return self::sqlCeiling($conn, $scope, $expr, $row, $result);
100100
case 'FLOOR':
101101
return self::sqlFloor($conn, $scope, $expr, $row, $result);
102+
case 'CONVERT_TZ':
103+
return self::sqlConvertTz($conn, $scope, $expr, $row, $result);
102104
case 'TIMESTAMPDIFF':
103105
return self::sqlTimestampdiff($conn, $scope, $expr, $row, $result);
104106
case 'DATEDIFF':
@@ -1548,6 +1550,49 @@ private static function getPhpIntervalFromExpression(
15481550
}
15491551
}
15501552

1553+
/**
1554+
* @param FakePdoInterface $conn
1555+
* @param Scope $scope
1556+
* @param FunctionExpression $expr
1557+
* @param array<string, mixed> $row
1558+
* @param QueryResult $result
1559+
*
1560+
* @return string|null
1561+
* @throws ProcessorException
1562+
*/
1563+
private static function sqlConvertTz(
1564+
FakePdoInterface $conn,
1565+
Scope $scope,
1566+
FunctionExpression $expr,
1567+
array $row,
1568+
QueryResult $result)
1569+
{
1570+
$args = $expr->args;
1571+
1572+
if (count($args) !== 3) {
1573+
throw new \InvalidArgumentException("CONVERT_TZ() requires exactly 3 arguments");
1574+
}
1575+
1576+
/** @var string|null $dtValue */
1577+
$dtValue = Evaluator::evaluate($conn, $scope, $args[0], $row, $result);
1578+
/** @var string|null $fromTzValue */
1579+
$fromTzValue = Evaluator::evaluate($conn, $scope, $args[1], $row, $result);
1580+
/** @var string|null $toTzValue */
1581+
$toTzValue = Evaluator::evaluate($conn, $scope, $args[2], $row, $result);
1582+
1583+
if ($dtValue === null || $fromTzValue === null || $toTzValue === null) {
1584+
return null;
1585+
}
1586+
1587+
try {
1588+
$dt = new \DateTime($dtValue, new \DateTimeZone($fromTzValue));
1589+
$dt->setTimezone(new \DateTimeZone($toTzValue));
1590+
return $dt->format('Y-m-d H:i:s');
1591+
} catch (\Exception $e) {
1592+
throw new \InvalidArgumentException("CONVERT_TZ() failed: " . $e->getMessage(), 0, $e);
1593+
}
1594+
}
1595+
15511596
/**
15521597
* @param FakePdoInterface $conn
15531598
* @param Scope $scope

tests/FunctionEvaluatorTest.php

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,59 @@ private static function getPdo(string $connection_string, bool $strict_mode = fa
103103
return new \Vimeo\MysqlEngine\Php7\FakePdo($connection_string, '', '', $options);
104104
}
105105

106+
/**
107+
* @dataProvider convertTzProvider
108+
*/
109+
public function testConvertTz(string $sql, ?string $expected)
110+
{
111+
$query = self::getConnectionToFullDB()->prepare($sql);
112+
$query->execute();
113+
114+
$this->assertSame($expected, $query->fetch(\PDO::FETCH_COLUMN));
115+
}
116+
117+
public function testConvertTzThrowsOnInvalidTimezone()
118+
{
119+
$this->expectException(\InvalidArgumentException::class);
120+
$this->expectExceptionMessage('CONVERT_TZ() failed: DateTimeZone::__construct(): Unknown or bad timezone (Invalid/Zone)');
121+
122+
$sql = "SELECT CONVERT_TZ('2025-09-23 02:30:00', 'Invalid/Zone', 'UTC');";
123+
$query = self::getConnectionToFullDB()->prepare($sql);
124+
$query->execute();
125+
}
126+
127+
public function testConvertTzThrowsOnInvalidDatetime()
128+
{
129+
$this->expectException(\InvalidArgumentException::class);
130+
$this->expectExceptionMessage('CONVERT_TZ() failed: Failed to parse time string (not-a-date) at position 0 (n): The timezone could not be found in the database');
131+
132+
$sql = "SELECT CONVERT_TZ('not-a-date', 'UTC', 'UTC');";
133+
$query = self::getConnectionToFullDB()->prepare($sql);
134+
$query->execute();
135+
}
136+
137+
private static function convertTzProvider(): array
138+
{
139+
return [
140+
'normal conversion' => [
141+
'sql' => "SELECT CONVERT_TZ('2025-09-23 02:30:00', 'UTC', 'Europe/Kyiv');",
142+
'expected' => "2025-09-23 05:30:00",
143+
],
144+
'same tz' => [
145+
'sql' => "SELECT CONVERT_TZ('2025-12-31 23:59:59', 'Europe/Kyiv', 'Europe/Kyiv');",
146+
'expected' => "2025-12-31 23:59:59",
147+
],
148+
'crossing DST' => [
149+
'sql' => "SELECT CONVERT_TZ('2025-07-01 12:00:00', 'America/New_York', 'UTC');",
150+
'expected' => "2025-07-01 16:00:00",
151+
],
152+
'null date' => [
153+
'sql' => "SELECT CONVERT_TZ(NULL, 'UTC', 'Europe/Kyiv');",
154+
'expected' => null,
155+
]
156+
];
157+
}
158+
106159
private static function getConnectionToFullDB(bool $emulate_prepares = true, bool $strict_mode = false) : \PDO
107160
{
108161
$pdo = self::getPdo('mysql:foo;dbname=test;', $strict_mode);

0 commit comments

Comments
 (0)