@@ -99,6 +99,10 @@ 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 );
104+ case 'TIMESTAMPDIFF ' :
105+ return self ::sqlTimestampdiff ($ conn , $ scope , $ expr , $ row , $ result );
102106 case 'DATEDIFF ' :
103107 return self ::sqlDateDiff ($ conn , $ scope , $ expr , $ row , $ result );
104108 case 'DAY ' :
@@ -114,6 +118,8 @@ public static function evaluate(
114118 return self ::sqlInetAton ($ conn , $ scope , $ expr , $ row , $ result );
115119 case 'INET_NTOA ' :
116120 return self ::sqlInetNtoa ($ conn , $ scope , $ expr , $ row , $ result );
121+ case 'LEAST ' :
122+ return self ::sqlLeast ($ conn , $ scope , $ expr , $ row , $ result );
117123 }
118124
119125 throw new ProcessorException ("Function " . $ expr ->functionName . " not implemented yet " );
@@ -347,9 +353,13 @@ private static function sqlCount(
347353 }
348354
349355 /**
350- * @param array<string, Column> $columns
356+ * @param FakePdoInterface $conn
357+ * @param Scope $scope
358+ * @param FunctionExpression $expr
359+ * @param QueryResult $result
351360 *
352- * @return ?numeric
361+ * @return float|int|mixed|string|null
362+ * @throws ProcessorException
353363 */
354364 private static function sqlSum (
355365 FakePdoInterface $ conn ,
@@ -362,6 +372,11 @@ private static function sqlSum(
362372 $ sum = 0 ;
363373
364374 if (!$ result ->rows ) {
375+ $ isQueryWithoutFromClause = empty ($ result ->columns );
376+ if ($ expr instanceof FunctionExpression && $ isQueryWithoutFromClause ) {
377+ return self ::evaluate ($ conn , $ scope , $ expr , [], $ result );
378+ }
379+
365380 return null ;
366381 }
367382
@@ -435,14 +450,20 @@ private static function sqlMin(
435450
436451 $ value = Evaluator::evaluate ($ conn , $ scope , $ expr , $ row , $ result );
437452
438- if (!\is_scalar ($ value )) {
453+ if (!\is_scalar ($ value ) && ! \is_null ( $ value ) ) {
439454 throw new \TypeError ('Bad min value ' );
440455 }
441456
442457 $ values [] = $ value ;
443458 }
444459
445- return self ::castAggregate (\min ($ values ), $ expr , $ result );
460+ $ min_value = \min ($ values );
461+
462+ if ($ min_value === null ) {
463+ return null ;
464+ }
465+
466+ return self ::castAggregate ($ min_value , $ expr , $ result );
446467 }
447468
448469 /**
@@ -470,14 +491,20 @@ private static function sqlMax(
470491
471492 $ value = Evaluator::evaluate ($ conn , $ scope , $ expr , $ row , $ result );
472493
473- if (!\is_scalar ($ value )) {
494+ if (!\is_scalar ($ value ) && ! \is_null ( $ value ) ) {
474495 throw new \TypeError ('Bad max value ' );
475496 }
476497
477498 $ values [] = $ value ;
478499 }
479500
480- return self ::castAggregate (\max ($ values ), $ expr , $ result );
501+ $ max_value = \max ($ values );
502+
503+ if ($ max_value === null ) {
504+ return null ;
505+ }
506+
507+ return self ::castAggregate ($ max_value , $ expr , $ result );
481508 }
482509
483510 /**
@@ -1533,4 +1560,181 @@ private static function getPhpIntervalFromExpression(
15331560 throw new ProcessorException ('MySQL INTERVAL unit ' . $ expr ->unit . ' not supported yet ' );
15341561 }
15351562 }
1563+
1564+ /**
1565+ * @param FakePdoInterface $conn
1566+ * @param Scope $scope
1567+ * @param FunctionExpression $expr
1568+ * @param array<string, mixed> $row
1569+ * @param QueryResult $result
1570+ *
1571+ * @return string|null
1572+ * @throws ProcessorException
1573+ */
1574+ private static function sqlConvertTz (
1575+ FakePdoInterface $ conn ,
1576+ Scope $ scope ,
1577+ FunctionExpression $ expr ,
1578+ array $ row ,
1579+ QueryResult $ result )
1580+ {
1581+ $ args = $ expr ->args ;
1582+
1583+ if (count ($ args ) !== 3 ) {
1584+ throw new \InvalidArgumentException ("CONVERT_TZ() requires exactly 3 arguments " );
1585+ }
1586+
1587+ if ($ args [0 ] instanceof ColumnExpression && empty ($ row )) {
1588+ return null ;
1589+ }
1590+
1591+ /** @var string|null $dtValue */
1592+ $ dtValue = Evaluator::evaluate ($ conn , $ scope , $ args [0 ], $ row , $ result );
1593+ /** @var string|null $fromTzValue */
1594+ $ fromTzValue = Evaluator::evaluate ($ conn , $ scope , $ args [1 ], $ row , $ result );
1595+ /** @var string|null $toTzValue */
1596+ $ toTzValue = Evaluator::evaluate ($ conn , $ scope , $ args [2 ], $ row , $ result );
1597+
1598+ if ($ dtValue === null || $ fromTzValue === null || $ toTzValue === null ) {
1599+ return null ;
1600+ }
1601+
1602+ try {
1603+ $ dt = new \DateTime ($ dtValue , new \DateTimeZone ($ fromTzValue ));
1604+ $ dt ->setTimezone (new \DateTimeZone ($ toTzValue ));
1605+ return $ dt ->format ('Y-m-d H:i:s ' );
1606+ } catch (\Exception $ e ) {
1607+ return null ;
1608+ }
1609+ }
1610+
1611+ /**
1612+ * @param FakePdoInterface $conn
1613+ * @param Scope $scope
1614+ * @param FunctionExpression $expr
1615+ * @param array<string, mixed> $row
1616+ * @param QueryResult $result
1617+ *
1618+ * @return int
1619+ * @throws ProcessorException
1620+ */
1621+ private static function sqlTimestampdiff (
1622+ FakePdoInterface $ conn ,
1623+ Scope $ scope ,
1624+ FunctionExpression $ expr ,
1625+ array $ row ,
1626+ QueryResult $ result
1627+ ) {
1628+ $ args = $ expr ->args ;
1629+
1630+ if (\count ($ args ) !== 3 ) {
1631+ throw new ProcessorException ("MySQL TIMESTAMPDIFF() function must be called with three arguments " );
1632+ }
1633+
1634+ if (!$ args [0 ] instanceof ColumnExpression) {
1635+ throw new ProcessorException ("MySQL TIMESTAMPDIFF() function should be called with a unit for interval " );
1636+ }
1637+
1638+ /** @var string|null $unit */
1639+ $ unit = $ args [0 ]->columnExpression ;
1640+ /** @var string|int|float|null $start */
1641+ $ start = Evaluator::evaluate ($ conn , $ scope , $ args [1 ], $ row , $ result );
1642+ /** @var string|int|float|null $end */
1643+ $ end = Evaluator::evaluate ($ conn , $ scope , $ args [2 ], $ row , $ result );
1644+
1645+ try {
1646+ $ dtStart = new \DateTime ((string ) $ start );
1647+ $ dtEnd = new \DateTime ((string ) $ end );
1648+ } catch (\Exception $ e ) {
1649+ throw new ProcessorException ("Invalid datetime value passed to TIMESTAMPDIFF() " );
1650+ }
1651+
1652+ $ interval = $ dtStart ->diff ($ dtEnd );
1653+
1654+ // Calculate difference in seconds for fine-grained units
1655+ $ seconds = $ dtEnd ->getTimestamp () - $ dtStart ->getTimestamp ();
1656+
1657+ switch (strtoupper ((string )$ unit )) {
1658+ case 'MICROSECOND ' :
1659+ return $ seconds * 1000000 ;
1660+ case 'SECOND ' :
1661+ return $ seconds ;
1662+ case 'MINUTE ' :
1663+ return (int ) floor ($ seconds / 60 );
1664+ case 'HOUR ' :
1665+ return (int ) floor ($ seconds / 3600 );
1666+ case 'DAY ' :
1667+ return (int ) $ interval ->days * ($ seconds < 0 ? -1 : 1 );
1668+ case 'WEEK ' :
1669+ return (int ) floor ($ interval ->days / 7 ) * ($ seconds < 0 ? -1 : 1 );
1670+ case 'MONTH ' :
1671+ return ($ interval ->y * 12 + $ interval ->m ) * ($ seconds < 0 ? -1 : 1 );
1672+ case 'QUARTER ' :
1673+ $ months = $ interval ->y * 12 + $ interval ->m ;
1674+ return (int ) floor ($ months / 3 ) * ($ seconds < 0 ? -1 : 1 );
1675+ case 'YEAR ' :
1676+ return $ interval ->y * ($ seconds < 0 ? -1 : 1 );
1677+ default :
1678+ throw new ProcessorException ("Unsupported unit ' $ unit' in TIMESTAMPDIFF() " );
1679+ }
1680+ }
1681+
1682+ /**
1683+ * @param FakePdoInterface $conn
1684+ * @param Scope $scope
1685+ * @param FunctionExpression $expr
1686+ * @param array<string, mixed> $row
1687+ * @param QueryResult $result
1688+ *
1689+ * @return mixed|null
1690+ * @throws ProcessorException
1691+ */
1692+ private static function sqlLeast (
1693+ FakePdoInterface $ conn ,
1694+ Scope $ scope ,
1695+ FunctionExpression $ expr ,
1696+ array $ row ,
1697+ QueryResult $ result
1698+ )
1699+ {
1700+ $ args = $ expr ->args ;
1701+
1702+ if (\count ($ args ) < 2 ) {
1703+ throw new ProcessorException ("Incorrect parameter count in the call to native function 'LEAST' " );
1704+ }
1705+
1706+ $ is_any_float = false ;
1707+ $ is_any_string = false ;
1708+ $ precision = 0 ;
1709+ $ evaluated_args = [];
1710+
1711+ foreach ($ args as $ arg ) {
1712+ /** @var string|int|float|null $evaluated_arg */
1713+ $ evaluated_arg = Evaluator::evaluate ($ conn , $ scope , $ arg , $ row , $ result );
1714+ if (is_null ($ evaluated_arg )) {
1715+ return null ;
1716+ }
1717+
1718+ if (is_float ($ evaluated_arg )) {
1719+ $ is_any_float = true ;
1720+ $ precision = max ($ precision , strlen (substr (strrchr ((string ) $ evaluated_arg , ". " ), 1 )));
1721+ }
1722+
1723+ $ is_any_string = $ is_any_string || is_string ($ evaluated_arg );
1724+ $ evaluated_args [] = $ evaluated_arg ;
1725+ }
1726+
1727+ if ($ is_any_string ) {
1728+ $ evaluated_str_args = array_map (function ($ arg ) {
1729+ return (string ) $ arg ;
1730+ }, $ evaluated_args );
1731+ return min ($ evaluated_str_args );
1732+ }
1733+
1734+ if ($ is_any_float ) {
1735+ return number_format ((float ) min ($ evaluated_args ), $ precision );
1736+ }
1737+
1738+ return min ($ evaluated_args );
1739+ }
15361740}
0 commit comments