Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

impelement MAKETIME and TIMESTAMP function parser #52

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Currently there are two ways to test code that reads and writes to a database:

- Mock SQL query execution<br/>
Mocks require an explicit list of queries that are expected to run and results to return. This leads to significant manual work setting up expectations, and tests which are fragile and must be updated even on benign changes to the code or queries. It also means the data access layer is not unit tested.

- Use an actual database<br />
It might make sense to test with a separate database instance – this is what we have done in the past at Vimeo. But databases like MySQL are designed to be filled with lots of long-lasting data, whereas unit tests write small amounts of very short-lived data. This means that extra care has to be taken to make sure that test databases are truncated between tests, which creates a performance issue.

Expand Down Expand Up @@ -93,4 +93,4 @@ and also Psalm's checks
vendor/bin/psalm
```

Thanks!
Thanks!
2 changes: 1 addition & 1 deletion src/Parser/ExpressionParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ private function getListExpression(array $tokens)
$arg = $tokens[$pos];


if ($arg->value === ',') {
if ($arg->value === ',' && $arg->type == "Separator") {
if ($needs_comma) {
$needs_comma = false;
$pos++;
Expand Down
174 changes: 173 additions & 1 deletion src/Processor/Expression/FunctionEvaluator.php
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,10 @@ public static function evaluate(
return self::sqlInetAton($conn, $scope, $expr, $row, $result);
case 'INET_NTOA':
return self::sqlInetNtoa($conn, $scope, $expr, $row, $result);
case 'MAKETIME':
return self::sqlMAKETIME($conn, $scope, $expr, $row, $result);
case 'TIMESTAMP':
return self::sqlTimestamp($conn, $scope, $expr, $row, $result);
}

throw new ProcessorException("Function " . $expr->functionName . " not implemented yet");
Expand Down Expand Up @@ -1533,4 +1537,172 @@ private static function getPhpIntervalFromExpression(
throw new ProcessorException('MySQL INTERVAL unit ' . $expr->unit . ' not supported yet');
}
}
}


/**
* @param FakePdoInterface $conn
* @param Scope $scope
* @param FunctionExpression $expr
* @param array $row
* @param QueryResult $result
* @return mixed
* @throws ProcessorException
*/
private static function sqlMAKETIME(
FakePdoInterface $conn,
Scope $scope,
FunctionExpression $expr,
array $row,
QueryResult $result
): mixed
{

if (\count($expr->args) != 3) {
throw new ProcessorException("MySQL MAKETIME() function must be called with three argument");
}
$created_time = "";
foreach ($expr->args as $index => $arg) {
$value = Evaluator::evaluate($conn, $scope, $arg, $row, $result);
switch ($index) {
case 0:
if ($value > 839) {
throw new ProcessorException("Hour cannot be greater than 839");
}
break;
case 1:
case 2:
if ($value > 59) {
throw new ProcessorException("Minute or second cannot be greater than 59");
}
}

if ($value < 10) {
$value = "0" . $value;
}

$created_time .= $value . ":";
}

return self::castAggregate(\rtrim($created_time, ":"), $expr, $result);
}

/**
* @param FakePdoInterface $conn
* @param Scope $scope
* @param FunctionExpression $expr
* @param array $row
* @param QueryResult $result
* @return mixed
* @throws ProcessorException
*/
private static function sqlTimestamp(
FakePdoInterface $conn,
Scope $scope,
FunctionExpression $expr,
array $row,
QueryResult $result
): mixed
{

if (\count($expr->args) > 2 || \count($expr->args) < 1) {
throw new ProcessorException("MySQL TIMESTAMP() function must be called with maximum two argument");
}

$value = Evaluator::evaluate($conn, $scope, $expr->args[0], $row, $result);

$exploded_value = explode(" ", $value);

$date = $exploded_value[0];

$time_str = "00:00:00";

if (isset($exploded_value[1])) {
$time = explode(":", $exploded_value[1]);

if (count($time) < 1 || count($time) > 3) {
throw new ProcessorException("The given time format is incorrect !!!");
}

if ($time[0] < 10) {
$time[0] = "0" . $time[0];
}

if (isset($time[1])) {
if ($time[1] < 10) {
$time[1] = "0" . $time[1];
}
} else {
$time[1] = "00";
}

if (isset($time[2])) {
if ($time[2] < 10) {
$time[2] = "0" . $time[2];
}
} else {
$time[2] = "00";
}

$time_str = $time[0] . ":" . $time[1] . ":" . $time[2];
}

if (!$first_date = \DateTime::createFromFormat("Y-m-d H:i:s", $date . " " . $time_str)) {
throw new ProcessorException("The given datetime format is incorrect !!!");
};


if (isset($expr->args[1])) {
$value = Evaluator::evaluate($conn, $scope, $expr->args[1], $row, $result);
$given_time = explode(":", $value);

switch (count($given_time)) {
case 1:
$processed_time = \array_map("strrev", \str_split(\strrev($given_time[0]), 2));
$interval = new \DateInterval(
'PT' . (isset($processed_time[2]) ? $processed_time[2] : '0') . 'H' .
(isset($processed_time[1]) ? $processed_time[1] : '0') . 'M' .
$processed_time[0] . 'S'
);
break;
case 2:
case 3:
if ($given_time[0] < 10) {
$processed_time[0] = "0" . $given_time[0];
} else {
$processed_time[0] = $given_time[0];
}

if (isset($given_time[1])) {
if ($given_time[1] < 10) {
$processed_time[1] = "0" . $given_time[1];
} else {
$processed_time[1] = $given_time[1];
}
} else {
$processed_time[1] = "00";
}

if (isset($given_time[2])) {
if ($given_time[2] < 10) {
$processed_time[2] = "0" . $given_time[2];
} else {
$processed_time[2] = $given_time[2];
}
} else {
$processed_time[2] = "00";
}
$interval = new \DateInterval(
'PT' . $processed_time[0] . 'H' . $processed_time[1] . 'M' . $processed_time[2] . 'S'
);
break;
default:
throw new ProcessorException("The given time format is incorrect !!!");
}

$first_date->add($interval);
}


return self::castAggregate($first_date->format('Y-m-d H:i:s'), $expr, $result);
}
}
42 changes: 42 additions & 0 deletions tests/EndToEndTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1221,4 +1221,46 @@ private static function getConnectionToFullDB(bool $emulate_prepares = true, boo

return $pdo;
}


public function testMakeTimeFunction()
{
$pdo = self::getConnectionToFullDB(false);

$query = $pdo->prepare('select MAKETIME(12,13,24) as time');

$query->execute();

$d = mktime(12, 13, 24, 1, 10, 2025);

$current_date = date("h:i:s", $d);

$this->assertSame(
[[
'time' => $current_date,
]],
$query->fetchAll(\PDO::FETCH_ASSOC)
);
}


public function testTIMESTAMPFunction()
{
$pdo = self::getConnectionToFullDB(false);

$query = $pdo->prepare("SELECT TIMESTAMP('2025-01-10','14:10:00') as date");

$query->execute();

$d = mktime(14, 10, 00, 1, 10, 2025);

$current_date = date("Y-m-d H:i:s", $d);

$this->assertSame(
[[
'date' => $current_date,
]],
$query->fetchAll(\PDO::FETCH_ASSOC)
);
}
}
28 changes: 28 additions & 0 deletions tests/SelectParseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -316,4 +316,32 @@ public function testBracketedFirstSelect()

$select_query = \Vimeo\MysqlEngine\Parser\SQLParser::parse($sql);
}

public function testMAKETIMEFunction()
{
$sql = "select MAKETIME(12,13,24)";

$select_query = \Vimeo\MysqlEngine\Parser\SQLParser::parse($sql);
$this->assertInstanceOf(SelectQuery::class, $select_query);

$sum_function = $select_query->selectExpressions[0];

$this->assertTrue(isset($sum_function->args[0]));
$this->assertEquals(12, $sum_function->args[0]->value);
}

public function testTIMESTAMPFunction()
{
$sql = "SELECT TIMESTAMP('2025-01-10', '14:10:00')";

$select_query = \Vimeo\MysqlEngine\Parser\SQLParser::parse($sql);
$this->assertInstanceOf(SelectQuery::class, $select_query);

$TIMESTAMP_function = $select_query->selectExpressions[0];

$this->assertTrue(isset($TIMESTAMP_function->args[0]));
$this->assertTrue(isset($TIMESTAMP_function->args[1]));
$this->assertEquals('2025-01-10', $TIMESTAMP_function->args[0]->value);
$this->assertEquals('14:10:00', $TIMESTAMP_function->args[1]->value);
}
}