Skip to content

Commit 19a7d7a

Browse files
authored
Merge pull request #63 from tarasom/timestampdiff_support
feat: Add support for TIMESTAMPDIFF() function
2 parents 5a1241f + 675df9e commit 19a7d7a

File tree

2 files changed

+184
-1
lines changed

2 files changed

+184
-1
lines changed

src/Processor/Expression/FunctionEvaluator.php

Lines changed: 73 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,75 @@ 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
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+
try {
1586+
$dtStart = new \DateTime((string) $start);
1587+
$dtEnd = new \DateTime((string) $end);
1588+
} catch (\Exception $e) {
1589+
throw new ProcessorException("Invalid datetime value passed to TIMESTAMPDIFF()");
1590+
}
1591+
1592+
$interval = $dtStart->diff($dtEnd);
1593+
1594+
// Calculate difference in seconds for fine-grained units
1595+
$seconds = $dtEnd->getTimestamp() - $dtStart->getTimestamp();
1596+
1597+
switch (strtoupper((string)$unit)) {
1598+
case 'MICROSECOND':
1599+
return $seconds * 1000000;
1600+
case 'SECOND':
1601+
return $seconds;
1602+
case 'MINUTE':
1603+
return (int) floor($seconds / 60);
1604+
case 'HOUR':
1605+
return (int) floor($seconds / 3600);
1606+
case 'DAY':
1607+
return (int) $interval->days * ($seconds < 0 ? -1 : 1);
1608+
case 'WEEK':
1609+
return (int) floor($interval->days / 7) * ($seconds < 0 ? -1 : 1);
1610+
case 'MONTH':
1611+
return ($interval->y * 12 + $interval->m) * ($seconds < 0 ? -1 : 1);
1612+
case 'QUARTER':
1613+
$months = $interval->y * 12 + $interval->m;
1614+
return (int) floor($months / 3) * ($seconds < 0 ? -1 : 1);
1615+
case 'YEAR':
1616+
return $interval->y * ($seconds < 0 ? -1 : 1);
1617+
default:
1618+
throw new ProcessorException("Unsupported unit '$unit' in TIMESTAMPDIFF()");
1619+
}
1620+
}
15481621
}

tests/EndToEndTest.php

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
namespace Vimeo\MysqlEngine\Tests;
33

44
use PDOException;
5+
use Vimeo\MysqlEngine\Parser\Token;
6+
use Vimeo\MysqlEngine\Query\Expression\ColumnExpression;
7+
use Vimeo\MysqlEngine\TokenType;
58

69
class EndToEndTest extends \PHPUnit\Framework\TestCase
710
{
@@ -530,6 +533,113 @@ public function testDateArithhmetic()
530533
);
531534
}
532535

536+
/**
537+
* Test various timestamp differences using the TIMESTAMPDIFF function.
538+
*
539+
* This method verifies the calculation of differences in seconds, minutes,
540+
* hours, days, months, and years.
541+
*/
542+
public function testTimestampDiff(): void
543+
{
544+
// Get a PDO instance for MySQL.
545+
$pdo = self::getPdo('mysql:host=localhost;dbname=testdb');
546+
$pdo->setAttribute(\PDO::ATTR_EMULATE_PREPARES, false);
547+
548+
// Prepare a single query with multiple TIMESTAMPDIFF calls.
549+
$query = $pdo->prepare(
550+
'SELECT
551+
TIMESTAMPDIFF(SECOND, \'2020-01-01 00:00:00\', \'2020-01-01 00:01:40\') as `second_diff`,
552+
TIMESTAMPDIFF(MINUTE, \'2020-01-01 00:00:00\', \'2020-01-01 01:30:00\') as `minute_diff`,
553+
TIMESTAMPDIFF(HOUR, \'2020-01-02 00:00:00\', \'2020-01-01 00:00:00\') as `hour_diff`,
554+
TIMESTAMPDIFF(DAY, \'2020-01-01\', \'2020-01-10\') as `day_diff`,
555+
TIMESTAMPDIFF(MONTH, \'2019-01-01\', \'2020-04-01\') as `month_diff`,
556+
TIMESTAMPDIFF(YEAR, \'2010-05-15\', \'2020-05-15\') as `year_diff`'
557+
);
558+
559+
$query->execute();
560+
561+
$results = $query->fetchAll(\PDO::FETCH_ASSOC);
562+
$castedResults = array_map(function($row) {
563+
return array_map('intval', $row);
564+
}, $results);
565+
566+
$this->assertSame(
567+
[[
568+
'second_diff' => 100,
569+
'minute_diff' => 90,
570+
'hour_diff' => -24,
571+
'day_diff' => 9,
572+
'month_diff' => 15,
573+
'year_diff' => 10,
574+
]],
575+
$castedResults
576+
);
577+
}
578+
579+
public function testTimestampDiffThrowsExceptionWithWrongArgumentCount(): void
580+
{
581+
$this->expectException(\UnexpectedValueException::class);
582+
$this->expectExceptionMessage('MySQL TIMESTAMPDIFF() function must be called with three arguments');
583+
584+
$pdo = self::getPdo('mysql:host=localhost;dbname=testdb');
585+
$pdo->setAttribute(\PDO::ATTR_EMULATE_PREPARES, false);
586+
587+
$query = $pdo->prepare(
588+
'SELECT
589+
TIMESTAMPDIFF(SECOND, \'2020-01-01 00:00:00\', \'2020-01-01 00:01:40\', \'2020-01-01 00:01:40\')',
590+
);
591+
592+
$query->execute();
593+
}
594+
595+
public function testTimestampDiffThrowsExceptionIfFirstArgNotColumnExpression(): void
596+
{
597+
$this->expectException(\UnexpectedValueException::class);
598+
$this->expectExceptionMessage('MySQL TIMESTAMPDIFF() function should be called with a unit for interval');
599+
600+
$pdo = self::getPdo('mysql:host=localhost;dbname=testdb');
601+
$pdo->setAttribute(\PDO::ATTR_EMULATE_PREPARES, false);
602+
603+
$query = $pdo->prepare(
604+
'SELECT
605+
TIMESTAMPDIFF(\'2020-01-01 00:00:00\', \'2020-01-01 00:01:40\', \'2020-01-01 00:01:40\')',
606+
);
607+
608+
$query->execute();
609+
}
610+
611+
public function testTimestampDiffThrowsExceptionWithWrongDates(): void
612+
{
613+
$this->expectException(\UnexpectedValueException::class);
614+
$this->expectExceptionMessage('Invalid datetime value passed to TIMESTAMPDIFF()');
615+
616+
$pdo = self::getPdo('mysql:host=localhost;dbname=testdb');
617+
$pdo->setAttribute(\PDO::ATTR_EMULATE_PREPARES, false);
618+
619+
$query = $pdo->prepare(
620+
'SELECT
621+
TIMESTAMPDIFF(SECOND, \'2020-01-01 00:0140\', \'2020-01-01 00:01:40\')',
622+
);
623+
624+
$query->execute();
625+
}
626+
627+
public function testTimestampDiffThrowsExceptionWithWrongInterval(): void
628+
{
629+
$this->expectException(\UnexpectedValueException::class);
630+
$this->expectExceptionMessage('Unsupported unit \'CENTURY\' in TIMESTAMPDIFF()');
631+
632+
$pdo = self::getPdo('mysql:host=localhost;dbname=testdb');
633+
$pdo->setAttribute(\PDO::ATTR_EMULATE_PREPARES, false);
634+
635+
$query = $pdo->prepare(
636+
'SELECT
637+
TIMESTAMPDIFF(CENTURY, \'2020-01-01 00:01:40\', \'2020-01-01 00:01:40\')',
638+
);
639+
640+
$query->execute();
641+
}
642+
533643
public function testCurDateFunction()
534644
{
535645
$pdo = self::getPdo('mysql:foo');
@@ -1221,7 +1331,7 @@ public function testUpdate()
12211331
$query->execute();
12221332
$this->assertSame([['type' => 'villain']], $query->fetchAll(\PDO::FETCH_ASSOC));
12231333
}
1224-
1334+
12251335
public function testNegateOperationWithAnd()
12261336
{
12271337
// greater than

0 commit comments

Comments
 (0)