@@ -100,6 +100,10 @@ public static function evaluate(
100100 return self ::sqlCeiling ($ conn , $ scope , $ expr , $ row , $ result );
101101 case 'FLOOR ' :
102102 return self ::sqlFloor ($ conn , $ scope , $ expr , $ row , $ result );
103+ case 'CONVERT_TZ ' :
104+ return self ::sqlConvertTz ($ conn , $ scope , $ expr , $ row , $ result );
105+ case 'TIMESTAMPDIFF ' :
106+ return self ::sqlTimestampdiff ($ conn , $ scope , $ expr , $ row , $ result );
103107 case 'DATEDIFF ' :
104108 return self ::sqlDateDiff ($ conn , $ scope , $ expr , $ row , $ result );
105109 case 'DAY ' :
@@ -115,6 +119,8 @@ public static function evaluate(
115119 return self ::sqlInetAton ($ conn , $ scope , $ expr , $ row , $ result );
116120 case 'INET_NTOA ' :
117121 return self ::sqlInetNtoa ($ conn , $ scope , $ expr , $ row , $ result );
122+ case 'LEAST ' :
123+ return self ::sqlLeast ($ conn , $ scope , $ expr , $ row , $ result );
118124 }
119125
120126 throw new ProcessorException ("Function " . $ expr ->functionName . " not implemented yet " );
@@ -348,9 +354,13 @@ private static function sqlCount(
348354 }
349355
350356 /**
351- * @param array<string, Column> $columns
357+ * @param FakePdoInterface $conn
358+ * @param Scope $scope
359+ * @param FunctionExpression $expr
360+ * @param QueryResult $result
352361 *
353- * @return ?numeric
362+ * @return float|int|mixed|string|null
363+ * @throws ProcessorException
354364 */
355365 private static function sqlSum (
356366 FakePdoInterface $ conn ,
@@ -363,6 +373,11 @@ private static function sqlSum(
363373 $ sum = 0 ;
364374
365375 if (!$ result ->rows ) {
376+ $ isQueryWithoutFromClause = empty ($ result ->columns );
377+ if ($ expr instanceof FunctionExpression && $ isQueryWithoutFromClause ) {
378+ return self ::evaluate ($ conn , $ scope , $ expr , [], $ result );
379+ }
380+
366381 return null ;
367382 }
368383
@@ -436,14 +451,20 @@ private static function sqlMin(
436451
437452 $ value = Evaluator::evaluate ($ conn , $ scope , $ expr , $ row , $ result );
438453
439- if (!\is_scalar ($ value )) {
454+ if (!\is_scalar ($ value ) && ! \is_null ( $ value ) ) {
440455 throw new \TypeError ('Bad min value ' );
441456 }
442457
443458 $ values [] = $ value ;
444459 }
445460
446- return self ::castAggregate (\min ($ values ), $ expr , $ result );
461+ $ min_value = \min ($ values );
462+
463+ if ($ min_value === null ) {
464+ return null ;
465+ }
466+
467+ return self ::castAggregate ($ min_value , $ expr , $ result );
447468 }
448469
449470 /**
@@ -471,14 +492,20 @@ private static function sqlMax(
471492
472493 $ value = Evaluator::evaluate ($ conn , $ scope , $ expr , $ row , $ result );
473494
474- if (!\is_scalar ($ value )) {
495+ if (!\is_scalar ($ value ) && ! \is_null ( $ value ) ) {
475496 throw new \TypeError ('Bad max value ' );
476497 }
477498
478499 $ values [] = $ value ;
479500 }
480501
481- return self ::castAggregate (\max ($ values ), $ expr , $ result );
502+ $ max_value = \max ($ values );
503+
504+ if ($ max_value === null ) {
505+ return null ;
506+ }
507+
508+ return self ::castAggregate ($ max_value , $ expr , $ result );
482509 }
483510
484511 /**
@@ -1575,4 +1602,181 @@ private static function getPhpIntervalFromExpression(
15751602 throw new ProcessorException ('MySQL INTERVAL unit ' . $ expr ->unit . ' not supported yet ' );
15761603 }
15771604 }
1605+
1606+ /**
1607+ * @param FakePdoInterface $conn
1608+ * @param Scope $scope
1609+ * @param FunctionExpression $expr
1610+ * @param array<string, mixed> $row
1611+ * @param QueryResult $result
1612+ *
1613+ * @return string|null
1614+ * @throws ProcessorException
1615+ */
1616+ private static function sqlConvertTz (
1617+ FakePdoInterface $ conn ,
1618+ Scope $ scope ,
1619+ FunctionExpression $ expr ,
1620+ array $ row ,
1621+ QueryResult $ result )
1622+ {
1623+ $ args = $ expr ->args ;
1624+
1625+ if (count ($ args ) !== 3 ) {
1626+ throw new \InvalidArgumentException ("CONVERT_TZ() requires exactly 3 arguments " );
1627+ }
1628+
1629+ if ($ args [0 ] instanceof ColumnExpression && empty ($ row )) {
1630+ return null ;
1631+ }
1632+
1633+ /** @var string|null $dtValue */
1634+ $ dtValue = Evaluator::evaluate ($ conn , $ scope , $ args [0 ], $ row , $ result );
1635+ /** @var string|null $fromTzValue */
1636+ $ fromTzValue = Evaluator::evaluate ($ conn , $ scope , $ args [1 ], $ row , $ result );
1637+ /** @var string|null $toTzValue */
1638+ $ toTzValue = Evaluator::evaluate ($ conn , $ scope , $ args [2 ], $ row , $ result );
1639+
1640+ if ($ dtValue === null || $ fromTzValue === null || $ toTzValue === null ) {
1641+ return null ;
1642+ }
1643+
1644+ try {
1645+ $ dt = new \DateTime ($ dtValue , new \DateTimeZone ($ fromTzValue ));
1646+ $ dt ->setTimezone (new \DateTimeZone ($ toTzValue ));
1647+ return $ dt ->format ('Y-m-d H:i:s ' );
1648+ } catch (\Exception $ e ) {
1649+ return null ;
1650+ }
1651+ }
1652+
1653+ /**
1654+ * @param FakePdoInterface $conn
1655+ * @param Scope $scope
1656+ * @param FunctionExpression $expr
1657+ * @param array<string, mixed> $row
1658+ * @param QueryResult $result
1659+ *
1660+ * @return int
1661+ * @throws ProcessorException
1662+ */
1663+ private static function sqlTimestampdiff (
1664+ FakePdoInterface $ conn ,
1665+ Scope $ scope ,
1666+ FunctionExpression $ expr ,
1667+ array $ row ,
1668+ QueryResult $ result
1669+ ) {
1670+ $ args = $ expr ->args ;
1671+
1672+ if (\count ($ args ) !== 3 ) {
1673+ throw new ProcessorException ("MySQL TIMESTAMPDIFF() function must be called with three arguments " );
1674+ }
1675+
1676+ if (!$ args [0 ] instanceof ColumnExpression) {
1677+ throw new ProcessorException ("MySQL TIMESTAMPDIFF() function should be called with a unit for interval " );
1678+ }
1679+
1680+ /** @var string|null $unit */
1681+ $ unit = $ args [0 ]->columnExpression ;
1682+ /** @var string|int|float|null $start */
1683+ $ start = Evaluator::evaluate ($ conn , $ scope , $ args [1 ], $ row , $ result );
1684+ /** @var string|int|float|null $end */
1685+ $ end = Evaluator::evaluate ($ conn , $ scope , $ args [2 ], $ row , $ result );
1686+
1687+ try {
1688+ $ dtStart = new \DateTime ((string ) $ start );
1689+ $ dtEnd = new \DateTime ((string ) $ end );
1690+ } catch (\Exception $ e ) {
1691+ throw new ProcessorException ("Invalid datetime value passed to TIMESTAMPDIFF() " );
1692+ }
1693+
1694+ $ interval = $ dtStart ->diff ($ dtEnd );
1695+
1696+ // Calculate difference in seconds for fine-grained units
1697+ $ seconds = $ dtEnd ->getTimestamp () - $ dtStart ->getTimestamp ();
1698+
1699+ switch (strtoupper ((string )$ unit )) {
1700+ case 'MICROSECOND ' :
1701+ return $ seconds * 1000000 ;
1702+ case 'SECOND ' :
1703+ return $ seconds ;
1704+ case 'MINUTE ' :
1705+ return (int ) floor ($ seconds / 60 );
1706+ case 'HOUR ' :
1707+ return (int ) floor ($ seconds / 3600 );
1708+ case 'DAY ' :
1709+ return (int ) $ interval ->days * ($ seconds < 0 ? -1 : 1 );
1710+ case 'WEEK ' :
1711+ return (int ) floor ($ interval ->days / 7 ) * ($ seconds < 0 ? -1 : 1 );
1712+ case 'MONTH ' :
1713+ return ($ interval ->y * 12 + $ interval ->m ) * ($ seconds < 0 ? -1 : 1 );
1714+ case 'QUARTER ' :
1715+ $ months = $ interval ->y * 12 + $ interval ->m ;
1716+ return (int ) floor ($ months / 3 ) * ($ seconds < 0 ? -1 : 1 );
1717+ case 'YEAR ' :
1718+ return $ interval ->y * ($ seconds < 0 ? -1 : 1 );
1719+ default :
1720+ throw new ProcessorException ("Unsupported unit ' $ unit' in TIMESTAMPDIFF() " );
1721+ }
1722+ }
1723+
1724+ /**
1725+ * @param FakePdoInterface $conn
1726+ * @param Scope $scope
1727+ * @param FunctionExpression $expr
1728+ * @param array<string, mixed> $row
1729+ * @param QueryResult $result
1730+ *
1731+ * @return mixed|null
1732+ * @throws ProcessorException
1733+ */
1734+ private static function sqlLeast (
1735+ FakePdoInterface $ conn ,
1736+ Scope $ scope ,
1737+ FunctionExpression $ expr ,
1738+ array $ row ,
1739+ QueryResult $ result
1740+ )
1741+ {
1742+ $ args = $ expr ->args ;
1743+
1744+ if (\count ($ args ) < 2 ) {
1745+ throw new ProcessorException ("Incorrect parameter count in the call to native function 'LEAST' " );
1746+ }
1747+
1748+ $ is_any_float = false ;
1749+ $ is_any_string = false ;
1750+ $ precision = 0 ;
1751+ $ evaluated_args = [];
1752+
1753+ foreach ($ args as $ arg ) {
1754+ /** @var string|int|float|null $evaluated_arg */
1755+ $ evaluated_arg = Evaluator::evaluate ($ conn , $ scope , $ arg , $ row , $ result );
1756+ if (is_null ($ evaluated_arg )) {
1757+ return null ;
1758+ }
1759+
1760+ if (is_float ($ evaluated_arg )) {
1761+ $ is_any_float = true ;
1762+ $ precision = max ($ precision , strlen (substr (strrchr ((string ) $ evaluated_arg , ". " ), 1 )));
1763+ }
1764+
1765+ $ is_any_string = $ is_any_string || is_string ($ evaluated_arg );
1766+ $ evaluated_args [] = $ evaluated_arg ;
1767+ }
1768+
1769+ if ($ is_any_string ) {
1770+ $ evaluated_str_args = array_map (function ($ arg ) {
1771+ return (string ) $ arg ;
1772+ }, $ evaluated_args );
1773+ return min ($ evaluated_str_args );
1774+ }
1775+
1776+ if ($ is_any_float ) {
1777+ return number_format ((float ) min ($ evaluated_args ), $ precision );
1778+ }
1779+
1780+ return min ($ evaluated_args );
1781+ }
15781782}
0 commit comments