Skip to content

Commit fa5883a

Browse files
authored
Merge branch 'vimeo:master' into javakky/distinct-group-concat
2 parents d165f3c + ba50273 commit fa5883a

9 files changed

Lines changed: 823 additions & 31 deletions

File tree

.github/workflows/phpunit.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ jobs:
1010
build:
1111

1212
runs-on: ubuntu-latest
13+
env:
14+
XDEBUG_MODE: off
1315

1416
steps:
1517
- uses: actions/checkout@v2

src/Processor/Expression/BinaryOperatorEvaluator.php

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ public static function evaluate(
154154
return !$expr->negatedInt;
155155
}
156156

157-
return $l_value == $r_value ? 1 : 0 ^ $expr->negatedInt;
157+
return ($l_value == $r_value ? 1 : 0 ) ^ $expr->negatedInt;
158158

159159
case '<>':
160160
case '!=':
@@ -167,35 +167,35 @@ public static function evaluate(
167167
return $expr->negatedInt;
168168
}
169169

170-
return $l_value != $r_value ? 1 : 0 ^ $expr->negatedInt;
170+
return ($l_value != $r_value ? 1 : 0) ^ $expr->negatedInt;
171171

172172
case '>':
173173
if ($as_string) {
174-
return (string) $l_value > (string) $r_value ? 1 : 0 ^ $expr->negatedInt;
174+
return ((string) $l_value > (string) $r_value ? 1 : 0) ^ $expr->negatedInt;
175175
}
176176

177-
return (float) $l_value > (float) $r_value ? 1 : 0 ^ $expr->negatedInt;
177+
return ((float) $l_value > (float) $r_value ? 1 : 0 ) ^ $expr->negatedInt;
178178
// no break
179179
case '>=':
180180
if ($as_string) {
181-
return (string) $l_value >= (string) $r_value ? 1 : 0 ^ $expr->negatedInt;
181+
return ((string) $l_value >= (string) $r_value ? 1 : 0) ^ $expr->negatedInt;
182182
}
183183

184-
return (float) $l_value >= (float) $r_value ? 1 : 0 ^ $expr->negatedInt;
184+
return ((float) $l_value >= (float) $r_value ? 1 : 0) ^ $expr->negatedInt;
185185

186186
case '<':
187187
if ($as_string) {
188-
return (string) $l_value < (string) $r_value ? 1 : 0 ^ $expr->negatedInt;
188+
return ((string) $l_value < (string) $r_value ? 1 : 0) ^ $expr->negatedInt;
189189
}
190190

191-
return (float) $l_value < (float) $r_value ? 1 : 0 ^ $expr->negatedInt;
191+
return ((float) $l_value < (float) $r_value ? 1 : 0) ^ $expr->negatedInt;
192192

193193
case '<=':
194194
if ($as_string) {
195-
return (string) $l_value <= (string) $r_value ? 1 : 0 ^ $expr->negatedInt;
195+
return ((string) $l_value <= (string) $r_value ? 1 : 0) ^ $expr->negatedInt;
196196
}
197197

198-
return (float) $l_value <= (float) $r_value ? 1 : 0 ^ $expr->negatedInt;
198+
return ((float) $l_value <= (float) $r_value ? 1 : 0) ^ $expr->negatedInt;
199199
}
200200

201201
// PHPCS thinks there's a fallthrough here, but there provably is not

src/Processor/Expression/FunctionEvaluator.php

Lines changed: 210 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -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
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,43 @@
11
<?php
2+
23
namespace Vimeo\MysqlEngine\Processor\Expression;
34

5+
use Vimeo\MysqlEngine\Processor\ProcessorException;
46
use Vimeo\MysqlEngine\Query\Expression\VariableExpression;
57
use Vimeo\MysqlEngine\Processor\Scope;
68

79
final class VariableEvaluator
810
{
911
/**
1012
* @return mixed
13+
* @throws ProcessorException
1114
*/
1215
public static function evaluate(Scope $scope, VariableExpression $expr)
1316
{
17+
if (strpos($expr->variableName, '@') === 0) {
18+
return self::getSystemVariable(substr($expr->variableName, 1));
19+
}
20+
1421
if (\array_key_exists($expr->variableName, $scope->variables)) {
1522
return $scope->variables[$expr->variableName];
1623
}
1724

1825
return null;
1926
}
27+
28+
/**
29+
* @param string $variableName
30+
*
31+
* @return string
32+
* @throws ProcessorException
33+
*/
34+
private static function getSystemVariable(string $variableName): string
35+
{
36+
switch ($variableName) {
37+
case 'session.time_zone':
38+
return date_default_timezone_get();
39+
default:
40+
throw new ProcessorException("System variable $variableName is not supported yet!");
41+
}
42+
}
2043
}

src/Processor/Processor.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ protected static function applyLimit(?LimitClause $limit, Scope $scope, QueryRes
125125
}
126126

127127
return new QueryResult(
128-
\array_slice($result->rows, $offset, $rowcount),
128+
\array_slice($result->rows, $offset, $rowcount, true),
129129
$result->columns
130130
);
131131
}

0 commit comments

Comments
 (0)