Skip to content

Commit 424f1df

Browse files
authored
Merge branch 'vimeo:master' into javakky/insert-select
2 parents 754176f + ba50273 commit 424f1df

File tree

9 files changed

+823
-31
lines changed

9 files changed

+823
-31
lines changed

.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
@@ -152,7 +152,7 @@ public static function evaluate(
152152
return !$expr->negatedInt;
153153
}
154154

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

157157
case '<>':
158158
case '!=':
@@ -165,35 +165,35 @@ public static function evaluate(
165165
return $expr->negatedInt;
166166
}
167167

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

170170
case '>':
171171
if ($as_string) {
172-
return (string) $l_value > (string) $r_value ? 1 : 0 ^ $expr->negatedInt;
172+
return ((string) $l_value > (string) $r_value ? 1 : 0) ^ $expr->negatedInt;
173173
}
174174

175-
return (float) $l_value > (float) $r_value ? 1 : 0 ^ $expr->negatedInt;
175+
return ((float) $l_value > (float) $r_value ? 1 : 0 ) ^ $expr->negatedInt;
176176
// no break
177177
case '>=':
178178
if ($as_string) {
179-
return (string) $l_value >= (string) $r_value ? 1 : 0 ^ $expr->negatedInt;
179+
return ((string) $l_value >= (string) $r_value ? 1 : 0) ^ $expr->negatedInt;
180180
}
181181

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

184184
case '<':
185185
if ($as_string) {
186-
return (string) $l_value < (string) $r_value ? 1 : 0 ^ $expr->negatedInt;
186+
return ((string) $l_value < (string) $r_value ? 1 : 0) ^ $expr->negatedInt;
187187
}
188188

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

191191
case '<=':
192192
if ($as_string) {
193-
return (string) $l_value <= (string) $r_value ? 1 : 0 ^ $expr->negatedInt;
193+
return ((string) $l_value <= (string) $r_value ? 1 : 0) ^ $expr->negatedInt;
194194
}
195195

196-
return (float) $l_value <= (float) $r_value ? 1 : 0 ^ $expr->negatedInt;
196+
return ((float) $l_value <= (float) $r_value ? 1 : 0) ^ $expr->negatedInt;
197197
}
198198

199199
// 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
@@ -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
}
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)