Skip to content

Commit 29be457

Browse files
author
Taras Omelianchuk
committed
feat: Add support for TIMESTAMPDIFF() function
1 parent 5a1241f commit 29be457

File tree

2 files changed

+121
-1
lines changed

2 files changed

+121
-1
lines changed

src/Processor/Expression/FunctionEvaluator.php

Lines changed: 77 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 'TIMESTAMPDIFF':
103+
return self::sqlTimestampdiff($conn, $scope, $expr, $row, $result);
102104
case 'DATEDIFF':
103105
return self::sqlDateDiff($conn, $scope, $expr, $row, $result);
104106
case 'DAY':
@@ -1545,4 +1547,79 @@ private static function getPhpIntervalFromExpression(
15451547
throw new ProcessorException('MySQL INTERVAL unit ' . $expr->unit . ' not supported yet');
15461548
}
15471549
}
1550+
1551+
/**
1552+
* @param FakePdoInterface $conn
1553+
* @param Scope $scope
1554+
* @param FunctionExpression $expr
1555+
* @param array<string, mixed> $row
1556+
* @param QueryResult $result
1557+
*
1558+
* @return int|null
1559+
* @throws ProcessorException
1560+
*/
1561+
private static function sqlTimestampdiff(
1562+
FakePdoInterface $conn,
1563+
Scope $scope,
1564+
FunctionExpression $expr,
1565+
array $row,
1566+
QueryResult $result
1567+
) {
1568+
$args = $expr->args;
1569+
1570+
if (\count($args) !== 3) {
1571+
throw new ProcessorException("MySQL TIMESTAMPDIFF() function must be called with three arguments");
1572+
}
1573+
1574+
if (!$args[0] instanceof ColumnExpression) {
1575+
throw new ProcessorException("MySQL TIMESTAMPDIFF() function should be called with a unit for interval");
1576+
}
1577+
1578+
/** @var string|null $unit */
1579+
$unit = $args[0]->columnExpression;
1580+
/** @var string|int|float|null $start */
1581+
$start = Evaluator::evaluate($conn, $scope, $args[1], $row, $result);
1582+
/** @var string|int|float|null $end */
1583+
$end = Evaluator::evaluate($conn, $scope, $args[2], $row, $result);
1584+
1585+
if ($start === null || $end === null || $unit === null) {
1586+
return null;
1587+
}
1588+
1589+
try {
1590+
$dtStart = new \DateTime((string) $start);
1591+
$dtEnd = new \DateTime((string) $end);
1592+
} catch (\Exception $e) {
1593+
throw new ProcessorException("Invalid datetime value passed to TIMESTAMPDIFF()");
1594+
}
1595+
1596+
$interval = $dtStart->diff($dtEnd);
1597+
1598+
// Calculate difference in seconds for fine-grained units
1599+
$seconds = $dtEnd->getTimestamp() - $dtStart->getTimestamp();
1600+
1601+
switch (strtoupper($unit)) {
1602+
case 'MICROSECOND':
1603+
return $seconds * 1000000;
1604+
case 'SECOND':
1605+
return $seconds;
1606+
case 'MINUTE':
1607+
return (int) floor($seconds / 60);
1608+
case 'HOUR':
1609+
return (int) floor($seconds / 3600);
1610+
case 'DAY':
1611+
return (int) $interval->days * ($seconds < 0 ? -1 : 1);
1612+
case 'WEEK':
1613+
return (int) floor($interval->days / 7) * ($seconds < 0 ? -1 : 1);
1614+
case 'MONTH':
1615+
return ($interval->y * 12 + $interval->m) * ($seconds < 0 ? -1 : 1);
1616+
case 'QUARTER':
1617+
$months = $interval->y * 12 + $interval->m;
1618+
return (int) floor($months / 3) * ($seconds < 0 ? -1 : 1);
1619+
case 'YEAR':
1620+
return $interval->y * ($seconds < 0 ? -1 : 1);
1621+
default:
1622+
throw new ProcessorException("Unsupported unit '$unit' in TIMESTAMPDIFF()");
1623+
}
1624+
}
15481625
}

tests/EndToEndTest.php

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -530,6 +530,49 @@ public function testDateArithhmetic()
530530
);
531531
}
532532

533+
/**
534+
* Test various timestamp differences using the TIMESTAMPDIFF function.
535+
*
536+
* This method verifies the calculation of differences in seconds, minutes,
537+
* hours, days, months, and years.
538+
*/
539+
public function testTimestampDiff(): void
540+
{
541+
// Get a PDO instance for MySQL.
542+
$pdo = self::getPdo('mysql:host=localhost;dbname=testdb');
543+
$pdo->setAttribute(\PDO::ATTR_EMULATE_PREPARES, false);
544+
545+
// Prepare a single query with multiple TIMESTAMPDIFF calls.
546+
$query = $pdo->prepare(
547+
'SELECT
548+
TIMESTAMPDIFF(SECOND, \'2020-01-01 00:00:00\', \'2020-01-01 00:01:40\') as `second_diff`,
549+
TIMESTAMPDIFF(MINUTE, \'2020-01-01 00:00:00\', \'2020-01-01 01:30:00\') as `minute_diff`,
550+
TIMESTAMPDIFF(HOUR, \'2020-01-02 00:00:00\', \'2020-01-01 00:00:00\') as `hour_diff`,
551+
TIMESTAMPDIFF(DAY, \'2020-01-01\', \'2020-01-10\') as `day_diff`,
552+
TIMESTAMPDIFF(MONTH, \'2019-01-01\', \'2020-04-01\') as `month_diff`,
553+
TIMESTAMPDIFF(YEAR, \'2010-05-15\', \'2020-05-15\') as `year_diff`'
554+
);
555+
556+
$query->execute();
557+
558+
$results = $query->fetchAll(\PDO::FETCH_ASSOC);
559+
$castedResults = array_map(function($row) {
560+
return array_map('intval', $row);
561+
}, $results);
562+
563+
$this->assertSame(
564+
[[
565+
'second_diff' => 100,
566+
'minute_diff' => 90,
567+
'hour_diff' => -24,
568+
'day_diff' => 9,
569+
'month_diff' => 15,
570+
'year_diff' => 10,
571+
]],
572+
$castedResults
573+
);
574+
}
575+
533576
public function testCurDateFunction()
534577
{
535578
$pdo = self::getPdo('mysql:foo');
@@ -1221,7 +1264,7 @@ public function testUpdate()
12211264
$query->execute();
12221265
$this->assertSame([['type' => 'villain']], $query->fetchAll(\PDO::FETCH_ASSOC));
12231266
}
1224-
1267+
12251268
public function testNegateOperationWithAnd()
12261269
{
12271270
// greater than

0 commit comments

Comments
 (0)